In my last post, I created a 3D particle system in a 2D canvas, by doing the necessary mathematics to project coordinates. Seeking to optimize the drawing stage, I have modified the code so that the particles are now copied from a sprite sheet instead of being drawn with graphics commands. This has improved performance, but also led me to discover some significant browser differences.

Click here or on the screenshot below to see the prettiest (but most CPU heavy) version of the particle effect. But if you try it in Chrome it will look different than it does in, say, Internet Explorer. I will share my observations about these differences below.

A zip file containing four different versions of the effect can be downloaded here: ParametricParticles2.zip

See my earlier post for more more comments on the basic code used here. The major difference here is that we are using a sprite sheet to create the particle images. (See my post HTML5 Canvas – A Simple Sprite Sheet Example for an introduction to using runtime created sprite sheets for optimizing animations.)

The sprite sheet must contain an image of a typical particle in all the different possible scales that will be needed to represent different distances from the viewer. Creating and copying from this sprite sheet involves a few interesting mathematical aspects that I will discuss more below.

I have created four versions of the effect, with some coding differences which create a range of CPU load. From heaviest to lightest (approximately, depending on your browser), here they are. (If you’re reading this on a mobile device, you may wish to try the fourth version.) I will explain the differences below.

### Browser differences – speed and pixel snapping

The prettiest version of the effect involves 4000 particles. In my testing (on a PC), it runs beautifully in Internet Explorer, but Firefox, Opera, and Safari struggle to keep up. The effect runs smoothly on Chrome, but it does not render in the same way due to some automatic pixel snapping. Evidently in Chrome, when the images are copied from the sprite sheet to the main canvas, the pixel coordinates are snapped to the nearest whole number positions rather than being smoothly antialiased with subpixel rendering.

Without a doubt, the “unsnapped” version is nicer looking than the snapped version. But on the other hand, if we purposefully snap the images to the closest whole number coordinates within the code, then this will greatly improve performance, and it still looks acceptable.

So the “with pixel snapping” versions linked above make use of the Math.round() function to compute the closest integer coordinates before drawing particles to the canvas. This creates an effect that is already being done in Chrome whether we want it or not.

Another option for optimization, of course, is simply to reduce the number of particles that will be rendered. The lighter versions above make use of fewer particles, and in my testing the moderate number of particles are handled well by all browsers. But the most significant optimization seems to come from using pixel snapping.

### The sprite sheet

The basic particle radius is set to 6 pixels, but this will be scaled according to perspective: particles further away must appear smaller. To handle all the possibilities, the sprite sheet contains images of a basic particle at various scaled sizes, from a diameter of 1 up to a maximum of 48. But to save space, they are placed right up next to each other (instead of each being placed in a 48 x 48 square).

Later, when we wish to copy from this sprite sheet, we must know where to find the particle of a given diameter D. To accomplish this, we use a well-known formula for the sum of the first N whole numbers:

1 + 2 + 3 + ... + N = N*(N+1)/2


You can see this in action within the code. Once we have determined what diameter scaledDiam the particle must be drawn with, we compute the x coordinate where we will find it in the sprite sheet:

spriteX = 0.5*(scaledDiam-1)*scaledDiam;

(Note that the formula is slightly different because the particle of diameter 1 is at x-coordinate 0.) Then we grab the image and copy it to the canvas using the context.drawImage() method:

context.drawImage(spriteSheetCanvas,
spriteX,0,
scaledDiam,scaledDiam,
scaledDiam,scaledDiam);

The parameters here define the image to copy from (an off-screen canvas), the rectangle within this image to copy from, and the destination rectangle. For the pixel-snapped versions, we apply some integer-rounding:

context.drawImage(spriteSheetCanvas,
spriteX,0,
scaledDiam,scaledDiam,
scaledDiam,scaledDiam);