wrap_value
We are going to start by defining wrap_value
, because are going to need it:
function wrap_value(value, from, to)
local range = to - from
return value - (range * math.floor((value - from) / range));
end
This function takes a value and wraps it in a range. As if you had gone past one end of the range and entered by the opposite one. For example wrap_value(14, 0, 10)
is 4
, and wrap_value(-4, 0, 10)
is 6
.
This formula is very similar to the generalization of mod
(remainder) to the real numbers:
value - size * math.floor(value / size)
There we are figuring out how many times size
fits in value
(value / size
). Except we want an integer number of times (math.floor(value / size)
), and we subtract it from value
so that value
is not bigger than size
. It also works with negatives.
The above formula is similar to math.fmod
, except without preserving the sign. To be more precise, it differs from math.fmod
in that the result always have the same sign as size
.
By the way, if you make size
equal to 1
, you get a formula for the fractional part (and unlike math.modf
the result is always positive, it gives you the fractional part towards zero).
Now, if we don't want a number between 0
and size
, but a number in some range from a
to b
, we still need to know how many times value
fits in the size of the range, so we are dividing b - a
. And thus we are also multiplying by b - a
. The only difference is that we need to offset the value
when we divide it:
value - (b - a) * math.floor((value - a) / (b - a))
Note that when a
is 0
, we have the same formula from before.
angle_diff
Now we can define a function to get the short difference between angles:
half_turn = 0.5;
function angle_diff(from, to)
return wrap_value(to - from, -half_turn, half_turn)
end
I believe you said the angles are in the range from 0
to 1
, meaning half turn is 0.5
. Otherwise you can put pi
if you are using radians, or 180
if you are using degrees or whatever.
What we are doing here is wrapping the difference of the angles (to - from
) in the range from -half_turn
to half_turn
. That gives us the shortest way to turn from one to the other (it is as much half turn one way or the other).
appr
And if you want to move one angle towards the other without overshooting, you would be doing this:
function appra(value, target, by)
local diff = angle_diff(value, target)
local sign = (diff > 0 and 1 or -1)
local offset = math.min(math.abs(by), math.abs(diff)) * sign
return value + offset
end
Here I have separated the sign and the magnitud (absolute value) of diff
. And that magnitud is by how much we can vary the value without overshooting. So we use it constraint the magnitud you want to vary by, then we apply the sign so it is turning in the correct direction.
Except that fails your tests. You want to wrap the result:
full_turn = 2.0 * half_turn;
function appra(value, target, by)
local diff = angle_diff(value, target)
local sign = (diff > 0 and 1 or -1)
local offset = math.min(math.abs(by), math.abs(diff)) * sign
return wrap_value(value + offset, 0, full_turn)
end
Those tests…
That code above… It does not pass your tests. Except I blame the tests. Here:
lfequal(appra(0, 0.4, 0.1), 0.9)
If we are at 0
and going towards 0.4
, it should be shorter to increase (it is at a distance of 0.4
) than to decrease (it is at a distance of 0.6
). Thus, advancing 0.1
from 0
towards 0.4
should bring the value to 0.1
not 0.9
.
Similarly, here:
lfequal(appra(0.3, 0.4, 0.1), 0.2)
If we are at 0.3
and going towards 0.4
, it should be shorter to increase (it is at a distance of 0.1
) than to decrease (it is at a distance of 0.9
). Thus, advancing 0.1
from 0.3
towards 0.4
should bring the value to 0.4
(which, by the way, is the goal) not 0.2
.