从角落旋转方形后计算偏移量

时间:2017-08-04 20:13:07

标签: javascript rotation trigonometry

我想在旋转正方形时计算旋转点的4个偏移量。

旋转轴最初位于正方形的左上角。当我执行旋转时,我想知道形状在所有4个方向上的长度(minX,minY,maxX,maxy)。

我目前有一般数学:

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))

使用这些数字,我看到如何使用它们来创建偏移数组

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

当我将方块从20度,110度,200度和290度旋转时,它将围绕图像上黑点标记的轴旋转。

rotated square

对于4轮旋转中的每一轮,我都有offests数组以及我想要的实际数字。正如你所看到的那样,数字有点......但是我最初认为阵列转换是我所需要的,但不仅如此。

// 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

如果需要,我当然可以移动阵列(例如每90度),但我怎样才能获得正确的数字?我正在为任何角度寻找神奇的公式。

2 个答案:

答案 0 :(得分:2)

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>

答案 1 :(得分:0)

我试了一下,并没有声称它是高效或最好的方式,但我无法与你的预期值相匹配。要么我做错了,要么你的第一组预期值不正确?

&#13;
&#13;
'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}`
  );
});
&#13;
&#13;
&#13;