Search code examples
javascripthtml5-canvas

HTML5 Canvas: how i can deal with inverted translate() after rotation?


I need to apply several matrix transformations before drawing a shape, however (if on somewhere) I use rotate() the coordinates are inverted and/or reversed and cannot continue without knowing if the matrix was previously rotated.
How can solve this problem?

Example:

<canvas width="300" height="300"></canvas>
<script>
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");

ctx.fillStyle = "silver";
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, canvas.height/2);
ctx.lineTo(canvas.width, canvas.height/2);
ctx.stroke(); 

ctx.beginPath();
ctx.moveTo(canvas.width/2, 0);
ctx.lineTo(canvas.width/2, canvas.height);
ctx.stroke();

ctx.translate(150, 150);
ctx.rotate(-90 * 0.017453292519943295);
ctx.translate(-150, -150);

// move the red rectangle 100px to the left (top-left)
// but instead is moved on y axis (right-bottom)
ctx.translate(-100, 0);

// more matrix transformations
// ....
// ....

// now finally draw the shape
ctx.fillStyle = "red";
ctx.fillRect(150, 150, 100, 50);
</script>

Can be this Translation after rotation the solution?


Solution

  • OK finally, i solved the problem by rotating the translation point before applying it. This function does the trick:

    function helperRotatePoint(point, angle) {
      let s = Math.sin(angle);
      let c = Math.cos(angle);
      return { x: point.x * c - point.y * s, y: point.x * s + point.y * c};
    }
    

    rotating the translation point using the inverted angle I obtain the corrected translation helperRotatePoint(translation_point, -rotation_angle);

    working code:

    let canvas = document.querySelector("canvas");
    
    // proper size on HiDPI displays
    canvas.style.width = canvas.width;
    canvas.style.height = canvas.height;
    canvas.width = Math.floor(canvas.width * window.devicePixelRatio);
    canvas.height = Math.floor(canvas.height * window.devicePixelRatio);
    
    let ctx = canvas.getContext("2d");
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    ctx.fillStyle = "whitesmoke";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    class UIElement {
    
      constructor(x, y, width, height, color) {
        // PoC
        this.draw_pos = {x, y};
        this.draw_size = {width, height};
    
        this.color = color;
    
        this.rotate = 0;
        this.scale = {x: 1, y: 1};
        this.translate = {x: 0, y: 0};
        this.skew = {x: 0, y: 0};
    
        this.childs = [];
      }
    
      addChild(uielement) {
        this.childs.push(uielement);
      }
    
      helperRotatePoint(point, angle) {
          let s = Math.sin(angle);
          let c = Math.cos(angle);
          return {
            x: point.x * c - point.y * s,
            y: point.x * s + point.y * c
          };
        }
    
      draw(cnvs_ctx, parent_x, parent_y) {
        // backup current state
        cnvs_ctx.save();
    
        let elements_drawn = 1;// "this" UIElement
    
        // step 1: calc absolute coordinates
        let absolute_x = parent_x + this.draw_pos.x;
        let absolute_y = parent_y + this.draw_pos.y;
    
        // step 2: apply all transforms
        if (this.rotate != 0) {
          cnvs_ctx.translate(absolute_x, absolute_y)
          cnvs_ctx.rotate(this.rotate);
          cnvs_ctx.translate(-absolute_x, -absolute_y);
          
          // rotate translate point before continue
          let tmp = this.helperRotatePoint(this.translate, -this.rotate);
          
          // apply rotated translate
          cnvs_ctx.translate(tmp.x, tmp.y);
        } else {
            cnvs_ctx.translate(this.translate.x, this.translate.y);
        }
        cnvs_ctx.scale(this.scale.x, this.scale.y);
        cnvs_ctx.transform(1, this.skew.y, this.skew.x, 1, 0, 0);
    
        // step 3: self draw (aka parent element)
        cnvs_ctx.fillStyle = this.color;
        cnvs_ctx.fillRect(absolute_x, absolute_y, this.draw_size.width, this.draw_size.height);
    
        // step 4: draw childs elements
        for (let i = 0; i < this.childs.length ; i++) {
            elements_drawn += this.childs[i].draw(
                cnvs_ctx, absolute_x, absolute_y
          );
        }
    
        // done, restore state
        cnvs_ctx.restore();
        return elements_drawn;
      }
    
    }
    
    
    
    // spawn some ui elements
    var ui_panel = new UIElement(120, 50, 240, 140, "#9b9a9e");
    var ui_textlabel = new UIElement(10, 10, 130, 18, "#FFF");
    var ui_image = new UIElement(165, 25, 90, 60, "#ea9e22");
    var ui_textdesc = new UIElement(17, 46, 117, 56, "#ff2100");
    var ui_icon = new UIElement(5, 5, 10, 10, "#800000");
    
    ui_panel.addChild(ui_textlabel);
    ui_panel.addChild(ui_image);
    ui_panel.addChild(ui_textdesc);
    ui_textdesc.addChild(ui_icon);
    
    // add some matrix transformations
    ui_textdesc.skew.x = -0.13;
    ui_textdesc.translate.x = 13;
    ui_image.rotate = -90 * 0.017453292519943295;
    ui_image.translate.y = ui_image.draw_size.width;
    ui_panel.rotate = 15 * 0.017453292519943295;
    ui_panel.translate.x = -84;
    ui_panel.translate.y = -50;
    
    
    // all ui element elements
    ui_panel.draw(ctx, 0, 0);
    <canvas width="480" height="360"></canvas>