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
    How I Stopped WordPress Comment Spam

    I love almost every part of being a tech blogger:  learning, preaching, bantering, researching.  The one part about blogging that I absolutely loathe:  dealing with SPAM comments.  For the past two years, my blog has registered 8,000+ SPAM comments per day.  PER DAY.  Bloating my database...

  • By
    Send Text Messages with PHP

    Kids these days, I tell ya.  All they care about is the technology.  The video games.  The bottled water.  Oh, and the texting, always the texting.  Back in my day, all we had was...OK, I had all of these things too.  But I still don't get...

Incredible Demos

  • By
    dwImageProtector Plugin for jQuery

    I've always been curious about the jQuery JavaScript library. jQuery has captured the hearts of web designers and developers everywhere and I've always wondered why. I've been told it's easy, which is probably why designers were so quick to adopt it NOT that designers...

  • By
    Face Detection with jQuery

    I've always been intrigued by recognition software because I cannot imagine the logic that goes into all of the algorithms. Whether it's voice, face, or other types of detection, people look and sound so different, pictures are shot differently, and from different angles, I...

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!