Numbers
- Import from
@gabreusi/hyrax- Runs in
- Node, browsers, Deno, Bun
- Size budget
- clamp ≤ 150 B
- Examples
- Run, answers asserted
Small functions for the arithmetic that shows up in every UI and game: keeping a value in range, blending two values, turning a value into a fraction of a range, moving a value from one range to another, wrapping around a range, snapping to a grid, stepping toward a goal, and listing the numbers of a range.
The hollow dot is value. The filled dot is what clamp keeps of it.
clamp(130, 0, 100)// => 100ratio(130, 100)// => 1.3remap(130, [0, 100], [0, 360])// => 468lerp(0, 100, ratio(130, 100))// => 130clamp
clamp(value, min, max) keeps value inside the range. With two arguments the second one is the maximum, and there is no minimum.
clamp(5, 0, 10); // => 5
clamp(-1, 0, 10); // => 0
clamp(11, 0, 10); // => 10
clamp(15, 10); // => 10If you pass the bounds the wrong way round, clamp swaps them instead of returning nonsense:
clamp(5, 10, 0); // => 5
clamp(50, 5, 100); // => 50It only takes numbers. The 0.x version also accepted numeric strings, and sorted its arguments as text, so clamp(50, 5, 100) gave the wrong answer. A string is now a type error, and a NaN stays NaN.
lerp
lerp(a, b, t) is linear interpolation: the value t of the way from a to b. It does not clamp t, so t above 1 overshoots. That is what you want for extrapolation, and clamp is one call away when you do not.
lerp(0, 10, 0.5); // => 5
lerp(10, 20, 0.25); // => 12.5
lerp(0, 10, 2); // => 20
lerp(0, 10, clamp(2, 0, 1)); // => 10ratio
ratio(value, max = 100, min = 0) answers "how far along is value in the range", as a fraction. It returns a fraction, and not a percentage, so that you can multiply it by anything and keep proportions. It does not clamp either. An empty range (max equal to min) gives 0, never NaN.
ratio(50); // => 0.5
ratio(30, 60); // => 0.5
ratio(75, 100, 50); // => 0.5
ratio(5, 0); // => 0
const progress = ratio(30, 60) * 200; // => 100It used to be called percent
The old name promised a percentage and returned a fraction. See the migration guide.
remap
remap(value, [inMin, inMax], [outMin, outMax]) moves a value from one range to another. The ranges are pairs, so the call reads the way you say it. An input range of zero width returns outMin, and the 0.x map returned NaN or Infinity.
remap(5, [0, 10], [0, 100]); // => 50
remap(0.5, [0, 1], [10, 20]); // => 15
remap(5, [10, 0], [0, 100]); // => 50
remap(5, [3, 3], [0, 100]); // => 0Combine it with clamp to keep the result in the output range:
clamp(remap(12, [0, 10], [0, 100]), 0, 100); // => 100wrap
wrap(value, min, max) is the cyclic sibling of clamp: a value that runs past one end of the range comes back in at the other, the way an angle wraps at 360 or a carousel goes from the last slide to the first. min is included and max is not. With two arguments the range is [0, max), which is what a list index needs.
wrap(370, 360); // => 10
wrap(-1, 3); // => 2
wrap(12, 0, 10); // => 2
wrap(5, 10, 0); // => 5Like clamp, it accepts the bounds in either order. An empty range gives min, and a value that is not finite gives NaN, since there is no sensible place to wrap infinity to.
inRange
inRange(value, min, max) checks that min <= value < max. The end is excluded so that ranges that touch, such as [0, 10) and [10, 20), never both claim a value. With two arguments the range is [0, max), so inRange(i, list.length) checks an index. NaN is never in range.
inRange(5, 0, 10); // => true
inRange(10, 0, 10); // => false
inRange(2, 3); // => true
inRange(5, 10, 0); // => truesnap
snap(value, step = 1, origin = 0) rounds to the nearest point of a grid that goes through origin every step. The result is rounded to the decimal places of step, so the float error of the arithmetic does not leak out.
snap(7, 5); // => 5
snap(8, 5); // => 10
snap(0.1 + 0.2, 0.1); // => 0.3
snap(12, 5, 1); // => 11
snap(2.6); // => 3A step of 0, or one that is not finite, leaves the value as it is instead of returning NaN. Halfway values round up, as with Math.round.
approach
approach(current, target, delta) moves current toward target by at most delta, and never past it. It is one step of an animation or a game loop that must land exactly on its goal, where current + speed would overshoot and then oscillate.
approach(0, 10, 3); // => 3
approach(9, 10, 3); // => 10
approach(10, 0, 4); // => 6The sign of delta does not matter. An infinite delta arrives at once, and a delta of NaN does not move instead of turning the value into NaN.
range
range lists numbers, with the end excluded. It has the same shapes as clamp: one argument is the end and the start is 0, which gives the indices of a list.
range(4); // => [0, 1, 2, 3]
range(1, 5); // => [1, 2, 3, 4]
range(0, 1, 0.25); // => [0, 0.25, 0.5, 0.75]
range(0, 0.4, 0.1); // => [0, 0.1, 0.2, 0.3]It counts down when the end is below the start, whatever the sign of the step, so a wrong sign can never loop forever. The values are rounded like snap's, so steps of 0.1 do not drift. A step of 0, or a bound that is not finite, gives [].
range(5, 0, 2); // => [5, 3, 1]
range(5, 0, -2); // => [5, 3, 1]
range(0, 10, 0); // => []Reference
The full signatures, with every option and error, are in the API reference: clamp, lerp, ratio, remap, wrap, inRange, snap, approach, range.