skip to Main Content

I’m trying to emulate Photoshop’s “Color Overlay” using CSS filters, and while doing so, found out the CSS filters operate on colors as consistently as an epileptic seizure.

Consider the color #FF0000. If we rotate its hue by 120deg, we should get #00FF00, and by 240deg we should get #0000FF. This is the realm of sanity. Now let’s enter CSS filters:

body { font: bold 99px Arial }
span { color: #F00; }
.daltonics-wont-notice {
    -webkit-filter: hue-rotate(120deg);
    filter: hue-rotate(120deg);
}
.precision-is-overrated {
    -webkit-filter: hue-rotate(240deg);
    filter: hue-rotate(240deg);
}
<span class="red">☺</span>
<span class="daltonics-wont-notice">☹</span>
<span class="precision-is-overrated">☹</span>

What should be #00FF00 is #007100, and what should be #0000FF is #0132FF. By using hue-rotate, the hue, saturation and brightness have been set to nonsensical levels, cross-browser.

I need to catch up with Cthulhu and figure out what logic He coded so I can work around it.

Is this a wierd color space unrelated to HSV or HSL? Is it possible to translate HSV, HSL or RGB coordinates into this whimsical dimension? Does it have a name? A standard? A cult following?

3

Answers


  1. Chosen as BEST ANSWER

    I still cannot believe this is cross-browser. I mean, I've been googling for color spaces and couldn't find any where their definition of "hue" makes sense. They pulled it completely out of their asses, as a big, spiky solid block of galvanized stupidity.

    Either way, I have read the inscriptions, and after careful examination of the magic incantations, I've produced a javascript version of the horribly-broken hue-rotate algorithm browsers are currently suffering from.

    Here's a jsfiddle version and here's it as a code snippet:

    function calculate() {
        // Get the RGB and angle to work with.
        var color = document.getElementById('color').value;
        if (! /^[0-9A-F]{6}$/i.test(color)) return alert('Bad color!');
        var angle = document.getElementById('angle').value;
        if (! /^-?[0-9]+$/i.test(angle)) return alert('Bad angle!');
        var r = parseInt(color.substr(0, 2), 16);
        var g = parseInt(color.substr(2, 2), 16);
        var b = parseInt(color.substr(4, 2), 16);
        var angle = (parseInt(angle) % 360 + 360) % 360;
        
        // Hold your breath because what follows isn't flowers.
        
        var matrix = [ // Just remember this is the identity matrix for
            1, 0, 0,   // Reds
            0, 1, 0,   // Greens
            0, 0, 1    // Blues
        ];
        
        // Luminance coefficients.
        var lumR = 0.2126;
        var lumG = 0.7152;
        var lumB = 0.0722;
        
        // Hue rotate coefficients.
        var hueRotateR = 0.143;
        var hueRotateG = 0.140;
        var hueRotateB = 0.283;
        
        var cos = Math.cos(angle * Math.PI / 180);
        var sin = Math.sin(angle * Math.PI / 180);
        
        matrix[0] = lumR + (1 - lumR) * cos - lumR * sin;
        matrix[1] = lumG - lumG * cos - lumG * sin;
        matrix[2] = lumB - lumB * cos + (1 - lumB) * sin;
        
        matrix[3] = lumR - lumR * cos + hueRotateR * sin;
        matrix[4] = lumG + (1 - lumG) * cos + hueRotateG * sin;
        matrix[5] = lumB - lumB * cos - hueRotateB * sin;
        
        matrix[6] = lumR - lumR * cos - (1 - lumR) * sin;
        matrix[7] = lumG - lumG * cos + lumG * sin;
        matrix[8] = lumB + (1 - lumB) * cos + lumB * sin;
        
        function clamp(num) {
            return Math.round(Math.max(0, Math.min(255, num)));
        }
        
        var R = clamp(matrix[0] * r + matrix[1] * g + matrix[2] * b);
        var G = clamp(matrix[3] * r + matrix[4] * g + matrix[5] * b);
        var B = clamp(matrix[6] * r + matrix[7] * g + matrix[8] * b);
        
        // Output the result
        var result = 'The original color, rgb(' + [r,g,b] + '), '
                   + 'when rotated by ' + angle + ' degrees '
                   + 'by the devil's logic, gives you '
                   + 'rgb(' + [R,G,B] + '). If I got it right.';
        document.getElementById('result').innerText = result;
    }
    // Listen for Enter key press.
    ['color', 'angle'].forEach(function(i) {
        document.getElementById(i).onkeypress = function(event) {
            var e = event || window.event, c = e.which || e.keyCode;
            if (c == '13') return calculate();
        }
    });
    body {
        font: 14px sans-serif;
        padding: 6px 8px;
    }
    
    input {
        width: 64px;
    }
    <p>
        This algorithm emulates the wierd, nonsensical and completely 
        idiotic <code>hue-rotate</code> CSS filter. I wanted to know
        how it worked, because it is out of touch with any definition
        of "hue" I've ever seen; the results it produces are stupid
        and I believe it was coded under extreme influence of meth,
        alcohol and caffeine, by a scientologist listening to Death Metal.
    </p>
    <span>#</span>
    <input type="text" id="color" placeholder="RRGGBB">
    <input type="text" id="angle" placeholder="degrees">
    <button onclick="calculate()">Calculate</button>
    <p id="result"></p>

    Note that at some point they may find out that the algorithm was coded by an intern on April 1st that wanted to pull a prank on everyone. They may even find the dice used to choose coefficients.

    One way or another, knowing the random logic employed helps me work around it, and that's why I did this. Hopefully someone will stuble upon it, and maybe some day we'll have fixed versions of hue-rotate and company shipped with browsers.


    As a bonus, in case it helps anyone: this is how Sepia is calculated:

    var newPixel = {
        newRed:   oldRed * 0.393 + oldGreen * 0.769 + oldBlue * 0.189,
        newGreen: oldRed * 0.349 + oldGreen * 0.686 + oldBlue * 0.168,
        newBlue:  oldRed * 0.272 + oldGreen * 0.534 + oldBlue * 0.131,
    };
    

  2. I found that not only CSS but also other implementation are using the same algorithm.

    Here is an reasonable explanation.

    A rotation of 120.0 degrees will exactly map Red into Green, Green into Blue and Blue into Red. This transformation has one problem, however, the luminance of the input colors is not preserved.

    So the algorithm makes hue rotation while preserving luminance against just rotating hue channel.

    Login or Signup to reply.
  3. That’s a luminance-preserving hue rotation. It’s supposed to work like that. If you hue-rotate an image before converting to monochrome, it should have the same perceived brightness. #00FF00 and #0000FF would not.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search