-
Notifications
You must be signed in to change notification settings - Fork 667
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] When mixing hue, there are two ways round the hue range #4735
Comments
I have been wondering about that myself. Maybe an extra parameter with two values, to go 'the long way round' or 'the short way round' with shorter path being the default if unspecified? It would also need tie logic, so that both paths are available when the path is exactly 180deg. |
Unless explicitly specified, this should always be the shortest arc. |
Once the spec is fixed to not mod the angle eagerly, the consistent answer is to just do a linear interpolation from the start to the end angle. If you want a particular direction, you can specify the angles to cause that direction, just like you do with 'rotate'. |
The big problem is that many authors are going to want to set gradient stops or color mix endpoints either (a) from coordinates provided by a graphical tool, (b) in a different coordinate system [mixing in polar-coordinate CIELAB (L*, C*, h*) space is apparently the default irrespective of the space in which a color is specified], or (c) from a specific named color not specified adjacent to the usage in a gradient/color mix. When this happens for e.g. a red and a purple, the gradient should go the short way through red–purple, but from what I can tell according to this proposal will instead go all the way around via orange→yellow→green→blue. This is going to be very confusing for authors, and difficult or even impossible for them to fix in a straightforward way. For creators of graphical tools for creating gradients or color mixtures, outputting to css, this is going to create some sticky edge cases which are difficult to work around. The hue angle specified for each color will need to be adjusted by an arbitrary multiple of 360° based on the specified angle of the previous color. If trying to make a collection of color mixes between several different colors, each original color may need to be specified multiple times for different numbers of turns around the circle, so that mixtures with others end up behaving as expected. |
You've mentioned authors using "a graphical tool" consistently in your case for using the shortest path between the angles. Presumably this tool could also convert a gradient from 340° to 20° into -20° to 20° in the generated CSS. |
There are 2 cases: (1) an author uses a graphical tool to obtain coordinates, and then copy/pastes those coordinates into a document. That puts the burden of figuring out what multiple of 360° to get the desired behavior onto the end user; (2) the graphical tool generates a whole gradient or color mix or whatever, and outputs CSS. In that case you are complicating the tool programmer’s job and making it more likely that their tools will have undesired behavior. For e.g. a gradient, the tool is going to need to keep track of the turning number of every gradient stop, and potentially change every one in response to any change to the gradient. What would previously be a local update to 1 value becomes a fiddly global update. |
@tab wrote:
Suppose I have
You seem to be saying that going the other way round I would need to inspect the values of the two custom properties and if needed, add or subtract 360 to make it do what I want.
while what I am suggesting does not require making adjusted copies of custom properties:
|
Also, doing calculations in LCH does not mean that the colors being mixed were originally specified in LCH. So tweaking the angle by adding or subtracting 360 is not even an option in those cases. Consider mixing a hex color (thus, sRGB) with a color in prophoto-rgb:
The two input colors are auto-converted to LCH and then mixed. Relying on the user tweaking hue angle implies always requiring the user to first convert the colors to LCH. |
Yeah, there's tradeoffs. Just doing a linear interpolation is simpler and more consistent with how all other values in CSS transition, and gives authors that are hand-authoring the values simple, straightforward control over how their transition proceeds. It does mean that authors doing generic work on custom property values don't have insight into how it'll work (tho the users of the component in question can always hand-tweak their values to get the desired result), and yes, when your endpoints aren't in a cylindrical space you don't have any control. Doing a "shortest-path" interpolation is a break from that: it might be more commonly what's desired, but it means we have to make an arbitrary decision for 180deg separation, and we have to add further controls to let the author opt into the other modes when that's desired (at least four settings, as {longest path, shortest path}×{CW when 180deg, CCW when 180deg} are all possible). |
After reading Chris's posts, I'm going to change my opinion. Under any other circumstances I'm very strongly for normal linear interpolation like we do everywhere else, but for The critical difference is that colors are likely being mixed in a different space to the one they're specified - even if both colors are specified in the same space. So the user has no real control over the components being mixed. I expect the vast, vast majority of people using this will be doing so in RGB, and most of them won't have even heard of Lch. If someone tries to mix from green to yellow and find it goes "the long way round", it's going to seem wrong and won't be obvious how to solve it. One solution is to explain the Lch colorwheel and give them a flag to set; another is for them to specify both colors in Lch. Neither is good. I think the principle of least surprise requires If both colors are Lch to start with, you could go either way - shortest path for consistency, normal interpolation for full control. I lean towards the latter, as it means for most people in most colorspaces it will do the right thing, and those that really care about the details can specify their colors in Lch. |
So I'm thinking of an optional second argument to the hue adjuster. Instead of
it will be
with the default, if omitted, being 'shorter' and the direction, if the hue difference is exactly 180deg, being 'clockwise'. So you get what you want most of the time, and you can get exactly what you want by being specific. |
|
@tabatkins wrote:
which is why I wrote, earlier:
|
I would swear I didn't read that, but it's in my email client, so it was part of the original too. Sorry about that. ^_^ |
After playing a little bit with interpolation code, I stumbled on the same issue @svgeesus is describing, and it's nasty. --color-red: hsl(0 80% 50%);
--color-blue: hsl(210 80% 55%);
--color-red-blue: color-mix(--color-red, --color-blue); /* using default lch interpolation */
Green is certainly not what I have in mind when combining red and blue. And basically, based on what angle you start with, you could get any angle back. The result is nonsensical, and if your color is not lch there is no way to control that. |
As I was defining the keywords
I realized that those are stupid names that people will get mixed up so I went with |
Indeed!
The recent edits are a good start, but there are still a bunch of things we need to define in regards to how these algorithms work. If interpolating between e.g. -360 and 720, which of those keywords give us 3 rainbows? It's obvious that I wonder if these are correct (and optimal) for the pre-interpolation fixup. If so, I can put them in the spec. // angle1, angle2 are hue angles in degrees
angle1 = ((angle1 % 360) + 360) % 360; // constrain to [0, 360)
angle2 = ((angle1 % 360) + 360) % 360; // constrain to [0, 360)
// Increasing:
if (angle2 < angle1) {
angle2 += 360;
}
// Decreasing:
if (angle1 < angle2) {
angle1 += 360;
}
// Longer:
if (angle2 - angle1 < 180) {
angle2 += 360;
}
else if (angle2 - angle1 > -180) {
angle1 += 360;
}
// Shorter:
if (angle2 - angle1 > 180) {
angle1 += 360;
}
else if (angle2 - angle1 < -180) {
angle2 += 360;
} |
|
Alright, since these seem to work in my experiments, and nobody has expressed any dissent about them, I'm keen to put them in the spec, and clarify that |
Agenda+ to discuss whether |
I agree it makes sense to split this out into another sections, so it can then be referenced both in Color 5 and also outside it. I suggest gamut mapping should happen before interpolation. This is because we wish to avoid multiple mapping stages, so it should happen as late as possible and ideally, once. |
I agree the gamut mapping is a large and separate issue. The mathematical definitions proposed by @LeaVerou are working well for me in testing. |
I went ahead and added an Interpolation section for now. |
Two comments on the pseudo-javascript in the Hue interpolation section: First, above you wrote: // angle1, angle2 are hue angles in degrees
angle1 = ((angle1 % 360) + 360) % 360; // constrain to [0, 360)
angle2 = ((angle1 % 360) + 360) % 360; // constrain to [0, 360) I think it would be good to include this explicitly in the definitions of Second, I think the pseudo-code for if (θ₂ > θ₁ && θ₂ - θ₁ < 180) {
θ₁ += 360;
}
else if (θ₁ > θ₂ && θ₁ - θ₂ < 180) {
θ₂ += 360;
} |
(Though it's not clear to me how |
... or how |
LGTM. For readability, I would change it to: if (0 < θ₂ - θ₁ > && θ₂ - θ₁ < 180) {
θ₁ += 360;
}
else if (0 < θ₁ - θ₂ && θ₁ - θ₂ < 180) {
θ₂ += 360;
} or even (since this is pseudo-JS): if (0 < θ₂ - θ₁ < 180) {
θ₁ += 360;
}
else if (0 < θ₁ - θ₂ < 180) {
θ₂ += 360;
} |
Earlier, I wrote:
The |
Done in 4e88f2c, forgot to cross-link in commit message. |
…rpolation types. This makes a few adjustments to the definitions of hue interpolation types to follow up on the discussions in w3c#4735. First, it adjusts the pseudo-code so that for 'shorter' and 'longer', angles that differ by 180 degrees will always go in the decreasing direction, as described in w3c#4735 (comment) Second, it adjusts the set notation to match the pseudo-code. (They were not matching even prior to this change.) The set notation previously seemed to assume that there were absolute-value functions present that were not actually there. However, given the asymmetry of always going in the decreasing direction, it's more correct to write it without absolute values.
The CSS Working Group just discussed
The full IRC log of that discussion<dael> Topic: [css-color-5] When mixing hue, there are two ways round the hue range<dael> github: https://github.com//issues/4735 <dael> leaverou: When interpolate between hues usually you don't want interpolate in same way. If going between hue 0 and hue 400 you don't want a whole rainbow <dael> leaverou: What we put in spec is by dfault use shortest arc which does expected in common. Have keywords for longest arc etc and also as-specified keyword to allow raw interp <dael> leaverou: Wasn't sure if all needed. Esp specified one. If impl want to store value as normalized keyword doesn't allow <dael> leaverou: I put algo in spec which tweaked by dbaron. Good to get sanity check. <smfr> https://drafts.csswg.org/css-color-5/#hue-interpolation <Rossen_> q? <dael> fantasai: Can you summerize the proposal? <dael> leaverou: Do we need all 5 keywords? <dael> leaverou: We need shorter b/c that's what you expect in most cases. Do we need specified which is interp as specified so if you go between 0 and 720 2 rainbows. Need increasing, decreasing, longest or is that completist <dael> fantasai: Are there use cases? We can add keywords. If there's not a use case might want to note possibility for future reference in case we need to add later. If not a use case don't need to add. <dael> fantasai: I think it's usefult o think of all and makes sure keywords are a set that make sense even if we only include 1 or 2 in spec <dael> dbaron: Intent is these would eventually apply to all gradients, animations, and color mix funct or only some? <dael> leaverou: Good to design with that in mind. Not sure how text for animation snad gradients but if we have a syntax making sense it would be good to have the option <miriam> q+ <astearns> for gradients and animations the workaround would be to add more steps/stops to mimic the non-short behavior? <dael> fantasai: My suggestion is draft all in spec, put an issue in saying we're not sure if we need all and we might limit to a subset with the subset that makes sense to you and also note might expand to gradients. Encourage people to think what that would look like <tantek> +1 to publishing at least one draft with more keywords to get the ideas published <dael> fantasai: Early stage WD so makes sense to put ideas and poke at them with people like Una to make cases <leaverou> http://localhost:8002/csswg-drafts/css-color-5/Overview.html#hue-interpolation <leaverou> https://drafts.csswg.org/css-color-5/#hue-interpolation <dael> leaverou: Does math make sense? This is the section ^ <florian> q+ <Rossen_> ack miriam <dael> miriam: THinking of specified I'd have use cases when comes to gradient. As pointed out in chat that could be do with extra stops. <dael> miriam: Can't think of cases when mixing colors. I don't know if that's separate but might be. Math makes sense. Shorter and longer fall apart at 180 which maybe implies need to determine direction without them <Rossen_> ack florian <dael> florian: I haven't reviewed math for correctnss, but intuitive seems right. Longer seems least useful. Wanting longer for being longer seems odd. Might pick if gives right thing. <miriam> +1 to longer being less useful than increase/decrease <dael> florian: Approach about putting in spec now with note for use cases sounds good <dael> dbaron: On math have a PR to tweak. I think set notation doesn't match pseudo code and I think pseudo code is right. I have some weaks for 180 case but it's not clear that's what we want <dael> leaverou: 180 chris said we can pick one as long as it's well defined. Doesn't matter increasing or decreasing <dael> florian: Makes sense. If you have a preference you can say it. <dael> fantasai: We use 'closest' in radial gradients so maybe that instead of 'shorter'? <dael> leaverou: Than what longer? <dael> fantasai: 'father'? <dael> florian: I don't think longer is needed so I don't mind not having a good replacement <fantasai> s/father/farthest/ <tantek> near and far, close and distant, short and long ? <dael> fantasai: We have farthest and closest side <dael> leaverou: That's differ than angles <dael> Rossen_: Apart from bikeshedding I hear 2 proposals. 1) let's push a version of the spec with all the keywords initially or as many as we want so we encourage more incubation. <dael> Rossen_: 2) I hear agreement that longer doesn't seem useful. I didn't hear a use case to prove otherwise. <dael> Rossen_: I don't want to bikeshed. <dael> Rossen_: SHould we resolve to keep the keywords becides longer and publish? <dael> leaverou: I'd rather hear from Una and Adam before we resolve. <dael> fantasai: This isn't final. We're drafting for dicussion to encourage participation. I think it's fine to put it all in the draft, explain the thoughts, and enougage feedback. We can publish often <dael> Rossen_: Objections to Publish a version with all keywords but longer? <fantasai> "Publish early, publish often" <tantek> +1 <dael> RESOLVED: Publish a version with all keywords but longer |
I'd like to experiment with adding these five hue interpolation types to my library, and I wondered — at the hue fixup step, should we also do something about grays, which in LCh have In the case of mixing two colors, the achromatic one can inherit the hue from the other color, if it has chroma. But if we were to extend hue interpolation to gradients, which accept more than one color stop, how should hues for achromatic colors be handled? Edit: Oops, found a separate discussion in #4928 |
I've added an interactive visualization of the various hue fixup methods, and changed the fixup algorithms to work with any number of hues. Here are the formulas I use currently; they need to be tested more thoroughly, but at first blush they seem correct. // shorter:
θ₂ = Math.abs(θ₂ - θ₁) <= 180 ? θ₂ : θ₂ - 360 * Math.sign(θ₂ - θ₁);
// longer:
θ₂ = Math.abs(θ₂ - θ₁) >= 180 || θ₂ === θ₁ ? θ₂ : θ₂ - 360 * Math.sign(θ₂ - θ₁);
// increasing:
θ₂ = θ₂ >= θ₁ ? θ₂ : θ₂ + 360 * (1 + Math.floor(Math.abs(θ₂ - θ₁) / 360));
// decreasing:
θ₂ = θ₂ <= θ₁ ? θ₂ : θ₂ - 360 * (1 + Math.floor(Math.abs(θ₂ - θ₁) / 360)); In the case of
Edit: To get it to work consistently, I ended up converting the list of hues from absolutes to relative values (deltas), apply the fixup rules, then convert back to absolutes. |
Trying to find justifying use-cases for each of the fixup methods, a couple of questions came up:
|
…rpolation types. This makes a few adjustments to the definitions of hue interpolation types to follow up on the discussions in w3c#4735. First, it adjusts the pseudo-code so that for 'shorter' and 'longer', angles that differ by 180 degrees will always go in the decreasing direction, as described in w3c#4735 (comment) Second, it adjusts the set notation to match the pseudo-code. (They were not matching even prior to this change.) The set notation previously seemed to assume that there were absolute-value functions present that were not actually there. However, given the asymmetry of always going in the decreasing direction, it's more correct to write it without absolute values.
Re-tagging to css color 4 because the color interpolation section moved. |
Correct. Normalization happens as a first stage of interpolation. The specified value of an I thought that the specification was clear about hue angles outside the range [0, 360) but it isn't. The existence and validity of such angles is mentioned in passing for LCH:
and, again in passing, for HSL:
However the main section on the |
The normalization range depends on the method though. So for
|
https://drafts.csswg.org/css-color-5/#colormix
Mixing hue in lch() leads to ambiguity (or undesirable results) because hue is an angle around the circle, so there are two paths between any two values. In some cases, authors may want a hue mix that traverses the 0deg/360deg point, at other times not.
The text was updated successfully, but these errors were encountered: