HTML5 canvas performance: Drawing circles
By Daniel Davistagawa. Thursday, December 13, 2012 2:59:29 AM
As it's nearly Christmas, I was playing with HTML5 canvas to draw baubles on a photo of a Christmas tree. Wondering what was the best way to do it, I came across this answer on Stack Overflow about drawing circles with just radial gradients.
As you probably know, the standard way of drawing circles is to use
// Drawing a circle the traditional way ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2, true); ctx.fillStyle = 'rgba(195, 56, 56, 1)'; ctx.fill(); ctx.closePath();
This way of drawing a circle is a bit cumbersome in my opinion, compared to SVG for example. I thought the idea of using just radial gradients was a clever alternative and wondered what the performance difference would be.
// Drawing a circle with a radial gradient var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0.95, 'rgba(195, 56, 56, 1)'); gradient.addColorStop(1, 'rgba(195, 56, 56, 0)'); ctx.fillStyle = gradient; ctx.fillRect(x - radius, y - radius, x + radius, y + radius);
Sure enough, using radial gradients is slower than
arc(). Several times slower! You can play with this canvas test page here to see the speed difference for yourself.
If I'd thought about it properly, I should have realised this without needing to test it and saved myself some time, but then I tried playing with spheres (well, circles with shading) as well.
There are two common ways to make spheres in canvas:
- Radial gradients
- Existing images using
// Drawing a sphere with radial gradients var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); gradient.addColorStop(0.2, 'rgba(255, 85, 85, 1)'); gradient.addColorStop(0.95, 'rgba(128, 0, 0, 1)'); gradient.addColorStop(1, 'rgba(128, 0, 0, 0)'); ctx.fillStyle = gradient; ctx.fillRect(x - radius, y - radius, x + radius, y + radius);
// Drawing a sphere with an existing image var img = new Image(); img.src = 'images/baubles.png'; ctx.drawImage(img, x, y, width, height);
- Using an image sprite of a particular image with varying colours.
- Using a greyscale image and applying a semi-transparent overlay with
Don't forget that using images means they have to be downloaded first so it's better to pre-load them if possible.
You can test the performance of these on the same canvas test page.
As you can see, the overlay approach is obviously slower but not as much as with gradients. It also gives you more freedom in controlling the colours, however the overall effect has lower contrast than the original image.
In general, the speed difference is not noticeable for simple applications or fast hardware, but could be an issue if you're using animation, making a high-performance game, or designing for a TV or set-top box. As always, every decision is a compromise so here's a summary of the various trade-offs and what I've learned:
- If you just want to draw a circle, use
- If you want to draw a sphere, use an image (and pre-load it).
- If you want to draw a variety of spheres, try using image sprites.
- If you want spheres with dynamically-changing colours, consider using an image with a semi-transparent overlay.
- Only use radial gradients if you really need to.
One final thing I've learned is that adding thousands of baubles does not enhance a Christmas tree's beauty!
Marcelo came up with the cool idea of creating a single image on a hidden canvas using a radial gradient, and then repeatedly drawing that with
drawImage(). This gets around the need to create images in advance and also means you can edit the colour on-the-fly. But here's the best part — ignoring the initial gradient creation time, it's actually faster than using
drawImage() on an existing image! The code looks something like this:
// Create a second "buffer" canvas but don't append it to the document var tmpCanvas = document.createElement('canvas'); var tmpCtx = tmpCanvas.getContext('2d'); // Add the necessary gradients here, as above // Draw the image from the second "buffer" canvas ctx.drawImage(tmpCanvas, x, y, width, height);
So, if you're drawing lots of circles or spheres, this is my recommended method. Nice one Marcelo!