BIT-101
Bill Gates touched my MacBook Pro
The next Function Of The Day is wrap
. This one works in a similar way to clamp
- if the value is in the range, it does nothing. But if the value is outside the given range, it will wrap it around to the other side of the range.
Sometimes you want a value to remain in a specific range, but if it goes outside that range you don’t want to just clamp it. Instead maybe you want it to come back in at the other end of the range. Think of the old computer game, Asteroids. If you fly off one edge of the screen, you reappear on the other side. That’s how wrap works.
An example:
for (i = 5; i <= 12; i++) {
x = wrap(i, 0, 10)
}
This should have x
set to 5, 6, 7, 8, 9, 0, 1, 2
.
In a simple example like this, wrap is acting as a modulus operator, just as if you said:
for (i = 5; i <= 12; i++) {
x = i % 10
}
But that’s only because our range starts at zero. With wrap
, the range can be anything. for example…
for (i = 2; i <= 12; i++) {
x = wrap(i, 5, 10)
}
This wraps the value to always be in the range of 5 to 9. So x
will equal 7, 8, 9, 5, 6, 7, 8, 9, 5, 6, 7
.
Note that the range is inclusive of the lower value, but exclusive of the upper value.
This also needs to work with negative numbers and ranges. We have to be careful here because, oddly, the built-in modulus function (usually %
) works differently in different languages when it comes to using negative numbers.
For example, in Go, as well as C, C++, Java, and many other languages:
-7 % 3 = -1
7 % -3 = 1
However, in Python and some other languages:
-7 % 3 = 2
7 % -3 = -2
In case you’re interested, here’s a full write up on why it’s different:
https://torstencurdt.com/tech/posts/modulo-of-negative-numbers/
To be careful with this, I just avoid using the built-in modulo operator and use floored division, as defined by Donald Knuth. The floored division implementation of modulo looks like this:
a - b * floor(a / b)
This gives us the same results that Python gives for modulo. Whether or not you agree that’s the “correct” way, it will work the way we expect it to for wrapping, and using this definition rather than the language default operator will work the same whatever language you use it in.
So finally we come to the implementation of wrap
function wrap(val, min, max) {
val -= min
max -= min
return min + val - max * floor(val / max)
}
First, we just subtract min from both val
and max
. This puts the range into the form of zero to whatever. Once it’s there, we can just do a modulo (using the floored division method), and then add min
back to the result.
In this example, I made 100 random points that move left to right. There’s a 400x400 canvas, but I want them to stay in a square in the center which goes from 100 to 300 on both the x and y axes.
I drew the center rectangle and then drew each point, then added 5 pixels to each point’s x value. And then I used wrap
to wrap the x value to a range of 100, 300.
// for each x, y point...
circle(x, y, 1)
x += 5
x = wrap(x, 100, 300)
Now when a point goes beyond an x of 300, it wraps back to 100.
Another way I use this function is in adjusting angles. Generally, it’s nice to have your angles in a range of 0 to 2 * PI. Or maybe -PI to PI.
But sometimes you calculate an angle using some complex algorith, which may result in the angle being way outside of those ranges. Luckily angles naturally wrap around a full circle, so that 370 degrees is the same as 10 degrees, and -10 degrees is the same as 350 degrees. So our wrap
function is an easy way to get these into line.
I made two functions that are useful for these cases.
// TAU = PI * 2
function wrapTau(value) {
return wrap(value, 0, PI * 2)
}
function wrapPi(value) {
return wrap(value, -PI, PI)
}
I know this isn’t always necessary. Trig functions will still work with multiples of angles, but if you need to display the angle number, or use the angle in a function like norm
or map
that requires it to be within a range, this is helpful.
Comments? Best way to shout at me is on Mastodon