I'm working on a library for canvas, and I can't figure out how I should be doing rotations around the center of an object. Below is the function I'm using to render them currently and an example object I'm giving it.
I feel there is a better way to do the 4 if statements, but I can't figure out the math for it. Currently I am taking each "pixel" of the object and rotating it around the center, but I can't see room for expansion. What am I doing wrong?
//Rendering function
Display.prototype.renderObject = function(object, direction) {
if (typeof object.rotation !== 'number') object.rotation = 0;
for (x=0;x<object.bounds.x;x++) {
for (y=0;y<object.bounds.y;y++) {
rotation = 45;
if (x==0 && y==0) rotation += 0;
if (x==0 && y==1) rotation += 90;
if (x==1 && y==0) rotation += 270;
if (x==1 && y==1) rotation += 180;
display.drawRect(object.color[x][y],
(display.width/2) - (players[playerIndex].position.x * 16) + (object.position.x * 16) - (object.bounds.x * object.scale)/4 - (object.bounds.x/3 * object.scale * Math.cos((object.rotation+rotation)*(Math.PI/180))),
(display.height/2) + (players[playerIndex].position.y * 16) - (object.position.y * 16) - (object.bounds.y * object.scale)/4 - (object.bounds.y/3 * object.scale * Math.sin((object.rotation+rotation)*(Math.PI/180))),
object.scale, object.scale, object.rotation * (direction || 1));
}
}
};
// Example object
block = {
"color": [
["#FFF","#CCC"], // Some colors to make
["#999","#666"] // a shaded illusion
],
"position": {
"x": 0,
"y": 0
},
"bounds": {
"x": 2, // color[0].length
"y": 2 // color.length
},
"rotation": 0, // 0 to 360
"scale": 4 // real pixels per object pixel
}
// Example usage
Display.renderObject(block);
- edit -
Maybe I need to have it calculate where each pixel's center coordinates would be, then get the distance from that to the origin of the object and the rotation each pixel would be offset at.
If x = 0 and y = 0, then it's +45 degrees with sin and -45 degrees with cos+45 degrees with sin. If (object.bounds.x-1)/2 gives us the center coords for dealing with x, then Math.sqrt(Math.pow(x,2)+Math.pow(y,2)) gives us the radius for the color block from the center of the object. I'm not sure where to go from there.
I am not 100% sure of what you're asking, but if it is how to rotate points around the points' center you have to translate all your points so that you object to rotate is centered around origin or point [0, 0] if you like.
A rotation matrix always rotates around origin so there is no way around (no pun intended).
So before you rotate your points calculate a delta value that you apply before the rotation, rotate and then reverse the delta and apply again to get the object back with its offset relative to origin.
So first step is to calculate the delta to find out how much you need to translate the points to get them into center.
For a rectangle it is easy: it is its x and y position plus half of its width and height:
deltaX = x + width * 0.5;
deltaY = y + height * 0.5;
This value you then need to subtract to the points. How you apply this delta is up to you: you can do it in advance with each point or inject it directly into the formula.
Then rotate and when done you simply apply add the delta value to each point again.
Here is a shorter version of a rotation matrix based on my article here:
var a = rotation * Math.PI / 180; /// calculate angle once
x -= deltaX; /// apply pre-translation
y -= deltaY;
/// rotate and apply post-translation
nx = x * Math.cos(a) + y * Math.sin(a) + deltaX;
ny = y * -Math.sin(a) + x * Math.cos(a) + deltaY;
The ifs you're using seem to come from code that use this for optimizations of the angles; when you use 0, 90 etc. as cos and sin would produce 1 and 0 respectively. You are however not utilizing this optimization as you could simply swap x for y etc. when these cases occurs. However, you bind then to x and y position so I am not sure what you try to achieve here..
I would suggest you just remove them as you calculate and applies sin/cos anyways - you could instead cache the sin and cos and use those for all the points:
var angle = rotation * Math.PI / 180;
sin = Math.sin(angle),
cos = Math.cos(angle),
...calc deltas here...
...enter loop here, then for each point:...
x -= deltaX;
y -= deltaY;
nx = x * cos + y * sin + deltaX;
ny = y * -sin + x * cos + deltaY;
Now the cost is lower for each point and your calculations will perform faster.
Hope this helps.
Related
I want to get the percent radius of an arc (circle).
The idea is, on click event on the canvas, find the element that collide with click point. I did it, there is no problem to find the element that collide.
Bu i want to calculate if an element's type is a circle, calculate the collide percentage from center of the arc to the outside.
The code below is for collision detection.
if(element.type == "circle") { // circle detection
let dx = x - element.left,
dy = y - element.top,
dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
if (dist <= element.width / 2) {
collision = {
hitTo: // calculate percentage???,
object: element
};
}
}
I don't know how to do this. Can you help?
Ensure you have x and y position from the center of the circle (not from the top-left) then you can calculate the percentage by using inverse tangent trigonometric function.
var x = 0.5;
var y = 0.5;
var angle = Math.atan(y/x);
var percentage = 100 * angle / (2 * Math.PI);
console.log("Percentage: ", percentage);
For example, with both x and y set to 0.5, you get percentage equal to 12.5
Is there any way in which, in javascript, I can call a function with an x and y co-ord and a direction (angle in degrees) and it will return a set of new co-ords that has been 'moved' by 10px in the direction given from the original co-ords? I looked around but all I can find is ways to get the angle of two given co-ords.
This function returns an array [xCoord, yCoord] of the new coordinates:
function myFunction(xCoord, yCoord, angle, length) {
length = typeof length !== 'undefined' ? length : 10;
angle = angle * Math.PI / 180; // if you're using degrees instead of radians
return [length * Math.cos(angle) + xCoord, length * Math.sin(angle) + yCoord]
}
I just wanted to point out, that the answers of are not correct IMHO. I've created a JSFiddle showing that the correct implementation must be something like this:
function getRelativeVector(angle, length, xOffset, yOffset) {
angle = angle * Math.PI / 180;
return {
X:length * Math.sin(angle) + xOffset,
Y:length * Math.cos(angle) + yOffset
};
}
The other solutions shown here from #Audrius and #Markus are simply twisted in cos and sin. They are working for angles between 0 and 45 degrees only.
The formula would be:
X = length * sin(angle) + xLocation
Y = length * cos(angle) + yLocation
The shift in x coordinate is L*cos(a) and shift in y coordinate is L*sin(a), where a is the angle ("direction given") and L is 10 px in your case.
I am not so familiar trigonometry, but I have only two points to rotate in 2D:
*nx, ny
. -
. -
. angle -
*cx,cy.................*x,y
cx, cy = rotation center
x,y = current x,y
nx, ny = new coordinates
How to calculate new points in a certain angle?
function rotate(cx, cy, x, y, angle) {
var radians = (Math.PI / 180) * angle,
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
return [nx, ny];
}
The first two parameters are the X and Y coordinates of the central point (the origin around which the second point will be rotated). The next two parameters are the coordinates of the point that we'll be rotating. The last parameter is the angle, in degrees.
As an example, we'll take the point (2, 1) and rotate it around the point (1, 1) by 90 degrees clockwise.
rotate(1, 1, 2, 1, 90);
// > [1, 0]
Three notes about this function:
For clockwise rotation, the last parameter angle should be positive. For counterclockwise rotation (like in the diagram you provided), it should be negative.
Note that even if you provide arguments that should yield a point whose coordinates are whole numbers -- i.e. rotating the point (5, 0) by 90 degrees about the origin (0, 0), which should yield (0, -5) -- JavaScript's rounding behavior means that either coordinate could still be a value that's frustratingly close to the expected whole number, but is still a float. For example:
rotate(0, 0, 5, 0, 90);
// > [3.061616997868383e-16, -5]
For this reason, both elements of the resulting array should be expected as a float. You can convert them to integers using Math.round(), Math.ceil(), or Math.floor() as needed.
Finally, note that this function assumes a Cartesian coordinate system, meaning that values on the Y axis become higher as you go "up" in the coordinate plane. In HTML / CSS, the Y axis is inverted -- values on the Y axis become higher as you move down the page.
First, translate the rotation center to the origin
Calculate the new coordinates (nx, ny)
Translate back to the original rotation center
Step 1
Your new points are
center: (0,0)
point: (x-cx, y-cy)
Step 2
nx = (x-cx)*cos(theta) - (y-cy)*sin(theta)
ny = (y-cy)*cos(theta) + (x-cx)*sin(theta)
Step 3
Translate back to original rotation center:
nx = (x-cx)*cos(theta) - (y-cy)*sin(theta) + cx
ny = (y-cy)*cos(theta) + (x-cx)*sin(theta) + cy
For deeper explanation, with some fancy diagrams, I recommend looking at this.
above accepted answer not work for me correctly, rotation are reversed , here is working function
/*
CX # Origin X
CY # Origin Y
X # Point X to be rotated
Y # Point Y to be rotated
anticlock_wise # to rotate point in clockwise direction or anticlockwise , default clockwise
return # {x,y}
*/
function rotate(cx, cy, x, y, angle,anticlock_wise = false) {
if(angle == 0){
return {x:parseFloat(x), y:parseFloat(y)};
}if(anticlock_wise){
var radians = (Math.PI / 180) * angle;
}else{
var radians = (Math.PI / -180) * angle;
}
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var nx = (cos * (x - cx)) + (sin * (y - cy)) + cx;
var ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
return {x:nx, y:ny};
}
According to Polar coordinate system artycle on Wikipedia:
x = r * cos(deg)
y = r * sin(deg)
r (radius) is equal to distance between Rotation Centre and Rotated Point
deg (degrees) is angle measured in degrees
I think it is better to use matrices for such operations.
Here is the example with gl-matrix (but you can use something like THREEJS as well).
import * as glm from 'gl-matrix';
const rotateVector = (() => {
const q = glm.quat.create();
// const m = glm.mat4.create(); // 2nd way
return (v: glm.vec3, point: glm.vec3, axis: glm.vec3, angle: number) => {
glm.quat.setAxisAngle(q, axis, angle);
// glm.mat4.fromRotation(m, angle, axis); // 2nd way
glm.vec3.sub(v, v, point);
glm.vec3.transformQuat(v, v, q);
// glm.vec3.transformMat4(v, v, m); // 2nd way
glm.vec3.add(v, v, point);
return v;
}
})();
In 2D case you need to rotate around z-axis:
rotateVector([x, y, 0], [cX, cY, 0], [0, 0, 1], angleInRadians);
I have 4 points 1,2,3,4 that closes a rectangle.
The points are in a array in this following way: x1 y1 x2 y2 x3 y3 x4 y4
The problem I have is that the rectangle can be rotated in a angle.
How can I calculate the original points (gray outline), and the angle?
I'm trying to reproduce this effect in javascript+css3-transform, so I need to first know the straight dimensions and then rotate with the css.
I just know if the rectangle is straight by comparing points e.g. y1==y2
if(x1==x4 && x2==x3 && y1==y2 && y4==y3){
rectangle.style.top = y1;
rectangle.style.left = x1;
rectangle.style.width = x2-x1;
rectangle.style.height = y4-y1;
rectangle.style.transform = "rotate(?deg)";
}
You can use any coordinate pair on the same side to calculate the rotation angle. Note that mathematic angles normally assume 0 as long the +ve X axis and increase by rotating anti–clockwise (so along the +ve Y axis is 90°, -ve X axis is 180° and so on).
Also, javascript trigonometry functions return values in radians that must be converted to degrees before being used in a CSS transform.
If the shape is not rotated more than 90°, then life is fairly simple and you can use the tanget ratio of a right angle triangle:
tan(angle) = length of opposite side / length of adjacent side
For the OP, the best corners to use are 1 and 4 so that rotation is kept in the first quadrant and clockwise (per the draft CSS3 spec). In javascript terms:
var rotationRadians = Math.atan((x1 - x4) / (y1 - y4));
To convert to degrees:
var RAD2DEG = 180 / Math.PI;
var rotationDegrees = rotationRadians * RAD2DEG;
If the rotation is more than 90°, you will need to adjust the angle. e.g. where the angle is greater than 90° but less than 180°, you'll get a -ve result from the above and need to add 180°:
rotationDegrees += 180;
Also, if you are using page dimentions, y coordinates increase going down the page, which is the opposite of the normal mathetmatic sense so you need to reverse the sense of y1 - y4 in the above.
Edit
Based on the orientation of points in the OP, the following is a general function to return the center and clockwise rotation of the rectangle in degrees. That's all you should need, though you can rotate the corners to be "level" yourself if you wish. You can apply trigonometric functions to calculate new corners or just do some averages (similar to Ian's answer).
/** General case solution for a rectangle
*
* Given coordinages of [x1, y1, x2, y2, x3, y3, x4, y4]
* where the corners are:
* top left : x1, y1
* top right : x2, y2
* bottom right: x3, y3
* bottom left : x4, y4
*
* The centre is the average top left and bottom right coords:
* center: (x1 + x3) / 2 and (y1 + y3) / 2
*
* Clockwise rotation: Math.atan((x1 - x4)/(y1 - y4)) with
* adjustment for the quadrant the angle is in.
*
* Note that if using page coordinates, y is +ve down the page which
* is the reverse of the mathematic sense so y page coordinages
* should be multiplied by -1 before being given to the function.
* (e.g. a page y of 400 should be -400).
*
* #see https://stackoverflow.com/a/13003782/938822
*/
function getRotation(coords) {
// Get center as average of top left and bottom right
var center = [(coords[0] + coords[4]) / 2,
(coords[1] + coords[5]) / 2];
// Get differences top left minus bottom left
var diffs = [coords[0] - coords[6], coords[1] - coords[7]];
// Get rotation in degrees
var rotation = Math.atan(diffs[0]/diffs[1]) * 180 / Math.PI;
// Adjust for 2nd & 3rd quadrants, i.e. diff y is -ve.
if (diffs[1] < 0) {
rotation += 180;
// Adjust for 4th quadrant
// i.e. diff x is -ve, diff y is +ve
} else if (diffs[0] < 0) {
rotation += 360;
}
// return array of [[centerX, centerY], rotation];
return [center, rotation];
}
The center of the rectangle is right between two opposite corners:
cx = (x1 + x3) / 2
cy = (y1 + y3) / 2
The size of the rectangle is the distance between two points:
w = sqrt(pow(x2-x1, 2) + pow(y2-y1, 2))
h = sqrt(pow(x3-x2, 2) + pow(y3-y2, 2))
The corners of the gray rectangle can be calculated from the center and the size, for example the top left corner:
x = cx - w / 2
y = cy - h / 2
The angle is the arctangent of a side of the square:
a = arctan2(y4 - y1, x4 - x1)
(I'm not sure exactly which angle it returns, or what angle you expect for that matter, so you get to test a bit.)
This is how you get the angle between the vertical pink line and the black line starting at the pink line intersection:
var deg = 90 - Math.arctan((x2-x1) / (y2-y1));
The dimensions can be calculated with the help of the Pythagoras theorem:
var width = Math.sqrt((x2-x1)^2 / (y2-y1)^2));
var height = Math.sqrt((x1-x4)^2) / (y4-y1)^2));
The positional coordinates (left and top) are the averages of x1 and x3 and y1 and y3 respectively.
var left = Math.floor((x1 + x3) / 2);
var top = Math.floor((y1 + y3) / 2);
You want to use the negative-margin trick.
var marginLeft = -Math.ceil(width / 2);
var marginTop = -Math.ceil(height / 2);
I need to retain coordinate information following a rotation and am struggling with the necessary math.
I have an image, with certain crop coordinates. These coordinates are given as the following object:
{
h: 139.68333435058594,
w: 139.68333435058594,
x: 60.31666564941406,
x2: 200,
y: 80,
y2: 219.68333435058594
}
I am then rotating this image (it is a canvas element) but I would like the new coordinates to accurately reflect the rotation, leaving the original crop intact.
e.g. if the top left quarter of a 400x400 image is selected the returned coordinates would be x:0, y:0, x2:200, y2:200. When rotated -90 degrees to the left, the new coordinates should be : x:0, y:200, x2:200, y:400.
How would I write a function to calculate these new coordinates?
Many thanks.
To help visualize this question properly I have included a quick image:
EDIT: The large square in the image above is the photo that I am rotating. It is being rotated around its midpoint and the crop coordinates are relative to the image itself. Upon rotation the actual coordinate plane is reset prior to recalculating the new coordinates meaning that the top left point of the image/container will always be 0,0.
This function uses the Rotation Matrix algorithm
function rotate(x, y, xm, ym, a) {
var cos = Math.cos, sin = Math.sin,
a = a * Math.PI / 180, // Convert to radians
// Subtract midpoints, so that midpoint is translated to origin
// and add it in the end again
xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm,
yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym;
return [xr, yr];
}
alert(rotate(0, 0, 200, 200, -90)); // 0,400
JSFiddle
The matrix for a 90 degree counterclockwise rotation around the (0,0) point (in your coordinate system) is
0 1
-1 0
Which translates in code to:
new_x = y; new_y = -x;
But you want to rotate around (200,200), so you will need to move your points before rotating them, and move them back afterwards.
new_x = (y-200) + 200;
new_y = -(x-200) + 200;
Simplified, this is just
new_x = y; new_y = 400-x;
You should be able to transform all of the crop coordinates like that.
Top left corner will be:
0, heightImg - heightCrop
Bottom Right will be:
widthCrop, widthImg
This is assuming the crop is snug with the top left corner before rotation.
I'll work out the general math in a bit.
The formulas to rotate a point (A) around a point (B) by angle (C) are as follows:
N.x = (A.x-B.x) * cos(c) - (A.y-B.y) * sin(c) + B.x
N.y = (A.y-B.y) * cos(c) + (A.x-B.x) * sin(c) + B.y
We can reduce these since we're rotating by 90 degrees. [cos(90) = 0, sin(90)=1]
N.x = 0 - (A.y-B.y) + B.x = B.x + B.y - A.y
N.y = 0 + (A.x-B.x) + B.y = B.y + A.x - B.x
So using this formula with the coordinates of the top-left point will give you the bottom-left point. (The x will be correct, but you'll have to subtract the crop area's height to get the correct y value for the new top-left point)