Why is hue-rotate(180deg) not its own inverse? [duplicate]
Asked Answered
X

1

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

Xeroderma answered 3/2, 2022 at 19:58 Comment(4)
Why shouldn't it have an effect?Infare
@Infare because in ordinary geometry, those rotations add to a single rotation by 360deg, which is equivalent to 0degXeroderma
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)Xeroderma
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.Alisha
P
2

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

Punctilio answered 3/2, 2022 at 21:30 Comment(6)
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 ...Xeroderma
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 meanXeroderma
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 ...Xeroderma
@Xeroderma the MDN is correct, the Spec also say the same here: 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 implementationPunctilio
@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.Lions
@TemaniAfif You missed a factor of 10^-17 in the non-diagonal matrix entries, so the product is indistinguishable from the identity. The real reason that hue-rotate(180) twice does something is that the color gets clipped to the RGB-cube between the operations, which can't be inverted by a linear transformation.Considering

© 2022 - 2024 — McMap. All rights reserved.