Search code examples
csssvgcolorssvg-filtershsl

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


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 saturation and lightness/value. Correct me if I am wrong, but these statements logically follow the previous definition:

  1. Rotating hue by 360 degrees yields the same color
  2. Rotating hue by 180 degrees twice yields the original color
  3. Rotating hue by 180 degrees followed by -180 degrees yields the original color

However, only the option 1 is correct. Rotating hue 4 times by +90 degrees yields a color that isn't even remotely similar to the original.

Furthermore, using SVG filters like this:

<filter>
    <feColorMatrix in="SourceGraphic" type="hueRotate" values="..." />
</filter>

versus the CSS filter property don't produce the same result for the same rotation. On the other hand, colors produced by SVG filters are consistent across browsers.

Is there any "hidden" property of hue rotation that makes the operation not associative?


Examples of both CSS and SVG filters:

.original {
    width: 150px;
    height: 50px;
    background-color: #E51922;
    margin-bottom: 10px;
}

.hue {
    filter: hue-rotate(180deg);
}

.hue90 {
    filter: hue-rotate(90deg);
}

.hue360 {
    filter: hue-rotate(360deg);
}

.hue-reverse {
    filter: hue-rotate(-180deg);
}
<h3>CSS filters</h3>

<div class="original">No filter</div>

<div class="hue360 original">
  Hue +360deg
</div>

<div class="hue">
    <div class="original hue-reverse">Hue +180 and -180deg</div>
</div>

<div class="hue90">
    <div class="hue90">
        <div class="hue90">
              <div class="hue90 original">Hue 4 * +90</div>  
        </div>
    </div>
</div>

<div class="original hue">Hue +180deg</div>

<div class="hue90">
    <div class="hue90 original">Hue +90 +90</div>
</div>

<h3>SVG filters</h3>

<svg width="500" height="280">
    <defs>
        <filter id="hue180">
            <feColorMatrix in="SourceGraphic" type="hueRotate" values="180" />
        </filter>
        <filter id="hue90">
            <feColorMatrix in="SourceGraphic" type="hueRotate" values="90" />
        </filter>
        <filter id="hueReverse">
            <feColorMatrix in="SourceGraphic" type="hueRotate" values="-180" />
        </filter>
    </defs>
    <rect width="100" height="100" fill="#E51922" />
    <text x="0" y="20">Original</text>
   <g transform="translate(0, 120)">
    <rect width="100" height="100" filter=url(#hue180) fill="#E51922" />
    <text x="0" y="20">Hue +180</text>
    
    <g filter=url(#hueReverse)>
        <rect width="100" height="100" x="120" fill="#E51922" filter="url(#hue180)" />
        <text x="120" y="20">Hue +180 -180</text>
    </g>
    
    <g filter="url(#hue90)">
        <rect x="240" width="100" height="100" fill="#E51922" filter="url(#hue90)"/>
        <text x="240" y="20">Hue +90 +90</text>
    </g>
   </g>
</svg>


Solution

  • 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