Search code examples
javascripthtml5-canvas

Why do we have to rotate main canvas when trying rotate sprite image?


In every tutorial that I could find for how to rotate a sprite image on a canvas the canvas itself is rotated before applying sprite to it:

function drawSprite(sprite, x, y, deg)
{
  const width = sprite.width / 2,
        height = sprite.height / 2;

  x = x + width;
  y = y + height;
  //clear main canvas
  mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
  // move origin to the coordinates of the center where sprite will be drawn
  mainCtx.translate(x, y);
  // rotate canvas
  mainCtx.rotate(deg);
  // draw sprite
  mainCtx.drawImage(sprite, -width, -height);
  // restore previous rotation and origin
  mainCtx.rotate(-deg);
  mainCtx.translate(-x, -y);
}

//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
      sprite = (() =>
      {
        const canvas = document.createElement("canvas"),
              ctx = canvas.getContext("2d"),
              width = canvas.width = ctx.width = 100,
              height = canvas.height = ctx.height = 50;

        ctx.font = '20px arial';
        ctx.textBaseline = "middle";
        ctx.textAlign = "center";
        ctx.fillStyle = "lightgreen";
        ctx.fillRect(0, 0, width, height);
        ctx.strokeRect(0, 0, width, height);
        ctx.strokeText("my sprite", width/2, height/2);
        return canvas;
      })();


let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
      w = mainCanvas.width = mainCtx.width = 400,
      h = mainCanvas.height = mainCtx.height = 200;

mainCtx.fillStyle = "pink";
setInterval(() =>
{
  const deg = r++*Math.PI/180;
  let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
      y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));

  drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>

To me this is counterintuitive, why can't we rotate sprite itself before drawing it on main canvas? Why doesn't this work?

function drawSprite(sprite, x, y, deg)
{
  const spriteCtx = sprite.getContext("2d");

  //clear main canvas
  mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
  // rotate sprite
  spriteCtx.rotate(deg);
  // draw sprite
  mainCtx.drawImage(sprite, x, y);
}

//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
      sprite = (() =>
      {
        const canvas = document.createElement("canvas"),
              ctx = canvas.getContext("2d"),
              width = canvas.width = ctx.width = 100,
              height = canvas.height = ctx.height = 50;

        ctx.font = '20px arial';
        ctx.textBaseline = "middle";
        ctx.textAlign = "center";
        ctx.fillStyle = "lightgreen";
        ctx.fillRect(0, 0, width, height);
        ctx.strokeRect(0, 0, width, height);
        ctx.strokeText("my sprite", width/2, height/2);
        return canvas;
      })();


let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
      w = mainCanvas.width = mainCtx.width = 400,
      h = mainCanvas.height = mainCtx.height = 200;

mainCtx.fillStyle = "pink";
setInterval(() =>
{
  const deg = r++*Math.PI/180;
  let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
      y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));

  drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>

Wouldn't it be faster rotate a 100x100 sprite vs 10000x10000 main canvas?


Solution

  • Because the drawImage function takes only x(s),y(s) coordinate and width(s),/height(s).

    I.e it only ever draws a straight rectangle, there is no way to make it draw anything skewed.

    So you have to rotate the context's Current Transformation Matrix (CTM), which is not the canvas, so that the drawing is transformed.

    Note that drawing a bitmap as a rectangle is a very basic model for drawing APIs.

    As for the speed, once again you don't rotate the canvas, only the CTM and this only affects the future drawings and costs almost nothing anyway.

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    
    ctx.font = "30px sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.translate(150, 75);
    
    const txtArr = [ "first", "second", "third", "fourth" ];
    const colors = [ "red", "blue", "green", "orange" ];
    for (let i = 0; i < txtArr.length; i++) {
      ctx.fillStyle = colors[i];
      ctx.fillText(txtArr[i], 0, 0);
      // This doesn't rotate the previous drawings
      ctx.rotate(Math.PI  / txtArr.length);
    }
    <canvas></canvas>

    So, yes, you could have your own drawImage(source, matrix) which would be something like

    this.save();
    this.setTransform(matrix);
    this.drawImage(source, 0, 0);
    this.restore();
    

    But as you can see this means actually more operations per draw call, and thus performing two drawings on the same CTM would actually cost more than setting the CTM only once.