Understanding lerp()

in

A lerp(a, b, t)—or linear-interpolation—is a function that returns a new value between two known samples, such that when tt is 0.00.0, then value of aa is returned, when tt is 1.01.0 then the value of bb is returned, and when tt is 0.50.5 then the value equidistant between aa and bb is returned.

Consider the following:

const lerp = (a: number, b: number, t: number) =>
  a + t * (b - a);

We can use this function to generate new values between two existing, known values:

const [start, end] = [10, 50];

console.log(lerp(start, end, 0.1));   /* at 10% */
// ❮ 14

console.log(lerp(start, end, 0.5));   /* at 50% */
// ❮ 30

console.log(lerp(start, end, 0.986)); /* at 98.6% */
// ❮ 49.44

But Wait!

There are potential risks involved with the previous implementation due to floating-point arithmetic errors. In this case, due to rounding errors, this method does not guarantee that lerp(a, b, 1.0) === b. For example, let’s try to lerp between one huge value and one very small value, using the previous implementation:

console.log(lerp(1e8, 1e-8, 0.0));
// ✅ 100,000,000

console.log(lerp(1e8, 1e-8, 1.0));
// ❌ 0

Ouchies! Because the difference between bb and aa is smaller that what a 64-bit floating point number can represent, it gets rounded out when doing (ba)(b-a). We end up getting a value of 00, which is outside the range of [a,b][a,b]. To correct for this, it is sometimes recommended to use this implementation, instead:

const lerp = (a: number, b: number, t: number) =>
  (1 - t) * a + t * b;

Given that tt is in the range of [0,1][0,1], this method will precisely start and end with aa and bb respectively.

console.log(lerp(1e8, 1e-8, 0.0));
// ✅ 100,000,000

console.log(lerp(1e8, 1e-8, 1));
// ✅ 0.00000001

However, this is not without its own caveat: outside the range of [0,1][0,1] then this function may not be monotonic. Again, this is due to the limitations of floating-point approximations.

Higher-Order Lerps

It is also sometimes useful to lerp between vectors or tuples, this can easily be achieved by lerping over each component of the vector/tuple:

type Vec2 = [number, number];

const vecLerp = (a: Vec2, b: Vec2, t: number): Vec2 =>
  [lerp(a[0], b[0], t), lerp(a[1], b[1], t)];

This can obviously be extended to vectors and tuples of any length.