A recurring problem in 2D geometry code is deciding whether a direction, given as an angle, falls inside an arc spanned by two other angles. It looks trivial at first: just check whether the angle is numerically between the two bounds. The trouble starts once angles are allowed to wrap around the circle, which they always are in practice.
Let \(\varphi\) be the angle to test and \(\theta_1\), \(\theta_2\) the two bounding angles, drawn above as solid rays with \(\varphi\) as the dashed one; the highlighted arc is the region where \(\varphi\) should count as between. As long as all three values happen to lie in the same \(360^\circ\) turn and \(\theta_1<\theta_2\), the naive comparison
\[ \theta_1 \le \varphi \le \theta_2 \]
works fine. But angles coming out of atan2, accumulated rotations, or user input rarely respect that assumption. \(\varphi\) might be \(-30^\circ\) where \(330^\circ\) was meant, or \(\theta_2\) might be \(400^\circ\) after a full extra turn. Comparing such values directly is meaningless, so the first real step is to bring every angle into one common representative range.
Normalizing an Angle to [0°, 360°)
The most direct way to normalize an angle is a loop that keeps nudging it by a full turn until it lands in range:
while (angle < 0) angle += 360;
while (angle >= 360) angle -= 360; This is correct, but it is a loop where a formula should do. The underlying fact is just that \(360^\circ\) rotations do not change direction, so \(\varphi\) and \(\varphi + k\cdot360^\circ\) describe the same angle for any integer \(k\). What is wanted is the representative of that equivalence class inside \([0^\circ,360^\circ)\), which is precisely what the modulo operation computes, up to one detail: in JavaScript (and in C, Java and most other mainstream languages), the result of % keeps the sign of the dividend, so angle % 360 lands in \((-360^\circ,360^\circ)\) rather than \([0^\circ,360^\circ)\). Adding one more \(360^\circ\) before taking the remainder a second time shifts the negative half of that range back up without touching the positive half:
angle = (360 + angle % 360) % 360; Tracing through both signs makes the two-step reduction concrete. For \(\text{angle}=750\), the inner remainder gives \(750 \bmod 360 = 30\), and \((360+30)\bmod360=30\) again, so the value is left alone once it is already in range. For \(\text{angle}=-100\), the inner remainder gives \(-100 \bmod 360=-100\), and \((360-100)\bmod360=260\), the equivalent positive angle.
Since only the sign of the intermediate result ever needs correcting, and \(-360^\circ\) is the worst case produced by angle % 360, adding exactly one extra \(360^\circ\) is enough; no larger multiple is needed for correctness. It is tempting to shortcut this into a single modulo by replacing the small offset with a much bigger one instead, for example
angle = (3600000 + angle) % 360; where \(3600000\) is \(360^\circ\) times \(10000\) full turns. The addition alone can then no longer produce a negative number for any realistic input, so the first modulo looks unnecessary. But this only works as long as no angle ever exceeds that offset in magnitude, which is a silent assumption rather than a guarantee, and the constant itself is arbitrary: nothing about the problem favors \(10000\) turns over \(100\) or \(1{,}000{,}000\). The two-step version above makes no such assumption, handles every finite \(\text{angle}\) correctly regardless of magnitude, and is the one used from here on:
angle = (360 + angle % 360) % 360; The Wrap-Around Case
With every angle normalized to \([0^\circ,360^\circ)\), the naive comparison \(\theta_1 \le \varphi \le \theta_2\) still fails whenever the arc itself wraps past \(0^\circ\), i.e. whenever \(\theta_2 < \theta_1\) once both are normalized, as drawn above: \(\theta_1\) lies just before \(0^\circ\), \(\theta_2\) just after it, and the highlighted arc wraps through \(0^\circ\) with \(\varphi\) inside it. The fix is to split the test into the two cases that can occur:
\[ \text{angleBetween}(\varphi,\theta_1,\theta_2) = \begin{cases} \theta_1 \le \varphi \le \theta_2 & \text{if } \theta_1 \le \theta_2 \\ \theta_1 \le \varphi \lor \varphi \le \theta_2 & \text{if } \theta_1 > \theta_2 \end{cases} \]
In the second case the arc runs from \(\theta_1\) up through \(360^\circ\) and continues from \(0^\circ\) up to \(\theta_2\), so \(\varphi\) qualifies if it lies in either of those two pieces. Putting the normalization and the case split together gives a complete, branch-light implementation:
function angleBetween(phi, theta1, theta2) {
phi = (360 + phi % 360) % 360;
theta1 = (360 + theta1 % 360) % 360;
theta2 = (360 + theta2 % 360) % 360;
if (theta1 <= theta2)
return theta1 <= phi && phi <= theta2;
return theta1 <= phi || phi <= theta2;
} All three inputs go through the same normalization, so it does not matter whether they arrive as small positive angles, negative angles, or values accumulated over many turns; only their position on the circle is compared in the end.
Angles.js implements this kind of normalization and interval check as part of a broader set of angle utilities, including differences, clamping and interpolation between angles, for cases where more than a single between-check is needed.