Why are multiple rotate() needed to place every number on the right spot? - Canvas Clock Numbers

Viewed 95

Following this tutorial which shows how to make an analog clock using HTML canvas, I've had a hard time understanding what is going on when placing numbers on the clock face.

The code is here, and the following is the part that I'd like to ask.

function drawNumbers(ctx, radius) {
  var ang;
  var num;
  ctx.font = radius * 0.15 + "px arial";
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";

  for(num = 1; num < 13; num++){
    ang = num * Math.PI / 6;
    ctx.rotate(ang);
    ctx.translate(0, -radius * 0.85);
    ctx.rotate(-ang);
    ctx.fillText(num.toString(), 0, 0);
    ctx.rotate(ang);
    ctx.translate(0, radius * 0.85);
    ctx.rotate(-ang);
  }
}

In a for loop, the first ctx.rotate(ang) sets the number on the place it's supposed to be.

The next rotate ctx.rotate(-ang) puts the number back to upright because it's tilted. (although I don't know why it works like this not putting the number back to the first position.)

Then, after ctx.fillText(…) shows the number up, it seems to do the same again.

Why are these two rotate() needed? Do they work differently from the ones in the upper? If do, how?

2 Answers

What this code tries to do is to go back to its previous position, the center of the canvas.

Think of the context as a sheet of paper that you can rotate and move (translate), with a fixed pen over it.

  • First they do rotate that sheet of paper so that tracing a vertical line will go in the desired direction.
  • Then they move the sheet of paper vertically, so that the pen is at the correct position. However here, the sheet of paper is still rotated, so if they were to draw the text horizontally from here, the drawing would be oblique.
  • So they do rotate again in the other way for the text to be at correct angle.
  • They draw the text.
  • Now they want to go back to point 1 to be able to draw the next tick. For this they do the same route but in the other way: rotate back the sheet of paper to the desired angle so that they can move vertically to the center.
  • Move vertically to the center
  • Finally rotate back so that the sheet of paper is in its original orientation for the next tick.

However you should not do this. rotate() may end up having rounding issues, so doing rotate(angle); rotate(-angle) can not come back to the initial orientation, but to some slightly rotated state, which can be catastrophic for your application since now when you'll try to draw pixel perfect lines, you won't be able and you will kill the whole performances of your app.

Instead use the absolute setTransform method to go back to the original position:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var radius = canvas.height / 2;
radius = radius * 0.90
drawNumbers(ctx, radius);

function drawNumbers(ctx, radius) {
  var ang;
  var num;
  ctx.font = radius * 0.15 + "px arial";
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";
  for(num = 1; num < 13; num++){
    ang = num * Math.PI / 6;
    // go (back) to center
    ctx.setTransform(1, 0, 0, 1, radius, radius);
    ctx.rotate(ang);
    ctx.translate(0, -radius * 0.85);
    ctx.rotate(-ang);
    ctx.fillText(num.toString(), 0, 0);
  }
  // reset to identity matrix;
  ctx.setTransform(1, 0, 0, 1, 0, 0); 
}
canvas {
  background-color: white;
}
<canvas id="canvas" width="400" height="400">
</canvas>

Here is another implementation without using rotate.
Instead I calculate the x, y with a bit of trigonometry.

The starting angle is var ang = Math.PI;
Then in the loop we decrease it ang -= Math.PI / 6;

Calculating the position is easy once you know the formula:

let x = radius * Math.sin(ang)
let y = radius * Math.cos(ang)

Below is a fully functional example

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.font = "16px arial";
ctx.textAlign = "center";

var radius = 60
var ang = Math.PI;

for (let num = 1; num < 13; num++) {
  ang -= Math.PI / 6;
  let x = radius * Math.sin(ang)
  let y = radius * Math.cos(ang)

  ctx.fillText(num.toString(), x, y);
  ctx.beginPath()
  ctx.arc(x, y - 6, 12, 0, 2 * Math.PI);
  ctx.stroke();  
  ctx.beginPath()
  ctx.arc(x, y - 6, 45, -ang-2,-ang);
  ctx.stroke();  
}
<canvas id="canvas" width="160" height="160"></canvas>

I personally never been a fan of using rotate, for a small static canvas image might be fine, but as we move to more complex animations with multiple object, when I have to debug with multiple rotation it quickly becomes painful and quite hard to follow.

Related