Calculate corners of rotated rectangle in container - javascript

I'm having some issues calculating the corners of a rotated rectangle within a rotated container with both having offset x/y co-ords.
The pivot is off but I'm not sure of the solution. The following scenarios work:
(x, y, rotation)
image = 0, 0, 45
container = 100, 100, 45
image = 200, 0, 45
container = 100, 100, 0
however setting the rotation of the container, and the image co-ords messes up the pivot e.g.
image = 200, 0, 45
container = 100, 100, 45
Below is the code for calculating the corners of the image in global co-ordinate space:
public get corners() {
const worldData = this.worldData;
//Get angle of object in radians;
const radAngle = worldData.rotation * Math.PI / 180;
const pivotX = worldData.pivotX;
const pivotY = worldData.pivotY;
const width = this.sourceWidth * worldData.scaleX;
const height = this.sourceHeight * worldData.scaleY;
const x = worldData.x;//this.x;
const y = worldData.y;//this.y;
//Get the corners
const c1 = this.getCorner(pivotX, pivotY, x, y, radAngle);
const c2 = this.getCorner(pivotX, pivotY, x + width, y, radAngle);
const c3 = this.getCorner(pivotX, pivotY, x + width, y + height, radAngle);
const c4 = this.getCorner(pivotX, pivotY, x, y + height, radAngle);
return {c1, c2, c3, c4};
}
public get worldData() {
let x = this.x;
let y = this.y;
let pivotX = this.x;
let pivotY = this.y;
let rotation = this.rotation;
let scaleX = this.scaleX;
let scaleY = this.scaleY;
let parent = this.parent;
while(parent) {
x += parent.x;
y += parent.y;
pivotX += parent.x;
pivotY += parent.y;
rotation += parent.rotation;
scaleX *= parent.scaleX;
scaleY *= parent.scaleY;
parent = parent.parent;
}
return {x, y, scaleX, scaleY, rotation, pivotX, pivotY}
}
protected getCorner(pivotX:number, pivotY:number, cornerX:number, cornerY:number, angle:number) {
let x, y, distance, diffX, diffY;
/// get distance from center to point
diffX = cornerX - pivotX;
diffY = cornerY - pivotY;
distance = Math.sqrt(diffX * diffX + diffY * diffY);
/// find angle from pivot to corner
angle += Math.atan2(diffY, diffX);
/// get new x and y and round it off to integer
x = pivotX + distance * Math.cos(angle);
y = pivotY + distance * Math.sin(angle);
return {x, y};
}

Let's suppose that the scenario is as follows:
where the lower left corner of the image (solid line) has coordinates (x_i, y_i) and the lower left corner of the container (dashed line) has coordinates (X_c, Y_c). Moreover, the image (of width w and height h) is rotated counter-clockwise by angle beta with respect to the laboratory frame, while the container itself is rotated (also counter-clockwise) by angle alpha.
Now, let's focus for example on the upper-right corner P. With respect to the laboratory frame (global canvas), its coordinates can be expressed as:
R(beta) . ( w, h ) + ( x_i, y_i )
where . denotes matrix multiplication, and R is a counter-clockwise rotation matrix
R(beta) = [ cos(beta) -sin(beta) ]
[ sin(beta) cos(beta) ]
Now, we need to transform this into a coordinate frame with respect to the container. Formally, this means that we need first to subtract the offset and then to rotate by -alpha (or alpha clock-wise). Thus with everything together:
R(-alpha).( R(beta) . (w, h) + (x_i, y_i) - (X_c, Y_c) )
The other corners can be handled similarly, just by replacing (w, h) with the proper coordinates...
In terms of code, one might implement these formulae as:
//counter-clock-wise rotation by given angle in degrees
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}
//shift by a multiple fac of an offset {xref, yref}
function offsetBy(fac, {x:xref, y:yref}, {x, y}) {
return {
x: fac*xref + x,
y: fac*yref + y
};
}
const image = {
coords: {x: 200, y: 0}, //lab-frame coordinates
angle: 45, //lab-frame rotation angle
width: 50,
height: 10
};
const container = {
coords: {x: 100, y: 100}, //lab-frame coordinates
angle: 45 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-right corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
EDIT:
In case the y-axis is supposed to point "downwards", the formulas above work as well, one just needs to interpret the angles as clock-wise instead of counter-clockwise (so in principle the function rotateCCWBy should be renamed to rotateCWBy). As an example, let's consider this scenario:
Here, the top-left corner of the container is located at position (2,1) and the container itself is rotated by 15 degrees. The image (black rectangle) of width 4 and height 2 is rotated by 30 degrees and its top-left corner is located at position (3, 3). Now, we want to calculate the coordinates (x, y) of point P with respect to the container.
Using:
const image = {
coords: {x: 3, y: 3}, //lab-frame coordinates
angle: 30, //lab-frame rotation angle
width: 4,
height: 2
};
const container = {
coords: {x: 2, y: 1}, //lab-frame coordinates
angle: 15 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-left corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
yields
{ x: 4.8296291314453415, y: 4.640160440463835 }
which can be (approximately) visually verified from the attached figure.
EDIT2:
After additional clarification, the coordinates of the image are not supposed to be "lab-frame" (i.e., with respect to the canvas), but with respect to the already rotated container. Thus the transformation needs to be adapted as:
const corner =
offsetBy(
+1, container.coords,
rotateCCWBy(container.angle,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}

Related

Calculate Z Rotation of a face in Tensorflow.js

Note: This question has NOTHING to do with Three.js, it's only Tensorflow.js and Trigonometry.
I am trying to rotate a 3D object in Three.js by rotating my face. I have used this code by akhirai560 for rotating in X and Y axis.:
function normal(vec) {
let norm = 0;
for (const v of vec) {
norm += v * v;
}
return Math.sqrt(norm);
}
function getHeadAnglesCos(keypoints) {
// Vertical (Y-Axis) Rotation
const faceVerticalCentralPoint = [
0,
(keypoints[10][1] + keypoints[152][1]) * 0.5,
(keypoints[10][2] + keypoints[152][2]) * 0.5,
];
const verticalAdjacent = keypoints[10][2] - faceVerticalCentralPoint[2];
const verticalOpposite = keypoints[10][1] - faceVerticalCentralPoint[1];
const verticalHypotenuse = normal([verticalAdjacent, verticalOpposite]);
const verticalCos = verticalAdjacent / verticalHypotenuse;
// Horizontal (X-Axis) Rotation
const faceHorizontalCentralPoint = [
(keypoints[226][0] + keypoints[446][0]) * 0.5,
0,
(keypoints[226][2] + keypoints[446][2]) * 0.5,
];
const horizontalAdjacent = keypoints[226][2] - faceHorizontalCentralPoint[2];
const horizontalOpposite = keypoints[226][0] - faceHorizontalCentralPoint[0];
const horizontalHypotenuse = normal([horizontalAdjacent, horizontalOpposite]);
const horizontalCos = horizontalAdjacent / horizontalHypotenuse;
return [horizontalCos, verticalCos];
}
It calculates the rotation by finding the cos of these points (original image source):
I also want to calculate the cos of Z axis rotation. Thanks!
Am not fully clear of the end goal of the question, but if simply looking to rotate about the Z axis, the following snippet will assist.
center = { x: 50, y: 50 };
points = [
{ x: 60, y: 60 },
{ x: 50, y: 100 }
]
function rotateAboutZ( xyRotationPoint, points, zRotation ) {
let result = points.map( p => {
// Adjust the point based on the center of rotation.
let x = p.x - xyRotationPoint.x;
let y = p.y - xyRotationPoint.y;
// Calculate the radius and angle in preparation for rotation
// about the Z axis.
let radius = Math.sqrt( x * x + y * y );
let angle = Math.atan2( y, x );
// Adjust the angle by the requested rotation.
let angleRotated = angle + zRotation;
// Finally, calculate the new XY coordinates, and re-adjust
// based on the center of rotation.
let xRotated = radius * Math.cos( angleRotated ) + xyRotationPoint.x;
let yRotated = radius * Math.sin( angleRotated ) + xyRotationPoint.y;
return { x: xRotated, y: yRotated };
} );
return result;
}
// Let's rotate the points by 180 degrees around the
// center point of (50,50).
let result = rotateAboutZ( center, points, Math.PI );
console.log( result );

How to get random point near edges of a square in javascript

I want to make a function that gives me a random point near the edges of a rectangle from a point. This is what I came up with so far, but I have absolutely no idea why it is not working.
function Point(x, y) {
this.x = x;
this.y = y;
}
function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomPointNearEdges(rectPos, width, height, border) {
var point = new Point(rectPos.x, rectPos.y);
if (randomNumber(0, 1) == 0) {
point.x = randomNumber(rectPos.x, rectPos.x + border);
if (randomNumber(0, 1) == 0) {
point.y = randomNumber(rectPos.y, rectPos.y + border);
}
else {
point.y = randomNumber(rectPos.y + height, (rectPos.y + height) + border);
}
}
else {
point.y = randomNumber(rectPos.y, rectPos.y + border);
if (randomNumber(0, 1) == 0) {
point.y = randomNumber(rectPos.x, rectPos.x + border);
}
else {
point.y = randomNumber(rectPos.x + height, (rectPos.x + width) + border);
}
}
return point;
};
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = 700;
canvas.height = 700;
var ctx = canvas.getContext("2d");
ctx.strokeRect(130, 130, 500, 500);
for (var i = 0; i < 30; i++) {
var point = getRandomPointNearEdges(new Point(130, 130), 500, 500, 100);
ctx.fillRect(point.x, point.y, 2, 2);
}
};
<canvas id="canvas"></canvas>
Just to clarify, the black region in this 'Not to scale' diagram is where I want to allow the point to generate. The width / height of that black region is the border property in the code snippet.
Why is my function not working?
Random with even distribution.
Just to point out that the answer by SimpleJ is statistical flawed with the distribution of random locations having a bias to the corners and then to the shorter sides, even though they cover much less area.
The ideal random location should be spread equally over the area in question, if the height of the box is less than the width then there is less chance of the the sides getting a point.
The example below provides a much faster and a much better distribution. I have added the given answers solution as well so you can compare.
The function that gets a random pos. The arguments x,y top left inside edge of rectangle, w,h inside width and height of the rectangle minDist, maxDist the min and max dist the random point can be from the inside edge of the box. You can also use negative values have the points outside the rectangle. Note that the distances are always from the inside edge of the box. The values are also floored when return (can easily be remove and still works)
function randomPointNearRect(x, y, w, h, minDist, maxDist) {
const dist = (Math.random() * (maxDist - minDist) + minDist) | 0;
x += dist;
y += dist;
w -= dist * 2
h -= dist * 2
if (Math.random() < w / (w + h)) { // top bottom
x = Math.random() * w + x;
y = Math.random() < 0.5 ? y : y + h -1;
} else {
y = Math.random() * h + y;
x = Math.random() < 0.5 ? x: x + w -1;
}
return [x | 0, y | 0];
}
Note there is a slight bias to the inside of the box. It can be removed with a little calculus with the bias rate of change f'(x) = 8*x 8 pixels per pixel inward and the anti derivative f(x)=4*(x**2) + c would directly relate to the distribution. Where x is dist from edge and c is related to perimeter length
Example to compare
The example has two canvases. Many random points are drawn. click the top canvas to add more points. Note how the bottom canvas sides and corners get darker due to the bias of the random points.
const ctx = canvas.getContext("2d");
canvas.onclick = ()=>{
getRandomPointsForBox(200, box,4, 18);
getRandomPoints(200);
}
const edgeClear = 30;
var box = {
x: edgeClear,
y: edgeClear,
w: canvas.width - edgeClear * 2,
h: canvas.height - edgeClear * 2,
edge: 4,
}
function drawBox(box) {
ctx.fillRect(box.x, box.y, box.w, box.h);
ctx.clearRect(box.x + box.edge, box.y + box.edge, box.w - box.edge * 2, box.h - box.edge * 2);
}
function drawPixel(x, y) {
ctx.fillRect(x, y, 1, 1);
}
function getRandomPointsForBox(count, box, min, max) {
min += box.edge;
max += box.edge;
while (count--) {
const [x, y] = randomPointNearRect(box.x, box.y, box.w, box.h, min, max);
drawPixel(x, y);
}
}
drawBox(box);
getRandomPointsForBox(200, box,4, 18);
ctx.font = "18px arial"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("Click to add more random points.",canvas.width / 2, canvas.height / 2);
function randomPointNearRect(x, y, w, h, minDist, maxDist) {
const dist = (Math.random() * (maxDist - minDist) + minDist) | 0;
x += dist;
y += dist;
w -= dist * 2
h -= dist * 2
if (Math.random() < w / (w + h)) { // top bottom
x = Math.random() * w + x;
y = Math.random() < 0.5 ? y : y + h -1;
} else {
y = Math.random() * h + y;
x = Math.random() < 0.5 ? x: x + w -1;
}
return [x | 0, y | 0];
}
/* The following is from the answer provided by SimpleJ https://stackoverflow.com/a/49581326/3877726 */
const ctx1 = canvas1.getContext('2d');
const rect = {
x: box.x, y: box.y,
width: box.w, height: box.h,
};
drawRect(rect);
ctx1.font = "18px arial"
ctx1.textAlign = "center"
ctx1.textBaseline = "middle"
ctx1.fillText("SimpleJ's method.",canvas1.width / 2, canvas1.height / 2);
ctx1.fillText("Note density of sides and corners.",canvas1.width / 2, canvas1.height / 2 + 20);
function getRandomPoints(count) {
while (count--) {
drawPoint(randomPointInRect(sample(rects)));
}
}
var rects = getBorderRects(rect, 10);
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
function drawRect({x, y, width, height}) {
ctx1.strokeRect(x, y, width, height);
}
function drawPoint({x, y}) {
ctx1.fillRect(x, y, 1,1);
}
getRandomPoints(200);
<canvas id="canvas" width="500" height="200"></canvas>
<canvas id="canvas1" width="500" height="200"></canvas>
If you think about the problem of getting a random point near an edge as getting a random point in one of four edge rectangles, this problem becomes much easier to break down:
Get edge rectangles.
Pick a random edge rectangle.
Generate a random point in the edge rectangle.
To generate edge rectangles, we need a max distance (how far from the edge can the point be?):
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
To pick a random rectangle from our array of edge rectangles, we can define a sample function:
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
Then to pick a random point in a rectangle, we just need some Math.random:
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
And putting everything together:
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const rect = {
x: 10, y: 20,
width: 300, height: 200,
};
drawRect(rect);
drawPoint(
randomPointInRect(
sample(
getBorderRects(rect, 10)
)
)
);
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
function drawRect({x, y, width, height}) {
context.strokeRect(x, y, width, height);
}
function drawPoint({x, y}) {
context.arc(x, y, 1, 0, Math.PI * 2);
context.fill();
}
<canvas width="500" height="500"/>
For anybody here like me, looking for a short, simple solution, this post is closest I found that is not talking trigonometry .. An while what I came up with might not directly be a solution to OPs problem, maybe someone will find this useful..
The approach is fairly simple.
Math.random() a number between 0 & 800. Make use of modulus and divide what's left by 200 to get a random side and axis point. Push the random side all the way, assign the random value to the other axis and yeah, that's about it .. here's an ex:
let rndm = Math.floor(Math.random()*800-1);
let offset = rndm % 200;
let side = (rndm - offset) / 200; // 0:top 1:right 2:btm 3:left
let y = side % 2 > 0 ? offset+1 : 100 * side ;
let x = side % 2 < 1 ? offset+1 : 100 * (side - 1) ;
point.y = y - 100;
point.x = x - 100;
In my case, I needed both negative and positive values with an origin point.
And if you want to spawn a point inside a border, just do another random number spanning the width of the border.
Just remember to adjust the corners.
offset += rndmBorder * 2; // creates an inward line in the corners
point.x = x - 100 + rndmBorder; // still keeping the origin point nice and center
_____________
|\_________/| <-// inward line
| | | |
| | | |
All I was in need for is to offset some letters .. and most of what I found seemed like overkill .. This actually works fairly well, hope it helps.

Finding the edge of a rectange from a vector and angle inside

In the example fiddle: https://jsfiddle.net/krazyjakee/uazy86m4/ you can drag the mouse around underneath the vector point shown as a blue square. You will see the line draw a path from the vector, through the mouse and to the edge of the viewport where a green square is shown, indicating it has found the edge. However, above the vector, the green square disappears as it fails to detect the edge of the viewport.
Here is the current logic I am using to detect the edge.
const angle = Math.atan2(mouse.y - vectorCenter.y, mouse.x - vectorCenter.x);
const cosAngle = Math.abs(Math.cos(angle));
const sinAngle = Math.abs(Math.sin(angle));
const vx = (viewport.width - vectorCenter.x) * sinAngle;
const vy = (viewport.height - vectorCenter.y) * cosAngle;
const vpMagnitude = vx <= vy ?
(viewport.width - vectorCenter.x) / cosAngle :
(viewport.height - vectorCenter.y) / sinAngle;
const viewportX = vectorCenter.x + Math.cos(angle) * vpMagnitude;
const viewportY = vectorCenter.y + Math.sin(angle) * vpMagnitude;
const viewPortEdge = {
x: viewportX,
y: viewportY,
};
Please help me figure out how to correctly detect the position in the top edge of the viewport.
I didn't look into why exactly this fails for the top because there's an easier approach to this than dealing with angles. You can get the position by some simple vector calculations.
First, for the sake of explicitness and to prevent hardcoding any values into the computation I've extended your viewport
const viewport = {
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight,
get left(){ return this.x },
get right(){ return this.x + this.width },
get top(){ return this.y },
get bottom(){ return this.y + this.height },
};
now the calculation:
//prevent division by 0
const notZero = v => +v || Number.MIN_VALUE;
let vx = mouse.x - vectorCenter.x;
let vy = mouse.y - vectorCenter.y;
//that's why I've extended the viewport, so I don't have to hardcode any values here
//Math.min() to check wich border I hit first, X or Y
let t = Math.min(
((vx<0? viewport.left: viewport.right) - vectorCenter.x) / notZero(vx),
((vy<0? viewport.top: viewport.bottom) - vectorCenter.y) / notZero(vy)
);
const viewPortEdge = {
x: vectorCenter.x + vx * t,
y: vectorCenter.y + vy * t,
};
so t is the factor by wich I have to scale the vector between the mouse and the vectorCenter to hit the closest edge in that particular direction.

Calculating offsets after square rotated from corner

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.
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>
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}`
);
});

finding angles 0-360

Need help with a math issue:
i need to get the true angle from 0 degrees using x and y cordinates
im using this at the moment:
Math.atan((x2-x1)/(y1-y2))/(Math.PI/180)
but /(Math.PI/180) limits results from -90 to 90
i need 0-360
note: I'm using the angle to indicate direction:
0=up
90=right
135=45 degree right+down
180=down
270=left
etc
The atan function only gives half the unit circle between -pi/2 and +pi/2 (0 on x axis), there is another library function that can give the whole unit circle between -pi and + pi, atan2
I would think you are better of using atan2 to get the right quadrant rather than branching yourself, then just scale as you have been, something like
Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI + 180
The multiply by 180 over pi is just the scale from radians to degrees as in the question (but with the division by a division simplified), the +180 makes sure its always positive i.e. 0-360 deg rather than -180 to 180 deg
Math.atan limits you to the two rightmost quadrants on the unit circle. To get the full 0-360 degrees:
if x < 0 add 180 to the angle
else if y < 0 add 360 to the angle.
Your coordinate system is rotated and inverted compared to mine (and compared to convention). Positive x is to the right, positive y is up. 0 degrees is to the right (x>0, y=0, 90 degrees is up (x=0,y>0) 135 degrees is up and to the left (y>0, x=-y), etc. Where are your x- and y-axes pointing?
The answers of #jilles de wit and #jk. led me on the right path but for some reason did not provide the right solution for my problem that i think is very similar to the original question.
I wanted to get up = 0°, right = 90°, down = 180°, left = 270° as in aeronautical navigation systems.
Presuming the question was referring to canvas drawing i reached this solution:
I first translated the canvas origin using ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2). I also halved e.offsetX and e.offsedY i got from a mouse event on the canvas to get x and y with the same coordinate system as the canvas.
let radianAngle = Math.atan2(y, x); // x has the range [-canvas.width/2 ... +canvas.width/2], y is similar
let northUpAngle = radianAngle * 180 / PI + 90; // convert to degrees and add 90 to shift the angle counterclockwise from it's default "left" = 0°
if (x < 0 && y < 0) { // check for the top left quadrant
northUpAngle += 360; // add 360 to convert the range of the quadrant from [-90...0] to [270...360] (actual ranges may vary due to the way atan2 handles quadrant boundaries)
}
northUpAngle.toFixed(2) // to avoid getting 360° near the "up" position
There might be a more concise solution using the modulo operation but i could't find it.
Also note:
if (y1==y2) {
if (x1>x2)
angle = 90;
else if (x1<x2)
angle = 270;
else
angle = 0;
}
This should do the trick:
If y2
If < 0, add 360.
Examples:
(x1,y1) = 0
(x2,y2) = (-1,1), atan() = -45, [add 360], 270
(x2,y2) = (1,1), atan() = 45
(x2,y2) = (1,-1), atan() = -45, [add 180], 135
(x2 ,y2) = (-1,-1), atan() = 45, [add 180], 225
angle = Math.atan(this.k) * 180 / Math.PI;
angle = 180 - (angle < 0 ? 180 + angle : angle);
angle = p2.Y > p1.Y || (p2.Y == p1.Y && p2.X > p1.X) ? 180 + angle : angle;
Here's two solutions, one with Math.atan (which takes a FRACTION opposite/adjacent) and one with Math.atan2 (which takes TWO ARGUMENTS)
solutions are written in ES6 (ES2015+) syntax, ironically, because the question predates this javascript.
note that 'to the right' is 0° (=0 Radians); up is 90° (= PI/2); left is 180° (PI), and down is 270° (PI*1.5)
angleGivenCoords(coord1,coord2) {
// given two coords {x,y}, calculate the angle in radians with
// right being 0° (0 radians)
if (coord1.x === coord2.x) return (coord1.y > coord2.y ? Math.PI * 0.5 : Math.PI * 1.5)
if (coord1.y === coord2.y) return (coord1.x > coord2.x ? Math.PI : 0 )
let opposite = coord2.x - coord1.x
let adjacent = coord1.y - coord2.y
let adjustor = ((coord2.x < coord1.x && coord2.y < coord1.y) || (coord2.x < coord1.x && coord2.y > coord1.y)) ? Math.PI : 0
let res = Math.atan(opposite/adjacent) + adjustor
if (res < 0) { res = res + Math.PI*2 }
return res ;
}
now with Math.atan2. notice that with this solution the guard clauses at the top (coord1.x === coord2.x, (coord1.y === coord2.y)) are unneeded
angleGivenCoords(coord1,coord2) {
// given two coords {x,y}, calculate the angle in radians with
// left being 0° (0 radians)
let opposite = coord2.x - coord1.x
let adjacent = coord1.y - coord2.y
let res = Math.atan2(adjacent, opposite)
if (res < 0) { res = res + Math.PI*2 }
return res ;
}
(I tried to keep the 'opposite' and 'adjacent' variable names in deference to the Trigonometry)
please note that here is my test suite written in Jest. Also note my function above returns radians and my code (not shown here) has a simple Trig.degreesToRadians() as you would expect
it('for right 0°', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 600, y: 500}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(0))
})
it('for up-right 45°', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 600, y: 400}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(45))
})
it('for 90° up', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 500, y: 400}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(90))
})
it('for 135° up to left', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 400, y: 400}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(135))
})
it('for 180° to left', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 400, y: 500}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(180))
})
it('for 225° to to bottom left', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 400, y: 600}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(225))
})
it('for 270° to the bottom', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 500, y: 600}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(270))
})
it('for 315° to the bottom', () => {
let coord1 = {x: 500, y: 500},
coord2 = {x: 600, y: 600}
expect(Trig.angleGivenCoords(coord1,coord2)).toEqual(Trig.degreesToRadians(315))
})
For 0=up,90=right,180=down,270=left etc (x=x2-x1,y=y2-y1)
you can use the formula:
f(x,y)=180-90*(1+sign(y))* (1-sign(x^2))-45*(2+sign(y))*sign(x)
-(180/pi())*sign(x*y)*atan((abs(y)-abs(x))/(abs(y)+abs(x)))
so with a single decesion (i hope this ugly basiclike notation explains it):
IF x = 0 THEN
360degree = 270 - (SIGN(x) + 1) * 90
ELSE
360degree = MOD(180 + (SIGN(y) + 1) * 90 + ATAN(x/y) , 360)
ENDIF
to draw a full circle from north 0degree to 360degree clockwise:
x=SIN(0to360) y=COS(0to360)
cheers, Lev
function angle(x1,y1,x2,y2)
{
eangle = Math.atan((x2-x1)/(y1-y2))/(Math.PI/180)
if ( angle > 0 )
{
if (y1 < y2)
return angle;
else
return 180 + angle;
} else {
if (x1 < x2)
return 180 + angle;
else
return 360 + angle;
}
}

Categories