Scaling in HTML’s Canvas

Jan 31 2013 Published by under JavaScript

Send to Kindle

I’m probably not dropping any huge knowledge bombs here on many of you, but there are a few neat things about scaling when drawing to an HTML5 Canvas that I didn’t realize until a few months ago. So there are probably one or two of you who might find the info handy too.

Let’s say we’re drawing a circle in a canvas. We do this:

  context.beginPath();
  context.arc(100, 100, 50, 0, Math.PI * 2, false);
  context.stroke();

And we get this:

scale_01

(I’m assuming in all of these examples that you have an HTML document with a canvas element, and you’ve grabbed a reference to its 2d context, stored in a variable named “context”. You laugh, but someone’s bound to paste the above code into a text editor somewhere and complain to me that it doesn’t work.)

You probably also know that you can scale the circle like so:

  context.save();
  context.scale(2, 2);
  context.beginPath();
  context.arc(100, 100, 50, 0, Math.PI * 2, false);
  context.stroke();
  context.restore();

This scales everything by a factor of 2, so we get a circle that’s twice as large:

scale_02

But what might not be immediately obvious is the fact that the size of the shape, and the size of the stroke can be scaled separately. In this case because we set the scale before doing anything and restored the transformation matrix after all was done, everything was doubled, the size, the position, the stroke’s width.

But let’s try moving the restore line up before the stroke:

  context.save();
  context.scale(2, 2);
  context.beginPath();
  context.arc(100, 100, 50, 0, Math.PI * 2, false);
  context.restore();
  context.stroke();

This set up gives us a double sized circle, but because we cancelled the transform before applying the stroke, it stays one pixel thick, the default:

scale_03

We can even do the opposite and draw the circle the normal size, then scale up, stroke it, and restore:

  context.beginPath();
  context.arc(100, 100, 50, 0, Math.PI * 2, false);
  context.save();
  context.scale(4, 4);
  context.stroke();
  context.restore();

scale_04

So how is this useful? At least one great way: drawing ellipses. There is no native way to draw an ellipse in canvas. You can of course use some trig and draw a bunch of line segments, but that can be problematic. How many segments to draw? Too few for a large ellipse and it looks choppy. As the size goes down, you need less segments, and drawing too many is a waste of resources. Another complex and inexact solution involves using bezier curves. The easiest way is to just draw a circle scaled differently on one axis than the other. Of course, if you’re using a stroke on the arc, you can wind up with something weird like this:

  context.save();
  context.scale(10, 2);
  context.beginPath();
  context.arc(20, 20, 10, 0, Math.PI * 2, false);
  context.stroke();
  context.restore();

scale_05

See how the stroke was also scaled more on the x axis than on the y, making it look more like an Oakley logo than anything else. To fix it, just do what we did in the second example, moving the restore to before the stroke:

  context.save();
  context.scale(10, 2);
  context.beginPath();
  context.arc(20, 20, 10, 0, Math.PI * 2, false);
  context.restore();
  context.stroke();

scale_06

With this, we can even make a neat little ellipse function like so:

  function ellipse(x, y, w, h) {
    context.save();
    context.translate(x + w / 2, y + h / 2);
    context.scale(w / 2, h / 2);
    context.beginPath();
    context.arc(0, 0, 1, 0, Math.PI * 2, false);
    context.restore();
    context.stroke();
  }
  
  ellipse(100, 100, 200, 80);

I’ll leave you to work through that on your own. Here’s what calling it gives you:

scale_07

Finally, you can go really funky on it, scaling the shape one way and the stroke the other way, like so:

  context.save();
  context.scale(1, 2);
  context.beginPath();
  context.arc(100, 100, 60, 0, Math.PI * 2, false);
  context.restore();
  
  context.save();
  context.scale(20, 1);
  context.stroke();
  context.restore();

With a bit of experimentation, you can wind up with something that’s a half decent fake perspective:

scale_08

Anyway, old news to some of you, new news to others (I hope). More stuff to play with.

Send to Kindle

Comments are off for this post