12 minute read

Sometimes you need to clamp angles, and it’s a bit harder when they’re actually vectors.

There’s a pretty good series of blog posts by the ever-brilliant Inigo Quilez about avoiding trigonometry in geometric code. I generally agree with this for cases where you’re developing a library for others to use, or when you have a lot of time to think about the approach. When you’re prototyping or writing gameplay code… I’m less certain it’s worth the trouble, at least unless you know how to do the operation already (or it’s one of the ones that has a blog post).

Anyway, let’s consider the case of clamping angles. This is already kind of tricky even when working with scalars, since they’re inherently modular. I have a utility I copy and paste around sometimes to help for this that I call modclamp:

pub fn modclamp(v: f32, min: f32, max: f32, min_of_range: f32, max_of_range: f32) -> f32 {
    debug_assert!(max_of_range > min_of_range);
    let modulus = max_of_range - min_of_range;
    let delta = (max - min).rem_euclid(modulus);
    let diff = (v - min).rem_euclid(modulus);
    let r = if diff <= delta {
        v
    } else if diff - delta <= modulus - diff {
        max
    } else {
        min
    };
    (r - min_of_range).rem_euclid(modulus) + min_of_range
}

Note: If you aren’t a Rust user, a.rem_euclid(b) is essentially the same as ((a % b) + b) % b. If b is positive, then it is a “wraparound”/”modulo” type function. I’m not a fan of the name it uses.

Anyway, modclamp is useful for angle clamping. In addition to taking the value, min, max that clamp normally does, it takes a range_min and range_max. This is good because sometimes you represent angles as -PI to PI radians, other times as 0 to TAU (aka 2.0 * PI). This kind of thing comes up in other cases too.

Note that the version in this code doesn’t validate the inputs at all, but you might want to check some things here.

Anyway, with that in hand, the all-scalar version of clamping angles will either be modclamp(v, min, max, -PI, PI) or modclamp(v, min, max, 0.0, TAU) depending on the conventions of your codebase (and if your codebase uses degrees for some reason, it would be 180.0 for PI and 360.0 for TAU).

Note: For those hard-coded cases, you’re going to likely be better off defining a dedicated function – modclamp is nice because it’s flexible, but it still has three float modulo operations, which aren’t known for their performance (to be clear, it’s far from the slowest thing we’ll talk about in this blog post, though).


Now, I hear you thinking: That’s all fine and good, but isn’t the point of this post supposed to be about avoiding trig and working with vectors? Sure, the code above has no trig in it, but if you’re working with scalar angles, trig isn’t far behind.

And the answer is yes, I’m getting to it.

So, if you aren’t going to work with scalar angles, the typical approach is to work with unit vectors in the direction of the angle. This has the nice property that everybody agrees what it means (unlike scalar angles, which as implied above, are split between degrees and radians, and then further split between whether or not negatives are allowed). But more than that, it’s much faster, and can avoid edge cases in the trigonometric functions. Honestly, if you’re not sold on this, go read the blog posts from Inigo Quilez I linked above, since they do a better job explaining why to avoid trig than I ever could. I’m just going to assume that it is something you actually want to do.

So, in Rust code this probably would look like defining a Rot2 type which guarantees that the input is normalized, and so on. That’s fine and good, but I’ve already wasted too much time in this post, so we’re just going to use Vec2’s that the caller has pinky-promised are already normalized. The naive approach is:

fn vec_to_angle(v: Vec2) -> f32 {
    v.y.atan2(v.x)
}

/// Inputs must all be unit (normalized) vectors.
pub fn clamp_angle_naive(v: Vec2, min: Vec2, max: Vec2) -> Vec2 {
    let min_a = vec_to_angle(min);
    let max_a = vec_to_angle(max);
    let v_a = vec_to_angle(v);
    // See above for a quick description of `rem_euclid`.
    let range_a = (max_a - min_a).rem_euclid(TAU);
    let a = (v_a - min_a).rem_euclid(TAU);
    if a <= range_a || min == max {
        v
    } else if a - range_a <= TAU - a {
        max
    } else {
        min
    }
}

This is pretty terrible, and in practice it’s actually better than some of the implementations I’ve seen… but only in that it avoids a superfluous sin/cos call at the end by observing that the result is always going to be one of the input vectors, so there’s no need to do something like computing the angle, and then call an angle_to_vec function.

It still has three invocations of atan2 (a very slow function), in addition to several modulo operations. A better approach is to avoid converting to scalar angles at all, so that’s what we’ll do.


Let’s start by thinking about what exactly it is that we’re looking for. That is, what does it mean for v to be “between” min and max when they’re angles? In the case where there’s no wraparound, we want vec_to_angle(min) <= vec_to_angle(v) and vec_to_angle(v) <= vec_to_angle(max)… but how would we explain this in a way that handles that (ideally without directly invoking modular arithmetic angle representation, or adding special cases)?

The solution is to realize that, because we know that min, max, and v are all unit (normalized) vectors, we know they’re points on a circle (the unit circle). And we also know that (conventionally) positive angle values represent counter-clockwise rotations, and therefore increasing the angle is travelling along that circle in a counter-clockwise direction. This means that we can say that v is “between” min and max if: when starting at min and travelling counter-clockwise around the unit circle, we reach v before we reach max.

Or, in other words, when the triangle made up by the points (min, v, max) is counter-clockwise, then v is between min and max, and when it is clockwise, v is not.

This might not be terribly intuitive, so here’s a demo that can help you play around with the three values, see when they are clockwise vs counterclockwise, and prove to yourself that this works. It’s probably a bit too fancy (or maybe not fancy enough1), but hopefully it helps.

Triangle between min, max, and v is:

Anyway, as I was saying, computing the winding (the term for whether or not a set of points is clockwise or counterclockwise) for these three points can be done straightforwardly, as ((v.x - min.x) * (max.y - min.y) >= (v.y - min.y) * (max.x - min.x)). (This is basically orient2d(min, v, max) >= 0 inlined, and the predicate moved around). Then, if that isn’t true, we need to either return min or max based on which is closer, which is just a dot product check.

So our final function is:

/// Inputs must all be unit (normalized) vectors representing angles.
///
/// Clamps `v` to be between `min` and `max`.
#[inline]
pub fn clamp_angle(v: Vec2, min: Vec2, max: Vec2) -> Vec2 {
    debug_assert!(v.is_normalized());
    debug_assert!(min.is_normalized());
    debug_assert!(max.is_normalized());
    if (v.x - min.x) * (max.y - min.y) >= (v.y - min.y) * (max.x - min.x) {
        // between min and max.
        v
    } else if v.dot(max) >= v.dot(min) {
        // closer to max
        max
    } else {
        // closer to min
        min
    }
}

And this works well. The main caveat is that if you intend to use this with min == max, you might want to add a boolean flag for how that’s interpreted (it could either mean “must be exactly min/max”, or it could mean allowing any value, depending on whether or not you’re going around the circle the long way). The versions of these functions on this page (besides modclamp, I suppose) choose to allow any values in that case, but that’s mainly because of the definition of “between” I come to above. You can change that by changing the >= to > in the first test (e.g. (v.x - min.x) * (max.y - min.y) > (v.y - min.y) * (max.x - min.x)), since when min == max, the points are colinear. (For the naive scalar version, should you choose to use it, just remove the explicit min == max test).

Anyway, hope that’s useful for someone.

P.S. It’s really been a while since I wrote a blog, huh? Anyway, I have another one coming up too, hopefully. (This one was written while procrastinating writing that one 😅).


Appendix: But What Does All That Math Do?

Oh… you’re, uh, still here?

Ah. I suppose it was a bit weird for me to write a whole dang blog post about a function, and then not explain the internals of that function at all. Well, I guess I can do a bit better. (Honestly, I’m not terribly happy with the explanation here, but it feels a bit bad to not even give it a shot, so here it goes).

Let’s start with the CW/CCW test.

Really, what’s happening here is that the triangle is translated so that min (the choice is arbitrary, you could choose any of the points) is at the origin, e.g.

let p = v - min;
let q = max - min;

And then the 2D cross product (p.x * q.y - p.y * q.x) is used on a and b, which is known to provide the winding in such a case in the sign. We then used p.x * q.y >= p.y * q.x instead of p.x * q.y - p.y * q.x >= 0.0 in order to avoid an extra arithmetic operation (the subtraction), but really that change is perhaps mostly taste.

So, what’s up with the 2D cross product, p.x * q.x - p.y * q.y, and why does it tell us whether the winding is CW vs CCW? Well, there are a bunch of different ways of looking at it, but here’s the one I like the best: do you remember the slope intercept form of a line from way back in like, Algebra 1 back in high/middle/secondary/whatever-school? The y = m*x + b (Or, y’know, y = m*x + y0. Same thing) stuff? This is basically that.

If you don’t remember, it’s a method for defining a linear function that makes the slope of the line (m) and the point where it intecepts the y-axis (b) easy to see at a glance. It also makes it easy to plug a number in for x and see the value of the y coordinate (e.g. the height).

Now let’s imagine that p.x > 0 (e.g. the triangle’s first edge points to the right) and we’re defining the line from the origin ({x: 0, y: 0}) to p. This gives us something like p.y/p.x for the slope, and y = x * p.y/p.x for the overall equation (the y-intercept being 0, since one of the ends is known to be y=0).

Now, because this line has two of the points of the triangle on it, the question of the winding is basically “Is q (the third point of the triangle) above this line, or is it below this line?” To find out the answer to this, we start by plugging q.x in for x, giving us q.x * p.y / p.x, which gives us the y value (the height) of the line when it has the same x position as q. Then, whether or not q.y is greater or lesser than q.x * p.y / p.x tells us whether q is above the line, and consequently whether or not the triangle is clockwise or counter-clockwise (e.g. q.y <=> q.x * p.y / p.x, where <=> is my hand-wavey stand-in for “idk some comparison”). To make it equivalent to the expression we had, we use good ol’ algebra to rewrite this as 0 <=> q.y - q.x * p.y / p.x, and then multiply through by p.x to get 0 <=> p.x * q.y - q.x * p.y.

Now, remember when I said “let’s imagine that p.x > 0”? If we instead assumed that p.x < 0, then a lot of this would be exactly backwards, until the end, when we multiplied through by p.x to flip it again.

Make sense? No? Ah well, it was worth a shot. Maybe a diagram would have helped, but tragically I already blew this blog post’s budget on the interactive diagram above (what, you think those things grow on trees? I’m not made of money). But… I bet you have a piece of paper and a pencil somewhere nearby, why don’t you draw it out. I think you can figure it out (I believe in you), but even if you can’t… when was the last time you drew something on paper? It was worth it just for that.


Now, for the dot product. That’s way easier. The important thing to know is that dot(a, b) is equivalent to (but usually much cheaper to compute than) vector_magnitude(a) * vector_magnitude(b) * angle_between(a, b).cos(). The reason for this has to do with the definition of cosine as the adjacent/hypotenuse in a right triangle (or at least that’s one way of thinking about it). The magnitude of unit vectors is 1.0 by definition, so dot(a, b) for unit vectors is just 1.0 * 1.0 * angle_between(a, b).cos(), or just angle_between(a, b).cos(). The cosine is largest when the input angle is smallest (in the range 0.0..=PI, at least), so v.dot(max) >= v.dot(min) is just how cool folks check if the angle between v and max is closer than the angle between v and min.

Anyway, that’s all I’ve got!

  1. It would probably be better if I let you move without repainting on every mouse move and scroll is annoying (but repainting on every move/scroll eats battery for no reason). After I started to read the IntersectionObserver docs, I decided that the sliders were Fine, Actually.

    Arguably a bigger problem is that it doesn’t even use the algorithm from this post! If you want to see a similar (by which I mean totally different) demo that does use the algorithm from this post and does support mouse input, see here. (The code is awful, of course, but it’s what I was messing with when I was trying to figure out how to do this).