-
Notifications
You must be signed in to change notification settings - Fork 639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[css-color-5] How should negative percentages behave in color-mix()? #6047
Comments
Agree we need to define this. Not sure what's the best way about it, but I'm not a huge fan of clipping, as it throws information away. It would probably be more useful to extrapolate (i.e. -10% and 110%) in that case, but that is not always possible, e.g. when we hit the color space bounds. But clipping at the colorspace bounds is probably no worse than clipping regardless. |
Its simple to define negative percentages - 10% of a color means you multiply the components by +10/100, while -10% means you multiply them by -10/100. Same for values greater than 100% Whether that is a particularly useful thing to do, though, is not clear. The language about mix percentages would also need to be updated, probably with an example color-mix(lch, red -10%, blue);
color-mix(lch, red -10%, blue 110%); // same |
Why do we need to define negative values here? I think this would just add confusion. When mixing paint colors, for example, there is no construct of negative mixing. I think of
What would this look like? What is I just don't think it makes sense to have negative values here. In the spec as its written, you would only have one percentage (for the first color) which prevents the percentages from not adding up to 100%, and anything over 100% would clip to one of the input values in the |
1.1 times the color components for that color, in the colorspace being used for mixing.
No (I think you are being led astray by the example mixing colors here, happening to be primary colors).
A fair question. It needs to be defined, if someone does that. Clipping to [0 .. 100] is a reasonable option. |
Interpolating is not mixing paints. Sometimes that mental model works, but not always. There is a well-defined meaning of values out of range when interpolating, so there is something reasonable we could do here. Whether that's also useful is another question and I have no strong opinions on that. If we don't allow percentages outside 0-100%, I wonder if we can make them invalid at parse time. Silently clipping them means there is zero feedback for authors to realize that something is off so I don't think it's a good idea. |
I think this is not unreasonable to expect (0-100%) as valid inputs. This seems like a documentation/education issue, and a place where Dev Tools can interject a warning signal for clipping, as with other CSS that is invalid (if we don't want negative values to cause this to fail and ignore the line). |
Any design flaw can and has been addressed via documentation/education, but that's usually a bandaid when it's too late to change the design. It's one of the basic usability antipatterns to avoid designing UIs that require documentation/education to make sense, since most people will not read much documentation, and you can never reach everyone to educate. UIs (including APIs) need to be designed to be understandable and usable even in the absence of documentation/education. The more I think about this, the more I think that the most reasonable thing is for percentages out of range to extrapolate along the same line. Even if there are no use cases, it provides useful feedback which aids debugging, it is no harder to implement than regular interpolation (if anything, not doing this is probably extra implementation work), and doesn't get in the way of regular usage in any way. Note that there is precedent of extrapolation in CSS, with transitions with certain timing functions that make the interpolated value go beyond 100% (demo). |
Going outside of the 0-100% range violates the mental model that this is mixing colors and makes this function behave differently than the other mixing functions ( That has further odd implications - it implies that if the %s sum to >100%, that doesn't get rebalanced, you just get a super vivid/light/etc color. Similarly, %s summing to <100% just makes a very achromatic/dark/etc color. That's all pretty weird, and nothing like the concept of "mixing" we're trying to evoke here! Without a fairly compelling use-case for this, I'm strongly against trying to generalize past the "mixing" concept. Like the other mixing functions, %s outside of 0-100% should just be invalid at parse time. |
Ah wait, I see, your mental model could be interpolation given the older syntax that we just resolved to change away from. If there's a single %, and you interpret it as an interpolation %age between the two colors, then a <0% or >100% value is indeed just further interpolation in the same manner. But since we resolved to instead stick closer to the other mixing functions and allow %s on both values, this analogy no longer holds. |
Tab, The percentages on both values are merely syntactic sugar to avoid authors having to manually subtract from 100%. It's still only one percentage really. |
I'm very confused - cross-fade() is literally the way that images interpolate. Implementation hasn't caught up, but when you transition background-image/etc, cross-fade() is just how you write the intermediate state. I'm not sure what distinction you're trying to draw here. Regardless, tho, my argument is at a different level - both of these functions are mixing two (or more) things. The fact that we can implement "mixing" as "scaling and adding" doesn't mean that "scaling and adding" is the correct mental model we should be exposing to authors; that's a leaky abstraction. Like cross-fade() does, the interface we should be exposing to authors is mixing-focused, and that implies certain things about how we interpret overconstrained % situations. Unless there's a very good reason otherwise, the various value-mixing operations should look as identical as possible to authors; giving authors bespoke mechanisms for what are, to them, identical operations with different value types is bad design on our part. If you think there's a very good reason why color-mix() should act like this, then presumably it applies to cross-fade() as well, and we should harmonize the specs. If you think there's a very good reason why color-mix() should act like this that doesn't apply to mixing images, I'd love to hear it, so I can be convinced that these mixing functions should indeed act differently from each other. But right now I see absolutely no difference between the two. cross-fade()'s definition of "linear weighted average of each pixel's colors, in premultiplied sRGB space" is mathematically the same as "scale and add (in a particular colorspace)", so any argument you can give for why colors should act in a certain way due to the underlying math would apply to images exactly as well. (Also, the
I don't understand what you mean by this, either. Could you elaborate? Unless I'm misunderstanding, it's omitting a % that allows authors to avoid manually subtracting from 100%. Tangentially, you had said in the call that there were reasons to limit color-mix() to only two colors, and I thought that I remembered you or Chris explaining why in one of the issues, but I can't find it now. Can you point me to it, or restate it? If the math of color-mix() is literally just "interpolate the components, via scaling and adding", then I'm not sure why three colors are problematic. |
The minutes-bot wasn't told the issue number for this discussion, but here are the minutes from yesterday's call:
@LeaVerou, are you saying that we should re-open the issue and change the resolution? Because the minutes are quite clear on what we decided - negative %s are invalid (and the intention of the resolution is that >100% individual percentages are invalid, too, it just didn't get caught by the resolution text). And the preceding discussion, similarly, said that when both colors give a % and they sum to >100%, we'll rebalance the %s to add to 100%, not just literally scale each as specified then add them together. The only remaining open question was about what to do when they both specified a % and they sum to <100%, because I thought there might be an argument to match cross-fade() and "fill in the remaining %" with |
I would be very surprised as an author if this function created colors outside the range of "mixing" values between the two colors. I support the existing resolution. I also think out-of-range numbers are most likely to result from |
If out-of-range values are syntactically invalid, then math functions automatically clamp to the range (and thus are always valid). |
No, it really doesn't. Indeed the very first color equivalency mixing experiments in the 1920s and 30s utilized negative colors.
Firstly,
Secondly no, mixing light can and does have percentages below zero or greater than 100%. This can be easily visualized on a chromaticity diagram. The two colors to be mixed are represented as points on the u'v' plane, colors produced by interpolation lie on the straight line between them and extrapolated colors lie on the continuation of that line beyond the endpoints. As an example, producing the secondary colors yellow, cyan and magenta from RGB primaries: But obviously any two colors can be mixed, not just the primaries.
If you are outside the [0-100] range it is not linear interpolation but linear extrapolation. The math is entirely the same though. That aside, it is exactly mixing colors. Having said all that, which expands on "This is easily defined" on the call, let me go back to my other point on the call "no-one really seems to be asking for this". Sam is implementing and just wanted it defined what happens when someone gives percentages outside [0% .. 100%]. The options are:
I don't much like option 1 but could get behind either 2. or 3. Note that picking 2. locks us out of ever moving to 3. after a while, because of Web compat. |
Looking at the spec, what is confusing is not the interpolation/extrapolation of negative numbers, it's the resolving two different percentages that are phrased as having to equal 100% but can be out of bounds both in the positive and negative direction. If I specify one angle at 110%, and using what the spec says, and calculate the other percentage with If there was a single percentage, and you knew which color it referred to, then you take out the complexity of resolving the two percentages (and the user thinking about how it resolves). You can feed in any percentage, negative or positive, and you can just send it straight through interpolation/extrapolation. This is just my take at reading the spec and what I've been having difficulty wrapping my brain around. |
I had not realized we resolved against this, I would have argued against it. As is evident in the minutes, this appears to have been a very quick discussion, blink and you've missed it. I suppose making them syntactically invalid per the resolution is better (more feedback) than just silently clamping. This is definitely not the hill I want to die on, but I will reply to the questions you raised.
I don't think defining this around interpolation is a leaky abstraction. Interpolation as a concept is pretty high level. There are numerous other things in CSS that use interpolation so authors already need to be familiar with it. Picturing the result of
It's extrapolation, and the math is identical. They're the two sides of the same coin.
How so? They are still normalized to sum to 100%. 110% and -10% sum to 100%. Nobody is proposing that the percentages shouldn't normalize.
You just said that
What I mean is there is there is still only one data point: the point along the progression. It can be expressed either as percentage of color1, or percentage of color2, or both that should add to 100% (and if it doesn't, it's normalized to be). This does not hold true if we resolve to handle sums under 100% differently.
This was the issue, though not sure it contains all relevant discussion: #4711 |
Ah, I was under the impression they had to be constrained between 0 - 100, not simply sum to 100%. That is helpful. |
So the math checks out with negatives and such, but how do you handle the case where |
So, I've coded up the percent normalization per the current specification to demonstrate the divde by zero issue. You can enter any number, negative or postive, and all cases seem to resolve to a sum of 100, except when given two percentages that sum to zero: https://codepen.io/facelessuser/pen/eYBLWjd. So, if things remain as specified in the spec to extrapolate colors beyond the bounds of 0 - 100, this case seems it would need to be defined as the current behavior is undefined. Do you just return a transparent black color? Do you just not mix in this case and return the first color? Side note: The spec should also put some round brackets around: |
Good point and that does indeed need to be specified. |
Yes! |
I still feel that adding negative values to I feel that most people's mental models of "color mixing" is taking two colors and mixing a bit of each together to result in a final value. The final value must add up to 100%, regardless of what percentage of each color was added. Therefore it makes sense to me that anything outside of a positive percentage of either of the input colors is invalid or clipped between [0...100]. Using color-mix() to introduce negative color adjustment feels like the wrong tool for the job (it should be |
Complexity for whom? For implementations, I think it might even be marginally less effort. For authors, it's not something they will likely have to deal with, unless they are intentionally exploring the boundaries of what
FYI
You are using a word in its definition here, basically saying that people's mental model of mixing is …mixing. This is a tautology. What is this mixing? How does it work? |
I think for the authors. If values outside of [0...100] are simply invalid and thus clipped, that simplifies the cognitive model of
Definition from Oxford Languages of mix:
These definitions supports the idea that |
"Subtractive color mixing" is a thing though, and they use the term "mix": https://en.wikipedia.org/wiki/Color_mixing. Context is important with terms. |
I'm somewhat confused here. The context for "sum to lt or gt than 100%" is two %s being specified, one on each color. If the two %s are 20% and 30%, or 100% and 150%, the "just scale and add the components, because that's how interpolation/extrapolation is defined" results in a very dark or very light color. Looking at this again, tho, I think I should be reading your comments as saying "in all cases, sum the %s, then rescale them to equal 100%". So if they were 100% and -50% (sum to 50%), we'd rescale them to, hm, I guess 200% and -100%? This doesn't seem like a great answer to land on, unfortunately - if all the %s are positive the effects of scaling seem pretty reasonable, but when one is negative the re-scaling starts having outsized effects. (It's also undefined if they sum to 0%, and summing to a negative % is either undefined or really wacky since it'll invert the %s.)
You said that cross-fade() wasn't about interpolation ("Tab, cross-fade() does compositing, not interpolation."), while color-mix() was (implying that color-mix() strictly sticking with interpolation math was okay even if cross-fade() didn't). I retorted that cross-fade() was literally defined as the way images interpolate in CSS. ^_^ It is also true that cross-fade() is designed to conform to the mental model of "mixing" images together, not strictly following the results of interpolation math even if there is a well-defined answer when you do so. Percentages are limited to the range [0%, 100%], because values outside that range don't make conceptual sense, even if the math still works out; percentages that sum to >100% are rebalanced to 100% exactly†; and percentages that sum to <100% fill the remainder with transparent to model the idea that you're only grabbing "some" of the image and mixing it in. †: The metaphor here is that if you're throwing in "too much" of the images, you'll just make a too-large pile of pixels. When we mix them together and scoop out the correct amount to fill 100%, you'll get a result with the same ratio as the inputs originally had. The same metaphor covers a sum < 100%; we mix them all together and measure out 100%, but the inputs didn't fill all that so there's "empty space" left in the result. |
I think negative mixing does make conceptual sense. Sometimes it can be more intuitive. For instance, if I had a purple, and I thought that purple looked too red, you could subtract a portion of the red until you were happy. This is quite a bit different than just decreasing the red channel, which will only decrease the red, but not increase the blue portion. This is actually using the algorithm described in the spec: It's really like the inverse of the mix. If we crank it up, we get blue (for the most part due to rounding stuff and such 🙂). But yeah, when |
Notably, your first example shows off the weirdness of negative percentages as well - your final result isn't just less red, it's more blue because the purple's omitted percentage automatically gets set to 140%. And the only reason it's less red is because your red starts with a higher R channel than your purple. If you were subtracting a less intense red and/or using a more intense purple, subtracting red would actually cause the result's red to go up, due to the purple supersaturating. I do not think this is actually an intuitive result. |
I have not seen anyone confused about color transitions which can extrapolate in the way proposed, have you? Also note that invalid and clipped are mutually exclusive solutions, not the same thing. Consider this: background: red;
background: color-mix(gray -10%, yellow); If values out of range are clipped, then this produces a background of yellow.
I wasn't being pedantic. I was looking for a more precise definition of this "mixing" that we can use in spec work, not synonyms from a thesaurus. I would also reiterate @svgeesus's point that the first color equivalency experiments with human subjects in the 20s and 30s were using mixing with negative percentages and the subjects seemed to be having no trouble with the concept. I will try to find a source to cite. |
https://www.sciencedirect.com/topics/engineering/color-matching-function |
I don't see how these experiments are relevant to our question.
This is not intuitive behavior. You're not meaningfully mixing anything, and the re-scaling to 100% produces unintuitive results when one of the arguments is negative. |
Isn't the question whether humans have trouble mixing colors with negative coefficients? If not, what's our question?
|
There's a few questions:
And I think the answer to (1) is no once one gets past simple, low-value examples. For example, under your proposal, I think an obvious way to try and avoid that brightening effect would be to instead write something like ``color-mix(rgb, rgb(0 0 100) 100%, rgb(100 0 0) -90%) Nothing about these situations is undefined, it just doesn't give intuitive results. (Tho the treatment of If we didn't rescale, the operations would at least make a little more sense.
I'm mixing in the RGB space, so I think it's reasonable to assume that the results should be predictable from the RGB data. ^_^ Even if I was thinking in terms of hue, I'd definitely expect removing 20% of a pretty vivid red to make the hue budge by more than 3deg; that's a barely-detectable shift. |
The more alike they are, the less it'll move. If you do |
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. This is basically how addition and subtraction work. Subtraction is the same as addition with a negative number; addition is the same as subtraction with a negative number.
Per-component clamping is dumb unless the oog color is really close to the gamut boundary, and the spec does not recommend it as a gamut mapping method. Otherwise, the hue can radically change. |
If we're at the point of this disagreement where you think I need first grade arithmetic explained to me, I'm not sure how to progress. |
I suspect if negative percentages are allowed, they will be used in small tweaks, i.e. "make this a little less blue", not with huge changes like -90%, so that's a red herring. There are numerous combinations of existing CSS with extreme values that produces weird results, as I'm sure you're aware. Anyway, this disagreement is starting to be non-productive, so let's step back for a moment. There have been arguments expressed that allowing negative percentages is more consistent with the rest of CSS (due to transitions), and arguments that it's less consistent with the rest of CSS (due to largely unimplemented In terms of intuitiveness, some participants (me, @svgeesus, @facelessuser) think handling negative percentages as extrapolation along the same line produces reasonable/useful results, and some (@tabatkins, @una) think it's unintuitive. What is the downside of allowing it? Authors who think it's unintuitive can just not use it, just like they have not been using color transitions out of 0-100%. We are not discussing two alternatives here, one of which is more intuitive than the other, we're discussing doing something vs doing nothing. Those who would prefer negative percentages to do nothing, can still simply …not use negative percentages. |
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:
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 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, @svgeezus
The behavior I'm describing is not due to the 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 (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.)
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 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. |
The CSS Working Group just discussed
The full IRC log of that discussion<dael> Topic: [css-color-5] How should negative percentages behave in color-mix()?<dael> github: https://github.com//issues/6047 <dael> Rossen_: Do we have right people to proceed? <dael> many: yes <dael> TabAtkins: chris leaverou_ feel free to jump in <leaverou_> q+ <dael> TabAtkins: A few weeks ago talked about colormix and grammar. Matching so each color has a % and normalize to 100% if don't sum <leaverou_> q+ ChrisL <dael> TabAtkins: Further question about what should allowed range be. crossfade only allows 0-100 and if you exceed it's invalid. leaverou_ and chris argue allowing other values make sense. Have justification for it that I can't parse. una and I on side of not doing that and matching crossfade <dael> TabAtkins: Arguement is that negative % and greater % don't accord with mixing colors we're trying to define. not intuitive, though mathematical, and would confuse. Also, when normalize to 100%, that is extrodinarily weird with negative % <dael> TabAtkins: You have to grow values to sum to 100% and get very strange large results. redd 100 and blue -100 there's no way to resolve. Sometimes you get sum but multiply by negative to get it. Even if it's well defined the edge cases are very weird. <dael> TabAtkins: leaverou_ and chris have added explinations, but I think they contridict <dael> leaverou_: not sure we should base on crossfade which is something not widely impl. Only 1 or 2 impl and at least WK is not per spec. Not sure why design based on that. <dael> leaverou_: main point is there is a place in css where we color interpolate and allow wider range. In transitions. You can have cubic bezier that's out of range and it does what chris and I advocate <dael> leaverou_: Can look in issue for results, don't think it's unintuitive. If you mix with blue -% you get a less blue color. Blue coord in sRGB is less but hue is less blue which is what matters <dael> leaverou_: If you mix -100% something it's weird, but edge cases that are weird in everything <dael> TabAtkins: What you said is wrong. It doesn't make it less blue. It's dependent on other color. I have an example where -blue + green is more green <dael> chris: we need to avoid saying you are wrong in these discussions. <Rossen_> q? <dael> chris: We're talking abotu a hue angle. not gamma encoded. <dael> TabAtkins: Agree you don't get a hue. I have green + negative blue and you get more green channel <dael> chris: You're looking at gamma encoded channels. Can't do it like that. Look at actual color <dael> TabAtkins: Would you like me to show you color and you can tell me hues <dael> chris: not going to do calc on the fly <miriam> q+ <dael> chris: You can't present as leaverou_ and chris have weird idea, color science has this idea. There is a positive case. You get ringing artifacts on the transition. We're not saying great use case for -300%. We're saying sometimes you have slightly over or under. You have line extend out pastt the 2 colors on chromatics. <dael> chris: I get it could be unintuitive, but possible to spec and reasonably useful. I'm fine with clampping 0-100, jsut suboptimal <TabAtkins> I'm ready to respond whenever btw <Rossen_> q? <dael> leaverou_: If we end up not allow they should be invalid. Silent clamp there's no feedback to authors and can't change <dael> Rossen_: Point of order. Convo is lively. chris I appriciate working with tone. I want to discourage blaming and direct language. Let's focus on issue here. <dael> TabAtkins: Real quick, I'm talking about facts here. Casting me as this is technically wrong is being heated is disappointing. I don't want this as TabAtkins is angry because I'm saying the math doesn't back up what you're saying. As far as I can tell my examples show that the math is wrong. I want to focus on that <dael> Rossen_: Let's continue to focus on that math. Leave personas out of discussion <chris> q? <leaverou_> q- <dael> Rossen_: There's 3 folks on the queue. leaverou_ and chris you had taken turns. Are you done? <dael> chris: let's miriam <chris> q- <Rossen_> ack miriam <Rossen_> ack chris <dael> miriam: Reading this from mental model I liked in TabAtkins last comment the sugestion if we want allow negative mix better framed as color interpolation. If we want extending past the mix that would get a new name and syntax. for mixing we would clamp or make invalid numbers above or below 100 or 0 <chris> q+ <dael> Rossen_: Hearing 2 folks suggesting clamping between 0 and 100 is fine as long as rest of range is handled as invalid <florian> q+ <leaverou_> q+ <dael> chris: agree. silent clamp we're struck so if restrict have to invalid <Rossen_> ack chris <dael> TabAtkins: agree <Rossen_> ack florian <dael> florian: chris said he won't do color math on fly. Reasonable. If he would do it offline it will show where the math mistake is. Based on that if he disproves one math there's no reason to continue. <dael> TabAtkins: Happy to do that. Have a trivial example with single numbers we can look at and talk about <dael> florian: I would suggest we do it offline. Make sure everyone has math right <dael> chris: If we agree we're on 0-100 only we don't have to care about math for others. We could get a decision. Would be okay with it. We can extend later if needed <dael> Rossen_: I want us to get to a decision. leaverou_ is on queue <dael> leaverou_: Silent clamp or invalid, cubic bezier used to be invalid outside 0-100. I suppose it wasn't deemed useful, realized authors wanted it, and we did it. Making it invalid would allow a similar path <dael> Rossen_: More support to makeing invalid <dael> Rossen_: I heard support from a few folks and TabAtkins saying it's fine, I think <dael> TabAtkins: That's my ideal result. Happy <fantasai> +1 to leaverou_ <dael> leaverou_: I'm not suggesting we make it invlid. suboptimal. but invalid is less evil than clamp <dael> Rossen_: and gives a path to make better when we're ready. Instead of continuing to argue on what we're not ready to agree on, we have something we are ready to agree on which gives us path to extend <dael> Rossen_: Prop: clamp values between 0-100, all other values are invalid <dael> Rossen_: Other comments on proposal or objections? <dael> RESOLVED: clamp values between 0-100, all other values are invalid <dael> Rossen_: Rest of discussion is important. Recommend we continue to have this, by way of examples, in the issue when people have time for math |
Luckily we decided in the direction I like, but I'd still like an answer to the color-mixing question I asked above; Chris strongly implied in the call that the results made perfect sense within proper color math and I'm misunderstanding things, and I desperately want to know where exactly I'm going wrong, so I can continue to contribute to Color editing usefully:
The example mixes (positive) middle-red with (negative) middle-green. The hue does not change at all - it doesn't become less green, or more red, or anything of the sort. All that changes is the saturation - you get a more vivid red. And no clamping occurs either; inputs, outputs, and intermediates all stay within the sRGB gamut. You get identical results if you used a negative middle-blue - the result is a more vivid red. So coming from either direction of the hue wheel, you get the exact same result; it thus can't be due to the specific location of the negative color. (In fact it's due to the positive and negative color having the same ratio of their shared non-zero channels; any negative color paired with a red (with a single non-zero channel) will solely adjust the saturation of the red, it's just a question of by how much it gets adjusted. Anything between "blue" and "green" gives the same results as above (increasing the saturation); anything closer to red will give a smaller increase, or perhaps decrease the saturation, depending on the exact color.) Is my math wrong? If so, what should I be doing instead? |
I'll note that my "the hue is unchanged" is talking about the HSL hue; given that this is doing mixing in RGB, it seems reasonable to talk about the major sRGB cylindrical system. If you look at the colors in LCH, you do get slight hue changes (using this converter just for ease):
So, the output has a slightly higher hue then the input. However, this doesn't depend on which negative input you use: if you use negative green, the output can be considered very slightly "more greenish" (hue difference drops from (The fact that the hue-differences are precisely opposite is actually a surprise; given that any negative color with a zero red channel will give identical results, and those colors span at least 160deg of hue angle, I'm pretty sure this is just a coincidence.) It's possible that I'm doing the mixing completely wrong, and the result is not If the end result is still some variety of pure red, tho, then my conclusions so far should still be right. Regardless, tho, I'd really appreciate any errors being pointed out. |
Tab, may we please have a crumb of context, without needing to re-read the entire thread? Which colors are you mixing exactly and with what percentages? It's impossible to check your math if you only provide the result without the operation. |
I limited myself to talking about the single example that is in my quoted text, in #6047 (comment): mixing positive middle-red with negative middle-green. No context is required beyond my most recent posts. |
I did see that comment, but "middle-red" and "middle-green" are not CSS colors, and "positive" and "negative" are not percentage values. Can you please provide values with actual numbers so we can also do the math? Is it about |
Yes, that's the example I provided and am talking about. |
I think the reason you are getting odd numbers is because the result is slightly out of gamut, as would happen in every case where the color with the negative coefficient has zeroes. However, if you compare the LCH hues, it does go slightly away from green. Also, if you compare the deltaE of the mixed color with pure green, you'll see the mixed color is slightly further from green than the middle-red was, though not by much if using deltaE 2000 (larger difference with 76). However, most importantly, using red and green to make a point here is misleading. It is well known that (gamma-encoded) sRGB interpolation between red and green is weird, even if you stay within 0-100%. E.g. (Editable) calculations here: https://colorjs.io/notebook/?storage=https%3A%2F%2Fgist.github.com%2FLeaVerou%2F5b36f292d4b86eb0ea7ad37413ba8180 |
The HSL hue is not at al perceptually uniform. As an example in CSS Color 4 shows a 30deg difference is barely noticeable in some areas while making a huge difference in others. The hue in LCH, while not perfect, is vastly more useful.
The converter you are using seems to be restricted to integer arithmetic, and also silently clamps sRGB values to [0..255] which of course alters the hue. And given your choice of mix colors right up against the gamut boundary (only one primary being used, the other two are zero), naturally any negative mix takes us out of gamut. However, the main reason for the small LCH hue changes you are seeing is the mixing being done in gamma-encoded sRGB space. Switching to linear-light sRGB as the mix space improves things because now we are actually simulating mixing two lights; while changing to LCH (the default, for Your two start colors have hue angles of 36.461(middleRed) and 134.391 (middleGreen). The mixture with -20% middleGreen has a hue angle of 16.875 (mix in LCH), 18.51 (mix in linear-light sRGB) and 34.62 (mix in gamma-encoded sRGB). The mixed result are all out of sRGB gamut (negative amount of the green component, as expected) mix example with three mixing colorspaces So to summarize: the assumption errors and calculation errors in your examples were:
|
Ah, thank you! That's what I was missing! I was assuming that negative channels weren't meaningful, but if you treat them as such and normalize it back in-gamut, you do indeed get the hue shift we were looking for. (Well, for (red - green), at least. (Magenta - green) still normalizes to a more vivid magenta, as do the other "two channels minus one channel" combos. I'm not sure if that's meaningful, but I'm willing to accept that these are "opposites" in the space being used, and so subtracting one will just increase the other.) I'd done a lot of examples earlier in the thread that suffered from this but that y'all treated as valid, so I didn't realize there was a mistake there, thank you. (The one spot where clamping was brought up, it was due to channels going above the max.) Having this notebook is really helpful.
Yeah, it doesn't look good, but the math is straightforward and exactly what I'd expect (converted back to 0-255 range I was using, that's
Nah, I was just working with easy numbers for my own usage. The RGB->Lab converter I was using definitely wasn't restricted in any such way, since the output shows 2 digits of decimal precision. (I then rounded the hue angle back to integer degrees myself, because the decimals weren't important for my point.)
While that has an effect on the magnitude of the output, it wasn't germane to the outcome. My mistake was in assuming that negative channels weren't even physically meaningful (unlike overly-large channels, which make sense). I'm still not sure what a negative channel could mean, but I'm happy to accept that running the math on them gives reasonable results.
Yup, I already understand how color spaces work and how some are more suitable than others for various tasks, and why. That's never been my problem in this thread. So, bringing this back to the topic at hand. We have a resolution on the books now for the %s to be restricted to the [0%-100%] range. I'm obviously fine with keeping this (I supported it in the first place), but I'm also happy to discuss negatives again, now that we're all on the same page. I'm still against negative %s in the current grammar. I don't think they produce a good intuition; you can get a well-defined result out of them, but they only "act like interpolation" for small values. As the magnitude of the negative components grows, the numbers get more and more out of whack with how you'd write it as an interpolation, due to the "normalize to 100%" behavior. That is, a This is a long-winded way of saying that interpolation is a fine mental model, but normalized component %s are not a great interface to that mental model - we're using affine inputs to control a linear progress. If we want to do interpolation (with the possibility of <0% or >100% progress), that's fine, we just need to use a grammar that maps well to that concept - |
They are.
Also if you treat them as such and don't gamut map back into gamut.They are still colors (unless they are actually outside the spectral locus, in which case they are non-physical imaginary colors, but you can still do math on them and get meaningful results. ProPhoto RGB, for example, has imaginary primaries; so does XYZ).
The original 1920-30s color matching experiments presented, one one side, a monochromatic color to match and on the other side, the mixture of three colored lights. (To ensure cross-lab compatibility, these were monochrromatic lights derived from the mercury emission spectrum). Subjects had to match the color by adjusting the intensity of the three lights until they matched the reference. For some colors (some wavelengths), no match could be obtained. In that case, a mixture of the three colored lights was added to the reference to change the color so it could be matched. That was counted as a negative amount of the respective light. This is how the CIE color matching functions were measured. Then (because this was 1932 and negative numbers were a hassle in the days of slide rules and log tables) they transformed the matching functions to a set of physically unrealizable, supersaturated primaries called X, Y and Z specifically so that all real-world colors never needed a negative amount to match. For physically realizable primaries, there will always be some real colors that need negative, or greater than 100%, amounts of a color to match. |
In CSS Color 5's color-mix() function, https://drafts.csswg.org/css-color-5/#color-mix, what should happen if a negative percentage is passed either as lone percentage or to an adjuster. For example, what should the behavior of the following two examples be:
In the investigative implementation in WebKit, I have implemented it as clamping to 0% in this case, but would like further clarity. Another, perhaps better, interpretation would be to keep it -10%, and use 110% of the hue of blue?
In general, being specific about values less than 0 and greater than 100 would be useful.
The text was updated successfully, but these errors were encountered: