Will Ridgers

drawing a square

· javascript canvas mathematics

One of the great things about the web and browser technology is that it’s so accessible. Anyone can pick it up and start playing because any computer with a browser is ready to go — and that makes it a great platform for learning and prototyping. In this post I’d like to show you how we can use some JavaScript and a bit of simple mathematics to draw a square (don’t worry, it’ll be an exciting square).

First, we’re going to need somewhere to draw. We’ll use the HTML5 <canvas> element — it goes in your markup just like any other.

<canvas id="canvas"></canvas>

To start drawing, we’ll fetch canvas Element and get a 2d drawing context. This just means we’ll get one particular API that’s used for simple 2D drawing. You could fetch a webgl context if you wanted to use WebGL features.

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

Here ctx is an instance of CanvasRenderingContext2D, which exposes to us a handy collection of methods to actually do something with our canvas. We’ll mostly work with this ctx object now, let’s start by drawing a rectangle.

ctx.fillRect(0, 0, 100, 100);

You should end up with something similar to this:

Easy, huh?! You’ll notice that mine is red. That’s because I set the fillStyle property like this: ctx.fillStyle = '#ff0000';.

fillRect is great but it’s not very flexible. What if you wanted to rotate your square? The fillRect function has no angle argument so we’re going to need to find a better way — luckily the CanvasRenderingContext2D gives us exactly that in the form of paths. The path methods let us trace out complex shapes, and then

ctx.beginPath();        // This starts a new path
ctx.moveTo(0, 0);       // Move to our starting location, without actually tracing anything yet
ctx.lineTo(0, 100);     // Left side
ctx.lineTo(100, 100);   // Bottom side
ctx.lineTo(100, 0);     // Right side
ctx.lineTo(0, 0);       // Top side

ctx.fill();
ctx.stroke();

And you should get something like this:

You might notice that the lines here are a little blurry, this is because of an effect called antialiasing and can be solved.

Now would be a good time to play with the moveTo and lineTo arguments x and y. You’ll notice that the origin (0,0) is in the top left. As we increase x, we move the point right. As we increase y, we move the point down, not up — so it’s not quite a traditional Cartesian coordinate plane.

Now we’ll make a series of improvements — first, lets wrap this code up into a function so we can easily draw multiple squares. Better yet, we’ll give the position of the centre of our square as an argument (using the centre will make everything much easier, as you will see soon).

function drawSquare(x, y) {
  ctx.beginPath();
  ctx.moveTo(x - 50, y - 50); // v0
  ctx.lineTo(x - 50, y + 50); // v1
  ctx.lineTo(x + 50, y + 50); // v2
  ctx.lineTo(x + 50, y - 50); // v3
  ctx.lineTo(x - 50, y - 50); // v0

  ctx.fillStyle = 'red';
  ctx.fill();
  ctx.stroke();
}

drawSquare(50, 50);

What we’re doing here is drawing four edges between four vertices — v0, v1, v2, and v3. We’ll examine v0 first. The calculations x - 50 and y - 50 are simple but let’s break them down to understand what is going on — it’ll be easier if we rearrange the equations to -50 + x and -50 + y. The first components form a pair, [-50, -50]. This is a vector, and it defines the position of v0 relative to the origin of our square (i.e., up and left a bit). These are known as local coordinates. The x and y variables form a vector too, [x, y], and this describes the translation of local coordinates to world coordinates. So, it’s all just vector addition!

Given a local coordinate vl and a position in world space p, the equation for vw is vw = vl + p. With this in mind, let’s improve our code a little.

var square = [
  [-10, -10],
  [-10, 10],
  [10, 10],
  [10, -10],
];

function draw(coords, pos, colour) {
  var world_coords = [];

  for (var i in coords) {
    world_coords[i] = [
      coords[i][0] + pos[0],
      coords[i][1] + pos[1]
    ];
  }

  world_coords.push(world_coords[0]);

  ctx.beginPath();
  ctx.moveTo(world_coords[0][0], world_coords[0][1]);

  for (i = 0; i < world_coords.length; i++) {
    ctx.lineTo(world_coords[i][0], world_coords[i][1]);
  }

  ctx.fillStyle = colour;
  ctx.fill();
  ctx.stroke();
}

draw(square, [50, 50], 'green');

This new square is a little smaller, so let’s make it bigger. We can do that with scalar multiplication. Let’s pick a scale factor s and add it to our equation above. Where does it go? Order is important here since the scaling will always happen around [0, 0]. So our equation must be vm = s * vl + p. If you applied the scalar multiplication after translating to world coordinates, the square would scalar relative to the top left corner of the canvas. Add a scale factor to your draw function, and change the first for loop to look more like this:

for (var i in coords) {
  coords[i] = [
    scale * coords[i][0] + pos[0],
    scale * coords[i][1] + pos[1]
  ];
}

And you should be able to do something like this:

Great! So we’ve got scaling and translation, the last hurdle is rotation. For this we’ll use a rotation matrix, specifically one for rotating a 2D point counter-clockwise around the origin. You can find it here. It looks something like this:

var angle = 45;
var matrix = [
  [Math.cos(angle), - Math.sin(angle)],
  [Math.sin(angle), Math.cos(angle)]]
];

Remember, this matrix operates around the origin — so the order we apply it is important. If our new matrix is r, then our equation becomes vm = s * (r * vl) + p. Since we’re only dealing with two dimensions here, we’ll expand the matrix multiplication out into two equations for the new point.

var a = x * Math.cos(angle) - y * Math.sin(angle)
var b = x * Math.sin(angle) + y * Math.cos(angle)

And our draw function becomes:

for (var i in coords) {
  var x = coords[i][0];
  var y = coords[i][1];

  world_coords[i] = [
    scale * (x * Math.cos(angle) - y * Math.sin(angle)) + pos[0],
    scale * (x * Math.sin(angle) + y * Math.cos(angle)) + pos[1]
  ];
}

The angle variable here is in radians, so a value of 2 * Math.PI equates to a full rotation. Now you should have something like this:

Excellent! Play with the value a little, do you notice anything in particular? Remember we used a rotation matrix “for rotating a 2D point counter-clockwise around the origin”, but our square rotates clockwise! This is because our y axis is flipped, as you increase y points move down and not up. This means our rotation matrix is operating slightly differently, but it still has the same effect.

One last thing we’ll discuss is animation — which is a lot easier than you might think. We’ll create a frame function that we can call multiple times. Before you draw anything clear the canvas with ctx.clearRect().

var rotation = 0;

function frame() {
  ctx.clearRect(0, 0, 150, 300);
  draw(square, [50, 50], 1, rotation, 'green');
  rotation += 0.03;

  window.requestAnimationFrame(frame);
}

frame();

And you should get this!

And we’re done. That’s one exciting square, just as promised. Hopefully by now the message will be clear — it’s all just mathematics. The canvas is just a vessel for our message, and because it’s so accessible and has a fast feedback loop, you can play with the numbers to get a deeper understanding of what is going on.

The ideas here can be applied in many ways, some of which I hope to explore in future posts.

Exercises

Resources