javascripthtml5-canvasrotatetransform

HTML5 Canvas rotate function


function drawNumbers(){
    var rad, num;

    cx.font= "30px Arial";
    cx.textAlign = "center";
    cx.textBaseline = "middle";

    //numbers around the inner circumference
     for(num=1; num < 13; num++){
    rad = num * Math.PI/6; //angle for every number
     cx.rotate(rad);
     cx.translate(0, -175);
     cx.rotate(-rad);
     cx.fillText(num.toString(),0,0);
     cx.rotate(rad);
     cx.translate(0, 175);
     cx.rotate(-rad);
    }
}

function drawHands(){
    //getting the time
    var time = new Date();
    var hours = time.getHours();
    var minutes = time.getMinutes();
    var seconds = time.getSeconds();

    //setting the radians based on the time
    //hour hand
    hours %= 12;
    hours = (hours * Math.PI/6) + (minutes * Math.PI/360) + (seconds * Math.PI/21600);
    hands(hours, radius * 0.04, radius * 0.5);

    //minute hand
    minutes = (minutes * Math.PI/30) + (seconds * Math.PI/1800);
    hands(minutes, radius * 0.03, radius * 0.65);

    //second hand
    seconds = (seconds * Math.PI/30);
    hands(seconds, radius * 0.01, radius * 0.68);
}

function hands(ang, width, length){
    cx.beginPath();
    cx.lineWidth = width;
    cx.lineJoin = "round";
    cx.lineCap = "round";
    cx.moveTo(0, 0);
    cx.rotate(ang);
    cx.lineTo(0, -length);
    cx.stroke();
    cx.rotate(-ang);
}

I was learning the HTML5 canvas in W3Schools and the tutorial was teaching how to make a working clock. 1. I just don't understand how the extra rotates work in the functions. 2. When applying a rotate function, does it always rotate from the center of origin (0, 0) of the canvas?


Solution

  • When you call the rotate function it rotates the entire canvas, imagine holding a painting and then tilting it. It happens around the origin always. The way to rotate around a different point is to translate the entire canvas first, then rotate it.

    Going back to the painting analogy, if we have rotated our painting, once we have drawn our line, we need to then restore the painting to being upright. Thus we rotate(-ang). If we had translated we would also have to undo our transation in a similar manner.

    Rotating and why we undo rotates

    In the code below you can see I'm drawing a base black rectangle, and then calling a function which rotates the canvas by 0.5 radians and draws another rectangle twice. I haven't undone my rotation so the 3rd rectangle is actually rotated at 1 radian.

    let canvas = document.getElementById("canvas");
    let ctx = canvas.getContext("2d");
    
    function drawRotatedRectangle() {
      ctx.rotate(0.5);
      ctx.fillRect(0, 0, 60, 50);
    }
    
    // Draw base rectangle
    
    ctx.fillRect(0, 0, 60, 50);
    
    // Rotate and draw second rectangle
    
    ctx.fillStyle = "#FF0000";
    
    drawRotatedRectangle();
    
    // rotate and draw third rectangle
    
    ctx.fillStyle = "#00FF00";
    
    drawRotatedRectangle();
    <canvas id="canvas"></canvas>

    To fix this we modify the drawRotatedRectangle() function to undo all translations and rotations that it made:

    let canvas = document.getElementById("canvas");
    let ctx = canvas.getContext("2d");
    
    function drawRotatedRectangle() {
      ctx.rotate(0.5);
      ctx.fillRect(0, 0, 60, 50);
      ctx.rotate(-0.5);
    }
    
    // Draw base rectangle
    
    ctx.fillRect(0, 0, 60, 50);
    
    // Rotate and draw second rectangle
    
    ctx.fillStyle = "#FF0000";
    
    drawRotatedRectangle();
    
    // rotate and draw third rectangle
    
    ctx.fillStyle = "#00FF00";
    
    drawRotatedRectangle();
    <canvas id="canvas"></canvas>

    Now we see the red (hidden) rectangle and the green rectangle are at the same angle.

    Rotating around a different point to the origin

    To demonstrate how we can rotate around a different location to the origin, we can first translate where our context origin is and then rotate our canvas. Below I move the origin to the center of the base rectangle, rotate the canvas and draw a rotated rectangle ontop of the base rectangle. Again the translations and rotations are restored in order of most recently applied.

    let canvas = document.getElementById("canvas");
    let ctx = canvas.getContext("2d");
    
    function drawRotatedRectangle() {
      // Move origin to center of rectangle
      ctx.translate(30, 25);
      // Rotate 0.5 radians
      ctx.rotate(0.5);
      // Draw rectangle where the center of the rectangle is the origin
      ctx.fillRect(-30, -25, 60, 50);
      // Undo our rotate
      ctx.rotate(-0.5);
      // Undo our translate
      ctx.translate(-30, -25);
    }
    
    // Draw base rectangle
    
    ctx.fillRect(0, 0, 60, 50);
    
    // Rotate and draw second rectangle
    
    ctx.fillStyle = "#FF0000";
    
    drawRotatedRectangle();
    
    // rotate and draw third rectangle
    
    ctx.fillStyle = "#00FF00";
    
    drawRotatedRectangle();
    <canvas id="canvas"></canvas>

    Edit - Handling Rounding Errors

    As Kaiido mentioned in the comments, the rotate(a) function will round the input it is given so simply doing the reverse rotate(-a) function will not return you to the original transformation.

    The solution they suggested is to set the transformation matrix to the desired location with setTransform(), in these example we are only returning to the original transform of the canvas so we can use the identity matrix:

    ctx.setTransform(1,0,0,1,0,0)

    Alternitively, you can also use save() and restore() methods. These will act like pushing the current state of the canvas context to a stack and when you restore() it will pop the latest state from the stack returning you to the previous transform. This article by Jakob Jenkov explains this method further with some examples.