3

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?!

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
jameshfisher
  • 34,029
  • 31
  • 121
  • 167
  • Why shouldn't it have an effect? – cloned Feb 03 '22 at 20:12
  • @cloned because in ordinary geometry, those rotations add to a single rotation by 360deg, which is equivalent to 0deg – jameshfisher Feb 03 '22 at 20:15
  • Alternatively: because if `hue-rotate(Xdeg)` means taking `hsl(h,s,l)` to `hsl(h+X, s, l)`, then `hue-rotate(180deg) hue-rotate(180deg)` takes `hsl(h,s,l)` to `hsl(h+360deg, s, l)`, which is equivalent to `hsl(h,s,l)` – jameshfisher Feb 03 '22 at 20:17
  • 1
    This might happen due to conversion between `rgba` and `hsla` as some colors cannot be converted correctly. And since `filter` values are being applied one-by-one (the next one is being applied to the *result* of the previous one) the conversion bias is getting even worse. – Kosh Feb 03 '22 at 20:35

1 Answers1

3

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".

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Amazing answer, thanks! So the MDN docs are misleading - it's not really a "hue rotation" at all, but a linear approximation to something like that which also preserves lightness ... – jameshfisher Feb 03 '22 at 21:59
  • Now my remaining confusion is - the matrix you gave (and which I also get from following the spec) does not actually seem to give the same result ... I'll update my question to show what I mean – jameshfisher Feb 03 '22 at 22:00
  • I've added my confusion to the end of the question - it seems like your answer is right by the spec, and yet Chrome and Firefox are doing something rather different ... – jameshfisher Feb 03 '22 at 22:19
  • 1
    @jameshfisher the MDN is correct, the Spec also say the same here: https://www.w3.org/TR/filter-effects/#funcdef-filter-hue-rotate but it seems I need to recheck the Spec again because I assumed that the SVG filter will give the same result as the CSS filter that's why I used the calculation of the SVG one but maybe there is a difference with the CSS implementation – Temani Afif Feb 03 '22 at 22:38
  • @jameshfisher: HSL is also an approximation: same L doesn't mean same lightness. Check your first band in both snippet. Does they seem constant lightness? Blue and yellow? In short: it is a trade-off of fast and parallelizable in CPU (and low memory) vs. exact, complex (you need to be much more precise on definitions, you may get surprises on some screens, etc., and you need to perform float arithmetic with complex functions). Also "hue" has a strange degree meaning in normally used HSL. – Giacomo Catenazzi Feb 04 '22 at 08:23