Rotate around a changing origin - Javascript - javascript

So I have an object rotating around an origin point. Once I rotate and then change the origin point. My object seems to jump positions. After the jump it rotates fine... Need help finding the pattern/why it's jumping and what I need to do to stop it.
Here's the rotation code:
adjustMapTransform = function (_x, _y) {
var x = _x + (map.width/2);
var y = _y + (map.height/2);
//apply scale here
var originPoint = {
x:originXInt,
y:originYInt
};
var mapOrigin = {
x:map.x + (map.width/2),
y:map.y + (map.height/2)
};
//at scale 1
var difference = {
x:mapOrigin.x - originPoint.x,
y:mapOrigin.y - originPoint.y
};
x += (difference.x * scale) - difference.x;
y += (difference.y * scale) - difference.y;
var viewportMapCentre = {
x: originXInt,
y: originYInt
}
var rotatedPoint = {};
var angle = (rotation) * Math.PI / 180.0;
var s = Math.sin(angle);
var c = Math.cos(angle);
// translate point back to origin:
x -= viewportMapCentre.x;
y -= viewportMapCentre.y;
// rotate point
var xnew = x * c - y * s;
var ynew = x * s + y * c;
// translate point back:
x = xnew + viewportMapCentre.x - (map.width/2);
y = ynew + viewportMapCentre.y - (map.height/2);
var coords = {
x:x,
y:y
};
return coords;
}
Also here is a JS Fiddle project that you can play around in to give you a better idea of what's happening.
EDITED LINK - Got rid of the originY bug and scaling bug
https://jsfiddle.net/fionoble/6k8sfkdL/13/
Thanks!

The direction of rotation is a consequence of the sign you pick for the elements in your rotation matrix. [This is Rodrigues formula for rotation in two dimensions]. So to rotate in the opposite direction simply subtract your y cosine term rather than your y sine term.
Also you might try looking at different potential representations of your data.
If you use the symmetric representation of the line between your points you can avoid shifting and instead simply transform your coordinates.
Take your origin [with respect to your rotation], c_0, to be the constant offset in the symmetric form.
You have for a point p relative to c_0:
var A = (p.x - c_0.x);
var B = (p.y - c_0.y);
//This is the symmetric form.
(p.x - c_0.x)/A = (p.y - c_0.y)/B
which will be true under a change of coordinates and for any point on the line (which also takes care of scaling/dilation).
Then after the change of coordinates for rotation you have [noting that this rotation has the opposite sense, not the same as yours].
//This is the symmetric form of the line incident on your rotated point
//and on the center of its rotation
((p.x - c_0.x) * c + (p.y - c_0.y) * s)/A = ((p.x - c_0.x) * s - (p.y - c_0.y) * c)/B
so, multiplying out we get
(pn.x - c_0.x) * B * c + (pn.y - c_0.y) * B * s = (pn.x - c_0.x) * A * s - (pn.y - c_0.y) * A * c
rearrangement gives
(pn.x - c_0.x) * (B * c - A * s) = - (pn.y - c_0.y) * (B * s + A * c)
pn.y = -(pn.x - c_0.x) * (B * c - A * s) / (B * s + A * c) + c_0.y;
for any scaling.

Related

Transformation matrix rotation not preserving local axis scaling?

I have a simple transform class to apply translations, scales and rotations on a div in any arbitrary order:
class TransformDiv{
constructor(div)
{
this.div = div;
this.translateX = 0;
this.translateY = 0;
this.scaleX = 1;
this.scaleY = 1;
this.shearX = 0;
this.shearY = 0;
}
translate(x, y)
{
this.translateX += x;
this.translateY += y;
this.setTransform();
}
scale(x, y, anchorX = 0, anchorY = 0)
{
this.scaleX *= x;
this.shearX *= x;
this.scaleY *= y;
this.shearY *= y;
this.translateX -= (this.translateX - anchorX) * (1 - x);
this.translateY -= (this.translateY - anchorY) * (1 - y);
this.setTransform();
}
rotate(rad, anchorX = 0, anchorY = 0)
{
let cos = Math.cos(rad);
let sin = Math.sin(rad);
// the composition of two successive rotations are additive
let newScaleX = this.scaleX * cos + this.shearX * sin;
let newShearX = this.scaleX * (-sin) + this.shearX * cos;
let newShearY = this.shearY * cos + this.scaleY * sin;
let newScaleY = this.shearY * (-sin) + this.scaleY * cos;
this.scaleX = newScaleX;
this.shearX = newShearX;
this.shearY = newShearY;
this.scaleY = newScaleY;
//rotation about an arbitrary point
let originX = (this.translateX - anchorX);
let originY = (this.translateY - anchorY);
this.translateX -= (originY * sin - originX * (cos - 1));
this.translateY -= (-originY * (cos - 1) - originX * sin);
this.setTransform();
}
setTransform()
{
this.div.style.transform = `matrix(${this.scaleX}, ${this.shearY}, ${this.shearX}, ${this.scaleY}, ${this.translateX}, ${this.translateY})`;
}
}
A problem arises when I wish to rotate after a non-uniform scale has been made.
Edit - Newer interactive example: https://codepen.io/manstie/pen/RwGGOmB
Here is the example I made:
https://jsfiddle.net/ft61q230/1/
In the example here:
div2.translate(100, 100);
div2.scale(2, 1, 100, 100);
div2.rotate(Math.PI / 2, 100, 100);
The expected result is for Test 1 Text and Test 2 Text to be the same length, as if you were rotating from the top left of the div clockwise 90 degrees; but as you can see the result is such that the rotation logic I am performing retains the scale on the world-space axis, so now Test 2 Text is twice as tall rather than twice as long.
Current outcome:
Desired outcome:
The current rotation logic is based on multiplying the existing transformation matrix that makes up rotation by another transformation matrix containing an angle to rotate by, but I realize it is not as simple as that and I am missing something to retain local-axial scale.
Thank you for your assistance.
Edit:
Was recommended DOMMatrix which does all this math for me, but it has the same problem, although there is some skew which I don't think is accurate:
https://jsfiddle.net/heqo7vrt/1/
The skew is caused by the scale function scaling it's local X axis while it is rotated, and then rotating after not keeping that local X axis scaling. Also, DOMMatrix translate function has the translations apply on its local axis which is not desired in my situation but if its rotate function worked as expected I would be able to use it.
I managed to fix it here:
Regular: https://jsfiddle.net/sbca61k5/
let newScaleX = cos * this.scaleX + sin * this.shearY;
let newShearX = cos * this.shearX + sin * this.scaleY;
let newShearY = -sin * this.scaleX + cos * this.shearY;
let newScaleY = -sin * this.shearX + cos * this.scaleY;
DOMMatrix version: https://jsfiddle.net/b36kqrsg/
this.matrix = new DOMMatrix([cos, sin, -sin, cos, 0, 0]).multiply(this.matrix);
// or
this.matrix = new DOMMatrix().rotate(deg).multiply(this.matrix);
The difference is to have the rotation matrix multiplied by the rest of the matrix to "add" it on, not the other way round:
[a c e] [cos -sin 0] [scx shy tx]
[b d f] = [sin cos 0] . [shx scy ty]
[0 0 1] [0 0 1] [0 0 1 ]
I'm unsure about the details of the anchor mathematics but the DOMMatrix version's anchor is relative to its own top left whereas the other is relative to the top left of the document.
From my interactive example the anchor maths does not work as after a multitude of rotations the objects get further away from the anchor origin.
https://codepen.io/manstie/pen/PoGXMed

what is the meaning of the expression " x * Math.cos(angle) - y * Math.sin(angle)"?

The above expression was taken from the below method.
I know that to rotate a point around the center we have to
Move the point to the origin
Make the rotation and
Move the point back
But the pieces I don't get my head around are:
r[0] = x * Math.cos(angle) - y * Math.sin(angle);
^
|
why we use the minus sign here?
r[1] = x * Math.sin(angle) + y * Math.cos(angle);
^
|
And why here we use plus sign instead of minus?
Vec2.prototype.rotate = function (center, angle) {
//rotate in counterclockwise
var r = [];
var x = this.x - center.x;
var y = this.y - center.y;
r[0] = x * Math.cos(angle) - y * Math.sin(angle);
r[1] = x * Math.sin(angle) + y * Math.cos(angle);
r[0] += center.x;
r[1] += center.y;
return new Vec2(r[0], r[1]);
};
The book was to be great but it doesn't explain most of the code it simply spits out.
I got it! Just saw a video of Dr Peyam on transformation matrix.
To get x' and y' we multiply the transformation matrix by the current coordinate (x,y)
enter image description here

Calculate if line, defined by 2 coordinates, intersects circle

I am trying to get a function to calculate if a line, that is defined by 2 coordinates, intersects a circle, defined by a coordinate and a radius. I previously used these 2 functions
getLineEquation(point1, point2) {
var lineObj = {
gradient: (point1.latitude - point2.latitude) / (point1.longitude - point2.longitude)
};
lineObj.yIntercept = point1.latitude - lineObj.gradient * point1.longitude;
return lineObj;
}
checkIntercept(y, m, circle) {
// y: y intercept of line
// m: gradient of line
// get a,b,c values
var a = 1 + (m * m);
var b = -circle.longitude * 2 + (m * (y - circle.latitude)) * 2;
var c = (circle.longitude * circle.longitude) + ((y - circle.latitude)* (y - circle.latitude)) - (circle.range * circle.range);
// get discriminant
var d = (b * b) - 4 * a * c;
if (d >= 0) {
// insert into quadratic formula
var intersections = [
(-b + Math.sqrt((b * b) - 4 * a * c)) / (2 * a),
(-b - Math.sqrt((b * b) - 4 * a * c)) / (2 * a)
];
if (d == 0) {
// only 1 intersection
return [intersections[0]];
}
return intersections;
}
// no intersection
return false;
}
but this didn't work as it converted the 2 points into an infinite line, which I don't want as it would return false readings for circles that aren't actually between the 2 points.
How could I fix these functions to make the calculation into a finite line?
Here is a possible approach.
If one of the two vertices is inside the circle, and the other is outside, then there will be an intersection. (This means two distance calculations.)
If both vertices lie inside the circle, there is no intersection.
If the first two steps weren't decisive, find s, the projection of the circle center to the line (p,q).
If the distance of s to the circle center is larger than the radius, there is no intersection.
Otherwise, if s is between a and b (so s_x between p_x and q_x, and also s_y between p_y and q_y, to include the case of horizontal and vertical edges), then there is an intersection, otherwise not.
To project a point on a line, you basically make a line equation in the form (px,py) + t * (dx,dy) with dx = qx - px and dy = qy - py for two points (px,py) and (qx,qy). The perpendicular line through a point (cx,cy) is then (cx,cy) + t * (-dy,dx). Setting both equations equal finds the intersection.
Or, if you prefer straight formulas:
sx = (d*px + (px - qx)*((cx - px)*(px - qx) + (cy - py)*(py - qy)))/d
sy = (d*py + (py - qy)*((cx - px)*(px - qx) + (cy - py)*(py - qy)))/d
with
d = (px - qx)^2 + (py - qy)^2

With HTML5 canvas, how to calculate the final point coordinates with an offset?

On a HTML5 canvas object, I have to subtract a distance from a destination point, to give the final destination on the same line.
So, first I have calculated the distance between the source and target points, with the Pythagorean theorem, but my memories of Thales's theorem are too faulty to find the final point (on same line), with the right x and y attributes.
function getDistance (from, to){
return Math.hypot(to.x - from.x, to.y - from.y);
}
function getFinalTo (from, to, distanceToSubstract){
//with Pythagore we obtain the distance between the 2 points
var originalDistance = getDistance(from, to);
var finalDistance = originalDistance - distanceToSubstract;
//Now, I was thinking about Thales but all my tries are wrong
//Here some of ones, I need to get finalTo properties to draw an arrow to a node without
var finalTo = new Object;
finalTo.x = ((1 - finalDistance) * from.x) + (finalDistance * to.x);
finalTo.y = ((1 - finalDistance) * from.y) + (finalDistance * to.y);
return finalTo;
}
Indeed, the arrowhead be hidden by the round node that can be about 100 pixels of radius, so I try to get the final point.
Thanks a lot.
Regards,
Will depend on the line cap. For "butt" there is no change, for "round" and "square" you the line extends by half the width at each end
The following function shortens the line to fit depending on the line cap.
drawLine(x1,y1,x2,y2){
// get vector from start to end
var x = x2-x1;
var y = y2-y1;
// get length
const len = Math.hypot(x,y) * 2; // *2 because we want half the width
// normalise vector
x /= len;
y /= len;
if(ctx.lineCap !== "butt"){
// shorten both ends to fit the length
const lw = ctx.lineWidth;
x1 += x * lw;
y1 += y * lw;
x2 -= x * lw;
y2 -= y * lw;
}
ctx.beginPath()
ctx.lineTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
}
For miter joins the following answer will help https://stackoverflow.com/a/41184052/3877726
You can use simple proportion by distance ratio:
(I did not account for round cap)
ratio = finalDistance / originalDistance
finalTo.x = from.x + (to.x - from.x) * ratio;
finalTo.y = from.y + (to.y - from.y) * ratio;
Your approach was attempt to use linear interpolation, but you erroneously mixed distances (in pixels, meters etc) with ratios (dimensionless - is this term right?)
ratio = finalDistance / originalDistance
finalTo.x = ((1 - ratio) * from.x) + (ratio * to.x);
finalTo.y = ((1 - ratio) * from.y) + (ratio * to.y);
Note that both approaches is really the same formula.

Arcing coords from A to B

What I am aiming to do is arc the position of a circle towards the position of the mouse cursor, this all being relative to the world viewed through the canvas. To keep a handle on the speed at which the circle moves I decided to make a boundary larger than the circle, if the mouse is outside the boundary then the "position" of the mouse is brought to the boundary so that when I arc towards the coords, if they arent super far from the position of the circle it doesnt move at crazy speeds. I have this working and this is the code which does it:
dx = Game.controls.mouseX - (this.x - xView); // get the distance between the x coords
dy = Game.controls.mouseY - (this.y - yView); // get the distance between the y coords
radii = this.radius + 1; // +1 because the "radius" of the mouse is 1
if((dx * dx) + (dy * dy) > radii * radii) // is the mouse not over the player?
{
if((dx * dx) + (dy * dy) < 301 * 301)
{
this.x += ((Game.controls.mouseX - (this.x - xView)) * 2 / (this.mass)) + step;
this.y += ((Game.controls.mouseY - (this.y - yView)) * 2 / (this.mass)) + step;
}
else
{
mx = Game.controls.mouseX;
my = Game.controls.mouseY;
do
{
dx = mx - (this.x - xView);
dy = my - (this.y - yView);
mx += (((this.x - xView) - mx) * 2 / (this.mass)) + step;
my += (((this.y - yView) - my) * 2 / (this.mass)) + step;
} while((dx * dx) + (dy * dy) > 301 * 301)
this.x += ((mx - (this.x - xView)) * 2 / (this.mass)) + step;
this.y += ((my - (this.y - yView)) * 2 / (this.mass)) + step;
}
}
The magic for 'outside the boundary' lies withing the do while. This is the best fix I could come up with and I cant see this as being an elegant or fast solution and am wondering what the proper course of action should be.
Im no artist but hopefully this image helps to illustrate what I am trying to achieve. The Black dot is the mouse pos, the black circle is the circle and the red circle is the boundary I have specified. I want to get the coords marked by the X.
Your question is a special case of Circle line-segment collision detection algorithm?, in this case with B and C being the same points, so you can use your center point for both of them.
That solution is given in C, but it translates to JavaScript very easily, just replace float with var, use Math.sqrt() and so on...
Oh, and then there is a JvaScript version here: Calculate the point of intersection of circle and line through the center, that's more appropriate :-)
If the black circle is in the center of the red circle and you have the radius of the red circle
// c is circle center
// mouse is the mouse position. Should have properties x,y
// radius is the circle radius;
// returns the point on the line where the circle intercepts it else it returns undefined.
function findX(c, mouse, radius)
var v = {};
// get the vector to the mouse
v.x = mouse.x - c.x;
v.y = mouse.y - c.y;
var scale = radius / Math.hypot(v.x,v.y);
if(scale < 1){ // is it outside the circle
return {
x : c.x + v.x * scale,
y : c.y + v.y * scale
};
}
return;
}
And if the the line start is not the center then a general purpose line circle intercept function will solve the problem. If the line starts inside the circle the function will return just one point. If the line is not long enough it will return an empty array..
// p1,p2 are the start and end points of a line
// returns an array empty if no points found or one or two points depending on the number of intercepts found
// If two points found the first point in the array is the point closest to the line start (p1)
function circleLineIntercept(circle,radius,p1,p2){
var v1 = {};
var v2 = {};
var ret = [];
var u1,u2,b,c,d;
// line as vector
v1.x = p2.x - p1.x;
v1.y = p2.y - p1.y;
// vector to circle center
v2.x = p1.x - circle.x;
v2.y = p1.y - circle.y;
// dot of line and circle
b = (v1.x * v2.x + v1.y * v2.y) * -2;
// length of line squared * 2
c = 2 * (v1.x * v1.x + v1.y * v1.y);
// some math to solve the two triangles made by the intercept points, the circle center and the perpendicular line to the line.
d = Math.sqrt(b * b - 2 * c * (v2.x * v2.x + v2.y * v2.y - radius * radius));
// will give a NaN if no solution
if(isNaN(d)){ // no intercept
return ret;
}
// get the unit distance of each intercept to the line
u1 = (b - d) / c;
u2 = (b + d) / c;
// check the intercept is on the line segment
if(u1 <= 1 && u1 >= 0){
ret.push({x:line.p1.x + v1.x * u1, y : line.p1.y + v1.y * u1 });
}
// check the intercept is on the line segment
if(u2 <= 1 && u2 >= 0){
ret.push({x:line.p1.x + v1.x * u2, y : line.p1.y + v1.y * u2});
}
return ret;
}

Categories