I'm trying to apply a noise effect to my canvas, based on a codepen I saw, which in turn appears to be very similar to an SO answer.
I want to produce a "screen" of randomly transparent pixels, but instead of that I get a field that's completely opaque red. I'm hoping someone who is more familiar with either canvas or typed arrays can show me what I'm doing wrong, and maybe help me understand a few of the techniques at play.
I refactored the codepen code significantly, because (for now) I don't care about animating the noise:
/**
* 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)
}
From what I can tell, the core of what's happening is that we wrap the ImageData
's raw data representation (a series of 8-bit elements that reflect the red, green, blue, and alpha values for each pixel, in series) in a 32-bit array, which allows us to operate on each pixel as a united tuple. We get an array with one element per pixel instead of four elements per pixel.
Then, we iterate through the elements in that array, writing RGBA values to each element (i.e. each pixel) based on our noise logic. The noise logic here is really simple: each pixel has a ~50% chance of being a "noise" pixel.
Noise pixels are assigned the 32-bit value 0x00000088
, which (thanks to the 32-bit chunking provided by the array) is equivalent to rgba(0, 0, 0, 0.5)
, i.e. black, 50% opacity.
Non-noise pixels are assigned the 32-bit value 0x00000000
, which is black 0% opacity, i.e. completely transparent.
Interestingly, we don't write the buffer32
to the canvas. Instead, we write the imageData
that was used to construct the Uint32Array
, leading me to believe that we're mutating the imageData object through some kind of pass-by-reference; I'm not clear exactly why this is. I know how value & reference passing works generally in JS (scalars are passed by value, objects are passed by reference), but in the non-typed array world, the value passed to the array constructor just determines the length of the array. That's evidently not what's happening here.
As noted, instead of a field of black pixels that are either 50% or 100% transparent, I get a field of all solid pixels, all red. Not only do I not expect to see the color red, there's zero evidence of the random color assignment: every pixel is solid red.
By playing with the two hex values, I've discovered that this produces a scattering of red on black that has the right kind of distribution:
buffer32[i] = Math.random() < 0.5
? 0xff0000ff // <-- I'd assume this is solid red
: 0xff000000 // <-- I'd assume this is invisible red
But it's still solid red, on solid black. None of the underlying canvas data shows through the pixels that should be invisible.
Confusingly, I can't get any colors other than red or black. I also can't get any transparency other than 100% opaque. Just to illustrate the disconnect, I've removed the random element and tried writing each of these nine values to every pixel just to see what happens:
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%
What's going on?
EDIT: similar (bad) results after dispensing with the Uint32Array
and the spooky mutation, based on the MDN article on 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)
}