Pixel Smoke

A retro 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. I'm quite fond of this chunky rotated pixel look (although of course Internet Explorer still ignores the image-rendering CSS attribute, so the demo may appear blurry.) One of the source blocks will follow your mouse/touch around the canvas.

Cellular Automata? It's just pixels

If you want to sound clever you can call this a two dimensional stochastic cellular automata. But we all know it's just messing around with pixels. Every update each pixel chooses at random one of three things to do:

That's all there is to it. We introduce some coloured pixels by manually setting a few (those are the static 3x3 pixel diamonds). The interesting shapes that seem to twist and morph as they drift upwards emerge from that simple 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. If you're concerned about performance this whole process should be moved over to the GPU. But this is just a little demo and computers are absurdly fast so we'll use CPU-side operations. 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. As this is is being nostalgic, we'll name them after the old PGET and PSET statements.

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 state is still accessible all through the update process, but because we only read from neighbours in two directions 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.

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 line formations perpendicular to the direction of motion.

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. With that active it's no longer a cellular automata, but it creates more varied shapes that I like the look of.