Emulate the Invert Filter Effect with Sass

By  on  

After figuring out how to get the result of the invert filter for a solid background, the next idea that came to mind was naturally doing this with Sass in order to reproduce the filter effect for browsers not supporting filters. Sass already has an invert function, but this one only reproduces the filter: invert(100%) effect. The goal was to write one that works for any percentage.

If you remember the JavaScript solution we got to last time, it was something like this:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    filter = (styles.webkitFilter || styles.filter),
    invert_arg = filter.match(/(0\.\d+)|\d+/)[0], 
    upper = ((filter.indexOf('%') > -1)?(invert_arg/100):invert_arg)*255, 
    lower = 255 - upper, 
    original = styles.backgroundColor.split('('), 
    channels = original[1].match(/(0\.\d+)|\d+/g), 
    alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, 
    inverted_channels = channels.map(function(ch) {
      return 255 - Math.round(lower + ch*(upper - lower)/255);
    }), 
    inverted;

if(alpha !== 1) {
  inverted_channels.splice(3, 0, alpha);
}

inverted = original[0] + '(' + inverted_channels.join(', ') + ')';

So the first step is that, given the red, green, blue channels (which should be between 0 and 255) and the argument of the invert function, which should be a value between 0 and 1 or a percentage between 0% and 100% (values outside the range ar permitted, but everything is capped to the limits), we compute the limits of the squished range (if you don't know what that is, check the previous article) and the inverted values for every channel.

So it would be something like this:

$red: 255;
$green: 165;
$blue: 0;
$percentage: 65%;

$original: rgb($red, $green, $blue);

$upper: ($percentage/100%)*255;
$lower: 255 - $upper;

$inverted-red: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-green: 255 - round($lower + $red*($upper - $lower)/255);
$inverted-blue: 255 - round($lower + $red*($upper - $lower)/255);

$inverted: rgb($inverted-red, $inverted-green, $inverted-blue);

But we have the same formula for the inverted channels, so it makes sense to put that into a function:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

So we can now replace for our final inverted value:

$inverted: rgb(invert-channel($red, $upper, $lower), 
               invert-channel(green, $upper, $lower), 
               invert-channel(blue, $upper, $lower));

But do we really need to expose all those calls to invert-channel? Well, no, so we're going to create another function that handles computing the squished limits and the calls to invert-channel:

@function _invert($red, $green, $blue, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel($red, $upper, $lower), 
                invert-channel($green, $upper, $lower), 
                invert-channel($blue, $upper, $lower));
}

Alright, but maybe we don't always want our original background to be an rgb() value. Maybe we want it to be a keyword or an hsl() value. Of course we could use some tool to do the conversion before we use it. But isn't there a way to get the channels no matter what format we start with? Well, there is! Sass has red, green and blue functions precisely for this.

This means we can simplify our _invert function:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

So our original code has become:

$original: orange;
$percentage: 65%;

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $red*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    @return rgb(invert-channel(red($original), $upper, $lower), 
                invert-channel(green($original), $upper, $lower), 
                invert-channel(blue($original), $upper, $lower));
}

$inverted: _invert($original, $percentage);

This looks a lot better! However, we can clean it further by dynamically calling invert-channel. This means we can rewrite our _invert function this way:

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    @return rgb($inverted-channels...);
}

This is much more elegant as we've eliminated all repetitions. There's just one more thing we need to handle: the possibility of our original value being semitransparent. That's actually pretty easy, as we can extract its alpha channel using the alpha function, and we can later append that value to the list of inverted channels. This way, our final code will be:

@function invert-channel($channel, $upper, $lower) {
    @return 255 - round($lower + $channel*($upper - $lower)/255);
}

@function _invert($original, $percentage) {
    $upper: ($percentage/100%)*255;
    $lower: 255 - $upper;
    $alpha: alpha($original);

    $inverted-channels: ();

    @each $channel-name in 'red' 'green' 'blue' {
        $channel: call($channel-name, $original);
        $inverted-channel: invert-channel($channel, $upper, $lower);
        $inverted-channels: append($inverted-channels, $inverted-channel);
    }

    $inverted-channels: append($inverted-channels, $alpha);

    @return rgba($inverted-channels...);
}

$inverted: _invert($original, $percentage);

The cool thing about the rgba function is that it's going to output an rgba() value in the resulting CSS only if its alpha is strictly less than 1.

You can see it working and play with it in this pen:

See the Pen Emulate invert() filter with custom Sass function by Ana Tudor (@thebabydino) on CodePen.

Special thanks to Sass wizard Hugo Giraudel for reviewing this.

Ana Tudor

About Ana Tudor

Loves maths, especially geometry. Enjoys playing with code. Passionate about experimenting and learning new things. Fascinated by astrophysics and science in general. Huge fan of technological advance and its applications in all fields. Shows an interest in motor sports, drawing, classic cartoons, rock music, cuddling toys and animals with sharp claws and big teeth. Dreams about owning a real tiger.

Recent Features

  • By
    Creating Scrolling Parallax Effects with CSS

    Introduction For quite a long time now websites with the so called "parallax" effect have been really popular. In case you have not heard of this effect, it basically includes different layers of images that are moving in different directions or with different speed. This leads to a...

  • By
    Create Namespaced Classes with MooTools

    MooTools has always gotten a bit of grief for not inherently using and standardizing namespaced-based JavaScript classes like the Dojo Toolkit does.  Many developers create their classes as globals which is generally frowned up.  I mostly disagree with that stance, but each to their own.  In any event...

Incredible Demos

  • By
    Creating Spacers with Flexbox

    I was one of the biggest fans of flexbox before it hit but, due to being shuffled around at Mozilla, I never had the chance to use it in any practice project; thus, flexbox still seems like a bit of a mystery to me.  This greatly...

  • By
    CSS :target

    One interesting CSS pseudo selector is :target.  The target pseudo selector provides styling capabilities for an element whose ID matches the window location's hash.  Let's have a quick look at how the CSS target pseudo selector works! The HTML Assume there are any number of HTML elements with...

Discussion

  1. Okay, this is just awesome.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!