9
\$\begingroup\$

I have the following JavaScript code to work out the angle between two points (clockwise). The code I have seems a bit "hacky" to me, as I have a while loop ensuring the number is not negative (to make the range 0-360), as well as I have to subtract 270 degrees to get it to work correctly. Is there a better way to do this?

this.angle = function(point)
{
    var delta = point.subtract(this);
    var aR = Math.atan2(delta.y, delta.x);
    var aD = aR * (180 / Math.PI);
    aD -= 270;
    while (aD < 0)
    {
        aD += 360;
    }
    return aD;
};
\$\endgroup\$
  • 2
    \$\begingroup\$ The angle relative to what? \$\endgroup\$ – Shmiddty Oct 10 '13 at 17:19
  • 3
    \$\begingroup\$ The angle has to be relative to something. The angle between two points is always 0. I assume you want the angle relative to the horizontal axis. \$\endgroup\$ – Shmiddty Oct 10 '13 at 17:21
  • 2
    \$\begingroup\$ Oh for the love of pedanticism \$\endgroup\$ – Frambot Oct 10 '13 at 17:21
  • 1
    \$\begingroup\$ @retailcoder "Angle in radians" and "angle in degrees", I believe. \$\endgroup\$ – 200_success Nov 14 '13 at 17:30
  • 1
    \$\begingroup\$ These aren't javascript, but you may want to look at these implementations: stackoverflow.com/questions/4633177/… \$\endgroup\$ – Andrew Hundt Apr 5 '15 at 4:47
14
\$\begingroup\$

First of all, let's define the problem precisely. According to your comment, this is the behavior you want:

You are correct in believing I am expecting it to be relative to 12 o'clock, and for it to move clockwise.

However, your code doesn't actually behave that way. Your angle() function, as written, computes the orientation of point relative to this, with 6 o'clock being 0°, angles increasing counterclockwise, with return values in the range [0, 360). (Alternatively stated, your angle() computes the orientation of this relative to point, with 12 o'clock being 0°, angles increasing counterclockwise, with return values in the range [0, 360).) So, I'm reviewing erroneous code.

As evidenced by the comments, many people had problems understanding what you meant by the "angle between two points". That's a vocabulary problem. I suggest that you borrow a term from navigation: bearing. In this review, I'll use the term "bearing" to refer to your desired behavior: when point is taken relative to this, 12 o'clock is considered to have a bearing of 0°, with values increasing clockwise, up to 360°. I'll continue to use the term angle to refer to the erroneous behavior of your angle() function.


First of all, let's see how we can calculate the angle without having to do aD -= 270. We can accomplish this by mapping the axes strategically when calling atan2(). The direction for which we want the arctangent to be 0 is an "x′ axis" of our mapped plane, and the direction for which we want the arctangent to be π/2 we will call our "y′ axis".

Axis-mapping diagram for nicerAngle()

Here it is written out:

// Same behavior as angle(), but implemented without aD -= 270.
Point.prototype.nicerAngle = function(point) {
    var delta = point.subtract(this);
    var aR = Math.atan2(delta.x, -delta.y); // Mapped axes!
    var aD = aR * (180 / Math.PI);
    if (aD < 0) {
        aD += 360;
    }
    return aD;
};

Note that your while loop can simply be an if conditional.


Note that Math.atan2() is guaranteed to return a value in the range [-Math.PI, +Math.PI]. (See footnote below.) We should now ask, is it possible to get rid of the conditional altogether?

One reason to get rid of the conditional is to reduce the number of code paths — fewer code paths means fewer cases to worry about in code-coverage tests. Another reason is performance — if the CPU can always execute the code sequentially, then it doesn't have to do branch prediction. When faced with a branch, CPUs speculatively execute code in advance, then throw away the results from the branch not taken. (That's true for a low-level language like C. It probably wouldn't matter at all for a naïve JavaScript interpreter. It might make a difference on a highly optimized JavaScript JIT. Anyway, the code-path-reduction argument alone should be a good reason to try to eliminate the conditional.)

Well, if we always add π to the value returned by Math.atan2(), then the result would be in the range [0, 2 * Math.PI]. It's a simple matter of mapping the axes to make that work for us. We want to map the axes such that 12 o'clock yields an atan2 of 0° (our x″ axis), increasing counterclockwise such that 9 o'clock is 90° (our y″ axis). If we can accomplish that, then adding 180° would map 6 o'clock to 0° and 3 o'clock to 90°. Here is a diagram:

Axis-mapping diagram for unconditionalAngle()

… and a solution:

// Same behavior as nicerAngle(), but implemented without if-statement.
Point.prototype.unconditionalAngle = function(point) {
    var delta = point.subtract(this);
    var aR = Math.atan2(-delta.x, delta.y);
    return 180 + aR * (180 / Math.PI);
};

So, what about computing bearing? Once again, we simply have to remap the axes when calling atan2(). Using the same ideas as before,

Axis-mapping diagram for bearing()

// Computes orientation of point relative to this, with 12 o'clock being 0 degrees, angles
// increasing clockwise, with return values in the range [0, 360].  Implemented like
// unconditionalAngle().
Point.prototype.bearing = function(point) {
    var delta = point.subtract(this);
    return 180 + 180 / Math.PI * Math.atan2(-delta.x, -delta.y);
};

Of course, if you really wanted top performance, you could make this a one-liner by doing away with the intermediate delta object.


Footnote about unconditionalAngle() and my unconditional implementation of bearing()

Note that I said that Math.atan2() returns values in the range [-Math.PI, +Math.PI], not [-Math.PI, +Math.PI). Therefore, with the unconditional implementations, it is possible to get a result of 360°, for example, with ORIGIN.unconditionalAngle(new Point(-0, -1)) and ORIGIN.bearing(new Point(-0, 1)). All I'll say here is that proper IEEE floating point math has a useful concept of negative zero. I'll leave it to you to decide whether you want to deliberately stamp out this manifestation of that concept.


Here's a jsFiddle of all the code above.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.