Detect Inverted Color with CSS and JavaScript

By  on  

There was something that bugged me after reading David's article on the invert filter last week. It was this sentence:

The values reported back by window.getComputedStyle(el) will be the original CSS values, however, so there's no way of getting the true inverted values of given properties.

But if I can read the original value for background-color or color and I can read the value for the filter, then I can compute the inverted value, which means there is a way after all.

Well, let's see how we do that.

Full inversion for a solid background

First of all we need to understand how the invert filter works and we'll start with the particular case of full inversion - invert(100%). For an arbitrary original value rgb(red, green, blue), where red, green and blue can take values between 0 and 255 applying an invert(100%) filter is going to produce rgb(255 - red, 255 - green, 255 - blue).

This is pretty straightforward and easy to code, especially since the values obtained via window.getComputedStyle(el) are rgb() or rgba() values in all browsers supporting CSS filters, depending on whether the initial value has an alpha channel that is less than 1 or not.

Note: CSS filters with filter function values (not just url() values) are supported by Chrome 18+, Safari 6.1+, Opera 15+ and Firefox 34+. Chrome, Safari and Opera need the -webkit- prefix, while Firefox doesn't need and never needed a prefix, but needs layout.css.filters.enabled set to true in about:config. IE does not support CSS filters yet, though they are listed as "Under Consideration". If implemented, it's probably not going to need a prefix either. So don't use -moz- or -ms- prefixes for filters!

So if we have an element for which we set a background-color and then we apply an invert(100%) filter on it:

.box {
    background-color: darkorange;
    -webkit-filter: invert(100%);
    filter: invert(100%);
}

Then we can read its styles and compute the inverted value from the original:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    original = styles.backgroundColor, 
    channels = original.match(/\d+/g), 
    inverted_channels = channels.map(function(ch) {
        return 255 - ch;
    }), 
    inverted = 'rgb(' + inverted_channels.join(', ') + ')';

It can be seen working in this pen, where the first box has the original background-color and no filter applied, the second one the same background-color and an invert(100%) filter, while the third one has a background that's the inverted value (as computed by the JavaScript code above) of the original background-color, assuming an invert(100%) filter.

See the Pen Getting the inverted value for `background-color` via JS #1 by Ana Tudor (@thebabydino) on CodePen.

What about semitransparent backgrounds?

But this is only for rgb() and if our background-color isn't fully opaque, then the value we read with JavaScript is going to be an rgba() value.

For example, the backgroundColor style value read via JavaScript is going to be "rgb(255, 140, 0)" if, in the CSS, we set background-color to any of the following:

  • darkorange
  • #ff8c00
  • rgb(255, 140, 0)
  • rgb(100%, 54.9%, 0%)
  • rgba(255, 140, 0, 1)
  • rgba(100%, 54.9%, 0%, 1)
  • hsl(33, 100%, 50%)
  • hsla(33, 100%, 50%, 1)

But it's going to be "rgba(255, 140, 0, .65)" if we set background-color to one of the following:

  • rgba(255, 140, 0, .65)
  • rgba(100%, 54.9%, 0%, .65)
  • hsla(33, 100%, 50%, .65)

Since the invert filter leaves the alpha channel unchanged and only modifies the red, green and blue channels, this means that our JavaScript code becomes:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    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 - ch; }), 
    inverted;

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

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

It can be seen working in this pen.

See the Pen Getting the inverted value for `background-color` via JS #2 by Ana Tudor (@thebabydino) on CodePen.

Note that this assumes that the value of the alpha channel of the background we set in the CSS is always greater than 0. If the value of the alpha channel is 0, then the value returned by styles.backgroundColor is going to be "transparent" and inversion really has no point anymore.

Alright, but this was all for a filter value of invert(100%). What happens if we want to have a value that's less than 100%?

General case

Well, for a value less than 100%, let's say 65%, the [0, 255] range for each of the red, green and blue channels is first squished around its central value. This means that the upper limit of the range goes from 255 to 65% of 255 and its lower limit goes from 0 to 100% - 65% = 35% of 255. Then we compute the squished equivalent of our original background-color which simply means taking each channel value, scaling it to the new squished range and then adding the lower limit. And finally, the last step is getting the symmetrical value with respect to the central one for each of the red, green and blue channels of the squished equivalent of our original background-color.

The following demo visually shows all this happening, taking the green channel as an example.

See the Pen Squishing a channel range by Ana Tudor (@thebabydino) on CodePen.

The original range goes from 0 to 255 and the original value for the green channel is taken to be 165. Any value less than 100% for the argument of the invert filter function squishes the range. A value of 50% means that the range is squished to nothing, as the upper limit equals the lower one. This also means that the final inverted rgb() value is always rgb(128, 128, 128), no matter what the initial background-color may be.

The squished equivalent of the original value for the green channel is the one having an s in front on its label. The inverted value of this squished equivalent with respect to the middle of the range has an i in front on its label. And we can see that this is the value of the green channel for the solid background of the second box at the bottom after applying an invert filter on it.

This means that our JavaScript code now becomes:

var box = document.querySelector('.box'), 
    styles = window.getComputedStyle(box), 
    invert_perc = (styles.webkitFilter || styles.filter).match(/\d+/), 
    upper = invert_perc/100*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(', ') + ')';

However, there is a problem: this assumes that the computed style value for the filter property is something like "invert(65%)". But most of the time, this is not the case. If we set the filter value to invert(65%) in the CSS, then the value we get this way via JavaScript is "invert(0.65)" in WebKit browsers and "invert(65%)" in Firefox. If we set the filter value to filter(.65) in the CSS, then the value we get via JavaScript is "invert(0.65)". Firefox returns it in the same format as the one we have set it in the CSS, while WebKit browsers always return it as a value between 0 and 1. So we need to handle this as well:

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(', ') + ')';

As it can be seen in this pen, it now works properly for both formats, provided that the alpha channel isn't 0.

See the Pen Getting the inverted value for `background-color` via JS #3 by Ana Tudor (@thebabydino) on CodePen.

Final words

This still only deals with the very simple case where the value of the filter property is just one invert function. It won't work when we have chained filter functions, for example, something like filter: hue-rotate(90deg) invert(73%).

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
    9 Mind-Blowing Canvas Demos

    The <canvas> element has been a revelation for the visual experts among our ranks.  Canvas provides the means for incredible and efficient animations with the added bonus of no Flash; these developers can flash their awesome JavaScript skills instead.  Here are nine unbelievable canvas demos that...

  • By
    Conquering Impostor Syndrome

    Two years ago I documented my struggles with Imposter Syndrome and the response was immense.  I received messages of support and commiseration from new web developers, veteran engineers, and even persons of all experience levels in other professions.  I've even caught myself reading the post...

Incredible Demos

  • By
    Upload Photos to Flickr with PHP

    I have a bit of an obsession with uploading photos to different services thanks to Instagram. Instagram's iPhone app allows me to take photos and quickly filter them; once photo tinkering is complete, I can upload the photo to Instagram, Twitter, Facebook, and...

  • By
    HTML5&#8217;s window.postMessage API

    One of the little known HTML5 APIs is the window.postMessage API.  window.postMessage allows for sending data messages between two windows/frames across domains.  Essentially window.postMessage acts as cross-domain AJAX without the server shims. Let's take a look at how window.postMessage works and how you...

Discussion

  1. It is important to note that the original article was talking about the invert filter inverting the colors of all content, not just the background color and the text color. If you notice the examples on that page you will see that the image’s colors are inverted. You will also notice that if you apply the filter to videos and other content that even the colors in the content are affected.

    • Is that a challenge? :P I believe it’s possible to invert images and videos frame by frame with JS using canvas (well, with the same domain restriction because I need to get the image data).

  2. PhistucK

    A small (but great!) update –
    – Firefox 35 will have CSS filters enabled by default, without changing anything in about:config.
    https://developer.mozilla.org/en-US/Firefox/Releases/35#CSS

    – Internet Explorer now lists CSS filters as “In Development”! We might just see them working in Internet Explorer 12.
    https://status.modern.ie/filters?term=filters

    • Yup, seen that, can’t wait! Firefox 35 should become stable in early January as far as I know. No clue about when they might actually land in IE at this point.

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