[Update: In this post, I remark on some differences seen between Chrome and other browsers in alpha blending computations, but it seems that the most recent version of Chrome (18) behaves the same way as other browsers in this respect. The comments here regarding fading trails are still relevant. I have found that the color-blending behavior of various browsers is unpredictable, and can even change with browser updates; open up the examples here in various browsers to observe the differences.]

I’m a big fan of particles. I’ve created quite a number of particle effects in Flash, and have posted many examples at flashandmath.com. So naturally I am drawn to particles while undertaking my new experiments in HTML5 Canvas. Animating lots of particles can create a heavy load on the CPU, but to my surprise the greatest bottleneck I have encountered is not in drawing the particles, but rather in getting them to fade out gradually. Click here or on the screenshot below to see the effect, and then stick around to read my observations on browser differences and resulting issues with fading trails.

ParticlesFading screencap

A slightly nicer version of the effect (larger and with more particles) is here. It looks lovely in Chrome, but the trails do not fade in the desired way in IE9 or FireFox. The reason for the differences will be explained below.

Download

Included in the source code are three versions of this effect which use three different methods for fading trails, which will be explained below: ParticlesFading.zip

About the Code

I won’t explain all the details behind the code, but rather will focus on the fading trail methods being used. The particle animation code is fairly well optimized, with particles stored in a linked list rather than an array. (For an excellent introduction to linked lists, see this tutorial by Michael Baczynski). There may be a few opportunities for optimization remaining in the code, but in my testing I found that the greatest bottleneck in performance is caused by the method I am using for fading out the picture gradually as new particles are drawn to the screen.

The particle motion

The particles are drawn as filled in circles, and they evolve over time. The way they evolve over time is controlled by some “envelope” time parameters: attack, hold, and decay. (These terms are borrowed from the audio synthesis world, where the attack, decay, sustain, and release parameters control how a tone changes over time while a key is pressed and held.) During these different time stages, a particle’s size will change from its initial size to its “hold” size, then decay back to a final size. Each particle has an age parameter which records how long the particle has been on the screen, to keep track of the time stage for the particle.

When particles either reach the end of their lifespan or go off the boundaries of the display area, they are removed from the list of active particles. However, they can be reused, so they are stored in a recycle bin (also a linked list) for future use when more particles need to be added to the screen. (The proper name for the recycle bin is an “object pool”.) This is an optimization technique which avoids the creation of new Objects.

Each particle has a Boolean parameter right, which controls whether the particle tends to turn to the right or to the left as it flies in the plane. The turning is controlled by changing the acceleration of the particle with randomized amounts, and always in a direction perpendicular to the direction of motion.

Fading trails

On every refresh of the screen, the particles are moved to new positions and then must be redrawn. If we simply want to see the new particles in their new positions, we should wipe out the screen by drawing a filled rectangle over the entire canvas before drawing the particles. But a more interesting effect is created by simply darkening the previous image and drawing the newly positioned particles over the top of the display. We then see a fading trail behind each particle as it flies through the plane.

But this is easier said than done in the HTML5 Canvas. One fading method is to draw a black rectangle over the whole display, but with a very low alpha value. To see this in action, click here, but be aware that this looks great in Chrome but doesn’t achieve the desired effect in IE9 or Firefox (you can check other browsers). The problem is that as the previous images fade out, they never reach color component values at zero, resulting in permanent gray marks as seen in the image below.

gray trails screencap

This effect is no doubt due to the way color component values are stored as integers ranging from 0 to 255, manipulated as floats, and then stored again as integers. Interestingly, Chrome seems to handle this arithmetic differently from the other browsers.

Here is what seems to be happening. If we draw a black rectangle with alpha 0.1 over an image, then the pixels behind the rectangle should darken to 90% of their previous value. But if the previous value was 5, then 0.9*5 = 4.5, which gets rounded back up to 5. In other words, the value will not change. The effect is more drastic if we wish for a slower fade: if we draw a black rectangle with alpha 0.01 over an image, then color component values at 50 will stay at 50 (because 0.99*50 = 49.5).

I can’t say for sure what is happening in Chrome, but I suspect that color component values are always rounded down to the nearest integer. So for example, when we darken a color component value of 5 by 10%, the resulting value of 4.5 is rounded down to 4. Because of the rounding down operation, colors eventually fade to black.

So how can we achieve a consistent look across browsers? One idea is to copy the current image to another canvas, and then redraw it to the screen with a lower alpha value, by adjusting the globalAlpha value prior to redrawing. But this essentially causes a similar kind of pixel arithmetic as in the black rectangle method, and we again see the gray trails (click here for an example using this method).

To create the desired effect, we have to realize that we really need to be subtracting a constant value from the alpha channel of previous pixels, rather than multiplying by a number less than one. Coming from a background in ActionScript, I am accustomed to using color transforms or color matrix filters for this type of effect. But in JavaScript, we have to manipulate the pixels ourselves, and unfortunately I have found that this creates a bit of a drag on the CPU. But I slightly lowered the number of particles, reduced the size of the display, and have been able to achieve fairly swift frame rates as a result. The final result is here.

Here is the code used for this alpha subtraction method. First we get the image data from the current display and access its pixel data:

var lastImage = context.getImageData(0,0,theCanvas.width,theCanvas.height);
var pixelData = lastImage.data;

We then iterate through the pixel data and lower the alpha channel of each pixel by 3, which creates a very slow fade:

var i;
var len=pixelData.length;
for (i=3; i<len; i += 4) {
     pixelData[i] -= 3;
}

Note that pixels are stored in groups of four numbers representing red, green, blue, and alpha channels, so the alpha channel of each pixel is stored in every fourth entry of the pixel data array. Finally, we put the image data back into the canvas context:

context.putImageData(lastImage,0,0);

After this, we can now draw the particles over the display in their new positions.

Looping through each pixel in the display and accessing the pixel data array repeatedly is time consuming, and takes more CPU resources than drawing a single rectangle over the whole display. Of course, in the internal workings of the browser, some similar kind of arithmetic must occur when blending the low alpha rectangle with the underlying display, but this is of course a more optimized process compared to the Javascript execution.

Final thoughts

So for consistency across browsers, we ended up requiring some code which directly accessed the pixels of the display and altered them to our liking. This is clearly the type of operation that would benefit from hardware acceleration, and to be sure this will become easier to do in the near future. I would be pleased to hear from you if you have any more thoughts on how to achieve this fading effect in an efficient manner!