w3c / csswg-drafts

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

[css-color-5] Findings on SCSS function usage to inform direction of Color 5 #5782

Open LeaVerou opened 3 years ago

LeaVerou commented 3 years ago

As part of our recent analysis of the state of CSS by querying the HTTPArchive corpus, we looked at preprocessor usage (SCSS only, where available via sourcemaps).

One thing of personal interest to me as co-editor of Color 5 was how frequently are the various color manipulation and extraction functions used? Here are the results for all SCSS function calls. We looked at 14 million function calls from nearly 300K SCSS stylesheets.

Here is the lowdown:

What does this tell us about future directions for Color 5?

CC @svgeesus @una @argyleink

svgeesus commented 3 years ago

It is super valuable to see what is being used in the wild, in a massive-scale project like the Web Almanac. Since native CSS does not yet have the color manipulation functionality, being able to reach back from the deployed CSS via sourcemaps to infer the original SCSS is the only way to get at this data.

SCSS only has the ability to manipulate in HSL, so that should be borne in mid when comparing functionality. Quoting from the HSL section of Color 4:

An advantage of HSL over LCH is that, regardless of manipulation, the result always lies inside the sRGB gamut. A disadvantage of HSL over LCH is that hue manipulation changes the visual lightness, and that hues are not evenly spaced apart.

It is thus easier in HSL to create sets of matching colors (by keeping the hue the same and varying the saturation and lightness), compared to maipulating the sRGB component values; however, because the lightness is simply the mean of the gamma-corrected red, green and blue components it does not correspond to the visual perception of lightness across hues.

The hue angle in HSL is not perceptually uniform; colors appear bunched up in some areas and widely spaced in others.

https://drafts.csswg.org/css-color-4/#the-hsl-notation

(I d suggest following the link, because Color 4 also has worked examples that show the magnitude of that effect. It is not small).

So, the lighten() and darken() usage that we have found in SCSS benefits from an advantage of HSL over LCH (any manipulation is always in gamut) and sidesteps one major disadvantage (HSL lightness cannot be compared for different hues) by keeping the hue the same.

In Color 5 we can approach that in various ways:

I need to read more on how exactly SCSS computes lighten and darken (is it a simple addition or subtraction, with clamp to keep the value in range, or is there a sigmoid function).

I also see some criticism of lighten() and darken():

Sass has color function to lighten and darken colors, but these don't always behave the way you might expect. Using mix() with black and white can produce more predictable results.

So that needs to be investigated. Mixing in white or black sounds like manipulation in HWB, or using color-mix()

The point about extracting, manipulating, and re-inserting color components is interesting. It also allows the manipulation to be done in JS, as an extensibility point. We should definitely explore it. In SCSS, re-inserting is free because it is just string concatenation; we would need a way to enable re-creating a color from components.

Would things like lch-hue() be too verbose? Should we have a generic hue() for LCH and a hsl-hue() for HSL?

I don't have a strong opinion on the naming, but yes they do need to be distinct (the same color has very different hue angles in different systems). I have a slight preference for lch-hue() both as self-documenting and also so if we get a better colorspace than Lab/LCH in the future, we are not stuck with "hue means hue in LCH for historical reasons".

svgeesus commented 3 years ago

Ok it is a simple addition or subtraction (followed by clamping):

The darken() function decreases lightness by a fixed amount, which is often not the desired effect. To make a color a certain percentage darker than it was before, use color.scale() instead.

https://sass-lang.com/documentation/modules/color

Because darken() is usually not the best way to make a color darker, it’s not included directly in the new module system. However, if you have to preserve the existing behavior, darken($color, $amount) can be written color.adjust($color, $lightness: -$amount).

I see scale-color and adjust-color are much less used, even though the SCSS documentation specifically says they are better. Which implies that ease of understanding and ease of use (simpler syntax) is valued over the end result.

Crissov commented 3 years ago

So my assumption in #3187 that authors will routinely be doing color component arithmetics if possible – even though their calculations are often too naive – seems to be close to reality.

LeaVerou commented 3 years ago

In SCSS, re-inserting is free because it is just string concatenation; we would need a way to enable re-creating a color from components.

We can already do that, e.g. hsl(calc(var(--hue) + 10) 50% 50%). In fact, we measured which functions custom properties are most frequently used in, and rgba() was the 3rd most popular, containing var() in 4% of pages. hsla() was 5th, also used in 4% of pages (which is very significant because in general hsla() was used far less than rgba().

argyleink commented 3 years ago

The findings shown here are exactly the type of things that powered Una and I to reach out with a request like "can we get some of the basic ones in first, before we solve the entire thing. Most of the usage we've seen in our teams/careers are super basic color manipulations, and designers aren't complaining about the results." We could pacify the majority of users with basic lighten, darken, mix and alpha functions on srgb colors. To me these results look like clear reasons to simplify, but I also really like the direction things are right now. Relative color manipulation, from css and js, looks super rad and I want it, today lol.

It's also worth noting from the HTTP Archive results, that most stylesheets did not create robust custom property graphs, and rarely went over 3 nested references. We're all definitely at the tip of the spear when it comes to color manipulation, fidelity and systems. I'd love to make sure my Mom or little brother can use these color functions too, but it's easy to get in the weeds of color space stretching and clamping and distortion. I'm stuck somewhere in the middle of these 2 worlds!

Could hue() accept a parameter instead of needing to be a separate function for HSL vs LCH? Allow folks to get picky wit-it when they want, but otherwise default them to something expected?

LeaVerou commented 3 years ago

Could hue() accept a parameter instead of needing to be a separate function for HSL vs LCH? Allow folks to get picky wit-it when they want, but otherwise default them to something expected?

I'm leaning towards that too, to keep the API surface small-ish. Same for lightness(). However, selecting a default has the problem that Chris pointed out:

if we get a better colorspace than Lab/LCH in the future, we are not stuck with "hue means hue in LCH for historical reasons".

A few more questions we need to answer (@svgeesus @argyleink @una):

If my co-editors agree and we reach consensus on these, I could make the edits.

una commented 3 years ago

Do we agree to ditch both color-adjust() and the relative function syntax, and instead add functions for component extraction?

I still like color-adjust because it enables more room to grow in the future.

I've felt that we should implement these with existing popular web color spaces today if the input colors are within those color spaces. I.e. if the input is RGB or HSL, we should still be able to make adjustments. This would make implementation in a timely manner much more likely, and also can still work with planning for future color spaces (the optional attribute to set which color space the mixing occurs in, or if any of the input arguments are in that color space).

Allowing for transformations in HSL or RGB color spaces would mean that developers could use these today (something they are clearly asking for, as evidenced above), and browsers could implement them without the additional performance hit (and pre-requisite) of supporting a more advanced color format.

Also, would we be able to adjust multiple components at a time?

How many functions do we add for component extraction? It looks like the no brainers are alpha(), hue(), lightness(), chroma(), saturation(). Perhaps also whiteness() and blackness()?

Same question above -- I want to avoid creating new custom properties for each transformation if we wanted to adjust both saturation and lightness of a single theme variable. (This would be a common example of translating a theme color for dark themes)

Do we also add red(), green(), blue()? They are used in SCSS (about 1% of pages each). If we do add them, can they extract components from RGB color spaces defined via color(), e.g. display-p3?

Regardless of usage, they would enable a more complete system, so I do think we should add them. RGB channels are often tweaked in SVG filters to create unique effects.

Do we need functions for Lab? If so, what to name them? I'd rather avoid single letter functions, but lab-a() is inconsistent with say hue().

If we go down this route, yes.

What to do with color() components?

I think we'll still need color() for serialization, but I don't think it conflicts with this.

Can browsers optimize something like lch(300 chroma(var(--mycolor)) lightness(var(--mycolor))) to avoid converting var(--mycolor) to LCH twice?

I agree we need a solution for multiple transforms at the same time

svgeesus commented 3 years ago

@argyleink wrote

Most of the usage we've seen in our teams/careers are super basic color manipulations, and designers aren't complaining about the results." We could pacify the majority of users with basic lighten, darken, mix and alpha functions on srgb colors. To me these results look like clear reasons to simplify,

Most of the usage in SCSS etc uses HSL because that is all that is available although I understand @mirisuzanne added some support for LCH recently. So concluding that sRGB-based HSL is all that is needed, because that is what people use, is cyclical.

That said, yes, we clearly need lighten() and darken() and ideally, they would have a better behavior than the simple addition they have in Sass:

--col1: hsl(120 100% 80%);
--col2: hsl(190 100% 70%);
--col3: hsl(40 100% 60%);
--col1d: lighten(var(--col1),40);
--col2d: lighten(var(--col2),40);
--col3d: lighten(var(--col3),40);

makes them all white. Which might be okay, but I have also been exploring sigmoid functions so there is more of a roll-off at the very light and very dark ends.

svgeesus commented 3 years ago

I've felt that we should implement these with existing popular web color spaces today if the input colors are within those color spaces. I.e. if the input is RGB or HSL, we should still be able to make adjustments. This would make implementation in a timely manner much more likely

That means:

argyleink commented 3 years ago

Most of the usage in SCSS etc uses HSL because that is all that is available

the problem, or gamut clamp, starts earlier than SCSS imo. starts with designers and their color tools. most designers hunt and peck inside the rgb spectrum square.. designers deliver hex, developers receive hex. eg: nearly every single "developer designer handoff tool" delivers hex colors. even most CSS libraries deliver hex. it's at this point, with these values, "most" devs are in SCSS and need to darken it on hover.. without asking the ${insert dependency here}.

and, tons of color function use is possible with css syntax today, but there's a belief that css can't do it. hsl can do most of it, but hsl is even too advanced for many people. it can't be ignored that massive amounts of scss stylesheets have code like opacity(#000, 20%) or lighten(blue, 5%) on hover. they wont see the benefits of the perceptual space work, they dont even see the edges of their gamut, the bunches of hues or the mud in their gradients. this main stream usage of ergonomic, natural language focused simplicity is all they need. "oh, css can lighten() now? handy! oh, docs say i can specify a colorspace, woh. maybe one day a designer will tell me which they want?". imo, they'll paste a colorspace once the tool they get their colors from has it in the easy copy button.

how much lch adoption was there in the scss source maps? i tried looking in the spreadsheet data, but saw no values? if it's low, let's make lch an upgrade / preference, and not a requirement. i've seen very low lch adoption in the postcss space, even though I feel i've talked about it plenty. again, i'm crazy excited for the capability, the channel interactions and apis, and overall coverage of the spec. i just can't ignore that there's engineering interest of a srgb version, engineering fear of performance implications of lch, as well as massive amounts of main stream satisfaction of srgb scss color functions.

LeaVerou commented 3 years ago

Do we agree to ditch both color-adjust() and the relative function syntax, and instead add functions for component extraction?

I still like color-adjust because it enables more room to grow in the future.

If you feel strongly about keeping it, we can keep both that and the relative syntax and have all three, but ultimately, we'll need to settle on one for FPWD. But if you want to keep it primarily because it may be useful in the future, note that we can always add it in the future.

I've felt that we should implement these with existing popular web color spaces today if the input colors are within those color spaces. I.e. if the input is RGB or HSL, we should still be able to make adjustments. This would make implementation in a timely manner much more likely, and also can still work with planning for future color spaces (the optional attribute to set which color space the mixing occurs in, or if any of the input arguments are in that color space).

Allowing for transformations in HSL or RGB color spaces would mean that developers could use these today (something they are clearly asking for, as evidenced above), and browsers could implement them without the additional performance hit (and pre-requisite) of supporting a more advanced color format.

On the contrary, if color manipulation motivates browsers to implement colors beyond sRGB, that's a Good Thing™, because it's about damn time. Most authors are fine to use sRGB for manipulation because that's all they've known from preprocessors, because there was no alternative. It's our job to give them good defaults that work well and manipulations in gamma-corrected sRGB are suboptimal. It's not good for the Web in the long run to favor speedy implementation for a subpar feature that we'll be stuck with for decades.

Also, would we be able to adjust multiple components at a time?

Of course. The functions are independent. E.g.

lch(lightness(var(--primary) lch) calc(chroma(var(--primary) lch) * 1.1) calc(hue(var(--primary) lch) + 10))

to manipulate both chroma and hue. Or, with shorter names like lch-h():

lch(lch-l(var(--primary)) calc(lch-c(var(--primary)) * 1.1) calc(lch-h(var(--primary)) + 10))

Yes, it's very verbose, but also it can do anything (including combine channels from multiple different colors). For the common manipulations like darkening, lightening, alpha, and mixing, we'd have shortcuts. Based on the SCSS data, it appears that other types of manipulations are significantly more rare, so I'm fine with them needing the verbose-but-does-everything syntax. If in the future other common manipulations emerge, we can add more shortcuts.

How many functions do we add for component extraction? It looks like the no brainers are alpha(), hue(), lightness(), chroma(), saturation(). Perhaps also whiteness() and blackness()?

Same question above -- I want to avoid creating new custom properties for each transformation if we wanted to adjust both saturation and lightness of a single theme variable. (This would be a common example of translating a theme color for dark themes)

I'm not sure why you'd think you need to define new custom properties? Is it for readability? Given your earlier question about whether manipulating multiple channels is possible, I think you may have misunderstood and thought I was proposing these functions as the way to manipulate individual components, e.g. hue(var(--mycolor) +20%) or something. That would indeed be very limiting, and is absolutely not what I was suggesting.

Do we also add red(), green(), blue()? They are used in SCSS (about 1% of pages each). If we do add them, can they extract components from RGB color spaces defined via color(), e.g. display-p3?

Regardless of usage, they would enable a more complete system, so I do think we should add them. RGB channels are often tweaked in SVG filters to create unique effects.

I believe in SVG filters, the color manipulation also happens in the SVG filter, e.g. via feColorMatrix. Are there any use cases in CSS?

Do we need functions for Lab? If so, what to name them? I'd rather avoid single letter functions, but lab-a() is inconsistent with say hue().

If we go down this route, yes.

We tend to add things if there are use cases, not for completeness. If there are no or very niche use cases for these, we could start without them and add them once/if use cases emerge.

What to do with color() components?

I think we'll still need color() for serialization, but I don't think it conflicts with this.

I'm not sure I follow, how does serialization relate?

LeaVerou commented 3 years ago

@argyleink

Most of the usage in SCSS etc uses HSL because that is all that is available

the problem, or gamut clamp, starts earlier than SCSS imo. starts with designers and their color tools. most designers hunt and peck inside the rgb spectrum square.. designers deliver hex, developers receive hex. eg: nearly every single "developer designer handoff tool" delivers hex colors. even most CSS libraries deliver hex. it's at this point, with these values, "most" devs are in SCSS and need to darken it on hover..

Libraries and tooling are going to hand off sRGB, if CSS only supports sRGB. If a value is generated anyway and edited visually, there is little value in handing off a more readable form of sRGB. However, lch(), lab(), and color() are not just about specifying sRGB colors a different way, they also give access to 50% more colors on modern P3 screens, which is a strong reason for tooling to change.

without asking the ${insert dependency here}.

I'm not sure I follow that one.

and, tons of color function use is possible with css syntax today, but there's a belief that css can't do it. hsl can do most of it, but hsl is even too advanced for many people. it can't be ignored that massive amounts of scss stylesheets have code like opacity(#000, 20%) or lighten(blue, 5%) on hover. they wont see the benefits of the perceptual space work, they dont even see the edges of their gamut, the bunches of hues or the mud in their gradients. this main stream usage of ergonomic, natural language focused simplicity is all they need. "oh, css can lighten() now? handy! oh, docs say i can specify a colorspace, woh. maybe one day a designer will tell me which they want?". imo, they'll paste a colorspace once the tool they get their colors from has it in the easy copy button.

You may be preaching to the choir here. Indeed, most manipulations are simple, hence why both Chris and I are in favor of lighten() and darken() shortcuts (and alpha). Ideally, like every good abstraction, these should perform well and produce good results, without designers having to care how they work, whether they're using a sigmoid function, or whether they work in LCH. They don't even need to understand how LCH works. However, the fact that they don't care or understand the inner workings is no reason to have simplistic inner workings that produce poor results. They do care about the results, even if they've learned to satisfice with poor results because that's what their tools produce. And perceptual uniformity absolutely matters when lightening and darkening, so that you get equal perceptual differences for an equal increase/decrease in lightness. Furthermore perceptual uniformity is not HSL's only problem. Its components are not orthogonal, adjusting lightness can adjust the perceptual hue and saturation (example)

how much lch adoption was there in the scss source maps? i tried looking in the spreadsheet data, but saw no values? if it's low, let's make lch an upgrade / preference, and not a requirement. i've seen very low lch adoption in the postcss space, even though I feel i've talked about it plenty. again, i'm crazy excited for the capability, the channel interactions and apis, and overall coverage of the spec. i just can't ignore that there's engineering interest of a srgb version, engineering fear of performance implications of lch, as well as massive amounts of main stream satisfaction of srgb scss color functions.

Are you seriously suggesting we determine course of action for LCH based on its adoption in preprocessors which a) has barely been an option for MONTHS and b) has no color modification functions tied to it? c) is not even built-in, but an add-on?! For a, as you would know from studying Almanac data yourself, things get adopted after being out there for YEARS. DECADES often. Not months! The SCSS stats I presented here are about functions that have existed in SCSS pretty much since its beginning. They are also built-in, not a plugin. I wouldn't make any assumptions about anything based on whether people use a certain plugin-in or library. For b, what's the point of specifying colors in LCH if you can't modify them in LCH? For c, it's extremely rare for an add-on to appear in such stats.

The Web can't stay in sRGB forever, screens have moved past it for years now. We already can't access one third of our screen colors with CSS. The "engineering fear" will need to get resolved eventually anyway, just like engineers eventually had to deal with higher pixel densities and didn't advocate that we pretend all monitors are still VGA to avoid "performance implications".

svgeesus commented 3 years ago

@una wrote

RGB channels are often tweaked in SVG filters to create unique effects.

Yes and those are linear-light sRGB values, not gamma-encoded sRGB.

svgeesus commented 3 years ago

The Web can't stay in sRGB forever, screens have moved past it for years now.

This is very true. The Web is in serious catch-up mode now. Native has had wide color gamut for a couple of decades, and even consumer Android and iOS phones and tablets have had P3 screens for four years. Streaming services like Netflix, Disney+ and Amazon Prime video have not only been shipping content in rec2020 wide gamut color, but also in rec2100 PQ High Dynamic Range. Gaming consoles are WCG and HDR.

argyleink commented 3 years ago

The Web can't stay in sRGB forever, screens have moved past it for years now.

I'm not suggesting we stay in sRGB forever. I'm suggesting we change our approach from graceful degradation (srgb fallback), to progressively enhance (lab/lch as an upgrade).

Are you seriously suggesting we determine course of action for LCH based on its adoption in preprocessors which a) has barely been an option for MONTHS and b) has no color modification functions tied to it? c) is not even built-in, but an add-on?!

Yes, I think we should look at adoption (if any). Even if it's a short blip of time in comparison for CSS. And it's not just about CSS's adoption of LCH, look at the whole industry. Lab / perceptual color spaces have been around since the 70's, have terrible adoption in tooling (designer and developer tooling), and while a very meaningful upgrade from sRGB, in many many ways (again i'm on your side here in 100% wanting lab/lch/etc color spaces to be available from CSS), I can't ignore this 50 year adoption curve.

I don't think this is the right carrot to dangle in front of browsers or engineers to get them to add color spaces. I don't think if browser shipped lab/lch today, that design tools or handoff tools would swift off of hex.

From what I can tell, we all agree on:

What we disagree on:

Again, I'm not advocating against adding color spaces. I'm advocating for unblocking. Allowing color functions and color spaces to progress individually. The package deal isn't selling well, to eng or the community. The outcome y'all pitch for, which again I want and agree with, this ability for CSS to use the higher quality colors in these powerful displays, can happen eventually. And again, I 100% see the value of color manipulation in lab/lch, it's the right goal. I think we can get there with smaller steps, instead of waiting for a big step. And yes, I understand this pitch is probably getting old. This pitch of, "can't we just use sRGB for today." I still believe there's room for everyone to win here.

LeaVerou commented 3 years ago

@argyleink

Lab / perceptual color spaces have been around since the 70's, have terrible adoption in tooling (designer and developer tooling)

Lab/LCH have poor adoption for user-facing stuff, but are used internally for a number of algorithms. We've already discussed why web-facing stuff doesn't use them.

From what I can tell, we all agree on:

  • when/if lab/lch are available in the browser, use it for the color functions not sRGB

What we disagree on:

  • blocking color functions with lab/lch color space support

There is no blocking: Engineers are free to implement these functions with an explicit srgb specifier, since they allow for color space specification. We just don't want to make gamma-corrected sRGB the default, because it's an awful space for manipulation. Here's an example of why: https://colorjs.io/notebook/?storage=https%3A%2F%2Fgist.github.com%2FLeaVerou%2Fd2894bb1281164c325e1bb40b12f1437

image

And here is a lovely video explainer: https://www.youtube.com/watch?v=LKnqECcg6Gw

Once we define lighten() and darken() and the other shortcuts as using HSL/sRGB by default, we're stuck with that forever. Same with color-mix() and the like. We can never change the meaning of existing syntax, you know that by now. Therefore, I would strongly object to defining convenience functions that produce poor results just to get a quick fix in. That is not a good way to evolve the Web Platform. We can do better.

One thing to communicate to your engineers is that perhaps they don't actually need to implement Lab/LCH color values to do simple color manipulation in these spaces. The math is actually pretty simple, and they can just implement that directly until they're ready to properly implement Lab/LCH. One downside if they take that route and output sRGB as the result is there will be pointless gamut clipping for colors outside sRGB but within the screen gamut. They could get around to that by implementing color(display-p3) like Safari did, and outputting values in that.

In any case, it would be good to invite said engineers either in this issue or another one to express their implementation concerns so @svgeesus and I could talk to them directly, as this game of broken telephone does not facilitate communication. We have both seen a lot of such concerns that upon closer investigation turned out to be unfounded, so I think it could be a very productive discussion.

From what I can tell, we all agree on:

  • middle ground complexity functions color-adjust(black lightness -20%)
  • advanced destructuring and relative functions hsl(from black h s calc(l - 20%))

I don't think these are things we all agree on, since I proposed removing both of these in an earlier comment. What I've seen in the data is huge, huge usage of simple manipulations (darken, lighten, opacity, mix) and then math on individual components for more complex manipulations. Middle ground complexity seems to serve very few use cases to be worth it; is too complex for the simple cases and not powerful enough for the complicated ones. For similar reasons, I'm not in favor of my own relative syntax proposal anymore. It may offer more power than color-adjust() (with the tradeoff of verbosity), but it's still limited in some ways, e.g. doesn't allow combining channels from different colors. As I expressed above, I'm now in favor of dead simple shortcuts for the common cases, which we've seen comprise the overwhelming majority of manipulations, and maximum power for the remaining few complex cases.

mirisuzanne commented 3 years ago

Just to clarify some of the Sass context here…

I see scale-color and adjust-color are much less used, even though the SCSS documentation specifically says they are better. Which implies that ease of understanding and ease of use (simpler syntax) is valued over the end result.

Part of this also comes down to timing. The lighten() and darken() functions predate all the more advanced options, and the warning about which is better.

Most of the usage in SCSS etc uses HSL because that is all that is available although I understand @mirisuzanne added some support for LCH recently.

I wrote the oddbird/blend library, which provides conversion between Sass colors & LCH, but we have not added any LCH functionality to the Sass core. We did add hwb() recently, since it describes sRGB space - but we haven't solved how we want to handle color-spaces. We're waiting for a more stable CSS spec before we add it to Sass.

Also worth note: all Sass output is in hex, color keywords, or rgba() – whichever is shortest, with the largest legacy browser support. Even if authors are writing hsl() or hwb() in their Sass, that will not appear in their production CSS.