I think this isn't the best way to do that, but this should work:
const getVectorAngle = ([x1, y1], [x2, y2]) => {
const x = x2 - x1
const y = y2 - y1
return (((Math.acos(y / Math.sqrt(x * x + y * y)) * (Math.sign(x) || 1)) * 180 / Math.PI) + 360) % 360
}
console.log(getVectorAngle([100, 100], [96, 105])) //321.3401
console.log(getVectorAngle([100, 100], [100, 101])) //0
console.log(getVectorAngle([100, 100], [101, 101])) //45
console.log(getVectorAngle([100, 100], [101, 100])) //90
console.log(getVectorAngle([100, 100], [101, 99])) //135
console.log(getVectorAngle([100, 100], [100, 99])) //180
console.log(getVectorAngle([100, 100], [ 99, 99])) //225
console.log(getVectorAngle([100, 100], [ 99, 100])) //270
console.log(getVectorAngle([100, 100], [ 99, 101])) //315
console.log(getVectorAngle([100, 100], [100, 100])) //NaN, start and end values are the same
Here's how it works:
Visalisation made by Geogebra Geometry
r is the displacement vector, which is given, and we're looking for α (alpha), the angle of that vector.
const getVectorAngle = ([x1, y1], [x2, y2]) => {
const x = x2 - x1
const y = y2 - y1
return (
(
( //Getting angle by `y = cos(alpha) * r` -> `alpha = cos^-1(y / r)`
Math.acos(
y /
Math.sqrt(x * x + y * y) //Pythagorean theorem to get the length of r, `r^2 = x^2 + y^2` -> `r = sqrt(x^2 + y^2)`
) //= angle in interval [0°; 180°] (in radians)
* ( //Detect the direction of angle by changing its sign
Math.sign(x) //Sign of x, +1 if positive, -1 if negative
|| 1 //Return +1 even if x is 0, to avoid cancelling out 180°
)
) //= angle in interval ]-180°; 180°] (still in radians)
* 180 / Math.PI //Convert angle from radians to degrees
) //= angle in degrees
+ 360 //Add 360° to avoid negative values
) //= angle in interval ]180°; 540°]
% 360 //Modulo by 360° to simplify angles >=360°
//= angle in degrees in interval [0°; 360°[
}
Edit
Somewhat later, I realized, that it can be made to work with Math.atan2
as well:
const getVectorAngle = ([x1, y1], [x2, y2]) => {
const x = x2 - x1
const y = y2 - y1
return x || y
? ((Math.atan2(x, y) * 180 / Math.PI) + 360) % 360
: NaN
}
console.log(getVectorAngle([100, 100], [96, 105])) //321.3401
console.log(getVectorAngle([100, 100], [100, 101])) //0
console.log(getVectorAngle([100, 100], [101, 101])) //45
console.log(getVectorAngle([100, 100], [101, 100])) //90
console.log(getVectorAngle([100, 100], [101, 99])) //135
console.log(getVectorAngle([100, 100], [100, 99])) //180
console.log(getVectorAngle([100, 100], [ 99, 99])) //225
console.log(getVectorAngle([100, 100], [ 99, 100])) //270
console.log(getVectorAngle([100, 100], [ 99, 101])) //315
console.log(getVectorAngle([100, 100], [100, 100])) //NaN, start and end values are the same
As you may notice, the outer shell of the calculation remained the same. That's because Math.atan2
already returns an angle in radians in the interval ]-180°;180°]
; so it's functionally same as first my solution's inner part.
The only change is an additional check for a null vector: Math.atan2
doesn't return NaN
in this case.