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
    Introducing MooTools Templated

    One major problem with creating UI components with the MooTools JavaScript framework is that there isn't a great way of allowing customization of template and ease of node creation. As of today, there are two ways of creating: new Element Madness The first way to create UI-driven...

  • By
    5 More HTML5 APIs You Didn’t Know Existed

    The HTML5 revolution has provided us some awesome JavaScript and HTML APIs.  Some are APIs we knew we've needed for years, others are cutting edge mobile and desktop helpers.  Regardless of API strength or purpose, anything to help us better do our job is a...

Incredible Demos

  • By
    Select Dropdowns, MooTools, and CSS Print

    I know I've harped on this over and over again but it's important to enhance pages for print. You can do some things using simple CSS but today's post features MooTools and jQuery. We'll be taking the options of a SELECT element and generating...

  • By
    Using MooTools For Opacity

    Although it's possible to achieve opacity using CSS, the hacks involved aren't pretty. If you're using the MooTools JavaScript library, opacity is as easy as using an element's "set" method. The following MooTools snippet takes every image with the "opacity" class and sets...

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!