1
\$\begingroup\$

I have a function for approaching a value to a target by a certain amount:

function appr(value, target, by)
  return value < target and min(target, value + target) or max(target, value - target)
end

I have an angle between 0..1 range, and a target angle.

How can I approach this angle to the target by smallest of the turns.

So these tests shall pass:

lrun('test1', function()


  lfequal(appra(0, 0.2, 0.1), 0.1)

  lfequal(appra(0, 0, 0.1), 0.0)
  lfequal(appra(0.5, 0.5, 0.1), 0.5)
  lfequal(appra(0.5, 0.55, 0.1), 0.55)

  lfequal(appra(0, 0.5, 0.1), 0.9)
  lfequal(appra(0, 0.4, 0.1), 0.9)
  lfequal(appra(0.3, 0.4, 0.1), 0.2)
  lfequal(appra(0, 0.6, 0.1), 0.9)
  lfequal(appra(0.1, 0.9, 0.1), 0.0)
end)

I tried this but it fails on the first test case.

function appra(value, target, by)
  local diff = (target - value + 0.5) % 1 - 0.5
  if math.abs(diff) < by then return target end
  return diff < -0.5 and value + by or (value - by + 1) % 1
end
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .