raw Math
RAW Math Geometry Angles and Arcs

Is an Angle Between Two Other Angles?

Robert Eisele

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.