htmlcsssvgcolorscss-filters

Why is hue-rotate(180deg) not its own inverse?


The hue-rotate() filter function "rotates the hue of an element ... specified as an angle". If this were true, filter: hue-rotate(180deg) hue-rotate(180deg) would have no effect. But it definitely does have an effect:

.square {
  height: 3rem;
  padding: 1rem;
  background: linear-gradient( 90deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%);
  font-family: monospace;
  font-weight: bold;
  color: white;
}

.double-invert {
  filter: hue-rotate(180deg) hue-rotate(180deg);
}
<div class="square">filter: none</div>
<div class="square double-invert">filter: hue-rotate(180deg) hue-rotate(180deg)</div>

What is happening here? What does hue-rotate actually do? And how can I achieve a hue rotation that is its own inverse? (Or, how can I come up with a filter that inverts the hue rotation?)

Update: following Temani Aiff's answer, it seems that hue-rotate(180deg) is actually a matrix multiplication. However, it's unclear what matrix it's actually using. The following shows that we can reimplement the SVG filter type="hueRotate" as a raw matrix, but the CSS filter hue-rotate does not actually match either of those:

.square {
  height: 3rem;
  padding: 1rem;
  background: linear-gradient( 90deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%);
  font-family: monospace;
  font-weight: bold;
  color: white;
}

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

.hue-rotate-svg {
  filter: url(#svgHueRotate180);
}

.hue-rotate-svg-matrix {
  filter: url(#svgHueRotate180Matrix);
}
<svg style="position: absolute; top: -99999px" xmlns="http://www.w3.org/2000/svg">
  <filter id="svgHueRotate180">
    <feColorMatrix in="SourceGraphic" type="hueRotate"
        values="180" />
  </filter>
  
  <!-- Following matrix calculated following spec -->
  <filter id="svgHueRotate180Matrix">
    <feColorMatrix in="SourceGraphic" type="matrix"
        values="
-0.574 1.43  0.144 0 0
 0.426 0.43  0.144 0 0
 0.426 1.43 -0.856 0 0
 0     0     0     1 0" />
  </filter>
</svg>

<div class="square">filter: none</div>
<div class="square hue-rotate">filter: hue-rotate(180deg)</div>
<div class="square hue-rotate-svg">using SVG hueRotate</div>
<div class="square hue-rotate-svg-matrix">using SVG raw matrix</div>

At least in Chrome and Firefox, hue-rotate is doing something distinct from the SVG filters. But what is it doing?!


Solution

  • hue-rotate(X) hue-rotate(X) is not equivalent to hue-rotate(X+X) as shown below:

    .square {
      height: 3rem;
      padding: 1rem;
      background: linear-gradient( 90deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%);
      font-family: monospace;
      font-weight: bold;
      color: white;
    }
    
    .single-invert {
      filter: hue-rotate(360deg);
    }
    
    .double-invert {
      filter: hue-rotate(180deg) hue-rotate(180deg);
    }
    <div class="square">filter: none</div>
    <div class="square single-invert">filter: hue-rotate(360deg) </div>
    <div class="square double-invert">filter: hue-rotate(180deg) hue-rotate(180deg)</div>

    To understand you need to dig in to the math formula. From the specification the hue-rotate() is:

    <filter id="hue-rotate">
      <feColorMatrix type="hueRotate" values="[angle]"/>
    </filter>
    

    and for feColorMatrix we have have a matrix calculation. I will give you the matrix for each case after the math (you can try it yourself following the specification)

    for 180deg

    -0.574  1.43   0.144
     0.426  0.43   0.144
     0.426  1.43  -0.856  
    

    For 360deg it's the identity matrix

    1 0 0
    0 1 0
    0 0 1
    

    When you apply two filters, it means you will use the same matrix twice which is nothing but a matrix multiplication. So you have to do the below multiplication:

    |-0.574  1.43   0.144|    |-0.574  1.43   0.144|
    | 0.426  0.43   0.144| x  | 0.426  0.43   0.144|
    | 0.426  1.43  -0.856|    | 0.426  1.43  -0.856| 
    

    to get:

    1     8.326  -1.387
    1.387 1       0
    0     0       1
    

    And it's not the identity so filtering twice using hue-rotate(180deg) is not equivalent to hue-rotate(360deg). In other words, don't see it as "sum" but rather as a "multiplication".