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-4] Achromatic colors converted to hue-ish spaces should treat hue as "missing", not NaN #6107

Closed
tabatkins opened this issue Mar 17, 2021 · 68 comments
Labels
Closed Accepted by Editor Discretion Commenter Satisfied Commenter has indicated satisfaction with the resolution / edits. css-color-4 Current Work Needs Testcase (WPT)

Comments

@tabatkins
Copy link
Member

tabatkins commented Mar 17, 2021

In https://drafts.csswg.org/css-color-4/#hue-syntax it's stated that:

For colors very close to the neutral axis, the hue angle becomes indeterminate (for example, in Lab, minute changes in near-zero a and b values give huge changes in LCH hue angle). Therefore, sometimes a hue angle of NaN (not a number) may be returned.

Then this is later referenced in interpolation:

If one of the angles has the value NaN, then for interpolation, NaN is replaced by the value of the other hue angle. If both angles have the value NaN, then for interpolation, NaN is replaced by the value 0 for both angles.

The intent of this spec text is good and correct; it means interpolating from gray to green in LCH just becomes greener, rather than the hue swinging from red to green or something.

However, the exact mechanics of this aren't workable. NaN is limited to existing within the bounds of a calculation; it gets censored to infinity if it would escape (and infinity then gets clamped to the actual largest value the impl supports). So we can't have a NaN value just hanging out in normal CSS values. I think instead this should be phrased as achromatic colors having a "missing" hue, and then interpolation can key off of whether the hue is "missing".

Even if NaN did persist into normal CSS values, I don't think we actually want to merge "calculation for the angle yields NaN" with "achromatic color interpolates with chromatic color"; the latter is a common non-error case with an obvious solution, while the former is explicitly the result of a bad or nonsensical calculation with no obvious answer. So we really shouldn't be keying off of NaN anyway; a "real" NaN should be treated the same as an infinity (which it luckily censors itself into already), where it's an error case that just has an arbitrary answer (per #6105, effectively 0deg).

@svgeesus
Copy link
Contributor

If a top-level calculation (a math function not nested inside of another math function) would produce a value whose numeric part is NaN, it instead act as though the numeric part is +∞.

and

a "real" NaN should be treated the same as an infinity

What I'm not seeing is

a) an explanation of why that spec text is a good and useful thing (divide by zero produces infinity or -infinity, not NaN)
b) how the value "missing" is supposed to be passed around in an API which has number, float or double parameters. Obviously it can be represented textually in CSS syntax, or in the (string-based) CSS OM but how can it

Several color APIs use NaN here (d3.js for example, which also uses it for missing data in general, colorio and color.js too. It seems natural that the Color API would do this too.

So first we need to understand why CSS V&U wants to map NaN to +∞.

I see @LeaVerou asked a similar question earlier

I don't understand the sentence "it produces a NaN, which is censored into an infinity". Mathematically, 0/0 does not tend to infinity and is an indeterminate form.

A quick search for NaN in V&U4 issues didn't turn up any motivation for this strange censoring.

@LeaVerou
Copy link
Member

It should be noted that there is a lot of prior art in Color libraries for using NaN for achromatic interpolation.

NaN is limited to existing within the bounds of a calculation; it gets censored to infinity if it would escape (and infinity then gets clamped to the actual largest value the impl supports).

That ...seems like hugely problematic behavior that we should, at the very least, get group consensus on before we accept it as something to design other specs around. NaN is conceptually completely different from Infinity.

@tabatkins
Copy link
Member Author

an explanation of why that spec text is a good and useful thing

Because NaN is a meaningless error state. Rather than putting the onus on every single function and property that accepts numeric values to define how they handle a NaN, censoring it immediately gives us a well-defined behavior. And, since you only produce NaN via writing erroneous code, the actual behavior doesn't much matter anyway; it just needs to be well-defined, and ideally it makes it somewhat obvious that something is wrong. infinity accomplishes both of those goals.

how the value "missing" is supposed to be passed around in an API which has number, float or double parameters.

That's quite a different question from what the spec is currently concerning itself with! Right now, this "missing hue" situation is solely caused by interpolation in a hue-ful space, when an achromatic color is originally defined in a hue-less space. It never shows up in a way that actually gets exposed to users, since the intermediate interpolation has a well-defined hue at all times.

If you want this state to be explicitly specifiable by userland code, we can do that; the hue parameter of the relevant color functions would have to take some appropriate keyword to indicate it's hue-less, and we'd reflect that in OM and TypedOM as well.

But this isn't directly relevant to the issue at hand. Right now, this "missing hue" state is not directly reflected in the serialization or specifiable by a user (we can't write hsl(calc(NaN * 1deg), 50%, 50%), for instance). It currently exists solely as a specification device, and as such we can talk about however we'd like; the point of me raising this issue is to suggest we talk about it as a "missing hue" rather than as an IEEE-float concept that can't actually be used for that argument anyway.

It should be noted that there is a lot of prior art in Color libraries for using NaN for achromatic interpolation.

Sure, a lot of programming languages (JS and C++, in particular) just use a double to represent the hue for hue-ful colorspaces, and as such, using NaN as a sentinel value isn't unreasonable. By doing so, they're not stating that a NaN is semantically a missing hue, they're just saying that NaN is outside the value range of a meaningful hue, and so it's safe to use it to communicate the "missing/present" bit, without having to complicate the user interface for the color code by carting around an extra boolean.

However, that's ultimately a hack due to the fact that these langs have weak/non-ergonomic type systems, and so smuggling in extra data via sentinel values is a reasonable compromise of usability vs semantics/type safety. In languages that have a better type system, like Rust, the standard practice is to actually specify the secondary values, like with an Option(double) or something to indicate that the angle might be missing; this lets NaN retain its normal meaning of "you done goofed", so the class can throw an error when it shows up. CSS similarly can ergonomically accept <angle> | missing to express the desired semantic in a straightforward and readable way.

That ...seems like hugely problematic behavior that we should, at the very least, get group consensus on before we accept it as something to design other specs around.

This has been the specified behavior of NaNs since 2014, and was discussed in https://lists.w3.org/Archives/Public/www-style/2014Apr/0101.html. Note the convo there reflects what I said above, too - NaN is produced solely as a result of an authoring mistake, so it doesn't matter what it turns into, but ideally it turns into something that makes the mistake likely to be noticed. Given the several subsequent discussions we've had over this chapter in CSSWG calls/meetings over the years, I think it's safe to say the group treats it as uncontroversial.

If you'd like to revisit it, okay, but I think you'd need to give a pretty good reason why we actually want hsl(calc(0deg/0), 50%, 50%) (or the other forms that resolve to NaN) to be a valid way to produce an achromatic color, versus writing it out explicitly as hsl(missing, 50%, 50%) or something. I think that would be a pretty hard sell, but I won't stop you from trying. ^_^

@tabatkins
Copy link
Member Author

A quick search for NaN in V&U4 issues didn't turn up any motivation for this strange censoring.

Oh yeah, as suggested by the mailing list link in the preceding comment, that's because the specification for this predates our use of GitHub Issues by a year or two. ^_^ Check that message thread for the reasoning.

@svgeesus
Copy link
Contributor

since you only produce NaN via writing erroneous code

No, that is incorrect.

the actual behavior doesn't much matter anyway;

It really does matter, as the cases cited demonstrate.

Right now, this "missing hue" situation is solely caused by interpolation

No, it is cased by colorspace conversion regardless of whether you plan to interpolate it

in a hue-ful space,

The correct term here is cylindrical polar color representation btw

when an achromatic color is originally defined in a hue-less space.

Right, when an achromatic color is converted to a CPCR

It never shows up in a way that actually gets exposed to users,

It sure does

since the intermediate interpolation has a well-defined hue at all times.

Happens outside of interpolation.

@svgeesus
Copy link
Contributor

If you want this state to be explicitly specifiable by userland code, we can do that; the hue parameter of the relevant color functions would have to take some appropriate keyword to indicate it's hue-less, and we'd reflect that in OM and TypedOM as well.

Yeah, we are going to need that. Well, we (and several color libraries) have that already, but if we conclude that censoring NaN is desirable then yeah, we will need some other value.

@svgeesus
Copy link
Contributor

NaN is produced solely as a result of an authoring mistake, so it doesn't matter what it turns into,

If that is your axiom then, since it is untrue, we need to revisit this decision.

Sure, NaN can be produced by authoring error. But that is not the only way it can.

@tabatkins
Copy link
Member Author

"NaN used as flag for smuggling an extra boolean into an attribute defined as a double" is different than "NaN produced as result of erroneous math". Like I said, languages with weaker or less ergonomic type systems do the former to avoid having to burden their APIs with carrying around an extra boolean, and are okay with accepting that it unfortunately conflates with the latter case (and thus some erroneous code is instead interpreted as correct code that does something different). CSS does not need to do that.

Are you really wanting to argue that hsl(calc(0deg/0), ...) should be treated as an achromatic color? Like, that's intentionally something you desire for this function? Because I think you'll have a really hard time selling that to the WG, when CSS already has a way of indicating non-double values (namely, a keyword). CSS has many examples of a value that is either numeric or a single keyword like none or normal; in exactly none of these do we smuggle the non-numeric value in via a numeric sentinel value, and I don't see why this case justifies doing so.

No, it is caused by colorspace conversion regardless of whether you plan to interpolate it

Sure, but all such conversions are censored away into a color with a hue before authors see it, right? That's the point I'm making there - this is a spec-internal concept that is only exposed to authors by way of the effect it has on other processes; it never shows up in a serialization, and authors can't write it directly on their own.

(Tho fwiw, as far as I can tell your general statement is not true; colorspace conversions don't produce NaN. The only example in the spec that describes converting from a hue-less to hue-ful representation is converting Lab to LCH, where H is calculated as atan2(b,a). If the Lab color is achromatic (a and b both 0), atan2(0,0) returns 0. The HSL section doesn't list an RGB->HSL conversion; the HWB section does, but it references an rgbToHsl() function that doesn't exist in the document or any of the supporting .js files, so I can't tell how it would calculate the hue of an achromatic RGB color. However, I think the common practice is to explicitly check for the max and min RGB channels being equal, and returning 0 for that case. The only mentions of a NaN hue are in 4.2, defining <hue, and the interpolation section.)

@LeaVerou
Copy link
Member

After Tab's replies, I can now see both sides of this, so I'm going to mostly step back and observe the rest of the discussion.

I still think that handling NaN as Infinity is completely inconsistent with any other language that uses NaN, as well as with IEEE 754, but if we don't use it here, perhaps the effects of that will be more limited.

But if we do what Tab is proposing, how would this "missing" value serialize in Typed OM and the like? It would need to be yet another object, which complicates the object model even more. And eventually we are going to have a Color object for the Web Platform, separate from Typed OM. How is it going to be represented there?

@svgeesus
Copy link
Contributor

Are you really wanting to argue that hsl(calc(0deg/0), ...) should be treated as an achromatic color? Like, that's intentionally something you desire for this function?

No. That is your model of "the only way NaN can occur", not mine. Mine is

let gray = new Color("Lab" [50, 0.00001, 0.00002]);
let angle = gray.to("LCH").hue;

CSS already has a way of indicating non-double values (namely, a keyword).

As I already said:

Obviously it can be represented textually in CSS syntax, or in the (string-based) CSS OM

and as I (meant to) continue

but how can it be represented in Typed OM or a Color API or when being passed back and forth between CSS and JS?

On the observability of the value

No, it is caused by colorspace conversion regardless of whether you plan to interpolate it

Sure, but all such conversions are censored away into a color with a hue before authors see it, right?

You keep stating this. No. Why would it? The spec is already very clear that some cylindrical polar color representations have an undefined hue. You seem to believe that it is censored away into +infinity which then becomes, I dunno, 360?

@svgeesus
Copy link
Contributor

The non-normative, deliberately simplified, sample code can omit this subtlety but you are right, the "Converting" sections should state this clearly.

So looking at real code:

		lab (Lab) {
			// Convert to polar form
			let [L, a, b] = Lab;
			let hue;
			const ε = 0.02;

			if (Math.abs(a) < ε && Math.abs(b) < ε) {
				hue = NaN;
			}
			else {
				hue = Math.atan2(b, a) * 180 / Math.PI;
			}

			return [
				L, // L is still L
				Math.sqrt(a ** 2 + b ** 2), // Chroma
				angles.constrain(hue) // Hue, in degrees [0 to 360)
			];
		}

@svgeesus
Copy link
Contributor

@tabatkins
Copy link
Member Author

I still think that handling NaN as Infinity is completely inconsistent with any other language that uses NaN, as well as with IEEE 754, but if we don't use it here, perhaps the effects of that will be more limited.

Other computer languages are allowed to throw errors when you pass in bad input, and so that's usually what happens when someone accidentally produces a NaN. CSS can't do that, so we work with the abilities we have: making it probably obvious that there's an error, by causing something to likely blow up with an infinity.

(It's uncommon, but we do similar things in other spots in CSS when a value is parsed as valid, but ends up being meaningless or an error in context; we choose an arbitrary answer this is easy to define and implement, and hopefully makes it obvious to the author that something has gone wrong. See, for example, referring to a non-existent grid line name in grid-row; the reference is instead interpreted as being the first implicit grid line. That's not because it's what we think they meant, it's because we need to do something, and that behavior is likely to be obviously wrong, since referring to a line name implies there's an explicit grid, and the item will get pushed outside of that.)

how would this "missing" value serialize in Typed OM and the like?

Assuming we did allow a "missing hue" color to be exposed to the author (rather than just being a spec-internal concept that produces a normal hued color in the values that are author-exposed), then it would just mean that the .hue attribute's type would be (CSSKeywordValue or CSSNumericValue), with checks to ensure that you can only pass the "missing" keyword. Very straightforward, not complex at all.

(Note that the TypedOM repr gives the hue as a TypedOM numeric object, because it's an angle which can have multiple units, and might be a math function. This gives us a lot of flexibility in the value, versus more JS-only APIs that would just use double.)


Are you really wanting to argue that hsl(calc(0deg/0), ...) should be treated as an achromatic color? Like, that's intentionally something you desire for this function?

No. That is your model of "the only way NaN can occur", not mine.

I apologize, I might not have made it clear enough why I was asking the specific question. This isn't a gotcha, or rhetorical, I was really wanting an answer from you, because it's important to how this question gets resolved. If the <hue> argument can be NaN, then the source of the NaN doesn't matter - whether it's produced due to a browser's automatic colorspace conversion or an author explicitly writing it, a NaN is a NaN and will give the same behavior.

So, given that, I re-ask my question: is hsl(calc(0deg/0), ...) something you intentionally want to support as defining an achromatic color? If it is, you can try to sell that to the WG, but I don't think you'll succeed. If you aren't intending that to be valid and mean an achromatic color, then "hue can be NaN" isn't what you actually want.

As I already said:

I apologize, but I don't know what you're trying to say here. Your terseness is really making it hard to figure out the points you're trying to make. :(

Sure, but all such conversions are censored away into a color with a hue before authors see it, right?

You keep stating this. No. Why would it? The spec is already very clear that some cylindrical polar color representations have an undefined hue. You seem to believe that it is censored away into +infinity which then becomes, I dunno, 360?

Again, I asked that question deliberately. In what situation, precisely, is a color with undefined hue exposed to the author, in such a way that they can actually see the "undefinedness" represented in the value? I pointed to all the places where undefined hue seems to be capable of occurring, and as far as I can tell, by the time a value actually gets out to the author, that value always has a defined hue.

In the spec, undefined hue appears to happen solely due to an achromatic hueless color being converted into a hueful space for the purpose of interpolating or mixing, and then the interpolation rules cause it to be treated as having a particular hue (that of the other color) at all points. Am I wrong? Does undefined hue happen at other times, in a way that would be exposed to the author by the OM? If so, where, and can you give me some sample code that would show it?

@LeaVerou
Copy link
Member

I agree that treating hsl(calc(0deg/0), ...) as an achromatic color is probably confusing. OTOH if we do not, how does that interpolate? Does it get converted to Infinity? What hue even is Infinity?

In what situation, precisely, is a color with undefined hue exposed to the author, in such a way that they can actually see the "undefinedness" represented in the value? I pointed to all the places where undefined hue seems to be capable of occurring, and as far as I can tell, by the time a value actually gets out to the author, that value always has a defined hue.

Chris already gave you an example where the missing hue needs to be exposed: Converting any achromatic Lab color (e.g. lab(50 0 0)) to LCH.

Regardless, I think the ability of authors being able to define color components as missing to have them automatically take the other color's value is quite useful.

@tabatkins
Copy link
Member Author

OTOH if we do not, how does that interpolate? Does it get converted to Infinity? What hue even is Infinity?

It's an error case, so it doesn't really matter so long as it's well-defined. (But per https://drafts.csswg.org/css-values/#numeric-types, it's defined to clamp to the nearest supported multiple of 360deg, so it'll be red. Note that this is the same behavior already defined in Color 4 for if you're interpolating between two colors that are both missing a hue, anyway.)

Chris already gave you an example where the missing hue needs to be exposed: Converting any achromatic Lab color (e.g. lab(50 0 0)) to LCH.

I was asking for sample code because it wasn't clear to me how this would actually happen in a way that's author-exposed. Y'all just keep saying "when you convert a color", but as far as I can tell Color 4 doesn't have any way to do so that doesn't immediately fill in a hue anyway.

I guess via Color 5's relative color syntax? lch(from gray ...)?

Regardless, I think the ability of authors being able to define color components as missing to have them automatically take the other color's value is quite useful.

Sure, no complaints here; the d3 example seems quite compelling.

@LeaVerou
Copy link
Member

LeaVerou commented Mar 23, 2021

I was asking for sample code because it wasn't clear to me how this would actually happen in a way that's author-exposed. Y'all just keep saying "when you convert a color", but as far as I can tell Color 4 doesn't have any way to do so that doesn't immediately fill in a hue anyway.

Most sample code for converting to polar coordinates will typically produce a random hue (typically 0) for achromatic colors instead of declaring it as undefined or missing, but it would be more correct to produce a missing hue. Technically, even converting a gray sRGB color (e.g. #777) to HSL should also return a missing hue.

I guess via Color 5's relative color syntax? lch(from gray ...)?

Yes, or color-mix(in lch, gray 100%, ...), or color-adjust(in lch gray lightness -0%) etc.

@tabatkins
Copy link
Member Author

Most sample code [...]

I meant some CSS that would actually cause a conversion and expose its results directly to the author, not color conversion code in JS or whatever. The various Color 5 examples suffice, thanks. (Afaict I was right that with Color 4 abilities only a missing hue is never author-exposed, tho.)

Does this suggest that we can push the "author-exposed missing hue" value to Color 5? Or, since it does at least have effects that can be triggered by interpolation, maybe we do want to add it to the color functions' grammars here in Color 4?

If we do, I think we should add a missing saturation/chroma as well, right? d3 defaults to treating black/white as missing a chroma, which makes sense to me. And if we do that, should we allow a missing lightness to round out the set? No color meaningfully starts with a missing lightness (except maybe transparent), but the effects on interpolation are easy to define alongside the others; is there a use-case here?

@LeaVerou
Copy link
Member

If we add a "missing value" keyword, I think it should be added in css-values, and referenced from Color 4.

@tabatkins
Copy link
Member Author

Nah, if it's color-specific it doesn't belong in V&U. If/when we end up using the same pattern elsewhere we can generalize and move it to V&U, but for now it's a color-specific value and should be defined in Color, imo.

@svgeesus
Copy link
Contributor

Y'all just keep saying "when you convert a color", but as far as I can tell Color 4 doesn't have any way to do so that doesn't immediately fill in a hue anyway.

I think the concept you are missing, in general about CSS Color 4 (explicitly) and all colors back to CSS 1 (implicitly) is that they have a colorimetric basis. They are tied to scientific, rigorous, measurable color sensations. CSS Color 4 tries to explain that, with a balance between completeness and readability.

This means that I see "a bunch of different ways to specify a color" with great care taken to say exactly what color that is. While you, I think, focus far more on "here are a bunch of different syntaxes" and what measured color that actually is, is sort of a by product.

Which explains the disconnect over the color part of Typed OM. You see it as deeply tied to syntax, while I see it as a way to set, query and manipulate color. And thus of course if the author sets a color in syntax A they need to be able to get that color in syntax B or C or Z because the crucial thing is getting the color and indeed getting the right color.

So yes, CSS Color 4 does not give a way to get a color in any given colorspace because it is not an object model. But it provides all the math and description to enable that conversion between colorspaces.

@svgeesus
Copy link
Contributor

It's an error case, so it doesn't really matter so long as it's well-defined.

No, it isn't an error case. The color #777 is not an error. But it has an undefined hue, because it is achromatic. And a range of visually-identical colors near that color (I mean, DeltaE2000 much less than 1, so indistinguishable visually) will end up with essentially random hue angles in LCH and HSL, because tiny numerical differences have the hue angle swinging around wildly while the chroma is almost zero.

@weinig
Copy link

weinig commented Mar 24, 2021

On the off chance that implementations will help, here are the links to sRGB to HSL:

https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ColorConversion.cpp#L64

and Lab to LCH:

https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ColorConversion.cpp#L247

(We are probably getting lucky and the implementation of atan2() we are using seems to return 0 when both arguments are 0, but the c spec seems to say that it is a domain error, so we should probably add an explicit check).

in WebKit.

@svgeesus
Copy link
Contributor

We are probably getting lucky and the implementation of atan2() we are using seems to return 0 when both arguments are 0

When they are exactly 0, yes. The issue is when a and b are supposed to both be zero but are actually like 0.00000005 and -0.0000003 and you get a meaningless, effectively random hue angle of -80.537

@facelessuser
Copy link

facelessuser commented Mar 24, 2021

(We are probably getting lucky and the implementation of atan2() we are using seems to return 0 when both arguments are 0, but the c spec seems to say that it is a domain error, so we should probably add an explicit check).

If both a and b are zero, you will get zero, but with Lab, often a and b are not zero when converting from an achromatic sRGB color unless you round them off.

> new Color("#777").to('lab')
Color$1 {
  _spaceId: 'lab',
  toString: [Function: toString],
  coords: [ 50.03434402595761, -0.0021727557177886325, -0.008043723319994811 ],
  alpha: 1
}

If they aren't rounded off, you get some wild values for hue as a and b get very close to zero. But, if you round them off, conversion back and forth gets more unstable. Though, conversion from Lab to sRGB doesn't seem that stable to begin with as you get really close to zero chroma.

But even if they yielded zero reliably, zero is still a hue (red), but an achromatic color doesn't have a hue, so 0 is not really correct. Gray doesn't have a red hue, and when you mix it with another color (in a cylindrical space), you don't want the red to influence the final mixed color as there was never any red to begin with.

What I think is trying to be done is setting the stage for proper mixing by providing a way to make clear when a hue value is meaningless. How that happens is the argument, I guess.

I'm just interested to see how it all plays out 🙂.

EDIT: Looks like it was answered before I hit send.

@weinig
Copy link

weinig commented Mar 24, 2021

We are probably getting lucky and the implementation of atan2() we are using seems to return 0 when both arguments are 0

When they are exactly 0, yes. The issue is when a and b are supposed to both be zero but are actually like 0.00000005 and -0.0000003 and you get a meaningless, effectively random hue angle of -80.537

Oh, sorry, I didn't mean anything by that other than a note to myself that I should probably make sure we are not running into C undefined behavior issues in WebKit.

@tabatkins
Copy link
Member Author

Chris, you've spent this issue thread repeatedly misinterpreting me and my question/arguments in the worst possible light, and at this point I can't help but assume it's intentional and malicious. I'm not going to further engage you in this thread since you've repeatedly acted like I have zero understanding of how colors or colorspaces work (despite working with you on Color and color-adjacent topics for years in this WG), and you've repeatedly taken every reference I make to an explicitly-produced NaN from a calculation and pretended that I'm instead talking about grays. This isn't a productive use of my time.

Lea, I'm happy to continue talking to you about this topic.

@LeaVerou
Copy link
Member

LeaVerou commented Mar 24, 2021

@tabatkins I can assure you that @svgeesus is not being malicious. You're just both just talking cross-purposes.
I believe he is talking based on the current version of the spec, which represents achromatic hues as NaN (in which case NaN is not an error condition), and you are talking about other instances where NaN can be returned, where it is clearly an error condition.
I understand that miscommunication can be frustrating (on both sides, I bet), but I hope you two can eventually reach an understanding, as your positions may not be as far apart as you think.

@facelessuser
Copy link

facelessuser commented Oct 3, 2021

I see that alpha being represented as 1 when undefined is not such a unique proposal and has been implemented by multiple libraries, so I guess this isn't so uncommon.

facelessuser added a commit to facelessuser/coloraide that referenced this issue Oct 3, 2021
- Allow none keyword
- Allow interpolation result channel to return with a undefined channel
  if both channels under interpolation during interpolation are none
- Allow printing CSS with none keywords (disabled by default)

Ref: w3c/csswg-drafts#6107
facelessuser added a commit to facelessuser/coloraide that referenced this issue Oct 3, 2021
- Allow none keyword
- Allow interpolation result channel to return with a undefined channel
  if both channels under interpolation during interpolation are none
- Allow printing CSS with none keywords (disabled by default)

Ref: w3c/csswg-drafts#6107
@svgeesus
Copy link
Contributor

svgeesus commented Oct 4, 2021

Are all the values above 100% "white", just extra-bright? Or can they still have a meaningful hue/chroma?

In theory they can be any color in an HDR context, whose lightness is greater than media white. In practice people rarely use Lab for HDR, outside academic studies.

Also, I don't think device-cmyk() ever has powerless components, right? Even if k is 100%, the other three components can still have an effect on making it black "warm" or "cool", right?

Yes, making it warmer or cooler or (more usually) a deeper black than just K=100% would give you. This is called TAC (Total Area Coverage) and can go up to 300%, or even more on heavier papers.

(The terrible naive conversion ignores the other components when k is 100%, but I think real conversions don't, right?

Right.

@tabatkins
Copy link
Member Author

@danburzo

Is it worth it to have alpha behave differently with none = 1?

It feels like transparent is a bad default for, let's say, hsl(45deg 50% 50% / none).

The defaults here aren't intended for usage, they're just here to give us an answer for what to do when none "leaks out" into normal rendering. None of the defaults are actually useful, because if they're used the author has made a mistake. As such, I'd prefer to keep this as simple and straightforward as possible and leave everything at zero. (And fwiw, "transparent black" is used elsewhere as a "something's messed up but we need a color/image of something", so it's consistent at least.)

I assume this includes the color() syntax as well, right?

Yup, marking a color() as taking some of its channels from the opposing color in a transition makes just as much sense as for any other color function.


@facelessuser

I'm curious, what if something like chroma is none is hue treated as undefined?

If a color with a none chroma gets displayed, then the none gets turned into 0%, and then the hue becomes powerless. That won't affect rendering directly, but if you then convert the color into another space, the hue will indeed become none itself and then have the defined effects.

This requires a little work to invoke manually, like rgb(from hsl(20deg none 50%), r g b). (Because the hue turns into none as well, all of the r/g/b variables becomes missing as well. We haven't carried these edits over to Color 5 to define exactly how infectious none works in relative color syntax, but that'll come shortly.)

Again, this is purely an error case, tho. An author should never use none directly in a color being displayed, just in colors being transitioned/animated.

I see that alpha being represented as 1 when undefined is not such a unique proposal and has been implemented by multiple libraries, so I guess this isn't so uncommon.

Note that this isn't behavior for when alpha is omitted (that works as you expect - rgb(255 0 0) does indeed default its alpha to 100%), but rather for when alpha is explicitly marked as "not provided so it can take the value from the other color in a transition". Alpha is never powerless, so this can never happen automatically; the author has to explicitly mark it, or use relative color syntax to calculate alpha from a color channel that is missing.


@svgeesus

In theory they can be any color in an HDR context, whose lightness is greater than media white. In practice people rarely use Lab for HDR, outside academic studies.

So should I leave it as-is (lab lightness >= 100% does not render the hue/chroma powerless), then? Or is this a theoretical ability only, and in practice we should go ahead and define that lightness >= 100% is some variety of "pure white", and leave "colored HDR white" as a something that another color function might handle?

@svgeesus
Copy link
Contributor

svgeesus commented Oct 4, 2021

we should go ahead and define that lightness >= 100% is some variety of "pure white", and leave "colored HDR white" as a something that another color function might handle?

I'm tending more towards that one.

@facelessuser
Copy link

If a color with a none chroma gets displayed, then the none gets turned into 0%, and then the hue becomes powerless. That won't affect rendering directly, but if you then convert the color into another space, the hue will indeed become none itself and then have the defined effects.

Okay, that sounds like in this case hue would not become "powerless' until none in the chroma channel is forced to be evaluated. So you could define chroma as none to interpolate with another color and take its chroma, and the hue would be unaffected and interpolated just fine. Only when displayed, or forced into a conversion would the hue be evaluated resulting in a "powerless" state. That sounds reasonable.

Note that this isn't behavior for when alpha is omitted (that works as you expect - rgb(255 0 0) does indeed default its alpha to 100%), but rather for when alpha is explicitly marked as "not provided so it can take the value from the other color in a transition". Alpha is never powerless, so this can never happen automatically; the author has to explicitly mark it, or use relative color syntax to calculate alpha from a color channel that is missing.

Right, I believe this was solely from an interpolation perspective, not alpha being powerless due to omitting. This is why first my statement was that I don't see how it would matter if alpha was represented as 0 or 1 if none, but I see the request from @danburzo was more from a user perspective wanting to evaluate the color, maybe through debugging or whatever, by having it treated as 1. It looks like there are a number of color libraries that do it this way, probably for that very reason. I'm not sure I have any strong feelings about it, but I can see how the request may be useful.

@tabatkins
Copy link
Member Author

I'm tending more towards that one.

Cool, I'll make the edits.

Okay, that sounds like in this case hue would not become "powerless' until none in the chroma channel is forced to be evaluated. So you could define chroma as none to interpolate with another color and take its chroma, and the hue would be unaffected and interpolated just fine. Only when displayed, or forced into a conversion would the hue be evaluated resulting in a "powerless" state. That sounds reasonable.

Yes, exactly.

@danburzo
Copy link

danburzo commented Oct 4, 2021

The defaults here aren't intended for usage, they're just here to give us an answer for what to do when none "leaks out" into normal rendering. None of the defaults are actually useful, because if they're used the author has made a mistake. As such, I'd prefer to keep this as simple and straightforward as possible and leave everything at zero. (And fwiw, "transparent black" is used elsewhere as a "something's messed up but we need a color/image of something", so it's consistent at least.)

Again, this is purely an error case, tho. An author should never use none directly in a color being displayed, just in colors being transitioned/animated.

I guess my hesitancy with going with zero-everywhere is that it does kind of mostly work in practice, and that it's not clear that using none in colors to be rendered is an authoring error.

rgb(255 none none) behaving the same as rgb(255 0 0) makes a lot of sense and without the proper context I might assume, as an author, that none is a kind of 0 with interpolation benefits. The color() syntax affords even more flexibility: color(xyz), color(xyz 0), and color(xyz none) all produce the same result: none = omitted = 0. It sounds to me that we're one alpha: none = omitted = 1 away from making none an integral part of the color toolkit (that aligns to the intuitive defaults), rather than insisting on authors not using it.

If none is in the spec (right there in every color syntax), and it mostly works as expected, is it really wrong?

@tabatkins
Copy link
Member Author

On the other hand, hsl(none 50% 100%) is... red, for some reason. It's an arbitrary choice.

It sounds to me that we're one alpha: none = omitted = 1 away from making none an integral part of the color toolkit (that aligns to the intuitive defaults), rather than insisting on authors not using it.

If anything, making it less convenient is better, because none is not equivalent to 0. When transitioning it has a completely different behavior (because that's what the keyword is for). Authors learning that none is a convenient shorthand for 0 (tho longer than the equivalent values in every case) would be very bad and confusing, in fact.

@danburzo
Copy link

danburzo commented Oct 4, 2021

If anything, making it less convenient is better.

Right. I would even argue that usage of none could be restricted further.

As it stands now, a color using none has a potential dependency on the other end of the interpolation. It's similar to the relative color syntax, with the added bonus of not knowing what color to relate it to.

/* this */
rgb(none none none);

/* is the same as */
rgb(from other, r g b);

I don't have the context necessary to think through the implications, but how would this work in practice for transitions, etc? Taking a use-case from the spec: For example, to animate a color to "grayscale", no matter what the color is, one can interpolate it with lch(none 0% none)., how would this paragraph change colors?

p {
  color: lch(none 0% none);
  transition: color 1s;
}

p:hover {
  color: lch(50% 100 40);
}

On mouseover: jumping from black to gray, then smoothly towards red?
On mouseout: smoothly from red to gray, then stay there or back to black?

Some types of interpolations (where the other color the value is relative to is not known beforehand) would only work smoothly in the cases where none stands for powerless components, i.e. in the cases where the missing values can't affect rendering.

So does it make sense to limit the scope of none to only be valid in powerless slots? I appreciate this would be dialing it back a lot. Or, conversely, accept the glitchiness of some contexts?

@svgeesus
Copy link
Contributor

On the other hand, hsl(none 50% 100%) is... red, for some reason.

And lch(50% 75 none) is a magenta, rgb(88.1% 8% 48.1%)

So does it make sense to limit the scope of none to only be valid in powerless slots? I appreciate this would be dialing it back a lot.

That would be reasonable (RGB components are not powerless, for example) but is sketchy in edge cases. Is hue powerless in lch(50% 0 none)? Yes. In lch(50% 10 none)? No. In lch(50% 0.5 none)? Maybe; it is visible, but only just.

image

Or, conversely, accept the glitchiness of some contexts?

That could also be a reasonable outcome ("not all uses of none make sense")

@tabatkins
Copy link
Member Author

The big point is, the entire reason we're adding none as a keyword is because authors can already get the "missing component" behavior for any component, per spec, by abusing relative color syntax. Since the transition behavior of missing components seems useful (required for reasonable hue transitions, but meaningfully useful for other components), it seems bad to require authors to use weird hacks to get them, so none lets authors do it in a non-hacky way.

This means that limiting none in any way defeats the whole point of adding it.

Any and all usage of none outside of transitions is an authoring mistake, but not one we can meaningfully prevent without a decent bit of complexity. So we're erring on the side of "simple but stupid" instead, hoping that the slightly-unreasonable behavior (and generally, longer spelling than just using a 0 component) will keep authors from reaching for it on purpose.

@svgeesus
Copy link
Contributor

@tabatkins given your most recent comment, and the edits to add none it seems we can close this issue?

@danburzo
Copy link

danburzo commented Oct 12, 2021

Thanks @tabatkins @svgeesus for indulging me in working out a mental model on this. I think one small leftover is updating the color() syntax to account for none?

@svgeesus
Copy link
Contributor

Yes, indeed

@tabatkins
Copy link
Member Author

Oh that was entirely my mistake in missing it, dunno how it slipped my mind. I'll take care of that.

@svgeesus
Copy link
Contributor

Hmm looks like we both did that?

@tabatkins
Copy link
Member Author

Yes, I didn't see your edit and did it myself, and then merged in the conflict (you hadn't applied it to the alpha).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed Accepted by Editor Discretion Commenter Satisfied Commenter has indicated satisfaction with the resolution / edits. css-color-4 Current Work Needs Testcase (WPT)
Projects
None yet
Development

No branches or pull requests

8 participants