Why doesn't hue rotation by +180deg and -180deg yield the original color?

前端 未结 3 1294
情深已故
情深已故 2020-12-01 11:36

By reading HSL/HSV color theory, I get the impression that hue component is a cyclical attribute that repeats every 360 degrees and can be changed independently of saturatio

相关标签:
3条回答
  • 2020-12-01 11:57

    In both CSS and SVG filters, there is no conversion into HSV or HSL - the hueRotation shorthands are using a linear matrix approximation in RGB space to perform the hue rotation. This doesn't conserve saturation or brightness very well for small rotations and highly saturated colors - as you're seeing.

    A true hue rotation, would first convert the input RGB color to HSL, adjust the H and then convert back to RGB. Filters don't do this. And this conversion can't be accurately approximated with a linear matrix, so while the hue is accurately changed(mostly), the saturation and brightness goes all over the place. These effects are non-linear, so adding smaller ops together results in different colors vs. doing one big operation.

    (The difference between huerotation in SVG and CSS filters could be due to using different color spaces (sRGB vs. linearRGB) - these should be the same.)

    Update: I got interested enough to go and do a manual comparison. As you can see, filters do a terrible job of hue rotating pure colors in the 0 to 180 degree range. This image compares a manual hue rotation done by plugging in hsl colors manually (outer ring) vs. a filter hue rotation on the base color (inner ring)

    Explicit HSL Hue Rotation vs. CSS Filter Hue Rotation

    But, they do a better job at less pure colors like hsl(0,50%,75%) as you can see. hue rotation with mid HSL

    codepen link in case you want to play: http://codepen.io/mullany/pen/fwHrd

    0 讨论(0)
  • 2020-12-01 12:05

    Michael's answer is awesome, and I wish I had seen it before; but since I need to not only understand they're damn wierd but also in which way (I want to work around their logic so I need the maths), I've coded a hue-rotate implementation in Javascript (which was mostly taken from reading Firefox's source code), which emulates the hue-rotate that Webkit/Blink/Gecko use.

    Again, the whole point here is just to understand what results it produces.

    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>

    The snippet was taken from this answer.

    0 讨论(0)
  • 2020-12-01 12:08

    tl;dr Error from converting colors from floats (inside the filter) to bytes (everywhere else).

    So it's a bit more complicated than that, the spec provides a good formula for hue rotation matrices, for instance the one for 180 degrees is (excluding alpha and shifts):

    -0.5747  1.4304   0.1444
     0.4252  0.4304   0.1444
     0.4252  1.4304  -0.8556
    

    Note, if you multiply that by itself you get (to four decimal places):

     0.9999  0.0001   0.0000
     0.0000  1.0      0.0
     0.0000  0.0000   1.0
    

    which is very close to the identity matrix, or a null transformation.

    That would be perfect, except that the browser is converting back to RGB between each filter. Look what happens when we hue-rotate bright red:

    -0.5747  1.4304   0.1444     1     -0.5747
     0.4252  0.4304   0.1444  *  0  =   0.4252
     0.4252  1.4304  -0.8556     0      0.4252
    

    We get a color that's impossible to represent in RGB with values from 0 to 255. So it gets bound and rounded to 0 0.4235 0.4235 during the RGB conversion, and when it's rotated again we end up with a dark desaturated red, 0.6667 0.2431 0.2431 instead of the bright pure red we started with.

    0 讨论(0)
提交回复
热议问题