color-js / color.js Goto Github PK
View Code? Open in Web Editor NEWColor conversion & manipulation library by the editors of the CSS Color specifications
Home Page: https://colorjs.io
License: MIT License
Color conversion & manipulation library by the editors of the CSS Color specifications
Home Page: https://colorjs.io
License: MIT License
API sketch:
color.steps(color2, {space, delta, steps, maxSteps})
Color.steps(color1, color2, {colorSpace, delta, steps, maxSteps})
If space is provided, it's used for the interpolation. If not, then a) if both colors have the same colorSpace, it's used for interpolation, otherwise Lab is used.
If delta is provided, the number of colors generated is such that the distance between any two consecutive ones does not exceed delta. maxSteps is used as a failsafe, to avoid getting way too many colors.
If steps are provided, the colors returned are at least that many (if delta is also provided they may need to be more)
Right now our algorithm is:
steps
delta
, insert midpoint, repeat.A better algorithm might be:
let totalDelta = color1.deltaE(color2)
let actualSteps = Math.max(steps, Math.ceil(totalDelta / delta))
// Then return an evenly spaced range of actualSteps steps
Problem: How does this work with non-linear-interpolation #12?
Alternatively, we can keep the current algorithm, but every time we determine we need to add a midpoint between two stops, we do it for the entire array, then rinse and repeat. This may be easier to adapt to non-linear interpolation.
Currently variables are local to each code snippet but the Color object they run on is the same as the rest of the page, so any global modifications (e.g. to Color.defaults
) affect other snippets too.
Also, when we have a playground (see #29) this will be insecure as well.
Use case 1:
Recently, I was trying to convert some published experimental data from CIE LCH (with an Illuminant C whitepoint) to CIE LCH (with the standard D50 whitepoint).
The required chain of conversions is:
LCH → Lab → XYZ(with C whitepoint) → CAT16(C to D50) →Lab → LCH
This involved copying the toXYZ(Lab)
and fromXYZ(XYZ)
functions and changing the hard-coded D50 whitepoint.
Use case 2:
The Jzazbz and JzCzHz colorspaces require input data as absolute (rather than relative) D65 (rather than D50) XYZ. The current implementation accepts relative D50 XYZ as input and performs a Bradford CAT to D65 before scaling the values to the recommended SDR media white luminance
Enhancement
To facilitate similar tasks, it would be better if these functions allowed an optional whitepoint to be passed (retaining D50 as the default) so the existing functions could be reused without copy-pasting.
This would work similarly to mix() in Sass.
Interpolation would work similarly to #2
The documentation should explain how to create a CSS gradient from a Range.
Perhaps it would be useful to have a convenience method to do this?
Snippets can be saved as gists or in Github repos, as Markdown.
This way, people can share entire Color notebook documents if they so desire.
Can be implemented with Mavo and a little bit of custom JS for saving a new Gist.
Precision should be implemented as significant digits, not as decimals, per discussion with @svgeesus
Right now, the algorithm is as follows:
Color.prototype
if there are no properties with such names already.hsl_lightness
, saturation
amd hsl_hue
properties.This is obviously problematic.
Potential solutions:
color.hsl.lightness
. This does read nicely, but it means we can't just return the coords array as-is when someone uses the coord property that corresponds to the current color space (i.e. when we have an sRGB color, we can't just return this.coords
for color.srgb
)Currently, if the specified method doesn't exist, we silently fall back to DeltaE 76, even if a better one is available (e.g. one specifies "2000", and only CMC is loaded).
It should be possible to specify an array instead with deltaE methods in order of preference.
Since we claim to support "every CSS Color 4" format, we should support this too.
In the convert app, if you select a color out of gamut, OOG is detectable because the color.toString()
and Color.prototype.toString.call(color)
values are not the same.
It would be good to more clearly indicate that, perhaps a salmon pink instead of pale grey background on the color.toString()
column, mainly to avoid people blindly copy and pasting the values without realizing gamut mapping has occurred.
A brief paragraph of introductory text might also help.
Not that this should be added for first release, or indeed soon, but just so I can close some browser tabs :)
node-transicc which is a node wrapper around
Possibly using some sort of sigmoid function, per @svgeesus recommendation
in this example precision is set as 4, and the LCH value is color(lch 85.9 140.11 135.6)
.
The Lightness has 3 digits of precision, the Chroma has 5 and the Hue has 4. Maybe there is a reason for this, in which case it should be explained in the docs, or maybe this is a bug in the precision code.
Also, the CSS Color 4 color()
function accepts Lab, as requested by Simon Fraser, but does not accept LCH
Also, the color()
stringification omits percent for LCH Lightness (presumably because the CSS Color 4 color()
function used to only allow but should use percent.
> slategray
< Color {colorSpaceId: "srgb", coords: Array(3), alpha: 1}
> slategray.chroma
< 11.234142037623341
> slategray.chroma = slategray.chroma * 1.2
< 13.480970445148008
> slategray.chroma
< 11.234142037623341
If the color argument to the interpolation functions is an array, we should interpolate between all these colors.
This brings up some interesting challenges:
steps
apply to the transition between each pair of colors or all of them? If the latter, what happens e.g. when we're interpolating between 3 colors in e.g. 4 steps? We don't get our second color at all?range
also accept an array of colors? If so, that means you can't just get 0 and 1 to read all color stops, because there could be any number of stops. Perhaps a property in the function stores the color stops?range
also accepts an array, do we need to be able to specify positions of the color stops too?Because people will ask for it 😢
Also in Color.parse()
Currently we just interpolate all color components in the interpolation space with no adjustment. This means that interpolating between lch(50% 80 0)
and lch(50% 80 720)
produces a double rainbow.
Case in point: https://colorjs.io/demos/gradients/?color1=lch(50%25%2080%200)&color2=lch(50%25%2080%20720)
Should we instead interpolate by smaller arc?
When interpolating lch(50% 80 0)
to lch(50% 80 360)
do we want a rainbow or a solid color? And if we mod and choose smaller arc, how do we do this in a color space agnostic way? Maybe color spaces can define their own pre-interpolation adjustments or something?
See
Pieri, E., & Pytlarz, J. (2018). Hitting the mark - A new color difference metric for HDR and WCG Imagery. In SMPTE 2017 Annual Technical Conference and Exhibition, SMPTE 2017 (Vol. 2018-January, pp. 1–13). Institute of Electrical and Electronics Engineers Inc. https://doi.org/10.5594/M001802
With the emerging demand for high-dynamic-range (HDR) and wide-color-gamut (WCG) technologies, display and projector manufacturers are racing to extend their color primaries in the cinema and in the home. With these brighter and wider colors, the question is: in calibration, how close is close enough? This answer is increasingly important for both the consumer and professional display/projector market as they balance design trade-offs. With HDR/WCG technology, an increasing issue is that many of the color difference metrics in common use today, such as ΔE00, substantially deviate from human perception and become unreliable for measuring color differences. This causes under and over prediction of color differences and can to lead to sub-optimal design decisions and difficulties during calibration. There is a large amount of perceptual color difference data in the field today, however the majority was collected using reflective surfaces and very little reaches the boundaries of modern display capabilities. To provide a better tool for facilitating design choices, this paper will present a 'ground truth' visible color difference dataset. These visual experiments were conducted between luminance levels of 0.1 and 1000 cd/m2 on high dynamic range laser cinema projectors with approximate BT.2100 color primaries. We will present our findings, compare against current metrics, and propose specifying color tolerances using the ΔICTCP metric for HDR and WCG imagery.
At first I thought this was an interpolation bug, because I noticed it when using outputSpace: "hsl"
for my comment in w3c/csswg-drafts#4735 (comment) .
However, it looks like it's a conversion bug, as it happens when just converting standalone sRGB colors to HSL. Converting in-gamut sRGB colors to HSL should not affect the color, yet it seems to do so.
(Note to self: Need some sort of sharing Color Notebook snippets that doesn't involve screenshotting!)
Questions to answer:
this.coords
be clipped to gamut?
("Clipped" here doesn't refer to per-component clipping, but chroma reduction until we're in gamut)
We should also provide a way for custom rendering intents, but maybe that's another issue.
This kind of thing should be possible:
let r = Color.range("lch(50 50 0)", "lch(90 50 20)");
Color.range(r, {progression: p => p ** 3});
@svgeesus says it's basically XYZ interpolation, which we do support, does that mean we don't need to do anything?
In the original Alvey Ray Smith paper defining HWB is the comment
H is returned UNDEFINED if W == 1 – B
Achromatic colors currently return odd hue angles; black has hue 0 (a red) while white has hue 137.281(a cyanish green).
The Dolby whitepaper on ICtCp defines two forward transforms (one from Rec.2020 linear light, one from Absolute D65 XYZ) but the reverse transform is left as an exercise for the reader.
I see an issue claiming the inverse is not numerically stable and another one.
The latter link includes Matlab code for the inverse EOTF:
EOTF = @(PQ) (max(PQ.^(32/2523)-(3424/4096),0) ./ ... ((2413/128)-(2392/128)*PQ.^(32/2523))).^(16384/2610)*10000;
Feedback from the ICC HDR WG meeting today:
static chromaticAdaptation (W1, W2, XYZ) {
with tests for W1 and W2 being the D50-D65 pair in either direction. If true, a linear Bradford CAT is used. If false, there is a hook and if the hook is not used, a TypeError is thrown.
This makes it difficult to use a non-Bradford CAT for that common pair of whitepoints.
Instead, chromaticAdaptation
should take an optional fourth parameter, method
with a default of "Bradford" and tests like
if (W1 === Color.whites.D65 && W2 === Color.whites.D50) {
would become
if (W1 === Color.whites.D65 && W2 === Color.whites.D50 && method = "Bradford") {
to allow the flow of control to pass through to the hook.
Currently, we use the same heuristic as I proposed for CSS: If both colors are in the same color space we interpolate in that color space, otherwise we interpolate in Lab.
However, in CSS we need this for backwards compat, whereas here we don't have the same issue. Furthermore, since many, many colors are defined via sRGB (as the most common formats are sRGB), this results in a lot of crappy interpolation, and most people don't know better.
In addition to this, as @svgeesus pointed out, I'm not sure Lab is a good default space either. Perhaps LCH is better, though to use LCH as a default, we would need to do mod and smaller arc (see #15).
Currently the default method for gamut mapping is reducing lch chroma by binary search until the color is very close to being in gamut (thanks @tabatkins).
However, this sometimes produces colors that are very slightly out of gamut. E.g. take a look at https://colorjs.io/apps/convert/?color=color(display-p3%200%201%200)&precision=5 which returns rgb(-0.001% 98.388% 16.188%)
.
I wonder if it would be a good strategy to finish off the smarter gamut mapping with clipping, so that we get rid of these epsilons above and below?
We currently only support deltaE 76, it would be good to support newer ones, especially for the maxDelta argument in interpolation.
Resources:
Depending on the amount of code involved, this could be done as either a plugin or built-in.
Currently, deltaE modules can add new deltaE methods that can be used via the method
parameter in color.deltaE()
. The default method is determined via Color.defaults.deltaE
.
However, it's a reasonable assumption that if someone imports e.g. DeltaE 2000, they expect this superior DeltaE to be used as the default. Currently, unless they override Color.defaults.deltaE
, DeltaE 76 will be the default.
We could make it so that DeltaE modules override the default (you can still set it yourself by setting Color.defaults.deltaE
). This does mean that importing e.g. the DeltaE 2000 module, you also set your default DeltaE algorithm to DeltaE 2000.
However, this has a bunch of drawbacks:
Color.steps()
since 2000 is better at small color differences.The documentation says that range()
returns a function that "accepts a percentage as a 0 - 1 number".
It turns out that the number can be negative or greater than 1, and the function returned by range()
will try to find a value that matches it, even if it is outside the color range.
let color = new Color("#FF0000");
let redgreen = color.range("#00FF00");
redgreen(0).toString({inGamut: false}); // rgb(100% 0% 0%)
redgreen(1.0).toString({inGamut: false}); // rgb(0% 100% 0%)
redgreen(-1).toString({inGamut: false}); // rgb(119.23% -543.12% 10.585%)
redgreen(2).toString({inGamut: false}); // rgb(-2,296.1% 157.96% 31.987%)
The full model of CAT02 and CAT16 requires:
our implementation assumes D = 1.0 (full adaptation), F = 1.0 (surround is not dim or dark) and thus LA is not used. Note that Hunt & Pointer "Measuring Color" p. 132 suggests a default value for D (in the absence of more precise values) of 0.95, not 1.0.
This is likely sufficient, but does mean that users who want to use partial adaptation need to look elsewhere.
It would be possible to add support for this, with an optional object literal of those three parameters and a more complex calculation of the eventual matrix M
if they are present. The calculations are in Hunt & Pointer pp. 131-133, with two worked examples on pp.140-141. Chapter 9 of Fairchild "Color Appearance Models" is also helpful.
Currently we only support D50 and D65, which cover most use cases but not all.
Right now we only support linear interpolation.
We should also support:
The inline examples in the Chromatic Adaptation documentation are not correct.
The first two, I tried to demonstrate XYZ Scaling and the von Kries adaptations, but found I could not because our code has a non-optional Bradford to D50 as soon as you convert to XYZ.
The example in Using CATs looks correct, but has type errors.
I wanted to add a second example showing how to override the default Bradford with another method, but didn't understand the hook well enough to create the example.
This will resolve #14 and #23.
If polar space colors are created, we should preserve their coordinates, but if we are converting to them, we should use NaN for coords that are undefined (let's start from hue for achromatic colors, not sure if this is needed for chroma/saturation).
This does mean that roundtripping an achromatic color would lose its hue data, but I don't think that's a significant problem.
Tasks needed:
toString()
since it needs to produce a valid colorConversion functions literally return the input for Jz, yet we have different values
return [
Jz, // Jz is still Jz
Math.sqrt(az ** 2 + bz ** 2), // Chroma
angles.constrain(hue) // Hue, in degrees [0 to 360)
];
return [
jzczhz[0], // Jz is still Jz
jzczhz[1] * Math.cos(jzczhz[2] * Math.PI / 180), // az
jzczhz[1] * Math.sin(jzczhz[2] * Math.PI / 180) // bz
];
d3-color seems to handle L=0 and C=0 specially, by
Now achromatic colors in HCL have undefined hue (NaN), and black has undefined chroma. And for white, L = 100, C = 0 for HCL, and A = B = 0 for Lab.
This produces more reasonable results when interpolating in LCH, but:
Related: w3c/csswg-drafts#4647 (comment)
I find myself doing a lot of:
const supportsP3 = window.CSS && CSS.supports("color", "color(display-p3 0 1 0)");
const outputSpace = Color.space(supportsP3? "p3" : "srgb");
...
color.to(outputSpace).toString()
There should be an easy way to get this outputSpace, and the string representation.
Most notably, when mapping color(display-p3 1 1 0)
to sRGB. Instead of producing yellow
, it produces… rgb(100% 97.5% 77.5%)
, i.e. a light yellow.
Tests here: https://colorjs.io/tests/gamut.html
It appears that this happens because the red component remains slightly above 1 + ε, even with substantial chroma reduction:
CC @tabatkins as he originally implemented the gamut mapping algorithm so he may be interested.
Some methods fail silently when using parameters instead of options. For example:
color.to("sRGB");
works as expected, but
color.inGamut("sRGB");
color.toGamut("sRGB");
do not, because the "sRGB"
parameter is ignored and the method uses the color's current space instead.
This would allow setting multiple properties at once and return the color instance, to enable chaining.
color.set("lightness", 50).set("chroma", 100);
color.set({
lightness: 50,
chroma: 100
});
color.set({
lightness: "* 1.2*",
chroma: c => c * c // custom math via a function!
});
color.set("hsl.lightness", 50);
Because people will ask for it.
E.g. color.hsl.hue = 0
does not actually modify the color.
A logical consequence of the fact that this is just an array generated and returned by a getter, but still confusing AF. Will likely be fixed in one fell swoop along with #16.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.