This gist isn't mathematically rigorous so don't expect it to be as formal as something you'd find in academia.
This problem arises in various math and computer science topics where your input range needs to be transformed to fit into a different range. For example, let's say you had frequency data of a song at a particular moment in time. Each sample is a value that ranges from 0
to 255
. In interval notation, it would be written as [0, 255]
. The square brackets denote that the endpoints are inclusive - in other words, 0
and 255
are acceptable. We want to take a sample in this range and map it to a different range. Let's say our output range is [-1, 1]
. The [-1, 1]
range actually pops up a lot in various fields such as graphics programming or trigonometry. For example the range for the sine
and cosine
functions is [-1, 1]
.
So we want to take the range [0, 255]
and map it to the range [-1, 1]
. Of course, we'd want the mapping to make sense where the lowest value in the input range maps to the lowest value in the output range, 0 -> -1
in this case. Likewise, the highest value in the input range, 255
should map to 1
and everything in between maps linearly. So how do we do this? It's actually a simple transformation that involves scaling and translating. To formally state the goal, we want to take a range [a, b]
and map it to a range [c, d]
.
First we want to shift the left value to 0
. We can do this by shifting the range by a
A(a, t) -> t - a
const A = (a: number) => (t: number): number => t - a;
This will slide the range of values to begin at the origin and we'll end up with the range [a - a, b - a]
or [0 - a, b - a]
. For the example, [0 - 0, 255 - 0]
or [0, 255]
. In this case there was no change because the input range already conveniently started at the origin. Even though we could have skipped this step, it's not always skippable and it will account for any input ranges where a =/= 0
.
Next we want to scale the input range to unit length, i.e. the range: [0, 1]
. We can do this by dividing by the difference from the highest value in the input range to the lowest, b - a
or in this case 255 - 0
.
B(a, b, t) -> t / (b - a)
const B = (a: number) => (b: number) => (t: number): number => t / (b - a)
This means that 0
will stay at 0
because 0 / (a - b) = 0
(Yes, this means a
and b
must be different because if they were the same, b - a = 0
and you'd get division by zero.) The highest value in the range would get mapped to 1
because (b - a) / (b - a) = 1
. Remember that in the first step we subtracted by a
. So with our example, [0 / (b - a), 255 / (b - a)]
becomes [0 / 255, 255 / 255]
or [0, 1]
.
Now we're currently at the range [0, 1]
. To get to [c, d]
we kinda do the inverse of steps 1 and 2 in reverse order. First we'll scale by d - c
or the difference between the highest and lowest values in the output range.
C(c, d, t) -> t * (d - c)
const C = (c: number) => (d: number) => (t: number): number => t * (d - c);
Using our example, d - c = 1 - (-1) = 2
. Therefore C
applied to [0, 1]
gives [0, 2]
.
After this we'll have the range [0, d - c]
. All we need to do now is add c
.
D(c, t) -> t + c
const D = (c: number) => (t: number): number => t + c;
This last step gets us from [0, d - c]
to [c, d]
. Using c = -1
from our example, D
applied to [0, 2]
-> [0 + (-1), 2 + (-1)]
-> [-1, 1]
. We've done it! We started with the range [a, b]
or [0, 255]
and made our way to the range [c, d]
or [-1, 1]
.
What's really nice is that we can compose all of these transformations into a single function. To start, we'll substitute A
into B
as follows:
A(a, t) -> t - a
B(a, b, t) -> t / (b - a)
B(A) -> B(t - a) -> (t - a) / (b - a) = E(a, b, t)
We can then substitute E
into C
:
C(t, c, d) -> t * (d - c)
C(E) -> C((t - a) / (b - a)) -> (t - a) / (b - a) * (d - c) = F(a, b, c, d, t)
And lastly we can substitute F
into D
:
D(c, t) -> t + c
D(F) -> D((t - a) / (b - a) * (d - c)) -> (t - a) / (b - a) * (d - c) + c = G(a, b, c, d, t)
G
is our final transformation function. It takes a value from the input range [a, b]
and maps it to a value in the output range [c, d]
.
In TypeScript this would look like:
const G =
(a: number, b: number, c: number, d: number) =>
(t: number): number =>
((t - a) / (b - a)) * (d - c) + c;
const a = 0;
const b = 255;
const halfway = (a + b) / 2;
const c = -1;
const d = 1;
const m = G(a, b, c, d);
const ts = [a, halfway, b];
console.log(ts.map(m)); // [-1, 0, 1]