Closed LeaVerou closed 3 years ago
Thank you for doing this. I'd love to see this happen.
I'm curious about how to make this apply to the underlying value so that you can, for example, have a generic saturate
class, or desaturate
class for that matter. We could define a syntax that applies to the cascaded value something like font-size: larger
does but that's a nightmare to implement (I suspect there are still plenty of animation edge cases here in particular) so I think we probably don't want to go that route. Perhaps we just need to define this in such a way that additive CSS will work, particularly now that we have definitions for how to add CSS values.
For example, with the current addition facilities for animation one can already bump up the green channel by adding rgb(0, 128, 0)
to the underlying value but there's no facility for subtracting from a channel, nor operating on, say, the luminance channel.
I like the re-use of the existing color functions for modifying the channels in-place. I wouldn't make any of the arguments optional, tho; that seems both unnecessary and confusing - your hsl(red 90%)
example shows that off. It's not just a parsing problem, it's an understanding problem - to understand that, you need to recognize that 90%
can't be the first value, so it must be the second value.
Instead, I think they should all be the same mandatory-ness as usual, and we use your same
idea, or simplify it even more: hsl(red _ 90% _)
, with the _
ident indicating that you take that value from the provided color. (And when values can be omitted, like alpha, they default to _
, I guess?).
Yeah, agreed. I guess then we need to find a syntax for same and relative values that is both sufficiently understandable and sufficiently terse. I’m a tiny bit worried that _
may be too terse, but mostly I think I like it.
One benefit of the optional argument syntax that is now lost is that it's also a color space conversion syntax. rgb(var(--mycolor))
clips to sRGB, color(var(--mycolor) myCMYK)
would convert to CMYK. Not sure how useful that was by itself and whether we want to preserve it. It can still be expressed via rgb(var(--mycolor) _ _ _)
and color(var(--mycolor) myCMYK _ _ _ _)
respectively.
I was a bit concerned about how this overlaps with additive CSS―assuming that happens sometime in the near future.
On that one hand, additive CSS seems to provide something similar, i.e. presumably one could write border-color: hsl(0deg 50% -50%) !add
and we'd define it so it combines with the underlying value by converting as necessary to HSL first (and similarly for Lab etc. too).
However, I think getting additive CSS to apply to a color in an arbitrary CSS variable is sufficiently awkward (you'd have to have a separate rule to make the underlying value use the CSS variable) that this proposal makes sense in its own right. This proposal is also a lot more elegant when altering the color component in a more complex value.
hsl(red 90%)
would create an HSL color with a lightness of 90%, and the saturation and hue from the provided color. Potential flaw: Inhsl(red 0% 50%)
: is the0%
saturation or lightness? We need to parse the next token to figure that out.
That is just like properties with optional values. We have all sorts of rules for which value is used to fill in missing ones. Iʼm not sure you are proposing the most intuitive one here. hsl(red 90%)
could expand to any of the following, all using the hue from red
:
hsl(red 90% 90% 90%)
– this and similar ones would make more sense if hsl(0deg 90%)
expanded to hsl(0deg 90% 90% 90%)
hsl(red 90% 90% red)
hsl(red 90% red 90%)
hsl(red red 90% 90%)
hsl(red 90% red red)
– I believe this is the most systematic expansionhsl(red red 90% red)
– assumed in the original proposal abovehsl(red red red 90%)
With RGB, the single provided percentage value might even apply to the first, red channel.
Since Tab convinced me that optional arguments are a bad idea, I updated the original post so that it's not reflecting a proposal that even I don't support anymore, and so we can focus on the parts of the syntax that matter more.
🆗
With the keywords for coordinates idea, you would not need the underscore: hsl(red h s 90%)
. Single letters may actually work better than words because of rgb(red red green 90%)
.
Had no idea that you can drop all the commas in those css functions. Wow, that feels weird and vague.
I like the idea of using single-letter keywords for placeholders as they match the functions' names. It creates a clear mapping. It only gets a bit weird when it comes to the alpha components on lab
/lch
as we don't have laba
/lcha
.
What about keywords and +/-?
hsl(h s 50% from red)
, set lightness to 50% from the color red
hsl(+20deg s l from green)
, rotate hue by +20deg from the color green
hsl(-15deg +10% -20% from lightblue)
, rotate hue by -15deg, add 10% saturation, subtract 20% lightness from the color lightblue
Or:
hsl(red to h s 30%)
, set red to lightness of 30%
hsl(darkblue to 0deg s +30%)
, set darkblue to hue of 0deg and add 30% lightness
I realise it might get confusing/conflict with absolute negative values. I think hsl can have negative hue values.
Perhaps there are ways to lean on calc
instead as that is meant for number tweaking? Given the placeholders:
hsl(calc(h + 5deg) calc(s + 10%) calc(l - 10%) from darkbrown)
hsl(calc(h - 20deg) s l from blue)
Testing longer keywords again:
hsl(calc(hue + 5deg) calc(saturation + 10%) calc(lightness - 10%) from darkbrown)
I kind of like using calc
for relative tweaking now that I think about it.
hsl(h calc(s * 2 - var(--prop, 50%)) l from darkred)
While keywords usually increase readability, in this case since there are no commas to group the different arguments. Therefore, keywords like from
or to
read like a separate argument, and don't look grouped with anything.
Using +
and -
to indicate relative modifications would indeed be the most readable, but unfortunately, it's not an option for disambiguation reasons. The parser sees +10%
and 10%
as equivalent.
I love the idea of using calc()
for tweaking! It would even work with the generic _
we were discussing above. It would be the first case of using keywords in calc()
, which may make it trickier to implement, but hopefully the flexibility is worth it? After all, we were always planning to use keywords in calc()
eventually.
I like the idea of working with the syntax we've got, although complicating the arguments of the color functions is starting to make dropping the commas look like a bad idea.
There definitely needs to be some sort of grouping structure, even if it's just a mandatory wrapping ()
without a calc
prefix, so that you can distinguish addition /subtraction from simply setting the next number in the sequence. And distinguish division from the new syntax of using /
to separate the alpha channel.
I find the use of the component initials as the placeholder for the starting value more intuitive & easier to read than _
. But I recognize that there are long-term benefits to having a single token that works in any colour function, especially when you consider custom color systems. But maybe it could be both? The definitive syntax could use a generic placeholder, but allow the letters a synonyms in the common functions?
I like the idea of using a from <color>
keyword approach, especially when you consider that the color would often be a variable, and some of the parameters may also be variables, so an extra keyword would help in readability.
So, what I'm thinking of is something like this:
rgb(from var(--backdrop), r g b/0.5) /* this is making alpha=0.5, not changing the blueness!*/
hsl(from var(--accent), h (s/2) (l - 20%))
lab(from var(--primary), var(--luminance) a b)
Re @birtles musing about an additive mode: we could define it so that the relative adjustments are valid without the from <color>,
bit. The color could default to transparent black, but in additive mode it could be whatever the base color in the adjustment stack is.
although complicating the arguments of the color functions is starting to make dropping the commas look like a bad idea.
If one only needs the commas to put potentially-clashing complex grammars together, that suggests one should be using functions to contain the grammars instead; as an added bonus, you can then use commas in the function without clashing with the channel separator.
As an added bonus, functions give names to the functionality, which makes them easier to recognize and search for.
that suggests one should be using functions to contain the grammars instead
Yeah, I kind of acknowledged that in my next sentence. It was just getting difficult to mentally parse some of the examples people were presenting earlier in the thread. I was sort of cheating by using brackets without a function name, but I agree with
functions give names to the functionality, which makes them easier to recognize and search for.
That said, I'm concerned about reusing calc()
if we're also going to add a context-specific token. So my vote would be for rel()
or adjust()
or something like that. But I still like the idea of having a token for the current variable, to clearly distinguish rel(s + 20%)
from rel(s*20%)
.
I agree that named components are far more intuitive, I'm just not sure what happens in the case of color()
and custom color profiles. I wonder if we can allow people to provide letters for the components in the @icc-profile
rule (or whatever it's called) and default to a, b, c, d, ... otherwise?
Note an issue with single-letter names: lab() has both an "a" component and an alpha.
Note an issue with single-letter names: lab() has both an "a" component and an alpha.
That's not really an issue if resolution of the token is always based on the position in the function. It's only an issue if we allow parameters to be omitted in the middle or if we allow complex color-matrix type math. The rel()
function could be defined to take any <custom-ident>
token, and that ident would always resolve to the underlying value of that component in the base color.
One benefit of the optional argument syntax that is now lost is that it's also a color space conversion syntax. rgb(var(--mycolor)) clips to sRGB,
I don't think that should be true. The color functions allow out-of-gamut parameters; this is especially necessary since computed color values are always converted to rgba()
format. Clipping only happens to device limitations at used value time.
But that makes me thinking that in addition to a relative adjustment function, there should maybe be a clamp()
function (or separate max/min functions) that can use the underlying value as starting point.
@AmeliaBR All existing color functions are defined as operating in sRGB, e.g. look here for rgb(). rgb(100%, 0, 0)
is not the brightest red the monitor can produce, it's the brightest red in sRGB. Out of range values are indeed allowed, but the resulting color is always in sRGB.
And yes, this does mean that until browsers implement lab()
and lch()
, we can't use the brightest colors in today's monitors with CSS.
And no, making rgb()
device-dependent is not the way to go.
@LeaVerou from a later paragraph in that section:
Values outside the device gamut should be clipped or mapped into the gamut when the gamut is known: the red, green, and blue values must be changed to fall within the range supported by the device.
So per spec, clamping applies to the device gamut, not the sRGB gamut. Not sure whether or not that's implemented: you'd have to test whether rgb(150%, 0%, 0%)
on wide-gamut devices is noticeably brighter than regular red
.
So per spec, clamping applies to the device gamut, not the sRGB gamut.
Yes, but in the proposed syntax, functions which operate on sRGB values will necessarily also involve a clip of the intermediate values to sRGB if the color you are modifying from is outside sRGB. In practice, that is easily avoided but the spec has to state what happens with all possible combinations, not just sensible combinations.
Not sure whether or not that's implemented
It is impossible to not implement it :) a device can't display colors outside it's gamut, by definition.
I was under the impression that rgb(400, 0, 0)
is first clipped to rgb(255, 0, 0)
and then that might be further clipped if the device gamut is smaller than sRGB (which was common when CSS Color 3 was authored, e.g. the 2013 MacBook Air gamut is around 62% of sRGB, and that's not super long ago!). This seems to be on par with what Chrome and Safari are doing (test), while Gecko erroneously treats colors as being specified in device RGB and not sRGB (it still clips to 255, but in this case the opposite would be outside the device gamut).
Sublime Text's minihtml engine had the need for color modifications last year, and I implemented them using the older draft of the color-mod function. Rather than invent our own syntax, it seemed to make sense to start from a foundation that had been previously discussed. Not all aspects of that have been implemented, but recently I ran into a use case that feels worth considering since this new proposal is being discussed.
We have two components of the UI that introduce color - the theme and the color scheme. The color scheme includes colors fo syntax highlighting, and the theme controls the editor chrome. Color schemes are relatively easy to create and include diverse palettes. Themes require much more work, including raster graphics. Because of this, themes can derive colors from the color scheme.
This introduces the issue of ensuring contrast. The original contrast()
modifier only allowed modifying a color to create a new color the was appropriately contrasted with the base. This may be useful in some situations, but in our use case the issue is tweaking a color so it is appropriately contrasted with a different base. For instance, a blue color from a color scheme needing to be high enough contrast to be used as foreground on a grey background.
So in addition to:
Getting white or black depending on which one contrasts better with the base color
It seems it would be nice to have an option for:
Hard-coding the ratio to 4.5 and then allowing blends between that and the maximum contrast color seems less useful than the user being able to specify the minimum contrast they want. It may not be that 4.5 is required (such as for headlines), or that the UI element is a decoration that requires some contrast, but not a full 4.5.
It seems most of the discussion on syntax so far has been for modifying components of a color in a specific color space. (My personal take on that is that the rel(50%)
syntax feels like one of the less complicated syntactic approaches.) So the following example is not endorsing a specific syntax, but proposing a basic concept that would be useful.
color-contrast(var(--red) var(--bg) 3.0)
This would modify the var(--red)
to have a contrast ratio of at least 3.0 when compared with var(--bg)
. I would be fine with the syntax being contrast()
, min-contrast()
or contrast-adjust()
.
If there was a function to pick color based on contrast, perhaps something like pick-contrast(white black var(--bg))
. There is also the idea proposed in #1627 of picking the closest contrasting color from a list and then adjusting it to meet the required contrast ratio.
In terms of blending, cross-fade()
makes me think of audio production, as opposed to colors. I think blend()
or lerp()
may be a better fit for the color domain.
I figured I'd make some edits to the original proposal that reflect the discussion so far, and my take on the issues raised.
This is loosely inspired from the current practice of defining major theme colors as color components in a variable (e.g. --color: 180, 60%
) and then using them in color functions (e.g. hsl(var(--color), 90%)
). However, this is simple textual substitution, which is insufficient, and imposes constraints on how the base color is defined.
I’m proposing to extend all color functions to allow for a <color>
argument, preceded by the keyword from
to make it clear that this is a color (in case a variable is used). If the color argument is present, it is first converted to the target color space of the function, then its coordinates are adjusted based on the remaining arguments. A _
keyword represents the value of each coordinate and can be used by itself to mean "no change", or in calc()
for relative modifications.
Edit: As of the June 5 F2F discussion, we are not going to use _
but keywords or letters. How we handle color()
is yet to be decided. I'm gonna use a, b, c etc in the examples below for now.
Before any modification, the color would be converted to the target color space of the function used. This means that modifications using hsl()
can be lossy (since the color would need to be converted to sRGB), but there is no such problem with lab()
or lch()
. However, any syntax that allows modifying HSL or RGB coordinates would have the same issue, at least this syntax makes the conversion more explicit.
With the syntax for relative modifications, this addresses most desired adjustments, and expands naturally with every new color function. Tint and shade could be relative modifications on hwb()
, though adjusting the L of Lab/LCH is better as it's lossless (and produces fantastic results).
lch(from var(--accent1), calc(l * 1.2) c h)
would adjust var(--accent
) to be a little lighter.lch(from var(--accent1), calc(l + 10) 230 h)
would adjust var(--accent
) to be very bright (sets its Chroma to 230) and slightly increase its lightness (add 10 to it).rgb(from indianred, 255 g b)
would produce the same color as indianred
but with 255 in the red channel. The result would be in sRGB since that's what the rgb()
function produces.color(from var(--main), myCmyk calc(a * 1.2) b c d)
produces a CMYK color (in a custom CMYK color space) with its Cyan channel increased.lab(from var(--mycolor), l a b / 40%)
sets the alpha of var(--mycolor)
to 40% regardless of what it originally was (and no gamut clipping occurs since lab()
encompasses all visible color).calc()
). No need to learn different ways to refer to the same color components.calc()
.hsl()
modifications because that's what they're familiar with, even though lch()
produces better results and takes maximum advantage of the device gamut. It’s easy to clip the modified color to sRGB, since authors are very familiar with HSL and many would use that. However, any color adjustment syntax that supports adjusting hue, saturation, lightness would have the same issue. At least this syntax makes it obvious that you are creating an hsl()
color, with the gamut limitations that this comes with.Another idea for implementation: https://gist.github.com/una/edcfa0d3600e0b89b2ebf266bf549721
The CSS Working Group just discussed Color Stuff
, and agreed to the following:
RESOLVED: Put all the proposals into css-color-5, ChrisL and future Una as editors
RESOLVED: Rename to put 'color' first, adjust-color -> color-mod()
RESOLVED: Add color-contrast() without currentbg
RESOLVED: Add color-mix(), try to align with cross-fade()
RESOLVED: Put both color adjustment proposals into css-color-5, with keywords instead of css-color-5
RESOLVED: Add Lea Verou as editor of css-color-5
@AmeliaBR said
That said, I'm concerned about reusing calc() if we're also going to add a context-specific token. So my vote would be for rel() or adjust() or something like that. But I still like the idea of having a token for the current variable, to clearly distinguish rel(s + 20%) from rel(s*20%).
I like the generality of calc here. Although the examples so far have been restricted to simple arithmetic operators (+ - * /) on the current channel, it is an advantage to be able to compute from cusstom properties and to use the recently added calc trig functions, for example.
I wonder if there is any useful overlap to be found between the _
proposal and other discussions about access to existing values?
currentValue
/cascadedValue
in #1594inherit()
for vars in #2864calc(inherit)
in #2764They all deal with access to existing, previously-inaccessible values – often with a context-specific token in calc()
. Color-space metrics aren't the same as inherited values, but it seems useful to consider them in relation to each other.
the
_
proposal
Btw as we discussed in the meeting, if the proposal is adopted, it won't be with _
but with keywords (not yet sure how that's gonna work with color()
). I added a quick note to the post above to clarify this, and hopefully I can soon draft it up in Colors 5.
Btw as we discussed in the meeting, if the proposal is adopted, it won't be with _ but with keywords (not yet sure how that's gonna work with color()).
Lea has now added that proposal to the spec, see https://drafts.csswg.org/css-color-5/#relative-colors
This syntax with “local constants” allows to easily switch channels, e. g. rgb(from currentcolor b g r)
, and it does allow inner colors defined in any valid syntax after from
, but authors are limited to the parameters of the outer functional notation they are using, i. e. they cannot access the ones of other notations. Iʼm not sure how useful this would actually be, but I assume authors would expect or even want to be able to do that nevertheless. Wouldnʼt it thus make sense to make them available as well?
This would require unambiguous, multiple-letter monikers for some components of hsl
and hwb
vs. lch
, and rgb
vs. hwb
vs. lab
. The keyword for the opacity channel common to all notations is already spelt alpha
instead of a
which would clash with lab
.
red[ness]
is a <percentage>
that corresponds to the origin color’s red channel after its conversion to sRGB
green[ness]
is a <percentage>
that corresponds to the origin color’s green channel after its conversion to sRGB
blue[ness]
is a <percentage>
that corresponds to the origin color’s blue channel after its conversion to sRGB
hue
is an <angle>
that corresponds to the origin color’s HSL hue after its conversion to sRGB, normalized to a [0°, 360°) range.
sat[uration]
is a <percentage>
that corresponds to the origin color’s HSL normalized chroma (i. e. saturation) after its conversion to sRGB
light[ness]
is a <percentage>
that corresponds to the origin color’s HSL lightness after its conversion to sRGB
white[ness]
is a <percentage>
that corresponds to the origin color’s HWB whiteness after its conversion to sRGB
black[ness]
is a <percentage>
that corresponds to the origin color’s HWB blackness after its conversion to sRGB
l-star
/ bright[ness]
is a <percentage>
that corresponds to the origin color’s CIE lightness
a-star
or chroma[ticity]-a
is a <number>
that corresponds to the origin color’s CIELab greenish-reddish a axis
b-star
or chroma[ticity]-b
is a <number>
that corresponds to the origin color’s CIELab bluish-yellowish b axis
chroma
is a <number>
that corresponds to the origin color’s LCH chroma
h-star
is an <angle>
that corresponds to the origin color’s LCH hue, normalized to a [0°, 360°) range.
alpha
is a <percentage>
that corresponds to the origin color’s alpha opacity transparency
This would also allow to make further values available in the future, which do not need to have dedicated notations, e. g. HSI intensity
(relative chroma), HSV relative value
, HSB absolute brightness
; grayness
; XYZ tristimulus-x
; luminosity
(W, J/s), YUV luminance
(linear y
, nit, cd/m²), Y'UV / Y'CC luma
(y-prime
), chroma[ticity]-u
, v-star
, chroma-blue
(Cb, Pb), red-cyan
(Cr, Pr), chrominance
, colorful[ness]
(saturation as relative colorfulness, chroma as comparative colorfulness;), blue-luminance
(U), luma-v
; YIQ orangish-bluish
(I), purple-green
(Q), CMYK cyan[ness]
, magenta[ness]
, yellow[ness]
, key
; tint
, tone
, shade
; radiance
…
Btw., I believe it is counter-intuitive that angular hue would be converted to a <number>
, so I kept <angle>
.
they cannot access the ones of other notations. Iʼm not sure how useful this would actually be
I can't imaging a use case for computing, say, 30% of the CIE L, 20% of the sRGB green channel, and 50% of the HSL hue.
Making the syntax more cumbersome to allow for a possibility with no use case does not seem like a win.
@Una wrote
Another idea for implementation: https://gist.github.com/una/edcfa0d3600e0b89b2ebf266bf549721
@una has now added that proposal (as modified by CSS WG discussions in Toronto) to the spec too, and the co-editors have been working on illustrative examples.
Itʼs probably impossible to imagine realistic use cases for every color component combination, but finding a single one would already mean, that the identifiers needed to be unique across functional notations, in my humble opinion.
Would it be conceivable, for instance, that someone wanted to use CMYK key
as HWB black
or its cyan
, magenta
and yellow
components as RGB channels?
hwb(from device-cmyk(10% 20% 30% 40%) 180deg 50% key /*= 40%*/)
rgb(from device-cmyk(10% 20% 30% 40%) magenta yellow cyan / key)
Would someone want to transplant an sRGB HSL hue or lightness into CIELab LCH?
lch(from hsl(180deg 50% 80%) lightness /* = 80% = 0.8 */ chroma hue /* = 180deg */)
hsl(from lch(0.5 0.8 180deg) h-star sat l-star /* = 0.5 = 50% */)
You could effectively write custom conversion operations this way.
realistic use cases for every color component combination
When and if such use cases arise, the author could use nested functions. The result of one adjustment could be the "from" value for another adjustment.
@AmeliaBR I first thought this made absolute sense, but I cannot come up with some actual nested syntax that would result in k
from device-cmyk()
being used for b
in hwb()
, for instance.
hwb(from device-cmyk(10% 20% 30% 40%) 180deg 50% b/*?*/)
Btw, I posted some of the findings from HTTPArchive that relate to this in #5782.
Closed by WG resolution to drop color-adjust()
and retain RCS
Edit: Most current proposal here, just go straight there unless you want to read the thread for historical reasons.
The lack of a color modification syntax is one of the few things that authors still use preprocessors for, and severely limits what CSS variables can do. I'm going to post this proposal I've been thinking about for a while. It's by no means perfect, but perhaps it can get the discussion going, so we end up with something decent eventually!
Goals of a color modification syntax
Proposal
This is loosely inspired from the current practice of defining major theme colors as color components in a variable (e.g.
--color: 180, 60%
) and then using them in color functions (e.g.hsl(var(--color), 90%)
). However, this is simple textual substitution, which is insufficient, and imposes constraints on how the base color is defined.I’m proposing to extend all color functions to allow for a
<color>
argument. If the color argument is present, it is converted to the target color space of the function, then the remaining arguments set or modify its coordinates in that color space. A_
orsame
keyword can be used to mean "no change". For example, if a function’s grammar is<number>{3}
, it will become<color>? (<number> | _){3}
.For example,
hsl(red _ _ 90%)
would create an HSL color with a lightness of 90%, and the saturation and hue from the provided color.Before any modification, the color would be converted to the target color space of the function used. This means that modifications using
hsl()
can be lossy (since the color would need to be converted to sRGB), but there is no such problem withlab()
orlch()
. However, any syntax that allows modifying HSL or RGB coordinates would have the same issue, at least this syntax makes the conversion more explicit.Directly setting arguments is not sufficient for most modifications, since it cannot perform relative modifications (e.g. "increase L by 10%" instead of "set L to .3"), which are far more common use cases. Therefore, we need to introduce a syntax for relative modifications as well.
Ideas for that:
rel(<percentage>)
: Multiplies the color component by<percentage>
. E.g.lab(red rel(60%) rel(100%) rel(100%))
would convertred
to Lab, and then multiply its L by 60%.up <percentage>
/down <percentage>
: Relative addition/subtraction. The previous example would belab(red down 40% up 0% up 0%)
. Given the lack of commas that we recently introduced, I'm finding this hard to read, since the keywords are not visually grouped with their percentages.calc()
. E.g. the previous example would belab(red calc(.4 * l) a b)
. Seems the most expressive and flexible, but probably too messy to define.With the syntax for relative modifications, this addresses most desired adjustments, and expands naturally with every new color function. Tint and shade could be relative modifications on
hwb()
, though adjusting the L of Lab/LCH is better as it's lossless (and produces fantastic results).Benefits of this syntax
Drawbacks
cross-fade()
, since we need to address this for interpolation too?hsl()
color, with the gamut limitations that this comes with.color(hsl(color(mycmyk .1 .2 .3 .4) mycmyk 60%) .4)
. However, a) any sufficiently powerful syntax will allow for silly things, and b) see point above.