Remapping Ranges in Sass

Life moves pretty fast. If you don't stop and look around once in a while, you could miss it.

Ferris Bueller

There’s no questioning that technology moves pretty fast. Ferris is too right, if we don’t pause to take stock and count our blessings, we might miss out in being able to appreciate them. It’s all too easy to get caught up in the speed and momentum of web development that we become engrossed on the goal—nailing the code review, delivering on time, etc.—that we forget to make sure we’re treading on solid ground in pursuit of our goals.

That’s why I feel extremely privileged to live and work in a time and field that has so many opportunities to learn about my interests and seek out new information. Whenever I’m unsure about a piece of work, or bored, or want to share ideas, I’m afforded the convenience of sating those desires through the Internet we’ve cultivated together.

One of the most engaging ways I’ve found, of recent, to keep myself open-minded and my tools sharp, is to seek out and participate in live coding sessions on Twitch, a website of which the primary purpose is for people to live stream video games and participate in chat rooms relating to the streams. Of particular note are the Creative category and Programming sub-category, where people showcase their painting skills, sculpt clay, perform music, and practise live coding, amongst many other interesting talents and skills. In a similar fashion, some people opt to use YouTube for live streaming, which provides a near-equivalent experience. It comes down to preference; although, I prefer Twitch’s way of doing things.

I’ve found a great deal of enjoyment in watching and participating in these live coding streams; it’s given me new perspectives on what I already know and expanded new horizons for things I don’t know but want to learn about.

Inspiration Strikes

It was during one of Daniel Shiffman’s streams on his YouTube channel, The Coding Train, that I was struck by a concept he was explaining, and spurned me to play around with it in a programming language I was well-versed in.

Daniel Shiffman explains the concept of mapping a value from one range to another in this thoroughly informative video:

Although CSS is meant for expressing presentation, and operations like this are best-suited for a real programming language, I wondered if this concept of remapping values from one range to another is possible with Sass.

In Theory

Like most of my endeavours, this Sass technique is neither revolutionary, nor is it particularly useful. But not everything need be born out of necessity and steeped in unit-testing for it to be fun. I find a lot of enjoyment in experimenting at the limits of CSS and finding unexplored avenues to solve challenges in a different way.

@function range-map($value, $ranges...) {
    @if not $value or not $ranges or not (length($ranges) == 2 or length($ranges) == 4) {
        @warn "`range-map()` requires three or five parameters: initial value, (old minimum), old maximum, (new minimum), and new maximum.";
        @return false;
    }

    $old-minimum: if(length($ranges) == 2, 0,               nth($ranges, 1));
    $old-maximum: if(length($ranges) == 2, nth($ranges, 1), nth($ranges, 2));
    $new-minimum: if(length($ranges) == 2, 0,               nth($ranges, 3));
    $new-maximum: if(length($ranges) == 2, nth($ranges, 2), nth($ranges, 4));

    @return ($value - $old-minimum) / ($old-maximum - $old-minimum) * ($new-maximum - $new-minimum) + $new-minimum;
}

Our @function takes a variable number of parameters, three or five, and we use Sass’ built-in if() as a ternary operator to utilise the parameters appropriately in determining the value remapped in the new range.

There really isn’t any magic going on here (as usual). Everything before the @return statement is just checking to make sure we’ve correctly passed in the right parameters, and that we’ve given the correct number of parameters. The @return statement performs a small calculation based on the 3–5 parameters and gives us back the remapped initial value.

In Practice

To be frank, I’m having a hard time finding a great deal of use out of this operation in CSS, but you might find a persuasive reason to use it. 😉

To serve as an example (not as an example of best practice), one way to use this function might be to map mouse position to something on the screen. Let’s say we want to display a globe of the Earth that rotates left and right based on whether the mouse is on the left or right of the window, and we want to try to do so without JavaScript. To do so, we’ll break the screen down into 5 equal-width, invisible columns. Hovering over each invisible column affects the rotation of the globe.

We’ll start with some basic HTML:

<div class="interact  interact--1"></div>
<div class="interact  interact--2"></div>
<div class="interact  interact--3"></div>
<div class="interact  interact--4"></div>
<div class="interact  interact--5"></div>

<div class="globe"></div>

Now let’s define some variables to plug into our remapping @function:

$number-of-columns: 5;

$rotation-start: 0deg;
$rotation-offset: 45deg;

Before we apply our different rotations to the globe, we can even use our @function to layout our equal-width columns by remapping the index of each column to a value for the left property:

.interact {
    width: (100% / $number-of-columns);
    height: 100%;
    position: absolute;
    top: 0;
}

@for $i from 1 through $number-of-columns {
    .interact--#{$i} {
        left: range-map($i, 1, $number-of-columns, 0%, (100% - 100% / $number-of-columns));
    }
}

The last parameter being passed to the @function looks a little unusual, but you have to remember that we’re setting a value for the left property, so the maximum value we should be setting is 100% - the width of a column. This means that we’re remapping the index of each column to a value between 0% and 100% - $number-of-columns.

Lastly, we’ll perform a similar operation as before, by remapping the index of the column to a parameter for the rotateY value of the transform property. In this case, we’re transitioning between ($rotation-start - $rotation-offset) (-45deg) and ($rotation-start + $rotation-offset) (45deg).

@for $i from 1 through $number-of-columns {
    .interact--#{$i}:hover ~ .globe {
        transform: rotateZ(range-map($i, 1, $number-of-columns, ($rotation-start - $rotation-offset), ($rotation-start + $rotation-offset)));
    }
}
    
    Check out this CodePen!
    
    Check out this CodePen!

Further Reading

🗓 published on
📚 reading length ~1000 words
🏷 tagged under , ,
🔗 shorturl repc.co/a4os1


NYER Unique Pairs in Sass