Calculating offsets after square rotated from corner

冷暖自知 提交于 2019-12-05 09:27:13

问题


I am wanting to calculate the 4 offsets from the point of rotation when I rotate a square.

The axis of rotation is initially the top left of the square. When I perform a rotation I would like to know how far the shape will spead in all 4 directions (minX, minY, maxX, maxy).

I currently have the general math:

const rotation = .35  // radians = 20 degrees
const size = 50 // size of original square

const o1 = Math.round(size * Math.sin(rotation))
const o2 = Math.round(size * Math.cos(rotation))

Using these numbers I see how I can use them to create an array of offsets

const offsets = [o1, 0, o2, o1 + o2]

When I rotate my square from 20, 110, 200 and 290 degrees it will rotate around the axis marked by the black dot on image.

For each of the 4 rotations I have the offests array as well as the actual numbers that I desire. As you can see the numbers are sort of there but... I initially thought an array shift was all I needed but its more than that.

// 20 degrees
console.log(offsets) // [17, 0, 47, 64] 
// The dimensions I actually need
// minX: -17,
// minY: 0
// maxX: 47
// maxY: -64

// 110 degrees
console.log(offsets) // [47, 0, -17, 30] 
// The dimensions I actually need
// minX: -64,
// minY: -17,
// maxX: 0,
// maxY: 47

// 200 degrees
console.log(offsets) // [-17, 0, -47, -64] 
// The dimensions I actually need
// minX: -47,
// minY: -64,
// maxX: 17,
// maxY: 0

// 290 degrees
console.log(offsets) // [-47, 0, 17, -30] 
// The dimensions I actually need
// minX: 0,
// minY: -47,
// maxX: 64,
// maxY: 17

I can certainly shift the array if needed (say for every 90deg) but how can I get the correct numbers? I'm looking for the magic formula for any angle.


回答1:


Transforming points

The easiest way to do this is create a simple rotation matrix. This is just the direction of the x and y axis as vectors each with a length the size of a pixel (or unit whatever that may be) and the location of the origin.

To rotate a point

First define the point

var x = ?;  // the point to rotate
var y = ?;

Then the origin and rotation

const ox = ?; // location of origin
const oy = ?;
const rotation = ?; // in radians

From the rotation we calculate to vector that is the direction of the x axis

var xAxisX = Math.cos(rotation);
var xAxisY = Math.sin(rotation);

Optionally you could have a scale as well

const scale = ?;

that would change the length of the x and y axis so the x axis calculation is

var xAxisX = Math.cos(rotation) * scale;
var xAxisY = Math.sin(rotation) * scale;

No we can apply the rotation to the point. First move the point relative to the origin.

x -= ox;
y -= oy;

Then move the point x distance along the x axis

var rx = x * xAxisX;
var ry = x * xAxisY;

Then move y distance along the y axis. The y axis is at 90 deg clockwise from the x. To rotate any vector 90deg you swap the x and y and negate the new x. Thus moving along the y axis is as follows

rx -= y * xAxisY;  // use x axis y for y axis x and negate
ry += y * xAxisX;  // use x axis x for y axis y 

Now the point has been rotated but is still relative to the origin, we need to move it back to the world space. To do that just add the origin

rx += ox;
ry += oy;

And rx,ry is the rotated point around the origin, and scaled if you did that.

Match rotation in 2D context

You can get the 2D context to do the same for you

ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, ox, oy);
ctx.fillRect(x,y,1,1); // draw the rotated pixel
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default transform

Or you can add the rotation via a function call

ctx.setTransform(1, 0, 0, 1, ox, oy);
ctx.rotate(rotation);
// and if scale then 
// ctx.scale(scale,scale)
ctx.fillRect(x,y,1,1); // draw the rotated pixel
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default transform

The various steps above can be compacted, the next part of the answer rotates a rectangle using the above method.

Rotating a rectangle

The following function will return the 4 rotated corners.

// angle is the amount of rotation in radians
// ox,oy is the origin (center of rotation)
// x,y is the top left of the rectangle
// w,h is the width and height of the rectangle
// returns an array of points as arrays [[x,y],[x1,y1],...]
// Order of returned points topLeft, topRight, bottomRight, bottomLeft
function rotateRect(angle,ox,oy,x,y,w,h){
    const xAx = Math.cos(angle);  // x axis x
    const xAy = Math.sin(angle);  // x axis y
    x -= ox;  // move rectangle onto origin
    y -= oy; 
    return [[ // return array holding the resulting points
            x * xAx - y * xAy + ox,   // Get the top left rotated position
            x * xAy + y * xAx + oy,   // and move it back to the origin
        ], [
            (x + w) * xAx - y * xAy + ox,   // Get the top right rotated position
            (x + w) * xAy + y * xAx + oy,   
        ], [
            (x + w) * xAx - (y + h) * xAy + ox,   // Get the bottom right rotated position
            (x + w) * xAy + (y + h) * xAx + oy,   
        ], [
            x * xAx - (y + h) * xAy + ox,   // Get the bottom left rotated position
            x * xAy + (y + h) * xAx + oy,   
        ]
    ]; 
}

Finding the offsets

To use the function

var angle = 1;  // amount to rotate in radians
var ox = 0;   // origin top left of rectangle
var oy = 0; 
const rotatedRect = rotateRect(angle,ox,oy,0,0,50,50);
const r = rotatedRect; // alias to make following code more readable
var leftOfOrigin  = Math.min(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var rightOfOrigin = Math.max(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var aboveOrigin   = Math.min(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
var belowOrigin   = Math.max(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;

I keep the distance calcs outside the function as that is a little more useful as you may want more information about the rotated points.

DEMO

As an example

const ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 512;



// angle is the amount of rotation in radians
// ox,oy is the origin (center of rotation)
// x,y is the top left of the rectangle
// w,h is the width and height of the rectangle
// returns an array of points as arrays [[x,y],[x1,y1],...]
// Order of returned points topLeft, topRight, bottomRight, bottomLeft
function rotateRect(angle,ox,oy,x,y,w,h){
    const xAx = Math.cos(angle);  // x axis x
    const xAy = Math.sin(angle);  // x axis y
    x -= ox;  // move rectangle onto origin
    y -= oy; 
    return [[ // return array holding the resulting points
            x * xAx - y * xAy + ox,   // Get the top left rotated position
            x * xAy + y * xAx + oy,   // and move it back to the origin
        ], [
            (x + w) * xAx - y * xAy + ox,   // Get the top right rotated position
            (x + w) * xAy + y * xAx + oy,   
        ], [
            (x + w) * xAx - (y + h) * xAy + ox,   // Get the bottom right rotated position
            (x + w) * xAy + (y + h) * xAx + oy,   
        ], [
            x * xAx - (y + h) * xAy + ox,   // Get the bottom left rotated position
            x * xAy + (y + h) * xAx + oy,   
        ]
    ]; 
}
function drawRectangle(angle, ox, oy, rect){
    ctx.strokeStyle = "red";
    ctx.lineWidth = 2;
    ctx.setTransform(1,0,0,1,ox,oy);
    ctx.rotate(angle);
    ctx.strokeRect(rect.x - ox, rect.y - oy, rect.w, rect.h);
    ctx.setTransform(1,0,0,1,0,0); // restore transform to default
}
function drawBounds(rotatedRect){
    const r = rotatedRect; // alias to make following code more readable
    const left     = Math.min(r[0][0], r[1][0], r[2][0], r[3][0]);
    const right    = Math.max(r[0][0], r[1][0], r[2][0], r[3][0]);
    const top      = Math.min(r[0][1], r[1][1], r[2][1], r[3][1]);
    const bottom   = Math.max(r[0][1], r[1][1], r[2][1], r[3][1]);

    ctx.strokeStyle = "#999";
    ctx.lineWidth = 2;
    ctx.strokeRect(left, top, right - left, bottom - top);
}

function drawDistance(text,x,y,dist,direction,textOverflowDir){
    if(dist.toFixed(2) == 0) { return }
    function drawArrows(){
        ctx.strokeStyle = "blue";
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.lineTo(8,-12);
        ctx.lineTo(0,-7);
        ctx.lineTo(8,-2);
        ctx.moveTo(dist - 8, -12);
        ctx.lineTo(dist, -7);
        ctx.lineTo(dist - 8, -2);
        ctx.stroke();
    }
    
    
    ctx.setTransform(1,0,0,1,x,y);
    ctx.rotate(direction);
    const width = ctx.measureText(text).width;
    ctx.fillStyle = "blue";
    ctx.fillRect(-1, - 16, 2, 14);
    ctx.fillRect(dist -1,  - 16, 2, 14);
    if(width + 8 > dist){
        ctx.fillRect(1, -8, dist - 2, 2);
        drawArrows();
        ctx.fillStyle = "black";
        if(textOverflowDir < 0){
            ctx.fillText(text, - width / 2 - 4, - 9);
        }else{
            ctx.fillText(text,dist + width / 2 + 6, - 9);
        }
    }else{
        ctx.fillRect(-1,       - 8, (dist - width) / 2 - 4, 2);
        ctx.fillRect(dist - 1 - ((dist - width) / 2 - 4), - 8, (dist - width) / 2 - 4, 2);
        drawArrows();
        
        ctx.fillStyle = "black";
        ctx.fillText(text, dist / 2, - 9);
    }
    ctx.setTransform(1,0,0,1,0,0); //restore default transform
}
// set up the font
ctx.font = "16px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";

var angle = 3.2;  // amount to rotate in radians
var ox = 256;   // origin top left of rectangle
var oy = 256; 
const rect = {
    x : 256,
    y : 256,
    w : 164,
    h : 164,
}




function mainLoop(){
    ctx.clearRect(0,0,512,512);
    angle += 0.01; // slowly rotate 
    // draw origin 
    ctx.fillStyle = "#FA2";
    ctx.fillRect(ox-1,0,2,512);
    ctx.fillRect(0,oy-1,512,2);
    
    const rotatedRect = rotateRect(angle, ox, oy, rect.x, rect.y, rect.w, rect.h);
    drawBounds(rotatedRect);
    drawRectangle(angle, ox, oy, rect);
    
    const r = rotatedRect; // alias to make following code more readable
    var leftOfOrigin  = Math.min(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
    var rightOfOrigin = Math.max(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
    var aboveOrigin   = Math.min(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
    var belowOrigin   = Math.max(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
    
    // draw distances
    
    drawDistance(leftOfOrigin.toFixed(2), ox + leftOfOrigin, oy +aboveOrigin, - leftOfOrigin, 0, -1);
    drawDistance(rightOfOrigin.toFixed(2), ox, oy + aboveOrigin, rightOfOrigin, 0, 1);
    drawDistance(belowOrigin.toFixed(2), ox + leftOfOrigin, oy + belowOrigin,  belowOrigin, - Math.PI / 2, -1);
    drawDistance(aboveOrigin.toFixed(2), ox + leftOfOrigin, oy, - aboveOrigin, - Math.PI / 2, 1);

    requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>



回答2:


I gave this a try, not claiming it to be efficient or the best way, but I couldn't match your expected values. Either I did something wrong or your first set of expected values is incorrect?

'use strict';

const degToRad = deg => (deg * Math.PI) / 180;

const rotatePoint = (pivot, point, radians) => {
  const cosA = Math.cos(radians);
  const sinA = Math.sin(radians);
  const [x, y] = pivot;
  const difX = point[0] - x;
  const difY = point[1] - y;

  return [
    Math.round(((cosA * difX) - (sinA * difY)) + x),
    Math.round((sinA * difX) + (cosA * difY) + y),
  ];
};

const rotateSquare = (square, pivot, angle) => {
  const radians = degToRad(angle);
  return square.map(point => rotatePoint(pivot, point, radians));
};

const extents = (points, pivot) => points.reduce((acc, point) => {
  const [difX, difY] = point.map((value, index) => value - pivot[index]);
  return [
    Math.min(acc[0], difX),
    Math.min(acc[1], difY),
    Math.max(acc[2], difX),
    Math.max(acc[3], difY),
  ];
}, [0, 0, 0, 0]);

const createSquare = (x, y, size) => [
  [x, y],
  [x + size, y],
  [x + size, y + size],
  [x, y + size],
];

const pivot = [0, 0];
const square = createSquare(...pivot, 50);
const angles = [20, 110, 200, 290];
const rotations = angles.map(angle => rotateSquare(square, pivot, angle));
const offsets = rotations.map(rotation => extents(rotation, pivot));

const expecteds = [
  [-17, 0, 47, -64],
  [-64, -17, 0, 47],
  [-47, -64, 17, 0],
  [0, -47, 64, 17],
];

offsets.forEach((offset, index) => {
  const actual = JSON.stringify(offset);
  const expected = JSON.stringify(expecteds[index]);
  console.log(
    `Actual:${actual}`,
    `Expected:${expected}`,
    `Same:${actual === expected}`
  );
});


来源:https://stackoverflow.com/questions/45514816/calculating-offsets-after-square-rotated-from-corner

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!