color.js
color.js copied to clipboard
Null hue logic question
So it seems the library, on convert, will identify cases where a hue is null in hsl, lch, and hwb (not so much hsv). In these cases, if the criteria are met, these colors will have a hue of NaN.
While this "magic" occurs on convert it is not applied to the initial input of color, instead, it seems the requirement is that you must explicitly insert NaN.
All of this is fine if that is the expectation, but it can be a bit confusing as NaNs at first seem to be an explicit thing, but when you convert, you have no choice and they are inserted automatically.
This may all be by design, and I do understand what is going on, but it can be confusing. This is more just a question about the implementation approach, and why it was chosen to do things this way.
let color = new Color("hsl(0 0% 100%)").mix("blue", {space: "hsl"});
let color = new Color("white").mix("blue", {space: "hsl"});
I guess, intuitively, I would kind of expect "on convert" and when initially loading the color to insert hue NaNs, but allow you to explicitly change things. Or, to not ever insert NaN, and the user must define NaN cases.
It seems if you want the above examples to work the same in both cases, in the first case, you have to be aware that you need to define the NaN, while in the second case you don't have any choice unless you convert manually yourself, change the hue how you want it, then run the mix.
I think I can better articulate confusion on this.
Observations
I understand that this is essentially the way in which a coordinate is flagged as undefined and masked off from interpolation.
It appears that this project models the idea of using NaN from projects like d3 and chroma.js, but it seems to differ as it seems that both of these projects when interpreting a string will interpret when the hue is null and replace it with a NaN.
In d3, the only time it doesn't do this is when you enter raw numbers. To me, this makes sense as I would expect if you were entering raw numbers you are signalling to the library that you are explicitly setting the coordinates to what you want.
Why the Confusion?
I kind of expect color.js to behave in a like manner as d3 for consistency when using the interface. Now with that said, the ability to insert NaN into a string is useful and unique to color.js, and I imagine in cases like these, you'd want no interpretation of "null" hues if a NaN was explicitly set or you'd mix nothing in cases like this as NaN may be treated as 0:
new Color('lch(52% 58.1 22.7)').mix('lch(nan nan 257.1)', 1 - 0.7523);
Possible Suggestion
So, in short, what seems to make sense in my brain, and I realize this is opinionated:
-
If interpreting a string, the hue should be transformed into
NaNif the color is achromatic. -
If using raw numbers, the user is asserting they know what they are doing, and no "magic" should occur.
-
If a
NaNis specified in the string, then the achromatic nature of the color can not be determined as the color is not fully defined; therefore, the onlyNaNwill be the ones specified by the user.
So something like this:
new Color('hsl(0 0% 100%)').coords;
would yield this:
[NaN, 0, 100]
and would match cases like
new Color('white').to('hsl').coords;
// [NaN, 0, 100]
While this:
new Color('lch(nan nan 257.1)').coords;
Would still yield:
[NaN, NaN, 257.1]
Thanks for the report, sorry it took a while to respond.
I think we do substitution of 0 for NaN on stringification, because many syntaxes do not support NaN, but I would expect this
let color = new Color("hsl(0 0% 100%)").coords // [0,0,100%]
to have given a NaN hue.
I agree. Consistency is the main thing.
Yeah, people are often stringifying to use directly in CSS, SVG, canvas etc, so it should try to produce working values. However, I can see a preserveNaN option to customize stringification.
@svgeesus I think we mainly use NaN when converting and preserve the hue we got when we directly get one. Should we not?
I think we should. But we only do it on numerically unstable achromatic hue angles. I think @facelessuser wants this in general, as an interpolation hint for lightness, chroma, or any other coordinate
I think @facelessuser wants this in general, as an interpolation hint for lightness, chroma, or any other coordinate
I was more using it as it was implemented and written to do so. You could easily just use the same values and it would essentially do the same thing:
new Color('lch(52% 58.1 22.7)').mix('lch(52% 58.1 257.1)', 1 - 0.7523);
How this part is implemented bothers me less. I think carrying around the NaN baggage adds complexity all throughout the code. I'd be fine if the null hue check was performed on the fly for interpolation TBH. As far as masking channels, I'd also be fine with just telling mix what channels I want to mix. The NaN could be completely hidden from me and I'd still be fine (even if it is inserted on the fly during interpolation).
My main interest was mainly the inconsistent treatment of null hues. In the case of colorjs, the NaN must be present or hues get mixed; therefore, I would expect if I created a color that should have a null hue that the hue would be NaN on the creation of that color, which is the behavior when you convert from one space to another.
That was my main concern. I mention the manual NaN inputs as you have to kind of reslove that when calculating the initial hues, if you want to preserve manual NaNs. If colorjs dropped manual Nans for masking channels, I'd be fine. There are other ways you can expose this behavior in the API.
On a side note, if at any time saturation (or chroma or whatever depending on the color space) becomes zero on cylindrical colors, I kind of think hue should also be treated as NaN or null. This is kind of why I think calculating null hues on the fly makes sense, at least in my head, maybe there are cases where this is not ideal :shrug:.
I know other libraries also employ NaN (D3-color in a very aggressive manner, and not just to hue's either), so I was just going to accept that as the way colorjs chose to do things. Most of libraries that do this always create a new color when modifications are performed and assign null hues when that is done. Since colorjs allows in place modifications, a more dynamic calculation of null hue may make more sense, or you need events every time other channels change. Currently, colorjs just accepts manual changes as the final result, but on convert, will assign NaN. Which can be unexpected.
I think I've rambled on quite long enough 🙂.
I think we do substitution of
0forNaNon stringification
NaN is in the numeric values for monochrome hues too. See this codepen: https://codepen.io/sidewayss/pen/VwqWoGx
CSS has defined none to handle this and color.js now supports none. I'm pretty sure this issue can be closed.
I will go ahead and close this as I feel I've come to an understanding, I no longer have questions.