Skip to content
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] Support all existing (non-legacy?) formats in color()? #6741

Closed
LeaVerou opened this issue Oct 18, 2021 · 31 comments
Closed

[css-color] Support all existing (non-legacy?) formats in color()? #6741

LeaVerou opened this issue Oct 18, 2021 · 31 comments
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-color-5 Color modification

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Oct 18, 2021

Yes, I know there are unresolved issues wrt to representing polar coordinates in color() and we'd need to solve these first. Maybe by changing its grammar to be [<number-percentage> | <angle> | none]+ instead of [<number-percentage> | none]+.

Why should we be able to represent all existing color formats through color()?

  • Right now the distinction between which color formats have a dedicated function and which ones are specified via color() is murky. The spec describes color() as "Profiled, Device-dependent Colors", but also includes the XYZ spaces there.
  • There is no way for authors to parameterize the color space for colors not available in color() so that e.g. they can use OKLCH if that's supported or LCH if not.
  • Even we are confused about whether to add new color spaces in color() or as separate functions, imagine how difficult authors will find it to remember the distinction.

Reasons against doing this:

@LeaVerou LeaVerou added the css-color-4 Current Work label Oct 18, 2021
@Crissov
Copy link
Contributor

Crissov commented Oct 18, 2021

I’m repeating myself here, but I’m now almost convinced that it would be a cleaner approach to add the color space identifier to existing color pseudo functions, e.g.: rgb(prophoto, 10% 20% 30% / 40%) for cartesian coordinates or hsl(prophoto, 10deg 20% 30% / 40%) for cylindrical coordinates.

@LeaVerou
Copy link
Member Author

LeaVerou commented Oct 19, 2021

@Crissov What are the use cases that justify this added complexity? Do we really need more usage of flawed color models like HSL? Also, this is an orthogonal issue to what we're discussing here. If you want to discuss that proposal, please open a new issue.

@svgeesus
Copy link
Contributor

@Crissov wrote:

I’m now almost convinced that it would be a cleaner approach to add the color space identifier to existing color pseudo functions

Proposals of the form "let us take a spec which is being implemented, and change all the things including stuff that has been stable for years if not decades" have a significant cost.

This particular one also, as @LeaVerou points out, encourages use of HSL and HWB.

@danburzo
Copy link

danburzo commented Oct 25, 2021

Yes, I know there are unresolved issues wrt to representing polar coordinates in color() and we'd need to solve these first. Maybe by changing its grammar to be [<number-percentage> | <angle> | none]+ instead of [<number-percentage> | none]+.

I think it's perfectly reasonable to only accept the <number> version of <hue> in the color() syntax to represent polar coordinates.

There is no way for authors to parameterize the color space for colors not available in color() so that e.g. they can use OKLCH if that's supported or LCH if not.

Currently the unifying aspect of color spaces that you can express with color() is the expected range of each of their components is [0, 1] / [0%, 100%], while the theoretically-unbounded color spaces don't share a scale. The chroma in oklch hovers inside [0, 0.3], while for lch it's more like [0, 130] (approximate numbers that fit in the sRGB gamut), so hotswapping these color spaces will only produce usable results if we scale oklch to match lch. (#6642 (comment))

@danburzo
Copy link

danburzo commented Oct 25, 2021

Added as a separate issue how I think lab() / lch() syntax interferes with the expectation that <number> = <percentage> / 100, relevant to opening up the color() syntax to potentially any available color space:

Update: Recalibrated my understanding of percentages in color(); <number> does not need to be <percentage> / 100, so the lab() "exception" is not particularly challenging.

@danburzo
Copy link

danburzo commented Oct 26, 2021

A first proposal that maintains the current [<number-percentage> | none]+ syntax:

  • Polar coordinates (e.g. the hue in hsl, hwb, lch, and oklch) accept <number> to mean degrees, with <percentage> mapping to the [0, 360] range.
  • Theoretically-unbounded coordinates with meaningful ranges, (e.g. the lightness in lab, oklab, lch, and oklch), accept <number> in their respective useful range ([0, 100] for lab / lch, [0, 1] for oklab / oklch).
  • Theoretically-unbounded coordinates without a meaningful range, (e.g. a, b, and chroma in the CIE/OK spaces), can accept <number> liberally. We don't, however, have the reference against which to map <percentage>, so the value falls back to 0, as if the component was omitted or specified with none. Other options include inventing a reference range (and there are quite a few ranges to invent), or treating the color as invalid.

On the invention front, Photoshop clamps a/b in CIELAB to [-128, 127]:

Photoshop dialog reads: An integer between -128 and 127 is required. Closest value inserted.

I'll add one more reason for allowing all color formats to be expressed as color(): enabling the interop-friendly <number> for L in CIE/OK spaces which, depending on the conclusion of this separate issue, may or may not be allowed in the dedicated color functions.

@LeaVerou
Copy link
Member Author

We were actually discussing this last night with @svgeesus. That even without OKLab and friends, currently our use of percentages is inconsistent because in Lab 0-100% maps to 0-100 (1x) whereas in RGB spaces 0-100% maps to 0-1 (1/100). I see 5 possible solutions, from worst to (IMO) better:

  1. Ditch <percentage> in color() altogether. I'd rather we didn't, it can be quite convenient.
  2. Define that <percentage> always maps to a specific range (e.g. 0-1), which will either give us weird numbers for Lab (where 10000% would be needed to specify 100) or weird numbers for RGB spaces (where 1% would specify 1). Let's not, this is awful.
  3. Adjust the numerical coordinates of spaces so that we can specify percentages with a fixed multiplier. E.g. either make L 0-1 or make RGB coordinates 0-100. Nope nope nope, this is hugely inconsistent with anything else. We need people to be able to copy/paste coordinates from other tools.
  4. Make the multiplier a property of the color space, so that percentages in Lab map to 0-100 (for all coordinates) and percentages in RGB spaces to 0-1. This means that for custom color spaces either we pick a multiplier and that's it, or we need to provide a descriptor.
  5. Each coordinate of each color space has a reference range, and percentages map to that range. For bounded coordinates, the range is obvious, for unbounded ones like in Lab or OKLCH etc we pick a reasonable one. Perhaps an algorithm for picking the reference range for coord X in color space S could be to calculate the range of S.X for all display-p3 colors, then expand it to the closest round-ish number.

The advantage of 5 is that it makes color formats portable. For example, people can experiment in OKLCH without knowing intimate details about OKLCH because there is already a range they can tap into. It also means they can use @supports to provide fallbacks for browsers that don't support color formats, and still get something semi-reasonable. It also gives percentages a distinct advantage over numbers, instead of being slightly weird syntactic sugar. And since the actual numerical coordinates don't change, people can still copy and paste from other tools. It's the best of both worlds.

It would also make it easier to develop meta tools, like color pickers if there's a reference range for every coordinate, so that's useful in its own right.

@svgeesus
Copy link
Contributor

On the invention front, Photoshop clamps a/b in CIELAB to [-128, 127]:

Which is really dumb because

  1. Squeezing Lab into 8 bits per component is a terrible idea and gives you significant posterization; and that is the sole justification for a [-128, 127] range
  2. Real-world surface colors need a and b up to 150.

On the other hand I have also seen systems clamp a and b to ±100 which is even worse.

@svgeesus
Copy link
Contributor

Perhaps an algorithm for picking the reference range for coord X in color space S could be to calculate the range of S.X for all display-p3 colors, then expand it to the closest round-ish number.

For all rec2020 colors, perhaps.

@danburzo
Copy link

danburzo commented Oct 26, 2021

The advantage of 5 is that it makes color formats portable. (...) It also gives percentages a distinct advantage over numbers, instead of being slightly weird syntactic sugar. And since the actual numerical coordinates don't change, people can still copy and paste from other tools. It's the best of both worlds. It would also make it easier to develop meta tools, like color pickers if there's a reference range for every coordinate, so that's useful in its own right.

I am very enthusiastic about the web platform defining the reference ranges and agree about the advantages of percentages in all components of the color() syntax. What would be a future-proof color space against which to compute them? Would rec2020 suffice? (Edit: asked before seeing @svgeesus response above)

@LeaVerou
Copy link
Member Author

As a general principle, we need to balance current utility with future utility, which won't happen if you can only go up to 50-60% on most coordinates. My thinking was that display-p3 offers a good balance, as it's already common, and mainstream screens are unlikely to surpass it for the next 5-10 years, and when they do, color() usage will be entrenched sufficiently, that people will be ok to go beyond 100% if needed. Also, the algorithm I proposed rounds the reference range up, so it still allows for some wiggle room above display-p3.

@danburzo
Copy link

Just to get a rough idea, I've computed ranges for some unbounded components against the rec2020, display-p3, and srgb gamuts, truncated to 3 decimal places.

Component rec2020 display-p3 srgb
lab.a [-160.725, 125.901] [-106.559, 105.781] [-79.287, 93.550]
lab.b [-126.249, 132.124] [-115.594, 122.031] [-112.029, 93.388]
lch.c [0, 194.330] [0, 148.113] [0, 131.207]
oklab.a [-0.415, 0.389] [-0.304, 0.318] [-0.233, 0.276]
oklab.b [-0.347, 0.237] [-0.321, 0.229] [-0.311, 0.198]
oklch.c [0, 0.468] [0, 0.368] [0, 0.322]

@LeaVerou
Copy link
Member Author

LeaVerou commented Oct 26, 2021

Thanks @danburzo, that's wonderful!

So, if we were to use display-p3 as a base, the reference ranges I'd propose based on the logic I described above would be:

  • lab.a: [-125, 125]
  • lab.b: [-125, 125] (a and b should have the same range I think)
  • lch.c: [0, 150]
  • oklab.a: [-0.4, 0.4]
  • oklab.b: [-0.4, 0.4] (see above re: a and b)
  • oklch.c: [0, 0.4]

@facelessuser
Copy link

Happy to see percentages being explored as being context-aware instead of forcing a color to map to 0 - 100 🙂. I think this makes way more sense.

@svgeesus
Copy link
Contributor

sqrt ( 0.4^2 + 0.4^2 ) = 0.566 so the reference range for OKLCH Chroma seems odd

@facelessuser
Copy link

sqrt ( 0.4^2 + 0.4^2 ) = 0.566 so the reference range for OKLCH Chroma seems odd

I think that is part of the problem with trying to treat both a and b with the same range. They are lopsided.

@danburzo
Copy link

danburzo commented Oct 26, 2021

I've made an Observable notebook to eyeball the ranges for CIE/OK color spaces:

https://observablehq.com/@danburzo/lab-reference-ranges

(Visually works best on Chrome 94+, which has support for the display-p3 color space in HTML <canvas>, on a screen that can render the full display-p3 gamut.)

@danburzo
Copy link

My conclusion after fiddling with the aforementioned prototype is that a range that accommodates an entire RGB gamut is at odds with a range in which picking colors at random has a good chance of landing in gamut, or perceptually close to in-gamut. This is due to how RGB gamuts waltz around on the a/b spectrum as you increase L from 0% to 100%. Here they are for a, b ∈ [-125, 125]:

cielab-125.mp4

The ideal balance doesn't seem readily apparent. If gamut mapping is good enough, so that even if you write an out-of-gamut color it still looks fairly like you'd expect, then I think that tilts the balance in favor of gamut accommodation, and to that end bounding box of display-p3 gamut expanded to round-feeling values sounds reasonable.

@svgeesus
Copy link
Contributor

svgeesus commented Feb 2, 2022

Getting back to this and playing with the observable some more, the values proposed by Lea look workable to me.

Agenda+ to get implementer input and agreement to add these reference percentage ranges to the spec.

@tabatkins
Copy link
Member

Ah, good, I updated Typed OM to reflect all the recent changes in Color 4, and ran across the xyz "numbers only, no percents" part of the grammar and was sad. I punted on it temporarily, so if this goes thru it'll mean I don't have to add extra complexity.

@svgeesus
Copy link
Contributor

svgeesus commented Feb 4, 2022

the xyz "numbers only, no percents" part of the grammar and was sad.

Okay so color(xyz-d65 0.9504 1.0000 1.0888) would also be expressible as color(xyz-d65 95.04% 100.00% 108.88%). That seems fine, since 0..1 and 0..100 ranges are both in common use.

@svgeesus
Copy link
Contributor

For the call, to see the full proposal in one place, the percentages would be explicitly defined so that 0% to 100% maps as follows::

  • lab.L: [0, 100]
  • lab.a: [-125, 125]
  • lab.b: [-125, 125] (a and b have the same range, for simplicity)
  • lch.c: [0, 150]
  • oklab.L: [0, 1.0]
  • oklab.a: [-0.4, 0.4]
  • oklab.b: [-0.4, 0.4] (see above re: a and b)
  • oklch.c: [0, 0.4]
  • xyz, xyz-d50, xyz-d65: [0, 1.0]
  • predefined RGB: [0, 1.0] (not a change, but a clarification)

No-one mentioned hue as a percentage so it remains an angle, in degrees by default if no unit is given.

This gives useful ranges for percentages on all colors, roughly covering the display-p3 gamut plus a bit, and should minimize the errors in implementations where something was either not divided by 100, or was and shouldn't be, etc (Safari and color.js and PostCSS all had these at various points).

It also means people can paste in lab/lch/oklab/oklch values from other software (which does not use percents) without getting an error and without having to remember if L is [0..1] or [0..100].

lch(29% 66.5 327) == lch(29% 44.33% 327) == lch(29 66.5 327)
oklch(58% 0.11 110) == oklch(58% 27.5% 110) == oklch(0.58 0.11 110)
color(xyz-d65 0.9504 1.0000 1.0888) == color(xyz-d65 95.04% 100.00% 108.88%)

@faceless2
Copy link

faceless2 commented Feb 16, 2022

If I'm going to be picky, it feels odd to set the oklab values to +/- 0.4. Making them +/- 0.5 (so the total range is 1) feels a bit more natural and less likely to trip people up, particularly as oklab/oklch are new and unfamiliar to most. (note this would also match the current text from css-color-4: "theoretically unbounded (but in practice do not exceed ±0.5).")

For Lab/LCH, those seem fine - it's arbitrary of course.

A big +1 to making XYZ primaries percentages, as I had the same reaction as Tab when adding these.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-color] Support all existing (non-legacy?) formats in color()?, and agreed to the following:

  • RESOLVED: Will add this to the color-4 module, after forking to a new issue
The full IRC log of that discussion <emeyer> Topic: [css-color] Support all existing (non-legacy?) formats in color()?
<emeyer> Github: https://github.com//issues/6741
<Rossen_> q:
<Rossen_> q
<emeyer> lea: Looking for consensus for our plan on percentages. Right now they’re aliased to 0-1 ranges.
<emeyer> …We were thinking o using them for a reference range based on p3 so people can use absolute coordinates if they want, or use percentages if they want something close enough.
<lea> https://github.com//issues/6741#issuecomment-1028141623
<emeyer> s/ o / of /
<emeyer> chris: Mostly I wanted more feedback just to be sure this is what we want to do.
<emeyer> lea: What’s unclear is whether this about the color() function or is it about other things?
<emeyer> chris: It’s about the other things.
<emeyer> Lea: we really need a new issues.
<emeyer> Rossen: We can definitely get a different issue. Chris, you framed this as a question toward implementors, so let’s hear from them.
<emeyer> Mike Bremford: Changing it isn’t a big deal either way.
<emeyer> TabAtkins: +1 from me as well. I like this from the perspective of “all color models accept the same input space”.
<lea> Note that this means that color(rec2020 50% 100% 50%) will NOT be equal to color(rec2020 .5 1 .5)
<emeyer> Rossen: Any objections?
<lea> (+1 from me too, in case it's not clear)
<emeyer> …As next steps, we’ll break this out into its own issue, build consensus, and get it into a spec.
<emeyer> RESOLVED: Will add this to the color-4 module, after forking to a new issue

@svgeesus
Copy link
Contributor

svgeesus commented Feb 16, 2022

Redirecting discussion back to original subject:

Right now the distinction between which color formats have a dedicated function and which ones are specified via color() is murky. The spec describes color() as "Profiled, Device-dependent Colors", but also includes the XYZ spaces there.

A little progress since the issue was first raised:

  1. With the move of custom color spaces to CSS Color 5, the section is now entitled 10. Predefined Color Spaces which is not necessarily optimal, but is better
  2. Again with that move, all the color spaces in that section are 3-component, rectilinear, [0,0,0] == black ones. (In CSS Color 5 that becomes n-dimensional but again with all zeroes == black).
  3. CIE XYZ is, when you look at it carefully, a hyper-saturated primary RGB space. This is easier to see in an x,y chromaticity diagram rather than a more modern u,v one. Red (X) is at x,y=1,0; green(Y) is at x,y=1,0 while blue(Z) is at x,y=0.0

The other ones are opponent color spaces (neutral central axis with perceptual Lightness); oklch and lch aim at being fully orthogonal (oklch is rather better at that, by design) while oklab and lab are sort of half-way houses, a and b don't mean that much in isolation. True, negating a value gets the opponent color, but they are much better expressed in polar form as orthogonal Chroma and Hue.

@LeaVerou
Copy link
Member Author

Again with that move, all the color spaces in that section are 3-component, rectilinear, [0,0,0] == black ones. (In CSS Color 4 that becomes n-dimensional but again with all zeroes == black).

In CMYK (0,0,0,0) is white.

@svgeesus
Copy link
Contributor

In CMYK (0,0,0,0) is white.

/facepalm

@svgeesus
Copy link
Contributor

CMYK is also in CSS Color 5. So the arguments given all apply, to CSS Color 4.

Incidentally ITC H.273 describes XYZ among its list of RGB color spaces, which is true as the primary chromaticities are:

red: x=1, y=0
green: x=0, y=1
blue: x=0, y=0 (and thus, z=1)

@svgeesus svgeesus added css-color-5 Color modification and removed css-color-4 Current Work labels Jun 17, 2022
@svgeesus
Copy link
Contributor

Closing this old issue since the spec has changed a fair bit since the issue was first raised.

@svgeesus svgeesus added the Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. label Jan 26, 2023
@danburzo
Copy link

danburzo commented Feb 7, 2023

Hi @svgeesus, just to make the conclusion explicit: unbound color spaces (that is oklab, oklch, lab, lch) are not available via the color() syntax in css-color-4. Is that correct?

@svgeesus
Copy link
Contributor

svgeesus commented Feb 7, 2023

Correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-color-5 Color modification
Projects
None yet
Development

No branches or pull requests

8 participants