1
votes

Say you have two x,y coordinates: Starting point of: (100, 100) and ending point of (96, 105). So I need to go left and up. I want to have an arrow point the way to the destination, rotated by the css rotate property, so I need the degree heading (from 0 to 360). 0 would mean go straight up, 180 would mean go straight down, 90 would be go right, etc.

Most posts say to get the bearing in degrees by using: (Math.atan2(105 - 100, 96 - 100) * 180) / Math.PI;. But I can't get that to work. With the above coordinates, I get 128.6598... which would be down and to the right. It also gives negative numbers which would throw it off as well. I've tried adding various degrees depending on if it's positive or negative, but haven't gotten anything to work.

Is there a way to get the correct heading (from 0 to 360) for the two points? Thanks!

2

2 Answers

2
votes

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:

Visualisation 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.

1
votes

I think, it should be {end point} - {starting point}

(Math.atan2(96 - 100, 105 - 100) * 180) / Math.PI;
// giving -38.659808254090095 = Down to the right