Simple math tricks to empower UI building skills
I started writing this due to a problem I had a few years ago: I was tasked with implementing an animation for which I didn't even know where to start.
I can be proud I delivered it flawlessly, but under the hood, the end result could be so much better. I stumbled a lot and messed around with numbers more than I'd like to admit. When you do something by trial and error, even though you deliver value, the code ends up messy.
It wasn't until a few months after this that I stumbled upon that the very same concepts I'll try to explain are fundamental, and by properly learning them I felt way more able to tackle this kind of challenge.
This is the first article in a three-part series.
☘️ Part I: Norm, Lerp, Map
There is a variety of simple but powerfull tricks which can be achieved with simple mathematical functions called norm
, lerp
and map
.
One of the simplest examples of such use is the reading progress bar seen on the top of this web page. Once you start scrolling you will surely see it. I made it skewed to enhance it's visibility.
It's a pretty straightforward relation: you scroll through the page and the width of the progress bar is set so that it matches the distance you have vertically scrolled so far. As you complete reading the article, the bar fully takes the window width.
So, thinking of it in more abstract terms: you take a number placed within a range so that you get a number placed within another range. Those two different numbers correspond to the same ratio.
Lets go over this step by step.
1️⃣ Norm, or Normalize
First thing we want to know is where the source number currentScroll
is located inside the range, the scrollable area.
Both statisticians and mathematicians prefer to translate the given value to a range from 0 to 1. That's usually called normalization.
The zero in the range does not correspond to absolute zero, but to the lower bound of the given range.
const min = 0;
// height is subtracted because
// once the browser hits the article bottom, scrolling stops.
const max = document.body.scrollHeight - window.innerHeight;
const currentScroll = document.documentElement.scrollTop;
// we get a value between 0 and 1 as the user scrolls
const currentValue = norm(currentScroll, min, max);
Norm formula in code is as follows:
function norm(value, minimum, maximum) {
return (value - minimum) / (maximum - minimum);
}
Let me entertain you with a short narrative that illustrates this:
John works 9 to 5. When noon hits, he's very eager to know how complete is the workday. So he punches these values into the normalize machine:
start: 9, end: 17, current: 12
It prints:
-> you are at 0.375 of your day.
The same principle can be applied to know your progress reading this article.
2️⃣ Lerp
Now that we know how much text we read, we need to translate it into some value we can use for the progress bar width.
Lerp stands for linear interpolation, and is used in the opposite way of normalization. You feed it an absolute range and a normalized value to get the corresponding absolute value.
Going back to our factory worker example:
John wants to know when the workday is half done so he can have a meal. He punches these values into the lerp machine:
- normalized lunch hour:
0.5
- start hour:
09
- end hour:
17
The machine returns:
-> You can have lunch at 13 o'clock.
Lerp formula in code is as follows:
function lerp(norm, minimum, maximum) {
return (maximum - minimum) * norm + minimum;
}
Assuming the bar width is 600px, if we want to know what would the width be at half of the process:
const min = 0;
const max = 600;
const widthAtTheMiddle = lerp(0.5, min, max);
console.log(widthAtTheMiddle) // -> 300
3️⃣ Map
Map is the glue function that composes normalize and lerp, taking a value within the source min and max and returning the same proportion in the destination min and max values.
function map({
value,
sourceMinimum,
sourceMaximum,
destinationMinimum,
destinationMaximum
}) {
return lerp(
norm(value, sourceMinimum, sourceMaximum), // eg: 0.5
destinationMinimum, // eg: 0
destinationMaximum // eg: 700
);
}
hint: using named params makes code much more readable and refactorable. JS doesn't have it but you can pass an object instead.
document.addEventListener('scroll', () => {
const progressWidth = map({
value: document.documentElement.scrollTop,
sourceMinimum: 0,
sourceMaximum: document.body.scrollHeight - window.innerHeight,
destinationMinimum: 0,
destinationMaximum: document.querySelector('.progress').offsetWidth
})
})
Some leftover notes
For this specific example, please disregard everything below normalize. You can just take the normalized scroll value, multiply it by 100 and set it as the CSS width property of the status bar as a percentage.
You may have noticed that I'm zeroing the minimums, and ask why not just set the minimum to 0? Because not all lower bonds start at zero. Imagine that the progress bar only starts filling in after scrolling past the page init banner.
const scrollTop = document.documentElement.scrollTop;
const sourceMaximum = document.body.scrollHeight - window.innerHeight;
const destinationMaximum = document.querySelector('.progress-wrapper').offsetWidth;
const progressWidth = Math.ceil(
map({
value: // 0,
sourceMinimum: 0,
sourceMaximum, // 3358
destinationMinimum: 0,
destinationMaximum, // 1440
})
);
document.querySelector('.progress').style.width = `0px`;
Like it so far?
In part II I'll show how normalization, linear interpolation and map are used to draw curves.
If you have any questions, want to suggest some corrections, or just tell me you appreciate it, feel free to reach me at goncalomarques101 at the Google email provider.