GithubHelp home page GithubHelp logo

color-js / color.js Goto Github PK

View Code? Open in Web Editor NEW
1.8K 21.0 76.0 2.8 MB

Color conversion & manipulation library by the editors of the CSS Color specifications

Home Page: https://colorjs.io

License: MIT License

JavaScript 62.94% HTML 21.69% CSS 7.57% Python 1.33% Nunjucks 2.84% TypeScript 3.62%
cielab color color-palette color-spaces esm oklab oklch rec2020 srgb display-p3

color.js's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

color.js's Issues

Interpolation

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)

max-ΔΕ interpolation is buggy

Right now our algorithm is:

  • Produce number of steps based on steps
  • Iterate over steps pair-wise, calculate ΔΕ. If > delta, insert midpoint, repeat.
  • This results in a lot of problematic boundary conditions, where some consecutive pairs have roughly delta/2 difference and others slightly below delta.

Check this out:
image

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.

[color-notebook] Execute in sandbox

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.

Avoid hard-coded D50 in XYZ and Lab definitions

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.

[color notebook] Live playground for sharing snippets

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.

Syntax for getting coordinates of various color spaces

Right now, the algorithm is as follows:

  • Coordinate names are added as properties to Color.prototype if there are no properties with such names already.
  • If there are clashes, the color space id is prepended. So, when adding HSL, hue and lightness conflict with LCH which is added earlier, so each color has hsl_lightness, saturation amd hsl_hue properties.

This is obviously problematic.

  • It's non-deterministic, as it depends on the order of color spaces. However, there should be a way to "privilege" certain color spaces. I.e. we do want chroma, hue, and lightness from LCH on every color, and we don't really want those to be as easily accessible for HSL.
  • It's inconsistent even within the same color space, as some coords may be prefixed and others not. This is terrible
  • Underscore_case is not common within JS, but camelCase here would make things hard to read.
  • There are way too many properties on every object, even for the few default color spaces. We need to be able to load a ton of color spaces without making Color objects super heavy.

Potential solutions:

  • Hang the properties on the coord accessors instead of the color instances (e.g. 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)
  • Instead of properties, have a special syntax for getting coords by name, e.g. `color.get("hsl.lightness"). This is what chroma.js does
  • Have accessors for the current color space and a few privileged ones (possibly opt-in on the color space level). This is kinda what D3 Color does, it only has accessors for current color space, but it uses subclasses. We want to be able to change the current color space at will, so this approach would make that more expensive, but not a dealbreaker.
  • If we keep the prefixed accessors, we should make sure either ALL coords get prefixed, or none of them.

Support hwb

Since we claim to support "every CSS Color 4" format, we should support this too.

Convert app could show OOG more clearly

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.

Precision is hard to understand in convert app

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.

ColorSpace coord accessors reject writes

> 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

Support interpolation between multiple colors

If the color argument to the interpolation functions is an array, we should interpolate between all these colors.

This brings up some interesting challenges:

  • Does 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?
  • Does 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?
  • If range also accepts an array, do we need to be able to specify positions of the color stops too?

LCH interpolation by smaller arc? Colorspace-specific pre-interpolation transformations?

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)

image

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?

Implement ΔE ITP (Delta ICtCp)

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.

Bug in sRGB to HSL conversion

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.

image

(Note to self: Need some sort of sharing Color Notebook snippets that doesn't involve screenshotting!)

API to check if in gamut, and get both coords

Questions to answer:

  • How to check whether the color is in gamut?
  • Should this.coords be clipped to gamut?
    • If not, how to get clipped coords?
    • If so, how to get original coords?

("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.

ICtCp reverse transformation to XYZ underspecified, may be unstable

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;

Avoid hard-coded linear Bradford CAT

We currently have

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.

Good default color space for interpolation?

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).

Finish smarter gamut mapping with clip?

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?

Sensible defaults for deltaE

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:

  • These superior color difference algorithms are also slower, so you may still want to use DeltaE 76 as a default.
  • Which default you end up with depends on the order of imported modules, since each of them is going to override the previous default.
  • You may even want a different default depending on application. E.g. you may want CMC or 76 in general, but 2000 in Color.steps() since 2000 is better at small color differences.

Midpoint values can be negative or greater than 1

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%)

CAT02 (and CAT16) implementation assumes complete adaptation

The full model of CAT02 and CAT16 requires:

  • LA, the luminance of the adapting white
  • F, a correction factor for the luminance of the surround
  • D, the degree of adaptation to the current white point

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.

Non-linear interpolation

Right now we only support linear interpolation.

We should also support:

  • Custom cubic bezier
  • Possibly keywords
  • Entirely custom functions

Adaptation documentation needs edits

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.

Use NaN for undefined coords

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:

  • 1. Handle NaN values in interpolation (interpolating with NaN returns the other coord)
  • 2. Output NaN for hue when converting achromatic colors to any color space with hue
  • 3. Output NaN as 0 in toString() since it needs to produce a valid color
  • 4. Handle NaN in deltaE (same as interpolation I guess)
  • 5. Handle NaN in color contrast
  • 6. Handle NaN in color conversions
  • 9. Handle NaN anywhere else

JzAzCz to JzCzhz is wrong

Conversion 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
];

image

Achromatic interpolation

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:

  • As one can see in the last bit, you still have to be explicit for white
  • This produces discontinuities at values very close to 0 but not 0.

Related: w3c/csswg-drafts#4647 (comment)

What happens when color changes after range function has been returned?

Consider this:

let color = new Color("red");
let color2 = new Color("black");
let gradient = color.range(color2);
color.coords[1] = 1;
color2.coords[2] = 1;
gradient(.5);

Should that interpolate with the original colors or the new colors?

Currently it interpolates the original colors:
image

Color.prototype.set()

This would allow setting multiple properties at once and return the color instance, to enable chaining.

  • Single-property syntax:
color.set("lightness", 50).set("chroma", 100);
  • Object literal:
color.set({
  lightness: 50,
  chroma: 100
});
  • It should also allow relative manipulations:
color.set({
  lightness: "* 1.2*",
  chroma: c => c * c // custom math via a function!
});
  • And deeply nested properties (this will become even more useful after #16):
color.set("hsl.lightness", 50);

ICtCp known values

Lacking a reference converter, this graphic at least gives rough estimates for Rec.2020 to ICyCp conversion.

image

Setting color[spaceId][i] does nothing

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.