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 to Create a Twitter Card

    One of my favorite social APIs was the Open Graph API adopted by Facebook.  Adding just a few META tags to each page allowed links to my article to be styled and presented the way I wanted them to, giving me a bit of control...

  • By
    Welcome to My New Office

    My first professional web development was at a small print shop where I sat in a windowless cubical all day. I suffered that boxed in environment for almost five years before I was able to find a remote job where I worked from home. The first...

Incredible Demos

  • By
    Use Custom Missing Image Graphics Using MooTools

    Missing images on your website can make you or your business look completely amateur. Unfortunately sometimes an image gets deleted or corrupted without your knowledge. You'd agree with me that IE's default "red x" icon looks awful, so why not use your own missing image graphic? The MooTools JavaScript Note that...

  • By
    Add Styles to Console Statements

    I was recently checking out Google Plus because they implement some awesome effects.  I opened the console and same the following message: WARNING! Using this console may allow attackers to impersonate you and steal your information using an attack called Self-XSS. Do not enter or paste code that you...

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!