Why does the CSS filter hue-rotate produce wierd results?
Asked Answered
T

3

11

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?

Tragopan answered 6/4, 2015 at 20:46 Comment(1)
possible duplicate of Why doesn't hue rotation by +180deg and -180deg yield the original color?Brauer
T
21

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,
};
Tragopan answered 8/4, 2015 at 17:13 Comment(0)
F
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.

Firstborn answered 15/3, 2021 at 5:8 Comment(2)
That makes sense for an image. I can totally see why you'd want that in an image. But if you want to get something like complementary colors, it's not what you'd normally expect, I don't think. I don't remember the specifics of why I was super mad about it 6+ years ago but it drove me nuts :DTragopan
It would make sense to have that if an actual hue rotate was still an option. I don't get why it isn't.Copalm
S
0

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.

Sartor answered 27/8, 2021 at 2:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.