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".
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:
… 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,
// 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.