问题
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