csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-color-3][css-color-4] why opacity is applied on premultiplied color ?

Open letochagone opened this issue 2 years ago • 4 comments

<div
     style=
     "opacity: 0.6; background-color: rgba( 155 , 66 , 200 , 0.5 );
      width:200px; height: 200px;
      ">
    <div 
       style=
       "background-color: rgba( 0, 77 , 100 , 0.8 );
        width: 200px; height: 200px;
        ">
    </div>
  </div>

I'm trying to figure out how the resulting color is calculated in the above div ( the div containing the opacity style )

First of all i calculated the composition of the inner div on the first div I apply the simple alpha compositing :

co = Cs * as + Cb * ab * ( 1- as) ao = as + ab * ( 1 - as)

here the Cs is the inner div and the Cb is the outer div (the one that contains the opacity style. Cs = ( 0 , 77/255 , 100/255 ) as = 0.8 Cb = ( 155/255 , 66/255 , 200/255 ) ab = 0.5

after calculation :

co = ( 0.06 , 0.27 , 0.39 ) ao = 0.9

this color obtained after compositing is premultiplied

I multiply all RGB and A by the opacity value

co = co * 0.6 ao = ao * 0.6

co = ( 0.04 , 0.16 , 0.23 ) ao = 0.54

finally I compose this color with the color of the body ( rgb=1,1,1 alpha=1) As this color is premultiplied, I apply the formula: co + (1,1,1) * 1 * (1 - ao) = 0.5 , 0.62 , 0.69

this result corresponds to the one after a screenshot and the reading of the color of a point of the displayed div

The formulas that I applied are not from my intuition, but after several tests of other forms of calculation

My question is : why is opacity applied to premultiplied terms?

I tried to apply after premultiplication but the screenshot gives a difference.

letochagone avatar Aug 23 '22 20:08 letochagone

When trying to simulate the compositing that a browser currently does, and you have multiple transparent colors, it is important to ensure that you take all colors into consideration. Most likely, you are missing the inclusion of white.

Now, I'm on a mac that uses a Display P3 monitor with a Display P3 color profile. So color management of the browser is going to cause my browser's composition to operate in Display P3. So for the sake of simplicity, I'm going to force my mac to use an sRGB profile to force the browser to calculate composition in sRGB. Behavior may vary from browser to browser depending on how they implement their color management.

Here are the browser results using Safari in https://codepen.io/. This is what we are going to simulate.

Screen Shot 2022-08-24 at 6 58 29 AM

Now, we have to layer our composition, not just with rgba( 155 , 66 , 200 , 0.5 ) and rgba( 0, 77 , 100 , 0.8 ), but with white as well, as the background will be visible through both transparent colors. So we must include the white background into the composition. So we are actually compositing 3 colors, not just the 2 we specified.

This is applying compositing via https://www.w3.org/TR/compositing/ using source-over.

We'll work from the bottom up overlaying each color until we get the final composition. After we are done calculating our final color, we will apply the div opacity to our result and overlay it on the white background.

  • c3 is rgba( 155 , 66 , 200 , 0.5 ) overlayed on white.
  • c4 takes c1 and overlays it on top of the result above (c3).
  • Lastly, c5 applies the 0.6 opacity to c4 and overlays it on white.
>>> working_space = 'srgb'
>>> c1 = Color('rgba( 0, 77 , 100 , 0.8 )')
>>> c2 = Color('rgba( 155 , 66 , 200 , 0.5 )')
>>> bg = 'white'
>>> c3 = c2.compose(bg, space=working_space, out_space=working_space)
>>> c4 = c1.compose(c3, space=working_space, out_space=working_space)
>>> c5 = c4.clone().set('alpha', 0.6).compose(bg)
>>> HtmlSteps([c3, c4, c5])
[color(srgb 0.80392 0.62941 0.89216 / 1), color(srgb 0.16078 0.36745 0.49216 / 1), color(srgb 0.49647 0.62047 0.69529 / 1)]

We can see that the final result (c5) matches the browser:

Screen Shot 2022-08-24 at 7 00 53 AM

Now, is there some shortcut to apply the opacity of the div earlier? I'm not sure. I haven't looked into to exactly how browsers sequence these events, but what we are doing above is equivalent.

You can play with the results live here.

Results may vary depending on the browser, your monitor, etc. You can change the working color space to display-p3 if you are on a mac using a Display P3 monitor and color profile.

facelessuser avatar Aug 24 '22 13:08 facelessuser

My question is not "where did I make a mistake ?",

Apologies, I probably should have read better. You are correct, your values are identical.

My question is: Does the calculation I show correspond to the "theory", and if so, why the "opacity" property is applied on the premultiplied color.

I would argue the color returned by composing the two divs isn't premultiplied anymore, as the resultant color is the combined color. The premultiplication already occurred to apply the weight to each color creating the resultant. In the example I demonstrated, the result of each step of the composition was fed back through with the next layer treating each color as if they were not premultipled. The end result was the same.

What I'm not quite following is the premultiplying the opacity to the resultant, and the modification of the composition. Though it does seem to give the same value, at least in this case. It seems to combine some steps, which alters the end equation slightly. Does this hold true with more complex cases?

I'd have to work through the math to see if why this working out. Maybe someone with more experience in this area will have an answer.

facelessuser avatar Aug 24 '22 21:08 facelessuser

Turns out that what I suggested only works when the background is white. So, I clearly don't understand how the opacity: 0.6 is distributed across already transparent colors. I'd be curious if the OP example also works when the background is not white.

facelessuser avatar Aug 24 '22 23:08 facelessuser

For any element where opacity is non-unity, that element and all it's children are rendered to an initially transparent black RGBA buffer (including the effect of any alpha components in the colors). The A channel of that buffer is then multiplied by the opacity. Then the modified buffer is composited against the background of that element.

svgeesus avatar Aug 25 '22 10:08 svgeesus

Okay, this makes sense, and I do remember seeing that in the spec. I apparently had a bug with my premultiplication handling in composition which broke my source-over being associative - I could only properly resolve bottom-up, so handling the isolated case within the div forced me out of this bottom-up model causing the breakage and my confusion.

facelessuser avatar Aug 25 '22 12:08 facelessuser