I'm trying to rotate an SVG object (a rectangle, which is also draggable using x and y attributes) via rotate(angle x y), x and y is the midpoint of the object which I calculate with this function:
getMidPoint(x, y, width, height, angle, oldMidPointX, oldMidPointY, drawPoint) {
let angleRad = angle * Math.PI / 180;
let cosa = Math.cos(angleRad);
let sina = Math.sin(angleRad);
// Find new x/y values after rotation
let tempX = x - oldMidPointX;
let tempY = y - oldMidPointY;
let xAfterRotation = (tempX * cosa) - (tempY * sina);
let yAfterRotation = (tempX * sina) + (tempY * cosa);
drawPoint(x, y, "aqua");
// translate back
x = xAfterRotation + oldMidPointX;
y = yAfterRotation + oldMidPointY;
drawPoint(x, y, "green");
let wp = width / 2;
let hp = height / 2;
let px = x + (wp * cosa) - (hp * sina);
let py = y + (wp * sina) + (hp * cosa);
drawPoint(x, y, "red");
return {
px: px,
py: py };
}
Consecutive rotations are no problem. The jumping occurs if the object gets rotated, resized (for example increasing the width of the object) and then rotated again. While the function above gives the correct midpoint, the object first jumps and rotates around the the calculated point.
I don't know what I'm missing here, maybe someone can spot the mistake or give me some ideas to try out.
EDIT
Here is an image to showcase my problem.
1: Starting position; red dot marks the midpoint of the object and green dot is the position used for calculating the midpoint.
2: Rotating the object with no problem.
3: Resizing the object.
4: Calculating new midpoint on start; blue dot marks x and y coordinates of the object.
5: Rotating the second time, "jumping" occurs.
6-8: The object rotates (the same rotation started at previous step) around the midpoint but with an offset.
9: 360° rotation puts the top left corner of the object to it's x/y position
Maybe someone can spot the mistake now.
Related
I have a specific question. I have two rectangles and I'm calculating the line between these two rectangles. Now I want to draw a circle on that line, which is moving forward on the line with a specific speed. I'm redrawing the circle always with new coordinates, that's how i solved the movement.
Now I'm adding to the x-coordinate of the Circle always 1 and then calculate the y-coordinate with my equation. There problem here is, the higher my line slope is, the faster the circle moves.
So how can I calculate the x-coordinate, that the speed of the ball is always the same?
Here is my following code. posX and posY are the positions of the ball that I overwrite. Gun and Ammo are my two rectangles.
this.posX = this.posX - 1;
this.posY = ((this.gunY - this.ammoY) / (this.gunX - this.ammoX)) * (this.posX - this.ammoX) + this.ammoY;
image to understand my calculation and my thoughts
Unit vector
Use the line's Unit (normalized) vector. The normalized vector is always one unit long (unless the line has no length) You can scale the vector to what ever speed you require
Normalize line
Example ? should be numbers
// the line start and end
const startX = ?
const startY = ?
const endX = ?
const endY = ?
function getLineNormal() {
// get the line vector
const vx = endX - startX
const vy = endY - startY
// get the line length
const len = Math.hypot(vx, vy)
// Only if the line has length
if (len > 0) {
// calculate normal of vector
return {x: vx / len, y: vy / len}
}
return return {x: 0, y: 0}
}
Scale vector and add unit vector
To use the vector to move at a constant speed. The speed scales the normal vector.
// set circle at start of line.
var circleX = startX
var circleY = startY
const lineVec = getLineNormal()
function moveCircle(speed) { // speed in pixels
circleX += lineVec.x * speed
circleY += lineVec.y * speed
}
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
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.
I was trying to do a perspective grid on my canvas and I've changed the function from another website with this result:
function keystoneAndDisplayImage(ctx, img, x, y, pixelHeight, scalingFactor) {
var h = img.height,
w = img.width,
numSlices = Math.abs(pixelHeight),
sliceHeight = h / numSlices,
polarity = (pixelHeight > 0) ? 1 : -1,
heightScale = Math.abs(pixelHeight) / h,
widthScale = (1 - scalingFactor) / numSlices;
for(var n = 0; n < numSlices; n++) {
var sy = sliceHeight * n,
sx = 0,
sHeight = sliceHeight,
sWidth = w;
var dy = y + (sliceHeight * n * heightScale * polarity),
dx = x + ((w * widthScale * n) / 2),
dHeight = sliceHeight * heightScale,
dWidth = w * (1 - (widthScale * n));
ctx.drawImage(img, sx, sy, sWidth, sHeight,
dx, dy, dWidth, dHeight);
}
}
It creates almost-good perspective grid, but it isn't scaling the Height, so every square has got the same height. Here's a working jsFiddle and how it should look like, just below the canvas. I can't think of any math formula to distort the height in proportion to the "perspective distance" (top).
I hope you understand. Sorry for language errors. Any help would be greatly appreciatedRegards
There is sadly no proper way besides using a 3D approach. But luckily it is not so complicated.
The following will produce a grid that is rotatable by the X axis (as in your picture) so we only need to focus on that axis.
To understand what goes on: We define the grid in Cartesian coordinate space. Fancy word for saying we are defining our points as vectors and not absolute coordinates. That is to say one grid cell can go from 0,0 to 1,1 instead of for example 10,20 to 45, 45 just to take some numbers.
At the projection stage we project these Cartesian coordinates into our screen coordinates.
The result will be like this:
ONLINE DEMO
Ok, lets dive into it - first we set up some variables that we need for projection etc:
fov = 512, /// Field of view kind of the lense, smaller values = spheric
viewDist = 22, /// view distance, higher values = further away
w = ez.width / 2, /// center of screen
h = ez.height / 2,
angle = -27, /// grid angle
i, p1, p2, /// counter and two points (corners)
grid = 10; /// grid size in Cartesian
To adjust the grid we don't adjust the loops (see below) but alter the fov and viewDist as well as modifying the grid to increase or decrease the number of cells.
Lets say you want a more extreme view - by setting fov to 128 and viewDist to 5 you will get this result using the same grid and angle:
The "magic" function doing all the math is as follows:
function rotateX(x, y) {
var rd, ca, sa, ry, rz, f;
rd = angle * Math.PI / 180; /// convert angle into radians
ca = Math.cos(rd);
sa = Math.sin(rd);
ry = y * ca; /// convert y value as we are rotating
rz = y * sa; /// only around x. Z will also change
/// Project the new coords into screen coords
f = fov / (viewDist + rz);
x = x * f + w;
y = ry * f + h;
return [x, y];
}
And that's it. Worth to mention is that it is the combination of the new Y and Z that makes the lines smaller at the top (at this angle).
Now we can create a grid in Cartesian space like this and rotate those points directly into screen coordinate space:
/// create vertical lines
for(i = -grid; i <= grid; i++) {
p1 = rotateX(i, -grid);
p2 = rotateX(i, grid);
ez.strokeLine(p1[0], p1[1], p2[0], p2[1]); //from easyCanvasJS, see demo
}
/// create horizontal lines
for(i = -grid; i <= grid; i++) {
p1 = rotateX(-grid, i);
p2 = rotateX(grid, i);
ez.strokeLine(p1[0], p1[1], p2[0], p2[1]);
}
Also notice that position 0,0 is center of screen. This is why we use negative values to get out on the left side or upwards. You can see that the two center lines are straight lines.
And that's all there is to it. To color a cell you simply select the Cartesian coordinate and then convert it by calling rotateX() and you will have the coordinates you need for the corners.
For example - a random cell number is picked (between -10 and 10 on both X and Y axis):
c1 = rotateX(cx, cy); /// upper left corner
c2 = rotateX(cx + 1, cy); /// upper right corner
c3 = rotateX(cx + 1, cy + 1); /// bottom right corner
c4 = rotateX(cx, cy + 1); /// bottom left corner
/// draw a polygon between the points
ctx.beginPath();
ctx.moveTo(c1[0], c1[1]);
ctx.lineTo(c2[0], c2[1]);
ctx.lineTo(c3[0], c3[1]);
ctx.lineTo(c4[0], c4[1]);
ctx.closePath();
/// fill the polygon
ctx.fillStyle = 'rgb(200,0,0)';
ctx.fill();
An animated version that can help see what goes on.
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)