w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.51k stars 669 forks source link

[css-color-4] Premultiplication in cylindrical spaces and mixing #11238

Open raphlinus opened 9 hours ago

raphlinus commented 9 hours ago

The current spec describes premultiplication as not multiplying the hue component in cylindrical spaces. I think I understand the motivation of that for doing interpolation and gradients, but it does not seem to be the correct logic for doing mixing and compositing. I'm wondering what implementations should do, and specifically whether there need to be two forms of premultiplication, one for lerp, one for mixing.

The basic rule for x over y in premultiplied spaces is x + (1 - x.alpha) * y. But this breaks down for cylindrical spaces premultiplied as per the spec, as the sum of the weights on the hue components exceeds 1. (It's not a problem for lerping, as the sum of weights is always 1)

Another way of phrasing this is that the premultiplication method in the Color Level 4 draft mismatches the definition of premultiplication for color-mix in Color Level 5, which does not make an exception for hue.

I can think of several ways of dealing with this:

  1. Simply forbid compositing in cylindrical spaces.
  2. When compositing in a cylindrical space is requested, actually do it in the associated rectangular space, for example oklab when oklch is requested.
  3. Define the over operation as (x.hue * x.alpha + y.hue * y.alpha * (1 - x.alpha)) / (x.alpha + y.alpha * (1 - x.alpha)) for the hue component.
  4. Have PremulColor and LerpPremulColor as separate types, with the former premultiplying all components, and the latter holding out hue.

One reason that choices (1) and (2) are on this list is that I'm not sure how useful it is to do compositing in a cylindrical space. I'm happy to be pointed to evidence on this.

The difference in behavior seems subtle, and it's not obvious to me that the CSS specified behavior is clearly more better or more correct than the simpler, compositing-friendly behavior. I searched for discussion where this was decided and couldn't find it. I can generate color ramps to illustrate the difference if that would be useful.

For context, this came up when we were starting to contemplate a color_mix method in our new Rust color crate. My original hope is that the PremulColor type we defined for CSS Color Level 4 interpolation would also efficiently support mixing/compositing, but that is not looking hopeful at the moment.

facelessuser commented 3 hours ago

It should be noted if you were to treat the cylindrical space in the rectangular space and apply the premultiplication, you'd still have the same hue, that's why it doesn't make sense to premultiply the hues. The hue doesn't change, just the colorfulness when it is mixed.

>>> color = Color('oklch', [0.5, 0.2, 85], 0.5)
>>> a, b = color.convert('oklab').get(['a', 'b'])
>>> a *= color.alpha()
>>> b *= color.alpha()
>>> Color('oklab', [color['lightness'] * color.alpha(), a, b]).convert('oklch')
color(--oklch 0.25 0.1 85 / 1)

I don't think compositing should really be done in a cylindrical space, but interpolating between two cylindrical colors, which is what color-mix is doing seems fine.

Another way of phrasing this is that the premultiplication method in the Color Level 4 draft mismatches the definition of premultiplication for color-mix in Color Level 5, which does not make an exception for hue.

If I had to guess, I think omitting the statement about not premultiplying hue is just an accident, not an explicit intention.

facelessuser commented 3 hours ago

I do realize that, in this scenario, we are referring to this alpha blending as compositing. When I say compositing probably shouldn't be done in a cylindrical space, I mean the browser itself should not apply compositing in this way when rendering colors or overlaying images, etc. due to how hue interpolations work, but there is nothing wrong with interpolating in a cylindrical space if that is what you want to do.

raphlinus commented 2 hours ago

Oops, I realize I made a mistake in formulating this question, as I believed that color-mix was capable of representing the Porter-Duff over operator, and, now, looking at it more closely, it seems that it is only capable of representing lerp but with an additional alpha scaling step.

So I think there are two separate concerns. One is the mismatch as pointed out, which I agree with @facelessuser is most likely a spec drafting issue. The second is whether the color representation is suitable for compositing, which is the main question I'm trying to raise right now. We do already have an interpolate method which does the color-mix functionality except for the scaling by 1 / (p1 + p2), and we have a separate mul_alpha which can perform that additional step.

To me, interpolation and compositing are closely related. In particular, I believe compositing color-a / alpha over opaque color-b should match color-mix(color-a alpha, color-b). In rectangular spaces, that is not controversial, but if we allow compositing in cylindrical spaces, it is.

So strike "another way of phrasing this," and the last paragraph should read: For context, this came up when we were starting to contemplate an over method in our new Rust color crate. My original hope is that the PremulColor type we defined for CSS Color Level 4 interpolation would also efficiently support compositing, but that is not looking hopeful at the moment.

Apologies for the confusion.

facelessuser commented 2 hours ago

To me, interpolation and compositing are closely related

Sure, interpolation is used in compositing.

I think when defining a general-purpose interpolation function, like what is done in CSS, you need to define clear, sane rules, and the rules for cylindrical spaces seem perfectly reasonable.

Due to how hue interpolation behaves, it doesn't lend itself well to compositing images and layers, but there are plenty of reasons to desire a hue interpolation, such as when doing gradients. I'm not sure there is a reason to specifically forbid alpha blending/compositing in cylindrical spaces as the current rules are quite reasonable. Would I choose to specifically use a cylindrical space to do compositing in an image? Probably not.