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

Time:04-28

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

CodePudding user response:

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>

CodePudding user response:

Move along transformed axis

To move along a rotated axis. 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

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>

CodePudding user response:

The other answers are nice and elegant, but you said you wanted to keep rotateCanvas and drawRotateRectangle around, right?

We can imitate the transform in your rotateCanvas function using some matrix transformations on a DOMPoint:

  const x = 60;
  const y = 30;
  const h = 30;
  const a = 0.5;
  const tx = rWidth / 2;
  const ty = h / 2;

  // (0, 0) is the upper left of the rectangle
  const pt = new DOMPoint(0, 0);
  // Create transformation matrix based on center of rectangle
  const mtr = new DOMMatrix()
    .translateSelf(-tx, -ty)
    // Important: rotateSelf takes degrees, not radians 
    .rotateSelf(a * 180 / Math.PI)
    .translateSelf(tx, ty);
  // Apply the transform
  const tPt = pt.matrixTransform(mtr);

  // Now the new call looks like this:
  drawRotateRectangle(tPt.x   x, tPt.y   y, rWidth, h, a);

Here's a demo. I drew an extra red dot to show that you can control where you want to start drawing the rectangle using x and y.

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 = false;

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

  const rWidth = which ? 100 : 200;

  const x = 30;
  const y = 30;
  const h = 30;
  const a = Math.PI / 3;
  const tx = rWidth / 2;
  const ty = h / 2;
  
  // Imitate the transform you use to calculate the offset point
  const pt = new DOMPoint(0, 0);
  const mtr = new DOMMatrix()
    .translateSelf(-tx, -ty)
    .rotateSelf(a * 180 / Math.PI)
    .translateSelf(tx, ty);
  const tPt = pt.matrixTransform(mtr);
  drawRotateRectangle(tPt.x   x, tPt.y   y, rWidth, h, a);

  // Just to show you can control the start point
  ctx.fillStyle = 'red';
  ctx.fillRect(x, y, 3, 3);
  ctx.fillStyle = 'black';

  which = !which;
}

render();

setInterval(render, 1000);
<canvas style="border: 1px solid black;"></canvas>

  • Related