Re: [csswg-drafts] [css-color-5] How should negative percentages behave in color-mix()? (#6047)

> There are numerous combinations of existing CSS with extreme values that produces weird results, as I'm sure you're aware.

Of course, extreme results aren't always relevant to our decision-making process; sometimes they just fall out of the behavior and that's okay. But it's useful to check extreme results and see if they do accord with our intuition; if something *only* makes sense for small nudges and goes wild for large values, that *implies* that our behavior model might not be shaped correctly, and the "reasonable" behavior for small values might just be an accident of stumbling onto a happy zone of otherwise overall bad behavior. (And to be clear, I think that's exactly what's happening here.)

In this case there are several bits of the behavior that end up unreasonable, I think:

* [Chris appears to be stating](https://github.com/w3c/csswg-drafts/issues/6047#issuecomment-796818001) that the obvious interpretation of `red -20%` to him is "make the result 20% more red", because you're moving 20% *past* red on the interpolation line. This confuses me! In particular, I would think that `red 100%` would ensure there's a lot of red to the result, `red 1%` would add very little red to the result, and continuing that line of reasoning, `red -10%` should be actively removing red from the result. There's apparently a *big* intuition mismatch here between us.
* You've repeatedly talked about %s as being best interpreted as interpolation progress between the two colors. In the previous syntax model, where there was only one percentage, this seemed okay, and a % that was negative or >100% had an obvious and reasonable interpretation. But now that we've resolved to match cross-fade()'s syntax more closely and allow %s on either or both, I don't think that interpretation is reasonable any longer, for two reasons:

    1. An interpolation % means that 0% refers *solely* to the start value and 100% refers *solely* to the end value. But if you can write either `red 0%, purple` or `red, purple 0%`, then *one* of those %s has that interpretation; the other has the opposite. (And if `red 0%, purple` means "all red", I think that's *definitely* opposite what people would naturally assume; a % visually paired with a value like that (almost?) always does the opposite.)
    2. We have to figure out what the interpolation % is when you supply two %s, and the simple "scale to 100%" logic does *not* give reasonable results in many cases. Like, `red -20%, purple` defaulting to `red -20%, purple 120%` due to filling in the missing % isn't *too* bad (tho I think it does give weird results, as I said in previous comments), but the fact that `red -20%, purple 100%` gives *even more extreme* results (`red -25%, purple 125%` after rescaling) is *very* unintuitive, I think. And as noted in previous comments, `red -100%, purple 100%` is undefined, and `red -200%, purple 100%` is defined but *weird* (it rescales to `red 200%, purple -100%`, inverting the signs).

        (I'd have to track down the email thread, but I remember 8-10 years ago there being a discussion about the unintuitiveness of negative %s in background-position when the image is larger than the background area, since it makes the image move *to the right*, while negative *lengths* make it move to the left. That ended up having sufficiently good motivation behind it that it was left alone.)

Overall, if the design we want to pursue *is* interpolation-based in its intuitions, then we should make sure the syntax is interpolation-focused so that authors develop the right intuition from the get-go. A generalized interpolate() function would look like `interpolate(<percentage>, <value-1>, <value-2>)`, clearly separating the % from the values; I think it's clear that `interpolate(0%, red, purple)` yields red, for instance (and negative or >100% values also similarly intuitive, extrapolating along the mix-line).

But if we're going with a cross-fade()-like syntax, then interpolation is *not* the intuition we need, but rather mixing, and in that case negative values are best treated as invalid because they don't have a meaningful interpretation. (>100% values *can* be interpreted reasonably in sum, but they don't have a reasonable metaphor on their own. That is, `150%, 50%` obviously rescales to `75%, 25%`, same as the valid `100%, 33%`, but what it means to "take 150% of the total from the first value" is somewhat nonsensical.)

---------

@svgeezus

> Yes, of course. You have a mix line going from the first color (a magenta) to the second color (a red) and your mix point is past the red ie more red.

The behavior I'm describing is *not* due to the `red`; using `red` actually *reduces* the amount of red in the result. The increase in the red channel happens *because magenta contains red*. You can swap it out for `green` and it'll just make the result *even redder*: `color-mix(rgb, rgb(100 0 100), rgb(0 100 0) -20%)` results in an even brighter magenta (specifically, `rgb(120 0 120)`).

Alternately, if I stick with red but make it *brighter* (250 rather than the original example's 230, compared with the magenta's 240), the result's red channel *goes down*.

So I think this is pretty good evidence for the described behavior being unintuitive, if your "of course" result is backwards.

And similarly, this is a strike against Lea's earlier explanation of it being intuitive with "The actual hue is indeed less red"; because the hue doesn't change *at all* when I mix with green. And it's not because purple and green are opposites, either; mixing red with negative green as in `color-mix(rgb, rgb(100 0 0), rgb(0 100 0) -20%)` resolves to `rgb(120 0 0)`, a brighter red. The hue only changed because both input colors touched the red channel; any combo that uses independent channels will leave the hue unchanged from the positive-% color.

(And that's ignoring that you and Lea appear to have *exactly opposite* intuitions here, since Lea found it intuitive and reasonable that the hue moved *further* from red in that example. One *might* charitably interpret the difference as you referring specifically to the red channel and not making a statement about the hue at all, but your reasoning about the mix line does suggest that the color should have been closer to red in hue as well.)

> Per-component clamping is dumb

While I agree with you, that's not relevant to the example; it just happens to occur with the numbers I chose because they were close to the max channel values.  If I start with more central values, for example `color-mix(rgb, rgb(100 0 100), rgb(80 0 0) -20%)`, then the result is `rgb(104 0 120)` - no clamping involved at any step, but the same result of the red channel being higher than either input's red channel.

-----------

So overall I don't see how we can conclude that the behavior is intuitive. Both of y'all have offered intuitive explanations of the results that were wrong, and closely-related examples (non-extreme ones, even) wouldn't be explainable with those intuitions at all. If y'all feel strongly that this should be interpreted as an interpolation, then the syntax we resolved on is wrong and we should re-open. If y'all like the syntax we resolved on, then this can't be interpreted as a naked interpolation, and <0% or >100% needs to be invalid like the other mixing function already in CSS.

-- 
GitHub Notification of comment by tabatkins
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/6047#issuecomment-797175211 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Friday, 12 March 2021 01:45:28 UTC