I built my own solution, then afterwards came across the Color.js repo. I have a strict "no dependencies" rule for my library; discovering Color.js is the closest I've ever come to breaking that rule.
If I see small 1 or 2 file libraries I generally just copy them in.
with that said, i did some experimentation with these new color spaces recently and found that even these are not foolproof for those of us who don't want to be steeped in the depths of color spaces and human perception. for instance, mentioned in the article is automatic gamut mapping for when you try to display a color that's outside the gamut. seems like that's a good thing since it would take care of some rare edge cases, right?
no, the edge cases are all over the place. the gamut "depth" is dependent on the component values (like lightness and chroma), so if you want to go from a light red hue to a light yellow hue, you can't just change the hue component, because the lightness and chroma values might put the target color out of bounds. that's where the automatic gamut mapping kicks in. but guess what? the various browser engines currently have their own algorithms, and they that don't pick the same nearby colors.
the evil martians oklch color picker[0] shows this visually. if you look at the hue vs. lightness graph, notice how jagged the (planar projection of the) gamut is there. for any given horizontal line (constant lightness), you might end up out of bounds on the chroma component for a chosen hue. this can happen in any of the 3 dimensions.
also, while oklch is more perceptually even generally, there are gradient examples where it's not much better (i think orange/brown gradients is one of those cases, but i'm not certain, offhand). color is apparently quite hard, so i'm not knocking the effort or the improvements, but there are still some gotchas remaining for designers/developers.
This is an unavoidable bug (feature?) of human vision and computer displays.
Using models like "HSL" or "HSV" is a way of trying to sweep it under the rug, but it doesn’t really work very well and ends up causing a lot more trouble.
I can't help but wonder why everyone still names their API functions Foo_to_Bar when the reverse Bar_from_Foo is so much more readable when chaining conversions.
Compare the example here:
let myoklch =
OKLab_to_OKLCH(XYZ_to_OKLab(D65_to_D50(lin_ProPhoto_to_XYZ(lin_ProPhoto(mycolor)))))
With what it could have been if it had used Bar_from_Foo naming instead: let myoklch =
OKLCH_from_OKLab(OKLab_from_XYZ(D50_from_D65(XYZ_from_linProPhoto(lin_ProPhoto(mycolor)))))The other solution in a procedural/functional language is to write code closer to the evaluation order, e.g. with a 'pipeline' operator:
let myoklch =
mycolor
|> lin_proPhoto
|> lin_proPhoto_to_XYZ
|> ...
or with a 'fluent' interface in an OOP language: let myoklch =
mycolor.to_linear().to_XYZ().to_D65().to_OKLAB().to_OKLCH();If you're doing generative artwork or any procedural work with a non-fixed palette, a good color library and working in linear or Oklab based colorspace is a must. Using sRGB or HSV/B when interpolating for gradients or generating palettes or complementary colors is extremely painful; a 50% brightness (HSV) yellow is visually much brighter than a 50% brightness purple, with similar issues cropping up for saturation. Balancing the lighting and contrast of programmatically generated colors is so much simpler when you have a perceptually uniform colorspace like OkLCh, or linear RGB if you're working with predefined hex values.
Color.js is admittedly overkill for this, but it let me import just the interpolate function and it did the trick.