Pixel Smoke

A simple smoke effect created by pixel fiddling.

pDoNothing

reverseScan

Flames of the Past

This is a recreation of an effect I created when I was first learning to program in the not-actually-that-glorious days of QBasic. It bothered me at the time that I was stuck with the "smoke" always going diagonally across the display. Fortunately <canvas> elements can be rotated and it only slightly destroys the page layout. I'm quite fond of this chunky rotated pixel look (although of course Internet Explorer still ignores the image-rendering CSS attribute and has removed its workaround, so the demo will appear all blurry.) One of the flaming squares will follow your mouse/touch around the canvas.

Cellular Automata? It's just pixels

It might be correct to call this a very simple two dimensional stochastic cellular automata. But it's better to call it messing around with pixels. Every update each pixel chooses at random one of three things to do. (I think stochastic/probabalistic cellular automata usually refers to automata that always do the same thing, but get updated at random rather than uniformly as is normal.)

That's all there is to it. We introduce some coloured pixels by manually setting a few (those are the static 3x3 pixel squares). All the shapes that twist and morph as they fly upwards are from that random copying of pixels.

Pixel Fiddling with Canvas

The <canvas> element isn't really made for pixel-by-pixel work, but you can convince it to play along. I've not done performance testing, so this may not be the best way if you're wanting to do serious pixel based work. getImageData() on the canvas' 2d context gives us access to a Uint8ClampedArray representing every pixel's red, green, blue, and alpha channels. We can do whatever we like with that data, then putImageData() to display it.

The image data is just a long array of values arranged by channel and position, so let's make a couple of functions to get and set pixel colours from it.

function pGet(data, x, y) { if (x < 0 || x > xCanvasSize || y < 0 || y > yCanvasSize) { return {r: 0, g: 0, b: 0, a: 0}; } var index = (x + y * xCanvasSize) * 4; return { r: data[index + 0], g: data[index + 1], b: data[index + 2], a: data[index + 3] }; } function pSet(data, x, y, r, g, b, a) { if (x < 0 || x > xCanvasSize || y < 0 || y > yCanvasSize) { return; } var index = (x + y * xCanvasSize) * 4; data[index + 0] = r; data[index + 1] = g; data[index + 2] = b; data[index + 3] = a; }

Now that we can read and write pixel colours, we just need to implement the update rules. Notice that we iterate over the pixels in an order that means we're always reading from pixels that haven't yet been updated. Normally cellular automata implementations need two buffers to flip between so the old version can always be read, but because we only read from certain neighbours we can get away with this single buffer.

function smear() { var image = context.getImageData(0, 0, xCanvasSize, yCanvasSize); var data = image.data; for (var y = yCanvasSize; y >= 0; y--) { for (var x = 0; x < xCanvasSize; x++) { var r = Math.random(); if (r < 0.33333) { var c = pGet(data, x - 1, y); pSet(data, x, y, c.r, c.g, c.b, c.a ); } else if (r < 0.66666) { var c = pGet(data, x, y + 1); pSet(data, x, y, c.r, c.g, c.b, c.a ); } } } context.putImageData(image, 0, 0); }

The colours are generated from a simple palette as described in another article. We do a little trigonometry to account for the effect of canvas rotation on mouse position. The update function is called through requestAnimationFrame(), with the update function limiting update rate to 30Hz for aesthetic reasons. The full javascript source is of course available.

Tweaking

You'll have noticed a slider and checkbox under the canvas. You'll have doubtlessly also fiddled with them.

pDoNothing adjusts the probability that each pixel will do nothing instead of copying from one of its neighbours. A high values makes the "smoke" move slower, a low value tends to create curious line formations perpendicular to the direction of motion. Emergent properties are fun!

reverseScan simulates a bug by iterating through the pixels in the wrong order. Now each pixel reads from the new version of its neighbour rather than the old version, which means a single update frame can spread colour from one pixel over to many. It's now a completely incorrect implementation of cellular automata, but it creates more rapidly changing shapes that I like the look of.

Comments are plaintext only and may be ruthlessly moderated.