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

none should be turned into null, not NaN #409

Closed
LeaVerou opened this issue Feb 9, 2024 · 19 comments
Closed

none should be turned into null, not NaN #409

LeaVerou opened this issue Feb 9, 2024 · 19 comments
Labels
API change For PRs that require API design review Breaking change Topic: JS API
Milestone

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Feb 9, 2024

This was a resolution in a past breakout with @svgeesus but I’m opening this so we can track it.

Color.js currently uses NaN values to represent CSS none (e.g. for achromatic colors).
However, CSS also now has a NaN value, which is currently impossible to represent in Color.js in a distinct way.

Therefore, In the next non-minor version, we will start using null to represent none.

Authors can actually handle both, by detecting which value is used via:

const NONE_COORD = new Color("rgb(none none none)").coords[0].valueOf()

They can then use NONE_COORD instead of a hardcoded NaN so that nothing breaks in their code once they upgrade.

I therefore plan to announce this change in the release notes for v.0.5.0 and recommend they handle it that way to prepare.

@LeaVerou LeaVerou added API change For PRs that require API design review Topic: JS API Breaking change labels Feb 9, 2024
@LeaVerou LeaVerou added this to the v0.6.0 milestone Feb 9, 2024
@LeaVerou
Copy link
Member Author

LeaVerou commented Mar 11, 2024

So now we’ve announced that the change is coming.
How should we go about with this? How much effort should we put in making the migration gentler?

  1. Should we just make the change and just announce it in the changelog, since we gave people a heads up? This involves the least effort on our side.
  2. Should we print out a one-time warning about the change if NaN is used, and silently convert it to null?
  3. Should we handle (and preserve) both NaN and null when found in coords, and print out a one-time warning for NaN?
  4. Something else?

@facelessuser
Copy link
Collaborator

facelessuser commented Mar 11, 2024

So, I'm curious, are NaN and none (null) to behave identically and only serialize differently? Are NaN values meant to be disallowed from the future syntax of Color CSS? Or are both going to be allowed?

Mathematically, they will not behave the same. I guess it will be easier to miss when null handling isn't preserved as null participates as 0 in math, but NaN is infectious and always yields NaN in math. But I guess also less likely to break in big ways, probably more subtle.

@facelessuser
Copy link
Collaborator

The more I think about it the more questions I have. If both are preserved and you are interpolating between both NaN and null, which is returned, NaN or null?

This seems like an unfortunate complication in CSS colors. It doesn't seem to provide anything extra, only confusion. It's a shame none can't just be an alias for NaN in colors, or that NaN just wasn't used from the beginning.

Is there value in preserving both NaN and null in colors? Especially if they are meant to behave the same? I guess I would opt for a solution that normalizes these two values. I'm not sure how having both null and NaN helps users.

@LeaVerou
Copy link
Member Author

So, I'm curious, are NaN and none (null) to behave identically and only serialize differently?

Nope, they mean different things. I was only suggesting they behave identically in v0.6.0 as a way to ease the transition.

The more I think about it the more questions I have.

These are excellent questions for CSS Color! Pinging the other editors, @svgeesus @tabatkins

NaN is not color specific, it's a css-values thing: https://drafts.csswg.org/css-values/#calc-error-constants
That said, since it exists in the language, it could be specified in a color. I would expect it behaves the same as any other number, meaning…

If both are preserved and you are interpolating between both NaN and null, which is returned, NaN or null?

NaN, since any number interpolated with none returns that number.

This seems like an unfortunate complication in CSS colors. It doesn't seem to provide anything extra, only confusion. It's a shame none can't just be an alias for NaN in colors, or that NaN just wasn't used from the beginning.

I believe @tabatkins has strong opinions on why that would have been a mistake, and I hope he is willing to explain or point to his comments on the matter (FWIW Chris and I had your view and he convinced us 😬 )

Is there value in preserving both NaN and null in colors? Especially if they are meant to behave the same? I guess I would opt for a solution that normalizes these two values. I'm not sure how having both null and NaN helps users.

They are not meant to behave the same, nor do they represent the same thing, see above.

@facelessuser
Copy link
Collaborator

facelessuser commented Mar 12, 2024

They are not meant to behave the same, nor do they represent the same thing, see above.

The representation difference I get. One is number thing and maybe stuff like calc() can return NaN, and in colors, none represent undefined values, but how do they differ in behavior?

So, functionality-wise, none is not infectious like NaN or is it? I get specifically in interpolation that none takes on the value of the other color. But if not in interpolation what happens? If it isn't infectious, I imagine it is very easy for it lose its meaning accidentally if you aren't careful.

Does NaN no longer obey the same interpolation rules as none? Does it just infect the other color with itself during interpolation? I guess this would mean it isn't checked for and just allowed to pass through as a normal number to wreck havoc?

Are NaNs no longer resolved to a real value at any time? The color spec doesn't speak to any of this currently. As far as Im aware, it still speaks to NaNs representing undefined coordinates.

@facelessuser
Copy link
Collaborator

I had remembered the discussion about using NaN vs none and found it here: w3c/csswg-drafts#6107. I think it didn't bother me at the time because, functionality-wise, the end result was going to be the same, or at least seemed to be. It seemed more of a syntax issue difference making clear the difference of what NaN meant vs none, but I guess it didn't clear up behavior.

At the time, there was no NaN in CSS. I think now that NaN is being introduced with none, now things become murky, and I don't think the spec really clears up how one behaves vs the other in colors. Or maybe I was always wrong and none was not meant to behave like NaNs.

@facelessuser
Copy link
Collaborator

Okay, I did find this in the Color 5 spec:

However, if calculations are done on missing values, none is treated as 0.

I guess this explains the treatment of undefined as a null. So, I guess that answers my question. Undefined is no longer infectious (which is a shame because it made sense), but I get its context when looking at CSS. And NaN just behaves as NaN. So this change will be a surprising change in Color.js.

@LeaVerou
Copy link
Member Author

So, functionality-wise, none is not infectious like NaN or is it?
I get specifically in interpolation that none takes on the value of the other color. But if not in interpolation what happens?

It's exactly the opposite actually: none + anything that is not none = not none

When doing math (e.g. calc(h + 10)) or color space conversions, none gets coerced to 0 IIRC (@svgeesus confirm?)

If it isn't infectious, I imagine it is very easy for it lose its meaning accidentally if you aren't careful.

Precisely! That's why it's important not to do conversions we don't need (such as converting colors that are in-gamut in a GMA that doesn't modify in-gamut colors).

Does NaN no longer obey the same interpolation rules as none? Does it just infect the other color with itself during interpolation? I guess this would mean it isn't checked for and just allowed to pass through as a normal number to wreck havoc?

I would imagine that interpolating NaN with none would just make the whole component NaN. @svgeesus we need to make sure the spec is clear here.

Are NaNs no longer resolved to a real value at any time? The color spec doesn't speak to any of this currently. As far as Im aware, it still speaks to NaNs representing undefined coordinates.

NaN is a first-class value in CSS, it resolves to itself.

I guess this explains the treatment of undefined as a null. So, I guess that answers my question. Undefined is no longer infectious (which is a shame because it made sense), but I get its context when looking at CSS. And NaN just behaves as NaN. So this change will be a surprising change in Color.js.

I don't understand what you're saying here, could you elaborate a bit more? Why is it a shame that undefined is no longer infectious? How are we treating undefined as null? (we shouldn't!) And why would it be a surprising change?

@facelessuser
Copy link
Collaborator

I don't understand what you're saying here, could you elaborate a bit more? Why is it a shame that undefined is no longer infectious? How are we treating undefined as null? (we shouldn't!) And why would it be a surprising change?

Undefined is no longer NaN, we are using null, quite literally, and the behavior is exactly described as null. Their behavior is very different. We are replacing one concept with the other, which is fine. It is a sizeable breaking change, but I get where it is coming from.

Before, undefined operated as NaN, at least how it was implemented. Originally that was intentional, obviously not anymore. It had no value; therefore, you could not do calculations with it, not until you explicitly defined it. This made sense, at least to me. How can I add to something that doesn't exist?

> a = NaN
NaN
> a + 3
NaN
> a = 0
0
> a + 3
3

Now it is null, and we will quite literally be using null. It is uniquely different, and has its own identity up until you do anything with it, then it is zero (at least in most languages). This describes the behavior in the spec and it quite literally mimics how null behaves.

> a = null
null
> a + 3
3

It's a change in concept and any library that implemented the NaN behavior will have to consider, do we require 100% parity with CSS or do we support parsing CSS color syntax only, which is fine. Color.js wants to represent CSS, not only in syntax, but behavior.

As a side note, I've personally found the very specific behavior of undefined == 0 (whether mimicking NaN or null) to sometimes be lacking in the user experience, from a general purpose color library standpoint. Consider below.

>>> Color('black').convert('acescc')
color(--acescc -0.35845 -0.35845 -0.35845 / 1)
>>> Color('black').convert('acescct')
color(--acescct 0.07291 0.07291 0.07291 / 1)

Neither of these two spaces define black with zeros , not that undefined has to resolve to black, per se, but if we apply them, we get out of gamut results for acescct. Not very helpful.

>>> Color('acescct', [0, 0, 0]).convert('srgb')
color(srgb -0.07781 -0.07781 -0.07781 / 1)

These cases are rare, but this inflexibility can break in certain aspects. I initially had this issue with spaces like HCT and JzCzhz, has zero chroma and zero hue no longer mean white, gray, etc. (at least not pure white and gray). At least with these spaces, I think I'm starting to come to the realization that they are accounting for adapting luminance and background luminance, and when you do this, the achromatic colors appear different than pure white, pure gray, etc. If you "discount the luminance", you get a more traditional achromatic response of pure achromatics. So maybe the concept of zero chroma and zero hue must be pure achromatic was flawed initially, at least when looking at JzCzhz, CAM16, ZCAM, etc.

@facelessuser
Copy link
Collaborator

facelessuser commented Mar 12, 2024

I may be incorrectly lumping JzCzhz in with CAM16 and ZCAM, it has a similar achromatic response, but maybe its response is simply based off of less accurate matrices 🤷🏻.

@tabatkins
Copy link
Collaborator

The arguments against lumping NaN with none in CSS were in w3c/csswg-drafts#6107, especially this comment. In short: while NaN can serve as a reasonable sentinel value in many situations (because it's not part of the normal value space), you don't actually want author-produced NaNs (which come from math errors) to trigger this behavior. CSS can easily avoid the sentinel value issue and just use a different value (a keyword, in this case), thus treating any NaNs that are accidentally fed into the functions as an error case.

However, @LeaVerou, note that the color functions never see a NaN. A NaN gets censored into infinity when it exits the the math function, and so the color function only ever sees infinity (which it then arbitrarily treats as 0deg). So there's no CSS-compat reason to support reflecting NaN in colors.

@facelessuser
Copy link
Collaborator

However, @LeaVerou, note that the color functions never see a NaN. A NaN gets censored into infinity when it exits the the math function, and so the color function only ever sees infinity (which it then arbitrarily treats as 0deg). So there's no CSS-compat reason to support reflecting NaN in colors.

Ah, that is correct, we can't directly use NaN or Infinity in colors as things are currently, not as far as CSS syntax is concerned. They would have to be wrapped in calc(), something they can't escape.

So really, assuming Color.js really prefers nulls in general, it could just convert NaNs to null and call it a day. If a change must be made, I imagine this would be the least breaking. So what this really boils down to is whether color.js prefers NaN behavior or null behavior.

@LeaVerou
Copy link
Member Author

@tabatkins I thought users can also directly specify NaN?

@facelessuser Even if NaNs don't naturally appear in CSS colors, they can absolutely appear in JS when math is involved, and they should be distinct than a value that means "take the other component when interpolating".

@facelessuser
Copy link
Collaborator

facelessuser commented Mar 13, 2024

@facelessuser Even if NaNs don't naturally appear in CSS colors, they can absolutely appear in JS when math is involved.

Yep, that is certainly true.

@facelessuser
Copy link
Collaborator

Putting my opinions aside, I think I understand the situation and how the behavior will change now, and I now understand the reasoning behind it.

Because this is still a < 1.0 version, and I'd hate to see the overhead required to validate every value on every set, I'm kind of leaning more towards just making the change, with a bold note in the changelog of the breaking change.

@tabatkins
Copy link
Collaborator

I thought users can also directly specify NaN?

Nope, they can say calc(NaN), but that's still identical to calc(infinity).

@LeaVerou
Copy link
Member Author

This issue is a blocker for v0.6.0 so I’m keen to resolve it ASAP.

Thinking back to…

So now we’ve announced that the change is coming. How should we go about with this? How much effort should we put in making the migration gentler?

  1. Should we just make the change and just announce it in the changelog, since we gave people a heads up? This involves the least effort on our side.
  2. Should we print out a one-time warning about the change if NaN is used, and silently convert it to null?
  3. Should we handle (and preserve) both NaN and null when found in coords, and print out a one-time warning for NaN?
  4. Something else?

It seems like there was no resolution?

If so, I think this what I’ll do:

  1. Parse none as null
  2. Parse NaN as NaN
  3. Serialize null as none
  4. Serialize NaN as calc(NaN)
  5. That’s it. No warnings, no migration code, no assumptions that if NaN is used you really meant null, they both have distinct purposes.

LeaVerou added a commit that referenced this issue May 23, 2024
Also don't clamp alpha if it's none, in preparation for #409
LeaVerou added a commit that referenced this issue May 23, 2024
In preparation for #409 but also allows removing code for this in the Color constructor
LeaVerou added a commit that referenced this issue May 23, 2024
This allows reading metadata without having to make the values objects, and is the *only* way to attach metadata to null (see #409)
LeaVerou added a commit that referenced this issue May 23, 2024
Besides being more semantically correct, it paves the way for #409
@lloydk
Copy link
Collaborator

lloydk commented May 23, 2024

Should NaN be converted to 0 when converting between colors or is it left as NaN?

@LeaVerou
Copy link
Member Author

Should NaN be converted to 0 when converting between colors or is it left as NaN?

I think it's fine to leave it as NaN, but feel free to open another issue to discuss that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API change For PRs that require API design review Breaking change Topic: JS API
Projects
None yet
Development

No branches or pull requests

4 participants