Search code examples
javascriptmathgeometryhtml5-canvastrigonometry

How to prevent a rotated rectangle from moving when adjusting its width?


I have a rectangle rotated around its center, and I'm trying to make it wider. However, because it's rotated around its center, making it wider also visually "moves" the rectangle.

What I'm looking for is a way to make it wider and then adjust its x and y components accordingly in order to make it visually not move. I have created a reproduction of the problem here, so you can see the issue:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

function rotateCanvas(x, y, a) {
  ctx.translate(x, y);
  ctx.rotate(a);
  ctx.translate(-x, -y);
}

function drawRotateRectangle(x, y, w, h, a) {
  rotateCanvas(x + w / 2, y + h / 2, a);
  ctx.fillRect(x, y, w, h);
  rotateCanvas(x + w / 2, y + h / 2, -a);
}

let which = true;

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const rWidth = which ? 100 : 200;

  drawRotateRectangle(30, 30, rWidth, 30, 0.5);

  which = !which;
}

render();

setInterval(render, 1000);
<canvas></canvas>

As you can see, when it gets wider, it also moves upwards. Effectively, I want the top left of the rectangle to be at the same point before and after the resize occurs.

However, I also want it to still be rotated around its center. That is, I am looking for how much I have to move the rectangle (change its x and y coordinates), to effectively anchor it to the same location, before and after the resize occurs.

Is this possible? If so, how would you calculate the value you have to modify the x and the y of the rectangle by in order to visually keep it locked to its current position?

I recognize this issue would not occur if I instead rotated the rectangle around its top left as opposed to its center, but for the purposes of my application, the rectangle has to be rotated around its center, so I am instead looking for how much I have to modify its x and y components in order to maintain its visual position.

As such, the answer to this question should not involve modifying the rotateCanvas or drawRotateRectangle functions in any way.

An example of the effect I'm looking for: enter image description here


Solution

  • To move along a rotated axis. The rotation as 'r' and the distance as 'd'. Assumes that the transform is uniform (ie the x, y scale are the same on this case 1 and there is no skew)

    Example to move dx along x axis and dy along y axis

    var r = ?; // rotation in radians
    var x = ?, y = ?;  // in pixels. coordinate to move
    var dx = ?; // distance in pixels along x axis
    var dy = ?; // distance in pixels along y axis
    
    const ax = Math.cos(r);
    const ay = Math.sin(r);
    x += dx * ax - dy * ay
    y += dx * ay + dy * ax;
    

    Demo

    Expanding a rectangle. The function rect.expand(left, right, top, bottom) changes the size of rectangle adjusting the origin to keep unchanged edges where the are.

    const ctx = canvas.getContext("2d");
    requestAnimationFrame(renderLoop);
    const W = canvas.width, H = canvas.height;
    var expanding = 1, expandCount = 0; 
    const rect = {
        x: W / 2, 
        y: H / 2,
        r: 0,  
        w: 100,
        h: 50,
        expand(left, right, top, bottom) {
            const ax = Math.cos(this.r) * 0.5;
            const ay = Math.sin(this.r) * 0.5;
            this.w += left + right;
            this.h += top + bottom;
            this.x += -left * ax + right * ax + top * ay - bottom * ay;
            this.y += -left * ay + right * ay - top * ax + bottom * ax;
        },
        draw() {
            const ax = Math.cos(this.r);
            const ay = Math.sin(this.r);
            ctx.setTransform(ax, ay, -ay, ax, this.x, this.y);
            ctx.strokeRect(-this.w * 0.5, -this.h * 0.5, this.w, this.h);
        }
    };
    
    function renderLoop() {
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0, 0, W, H);
        if (expanding === 1) {
            expandCount += 1;
            expandCount === 50 && (expanding = -1);
        } else if (expanding === -1) {
            expandCount -= 1;
            expandCount === 0 && (expanding = 1);
        }
        
        rect.r += 0.01;
        rect.expand(0, expanding, 0, 0);
    
        rect.draw();
        requestAnimationFrame(renderLoop);
    }
    
    
     
    canvas {
        border: 1px solid black;
    }
       <canvas id = "canvas" width="200" height="200"></canvas>