29 |
30 |
31 |
32 | @@include('_footer.html')
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/docs/manipulation.md:
--------------------------------------------------------------------------------
1 | # Color manipulation
2 |
3 | ## By color coordinates
4 |
5 | We've seen in the [previous section](the-color-object) how we can manipulate a color
6 | by directly manipulating coordinates in any of the color spaces supported.
7 | LCH coordinates are particularly useful for this so they are available directly on color objects:
8 |
9 | ```js
10 | let color = new Color("rebeccapurple");
11 | color.lightness *= 1.2;
12 | color.chroma = 40;
13 | color.hue += 30;
14 | ```
15 |
16 | You can also use `color.set()` to set multiple coordinates at once.
17 | In addition, it returns the color itself, so further methods can be called on it:
18 |
19 | ```js
20 | let color = new Color("lch(50% 50 10)");
21 | color = color.set({
22 | hue: h => h + 180, // relative modification!
23 | chroma: 60,
24 | "hwb.whiteness": w => w * 1.2
25 | }).lighten();
26 | ```
27 |
--------------------------------------------------------------------------------
/apps/convert/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Convert
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
Id
28 |
Coords
29 |
color.toString()
30 |
Color.prototype.toString.call(color)
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/spaces/p3.js:
--------------------------------------------------------------------------------
1 | import Color from "./srgb.js";
2 |
3 | Color.defineSpace({
4 | inherits: "srgb",
5 | id: "p3",
6 | name: "P3",
7 | cssId: "display-p3",
8 | // Gamma correction is the same as sRGB
9 | // convert an array of display-p3 values to CIE XYZ
10 | // using D65 (no chromatic adaptation)
11 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
12 | // Functions are the same as sRGB, just with different matrices
13 | toXYZ_M: [
14 | [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
15 | [0.2289745640697488, 0.6917385218365064, 0.079286914093745],
16 | [0.0000000000000000, 0.04511338185890264, 1.043944368900976]
17 | ],
18 | fromXYZ_M: [
19 | [ 2.493496911941425, -0.9313836179191239, -0.40271078445071684],
20 | [-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
21 | [ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
22 | ]
23 | });
24 |
--------------------------------------------------------------------------------
/src/multiply-matrices.js:
--------------------------------------------------------------------------------
1 | // A is m x n. B is n x p. product is m x p.
2 | export default function multiplyMatrices(A, B) {
3 | let m = A.length;
4 |
5 | if (!Array.isArray(A[0])) {
6 | // A is vector, convert to [[a, b, c, ...]]
7 | A = [A];
8 | }
9 |
10 | if (!Array.isArray(B[0])) {
11 | // B is vector, convert to [[a], [b], [c], ...]]
12 | B = B.map(x => [x]);
13 | }
14 |
15 | let p = B[0].length;
16 | let B_cols = B[0].map((_, i) => B.map(x => x[i])); // transpose B
17 | let product = A.map(row => B_cols.map(col => {
18 | if (!Array.isArray(row)) {
19 | return col.reduce((a, c) => a + c * row, 0);
20 | }
21 |
22 | return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
23 | }));
24 |
25 | if (m === 1) {
26 | product = product[0]; // Avoid [[a, b, c, ...]]
27 | }
28 |
29 | if (p === 1) {
30 | return product.map(x => x[0]); // Avoid [[a], [b], [c], ...]]
31 | }
32 |
33 | return product;
34 | }
35 |
--------------------------------------------------------------------------------
/src/angles.js:
--------------------------------------------------------------------------------
1 | export const range = [0, 360];
2 | range.isAngle = true;
3 |
4 | export function constrain (angle) {
5 | return ((angle % 360) + 360) % 360;
6 | }
7 |
8 | export function adjust (arc, angles) {
9 | if (arc === "raw") {
10 | return angles;
11 | }
12 |
13 | let [a1, a2] = angles.map(constrain);
14 |
15 | let angleDiff = a2 - a1;
16 |
17 | if (arc === "increasing") {
18 | if (angleDiff < 0) {
19 | a2 += 360;
20 | }
21 | }
22 | else if (arc === "decreasing") {
23 | if (angleDiff > 0) {
24 | a1 += 360;
25 | }
26 | }
27 | else if (arc === "longer") {
28 | if (-180 < angleDiff && angleDiff < 180) {
29 | if (angleDiff > 0) {
30 | a2 += 360;
31 | }
32 | else {
33 | a1 += 360;
34 | }
35 | }
36 | }
37 | else if (arc === "shorter") {
38 | if (angleDiff > 180) {
39 | a1 += 360;
40 | }
41 | else if (angleDiff < -180) {
42 | a2 += 360;
43 | }
44 | }
45 |
46 | return [a1, a2];
47 | }
48 |
--------------------------------------------------------------------------------
/docs/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Color.js Docs
7 |
8 | @@include('_head.html')
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | @@include('_header.html', {
17 | "title": "Color.js Docs"
18 | })
19 |
20 |
21 |
26 |
27 |
37 |
38 |
39 |
40 | @@include('_footer.html')
41 |
42 |
43 |
--------------------------------------------------------------------------------
/get/style.src.css:
--------------------------------------------------------------------------------
1 | [mv-app="colorBundler"] {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | grid-gap: .6em 1em;
5 |
6 | & #core,
7 | & #spaces,
8 | & #optional,
9 | & > pre,
10 | & > button,
11 | & a[download] {
12 | grid-column-end: span 2;
13 | }
14 | }
15 |
16 | fieldset {
17 | & ul {
18 | list-style: none;
19 |
20 | & li {
21 | position: relative;
22 | break-inside: avoid;
23 | margin-bottom: .5em;
24 |
25 | & input[type=checkbox],
26 | & input[type=radio] {
27 | position: absolute;
28 | right: 100%;
29 | margin: .8em;
30 | }
31 | }
32 | }
33 |
34 | & .description {
35 | margin: 0;
36 | font-size: 75%;
37 | }
38 | }
39 |
40 | #spaces ul {
41 | columns: 20em 2;
42 | }
43 |
44 | a[download] {
45 | display: block;
46 | padding: .5em;
47 | border-radius: .2em;
48 | color: white;
49 | text-align: center;
50 | font-weight: 900;
51 | background: var(--rainbow);
52 | animation: var(--rainbow-scroll);
53 |
54 | &:hover {
55 | text-decoration: none;
56 | background: var(--color-green);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/apps/convert/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font: 120%/1.5 Helvetica Neue, Helvetica, Segoe UI, sans-serif;
3 | width: 90vw;
4 | max-width: fit-content;
5 | margin: 1em auto;
6 | padding: 0 2em;
7 | }
8 |
9 | input {
10 | font: inherit;
11 | font-size: 150%;
12 | width: 100%;
13 | padding: 0 .2em;
14 | box-sizing: border-box;
15 | }
16 |
17 | .inputs {
18 | display: grid;
19 | grid-template-columns: 1fr auto;
20 | grid-gap: 1em;
21 | margin: 3em 0;
22 | }
23 |
24 | td {
25 | padding: .3em .5em;
26 | font-family: Consolas, Monaco, monospace;
27 | }
28 |
29 | td:nth-child(3):hover,
30 | td:nth-child(4):hover {
31 | cursor: pointer;
32 | text-decoration: underline;
33 | }
34 |
35 | tr:nth-child(even) {
36 | background: rgba(0,0,0,.05);
37 | }
38 |
39 | #colorOutput {
40 | position: absolute;
41 | top: 0; left: 0; right: 0;
42 | width: 100%;
43 | height: 2em;
44 | --red-stripe-stops: transparent calc(50% - .05em), red 0 calc(50% + .05em), transparent 0;
45 | --error-background: linear-gradient(to bottom right, var(--red-stripe-stops)), linear-gradient(to top right, var(--red-stripe-stops)) gray;
46 | }
47 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // Import all modules of Color.js
2 | import Color, {util} from "./color.js";
3 |
4 | // Import color spaces
5 | import "./spaces/lab.js";
6 | import "./spaces/lch.js";
7 | import "./spaces/srgb.js";
8 | import "./spaces/srgb-linear.js";
9 | import "./spaces/hsl.js";
10 | import "./spaces/hwb.js";
11 | import "./spaces/hsv.js";
12 | import "./spaces/p3.js";
13 | import "./spaces/a98rgb.js";
14 | import "./spaces/prophoto.js";
15 | import "./spaces/rec2020.js";
16 | import "./spaces/absxyzd65.js";
17 | import "./spaces/jzazbz.js";
18 | import "./spaces/jzczhz.js";
19 | import "./spaces/ictcp.js";
20 | import "./spaces/rec2100.js";
21 | import "./spaces/oklab.js";
22 | import "./spaces/oklch.js";
23 | import "./spaces/acescc.js";
24 |
25 |
26 | // Import optional modules
27 | import "./interpolation.js";
28 | import "./deltaE/deltaECMC.js";
29 | import "./deltaE/deltaE2000.js";
30 | import "./deltaE/deltaEJz.js";
31 | import "./deltaE/deltaEITP.js";
32 | import "./deltaE/deltaEOK.js";
33 | import "./CATs.js";
34 | import "./keywords.js";
35 |
36 | // Re-export everything
37 | export default Color;
38 | export {util};
39 |
--------------------------------------------------------------------------------
/src/spaces/absxyzd65.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./../color.js";
2 |
3 | Color.defineSpace({
4 | // Absolute CIE XYZ, with a D65 whitepoint,
5 | // as used in most HDR colorspaces as a starting point.
6 | // SDR spaces are converted per BT.2048
7 | // so that diffuse, media white is 203 cd/m²
8 | id: "absxyzd65",
9 | name: "Absolute XYZ D65",
10 | coords: {
11 | Xa: [0, 9504.7],
12 | Ya: [0, 10000],
13 | Za: [0, 10888.3]
14 | },
15 | white: Color.whites.D65,
16 | Yw: 203, // absolute luminance of media white
17 | inGamut: coords => true,
18 | fromXYZ (XYZ) {
19 |
20 | const {Yw} = this;
21 |
22 | // Make XYZ absolute, not relative to media white
23 | // Maximum luminance in PQ is 10,000 cd/m²
24 | // Relative XYZ has Y=1 for media white
25 |
26 | return XYZ.map (function (val) {
27 | return Math.max(val * Yw, 0);
28 | });
29 | },
30 | toXYZ (AbsXYZ) {
31 |
32 | // Convert to media-white relative XYZ
33 |
34 | const {Yw} = this;
35 |
36 | let XYZ = AbsXYZ.map (function (val) {
37 | return Math.max(val / Yw, 0);
38 | });
39 |
40 | return XYZ;
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Lea Verou, Chris Lilley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/get/style.css:
--------------------------------------------------------------------------------
1 | [mv-app="colorBundler"] {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | grid-gap: .6em 1em
5 | }
6 |
7 | [mv-app="colorBundler"] #core,
8 | [mv-app="colorBundler"] #spaces,
9 | [mv-app="colorBundler"] #optional,
10 | [mv-app="colorBundler"] > pre,
11 | [mv-app="colorBundler"] > button,
12 | [mv-app="colorBundler"] a[download] {
13 | grid-column-end: span 2;
14 | }
15 |
16 | fieldset ul {
17 | list-style: none
18 | }
19 |
20 | fieldset ul li {
21 | position: relative;
22 | break-inside: avoid;
23 | margin-bottom: .5em
24 | }
25 |
26 | fieldset ul li input[type=checkbox],
27 | fieldset ul li input[type=radio] {
28 | position: absolute;
29 | right: 100%;
30 | margin: .8em;
31 | }
32 |
33 | fieldset .description {
34 | margin: 0;
35 | font-size: 75%;
36 | }
37 |
38 | #spaces ul {
39 | columns: 20em 2;
40 | }
41 |
42 | a[download] {
43 | display: block;
44 | padding: .5em;
45 | border-radius: .2em;
46 | color: white;
47 | text-align: center;
48 | font-weight: 900;
49 | background: var(--rainbow);
50 | animation: var(--rainbow-scroll)
51 | }
52 |
53 | a[download]:hover {
54 | text-decoration: none;
55 | background: var(--color-green);
56 | }
57 |
--------------------------------------------------------------------------------
/tests/hooks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Color modification tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
Color modification Tests
21 |
These tests modify one or more coordinates and check the result.
161 |
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/docs/gamut-mapping.md:
--------------------------------------------------------------------------------
1 | # Gamut mapping
2 |
3 | ## What is color gamut?
4 |
5 | [Color Gamut](https://en.wikipedia.org/wiki/Gamut) is the range of colors a given color space can produce.
6 | Some color spaces (e.g. Lab, LCH, Jazbz, CAM16) are mathematical models that encompass all visible color
7 | and thus, do not have a fixed gamut.
8 | Others however cannot produce all visible color without *values out of range*.
9 | For example. all the RGB spaces (sRGB, P3, Adobe® RGB, ProPhoto, REC.2020) have a gamut that is smaller than all visible color.
10 | Therefore, there are visible colors that cannot be represented by certain color spaces.
11 | For example, the P3 lime (`color(display-p3 0 1 0)`) is outside of the gamut of sRGB.
12 | In addition, colors that are **not visible to humans** can sometimes be represented by some color spaces!
13 | Most notably, two of ProPhoto's three primaries (pure green, pure blue) are **outside the gamut of human vision**!
14 |
15 | The process of transforming a color outside of a given gamut to a color that is as close as possible but is *inside gamut* is called *gamut mapping* and is the subject of [entire books](https://www.google.com/books/edition/Color_Gamut_Mapping/Yy0uK3pvfRMC?hl=en&gbpv=1&printsec=frontcover).
16 |
17 | ## So how does Color.js handle all this?
18 |
19 | **Color.js does not do gamut mapping by default**, as this is lossy: If you convert from a larger color space to a smaller one and then back, you need to be able to get your original color (possibly with some roundoff error due to the calculations).
20 |
21 | You can call `color.inGamut()` to check if the current color is in gamut of its own color space, or you can pass a different color space to check against:
22 |
23 | ```js
24 | let lime = new Color("p3", [0, 1, 0]);
25 | lime.inGamut();
26 | lime.inGamut("srgb");
27 | let sRGB_lime = lime.to("srgb");
28 | sRGB_lime.inGamut();
29 | ```
30 |
31 | Note that while the coordinates remain unchanged, the string representation of a Color object is, by default, _after_ gamut mapping, unless you explicitly turn that off:
32 |
33 | ```js
34 | let lime = new Color("p3", [0, 1, 0]).to("srgb");
35 | lime.coords;
36 | lime.toString();
37 | lime.toString({inGamut: false});
38 | ```
39 |
40 |
41 | If you want gamut mapped coordinates, you can use `color.toGamut()`, which by default returns a new color that is within gamut (if you want to mutate your own color instead you can use `{inPlace: true}`).
42 | You can also pass a different color space whose gamut you are mapping to via the `space` parameter.
43 |
44 | ```js
45 | let lime = new Color("p3", [0, 1, 0]);
46 | let sRGB_lime = lime.to("srgb");
47 | lime.toGamut({space: "srgb"});
48 | sRGB_lime.toGamut();
49 | ```
50 |
51 | Perhaps most important is the `method` parameter, which controls the algorithm used for gamut mapping.
52 | You can pass `"clip"` to use simple clipping (not recommended), or any coordinate of any imported color space, which will make Color.js reduce that coordinate until the color is in gamut.
53 |
54 | The default method is `"lch.chroma"` which means LCH hue and lightness remain constant while chroma is reduced until the color fits in gamut.
55 | Simply reducing chroma tends to produce good results for most colors, but most notably fails on yellows:
56 |
57 | 
58 |
59 | Here is P3 yellow, with LCH Chroma reduced to the neutral axis. The RGB values are linear-light P3. The color wedge shows sRGB values, if in gamut; salmon, if outside sRGB and red if outside P3. Notice the red curve goes up (so, out of gamut) before finally dropping again. The chroma of P3 yellow is 123, while the chroma of the gamut-mapped result is far to low, only 25!
60 |
61 | Instead, the default algorithm reduces chroma (by binary search) and also, at each stage, calculates the deltaE2000 between the current estimate and a channel-clipped version of that color. If the deltaE is less than 2, the clipped color is displayed. Notice the red curve hugs the top edge now because clipping to sRGB also means it is inside P3 gamut. Notice how we get an in-gamut color much earlier. This method produces an in-gamut color with chroma 103.
62 |
63 | 
64 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
160 |
--------------------------------------------------------------------------------
/src/spaces/srgb.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./../color.js";
2 |
3 | Color.defineSpace({
4 | id: "srgb",
5 | name: "sRGB",
6 | coords: {
7 | red: [0, 1],
8 | green: [0, 1],
9 | blue: [0, 1]
10 | },
11 | white: Color.whites.D65,
12 |
13 | // convert an array of sRGB values in the range 0.0 - 1.0
14 | // to linear light (un-companded) form.
15 | // https://en.wikipedia.org/wiki/SRGB
16 | toLinear(RGB) {
17 | return RGB.map(function (val) {
18 | let sign = val < 0? -1 : 1;
19 | let abs = Math.abs(val);
20 |
21 | if (abs < 0.04045) {
22 | return val / 12.92;
23 | }
24 |
25 | return sign * Math.pow((abs + 0.055) / 1.055, 2.4);
26 | });
27 | },
28 | // convert an array of linear-light sRGB values in the range 0.0-1.0
29 | // to gamma corrected form
30 | // https://en.wikipedia.org/wiki/SRGB
31 | toGamma(RGB) {
32 | return RGB.map(function (val) {
33 | let sign = val < 0? -1 : 1;
34 | let abs = Math.abs(val);
35 |
36 | if (abs > 0.0031308) {
37 | return sign * (1.055 * Math.pow(abs, 1/2.4) - 0.055);
38 | }
39 |
40 | return 12.92 * val;
41 | });
42 | },
43 |
44 | // This matrix was calculated directly from the RGB and white chromaticities
45 | // when rounded to 8 decimal places, it agrees completely with the official matrix
46 | // see https://github.com/w3c/csswg-drafts/issues/5922
47 | toXYZ_M: [
48 | [ 0.41239079926595934, 0.357584339383878, 0.1804807884018343 ],
49 | [ 0.21263900587151027, 0.715168678767756, 0.07219231536073371 ],
50 | [ 0.01933081871559182, 0.11919477979462598, 0.9505321522496607 ]
51 | ],
52 |
53 | // This matrix is the inverse of the above;
54 | // again it agrees with the official definiton when rounded to 8 decimal places
55 | fromXYZ_M: [
56 | [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ],
57 | [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ],
58 | [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ]
59 | ],
60 |
61 | // convert an array of sRGB values to CIE XYZ
62 | // using sRGB's own white, D65 (no chromatic adaptation)
63 | toXYZ(rgb) {
64 | rgb = this.toLinear(rgb);
65 |
66 | return util.multiplyMatrices(this.toXYZ_M, rgb);
67 | },
68 | fromXYZ(XYZ) {
69 | return this.toGamma(util.multiplyMatrices(this.fromXYZ_M, XYZ));
70 | },
71 | // Properties added to Color.prototype
72 | properties: {
73 | toHex({
74 | alpha = true, // include alpha in hex?
75 | collapse = true // collapse to 3-4 digit hex when possible?
76 | } = {}) {
77 | let coords = this.to("srgb", {inGamut: true}).coords;
78 |
79 | if (this.alpha < 1 && alpha) {
80 | coords.push(this.alpha);
81 | }
82 |
83 | coords = coords.map(c => Math.round(c * 255));
84 |
85 | let collapsible = collapse && coords.every(c => c % 17 === 0);
86 |
87 | let hex = coords.map(c => {
88 | if (collapsible) {
89 | return (c/17).toString(16);
90 | }
91 |
92 | return c.toString(16).padStart(2, "0");
93 | }).join("");
94 |
95 | return "#" + hex;
96 | },
97 |
98 | get hex() {
99 | return this.toHex();
100 | }
101 | },
102 | // Properties present only on sRGB colors
103 | instance: {
104 | toString ({inGamut = true, commas, format = "%", ...rest} = {}) {
105 | if (format === 255) {
106 | format = c => c * 255;
107 | }
108 | else if (format === "hex") {
109 | return this.toHex(arguments[0]);
110 | }
111 |
112 | return Color.prototype.toString.call(this, {
113 | inGamut, commas, format,
114 | name: "rgb" + (commas && this.alpha < 1? "a" : ""),
115 | ...rest
116 | });
117 | }
118 | },
119 |
120 | parseHex (str) {
121 | if (str.length <= 5) {
122 | // #rgb or #rgba, duplicate digits
123 | str = str.replace(/[a-f0-9]/gi, "$&$&");
124 | }
125 |
126 | let rgba = [];
127 | str.replace(/[a-f0-9]{2}/gi, component => {
128 | rgba.push(parseInt(component, 16) / 255);
129 | });
130 |
131 | return {
132 | spaceId: "srgb",
133 | coords: rgba.slice(0, 3),
134 | alpha: rgba.slice(3)[0]
135 | };
136 | }
137 | });
138 |
139 | Color.hooks.add("parse-start", env => {
140 | let str = env.str;
141 |
142 | if (/^#([a-f0-9]{3,4}){1,2}$/i.test(str)) {
143 | env.color = Color.spaces.srgb.parseHex(str);
144 | }
145 | });
146 |
147 | export default Color;
148 | export {util};
149 |
--------------------------------------------------------------------------------
/docs/the-color-object.md:
--------------------------------------------------------------------------------
1 | # The Color Object
2 |
3 | The first part to many Color.js operations is creating a Color object,
4 | which represents a specific color,
5 | in a particular colorspace,
6 | and has methods to convert the color to other spaces
7 | or to manipulate it..
8 | There are many ways to do so.
9 |
10 | ## Passing a CSS color string
11 |
12 | ```js
13 | let color = new Color("slategray");
14 | let color2 = new Color("hwb(60 30% 40% / .1)");
15 | let color3 = new Color("color(display-p3 0 1 0)");
16 | let color4 = new Color("lch(50% 80 30)");
17 | ```
18 |
19 | You can even use CSS variables, optionally with a DOM element against which they will be resolved (defaults to document root):
20 |
21 | ```js
22 | new Color("--color-blue");
23 | new Color("--color-green", document.querySelector("h1"));
24 | ```
25 |
26 | ## Color space and coordinates
27 |
28 | Internally, every Color object is stored this way, so this is the most low level way to create a Color object.
29 |
30 | ```js
31 | let lime = new Color("sRGB", [0, 1, 0], .5); // optional alpha
32 | let yellow = new Color("P3", [1, 1, 0]);
33 | new Color("lch", [50, 30, 180]);
34 |
35 | // Capitalization doesn't matter
36 | new Color("LCH", [50, 30, 180]);
37 |
38 | // Color space objects work too
39 | new Color(Color.spaces.lch, [50, 30, 180]);
40 | ```
41 |
42 | The exact ranges for these coordinates are up to the
43 | [color space](spaces.html) definition.
44 |
45 | You can also pass another color, or an object literal with `spaceId`/`space`, `coords`, and optionally `alpha` properties:
46 |
47 | ```js
48 | let red1 = new Color({space: "lab", coords: [50, 50, 50]});
49 | let red2 = new Color({spaceId: "lab", coords: [50, 50, 50]});
50 | let redClone = new Color(red1);
51 | ```
52 |
53 | ## Color object properties
54 |
55 | The three basic properties of a color object are its color space, its coordinates, and its alpha:
56 |
57 | ```js
58 | let color = new Color("deeppink");
59 | color.space; // Color space object
60 | color.space === Color.spaces.srgb;
61 | color.spaceId; // same as color.space.id
62 | color.coords;
63 | color.alpha;
64 | ```
65 |
66 | However, you can also use color space ids to get the color's coordinates in any other color space:
67 |
68 |
69 | ```js
70 | let color = new Color("deeppink");
71 | color.srgb;
72 | color.p3;
73 | color.lch;
74 | color.lab;
75 | color.prophoto;
76 | ```
77 |
78 | In fact, you can even manipulate the color this way!
79 |
80 |
81 | ```js
82 | let color = new Color("deeppink");
83 | color.lch[0] = 90;
84 | color;
85 | ```
86 |
87 | Named coordinates are also available:
88 |
89 | ```js
90 | let color = new Color("deeppink");
91 | color.srgb.green;
92 | color.srgb.green = .5;
93 | color;
94 | ```
95 |
96 | Note that unless we explicitly change a color's color space, it remains in the same color space it was when it was created.
97 | Manipulating coordinates of other color spaces do not change a color's space, it is just internally converted to another space and then back to its own.
98 | To convert a color to a different color space, you need to change its `space` or `spaceId` properties.
99 | Both accept either a color space object, or an id:
100 |
101 |
102 | ```js
103 | let color = new Color("srgb", [0, 1, 0]);
104 | color.space = "p3";
105 | color;
106 | color.space = Color.spaces.prophoto;
107 | color;
108 | ```
109 |
110 | Often, we want to keep our color intact,
111 | but also convert it to another color space,
112 | creating a new Color object.
113 | This is exactly what `color.to()` is for:
114 |
115 | ```js
116 | let color = new Color("srgb", [0, 1, 0]);
117 | let colorP3 = color.to("p3");
118 | color;
119 | colorP3;
120 | ```
121 |
122 | Sometimes, when converting to a color space with a smaller gamut, the resulting coordinates may be out of gamut.
123 | You can test for that with `color.inGamut()` and get gamut mapped coordinates with `color.toGamut()`:
124 |
125 |
126 | ```js
127 | let funkyLime = new Color("p3", [0, 1, 0]);
128 | let boringLime = funkyLime.to("srgb");
129 | boringLime.coords;
130 | boringLime.inGamut();
131 | boringLime.toGamut();
132 | ```
133 |
134 | Note that `color.toString()` returns gamut mapped coordinates by default.
135 | You can turn this off, via the `{inGamut: false}` option.
136 | You can read more about gamut mapping in the [Gamut mapping](manipulation.html#gamut-mapping) section.
137 |
--------------------------------------------------------------------------------
/src/CATs.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./color.js";
2 |
3 | Color.CATs = {};
4 |
5 | Color.hooks.add("chromatic-adaptation-start", env => {
6 | if (env.options.method) {
7 | env.M = Color.adapt(env.W1, env.W2, env.options.method);
8 | }
9 | });
10 |
11 | Color.hooks.add("chromatic-adaptation-end", env => {
12 | if (!env.M) {
13 | env.M = Color.adapt(env.W1, env.W2, env.options.method);
14 | }
15 | });
16 |
17 | Color.defineCAT = function ({id, toCone_M, fromCone_M}) {
18 | // Use id, toCone_M, fromCone_M like variables
19 | Color.CATs[id] = arguments[0];
20 | };
21 |
22 | Color.adapt = function (W1, W2, id = "Bradford") {
23 | // adapt from a source whitepoint or illuminant W1
24 | // to a destination whitepoint or illuminant W2,
25 | // using the given chromatic adaptation transform (CAT)
26 | // debugger;
27 | let method = Color.CATs[id];
28 |
29 | let [ρs, γs, βs] = util.multiplyMatrices(method.toCone_M, W1);
30 | let [ρd, γd, βd] = util.multiplyMatrices(method.toCone_M, W2);
31 |
32 | // all practical illuminants have non-zero XYZ so no division by zero can occur below
33 | let scale = [
34 | [ρd/ρs, 0, 0 ],
35 | [0, γd/γs, 0 ],
36 | [0, 0, βd/βs ]
37 | ];
38 | // console.log({scale});
39 |
40 | let scaled_cone_M = util.multiplyMatrices(scale, method.toCone_M);
41 | let adapt_M = util.multiplyMatrices(method.fromCone_M, scaled_cone_M);
42 | // console.log({scaled_cone_M, adapt_M});
43 | return adapt_M;
44 | };
45 |
46 | Color.defineCAT({
47 | id: "von Kries",
48 | toCone_M: [
49 | [ 0.4002400, 0.7076000, -0.0808100 ],
50 | [ -0.2263000, 1.1653200, 0.0457000 ],
51 | [ 0.0000000, 0.0000000, 0.9182200 ]
52 | ],
53 | fromCone_M: [
54 | [ 1.8599364, -1.1293816, 0.2198974 ],
55 | [ 0.3611914, 0.6388125, -0.0000064 ],
56 | [ 0.0000000, 0.0000000, 1.0890636 ]
57 | ]
58 | });
59 | Color.defineCAT({
60 | id: "Bradford",
61 | // Convert an array of XYZ values in the range 0.0 - 1.0
62 | // to cone fundamentals
63 | toCone_M: [
64 | [ 0.8951000, 0.2664000, -0.1614000 ],
65 | [ -0.7502000, 1.7135000, 0.0367000 ],
66 | [ 0.0389000, -0.0685000, 1.0296000 ]
67 | ],
68 | // and back
69 | fromCone_M: [
70 | [ 0.9869929, -0.1470543, 0.1599627 ],
71 | [ 0.4323053, 0.5183603, 0.0492912 ],
72 | [ -0.0085287, 0.0400428, 0.9684867 ]
73 | ]
74 | });
75 |
76 | Color.defineCAT({
77 | id: "CAT02",
78 | // with complete chromatic adaptation to W2, so D = 1.0
79 | toCone_M: [
80 | [ 0.7328000, 0.4296000, -0.1624000 ],
81 | [ -0.7036000, 1.6975000, 0.0061000 ],
82 | [ 0.0030000, 0.0136000, 0.9834000 ]
83 | ],
84 | fromCone_M: [
85 | [ 1.0961238, -0.2788690, 0.1827452 ],
86 | [ 0.4543690, 0.4735332, 0.0720978 ],
87 | [ -0.0096276, -0.0056980, 1.0153256 ]
88 | ]
89 | });
90 |
91 | Color.defineCAT({
92 | id: "CAT16",
93 | toCone_M: [
94 | [ 0.401288, 0.650173, -0.051461 ],
95 | [ -0.250268, 1.204414, 0.045854 ],
96 | [ -0.002079, 0.048952, 0.953127 ]
97 | ],
98 | // the extra precision is needed to avoid roundtripping errors
99 | fromCone_M: [
100 | [ 1.862067855087233e+0, -1.011254630531685e+0, 1.491867754444518e-1 ],
101 | [ 3.875265432361372e-1, 6.214474419314753e-1, -8.973985167612518e-3 ],
102 | [ -1.584149884933386e-2, -3.412293802851557e-2, 1.049964436877850e+0 ]
103 | ]
104 | });
105 |
106 | Object.assign(Color.whites, {
107 | // whitepoint values from ASTM E308-01 with 10nm spacing, 1931 2 degree observer
108 | // all normalized to Y (luminance) = 1.00000
109 | // Illuminant A is a tungsten electric light, giving a very warm, orange light.
110 | A: [1.09850, 1.00000, 0.35585],
111 |
112 | // Illuminant C was an early approximation to daylight: illuminant A with a blue filter.
113 | C: [0.98074, 1.000000, 1.18232],
114 |
115 | // The daylight series of illuminants simulate natural daylight.
116 | // The color temperature (in degrees Kelvin/100) ranges from
117 | // cool, overcast daylight (D50) to bright, direct sunlight (D65).
118 | D55: [0.95682, 1.00000, 0.92149],
119 | D75: [0.94972, 1.00000, 1.22638],
120 |
121 | // Equal-energy illuminant, used in two-stage CAT16
122 | E: [1.00000, 1.00000, 1.00000],
123 |
124 | // The F series of illuminants represent flourescent lights
125 | F2: [0.99186, 1.00000, 0.67393],
126 | F7: [0.95041, 1.00000, 1.08747],
127 | F11: [1.00962, 1.00000, 0.64350],
128 | });
129 |
--------------------------------------------------------------------------------
/tests/multiply-matrices.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Matrix multiplication Tests
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
86 |
87 |
88 |
89 |
90 |
Matrix multiplication Tests
91 |
92 |
93 |
Basic 3x3 and vectors
94 |
95 |
96 |
97 |
math.js
98 |
multiplyMatrices()
99 |
100 |
101 |
102 |
103 |
104 |
105 |
108 |
109 |
110 |
113 |
114 |
115 |
118 |
119 |
120 |
123 |
124 |
125 |
128 |
129 |
130 |
131 |
132 |
133 |
Incorrect data
134 |
135 |
These are expected to fail, as multiplyMatrices does not dimension checking.The point of them is to see how it fails.
136 |
137 |
138 |
139 |
140 |
math.js
141 |
multiplyMatrices()
142 |
143 |
144 |
145 |
148 |
149 |
150 |
153 |
154 |
155 |
158 |
159 |
160 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/src/spaces/jzazbz.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./../color.js";
2 |
3 | Color.defineSpace({
4 | id: "jzazbz",
5 | cssid: "Jzazbz",
6 | name: "Jzazbz",
7 | coords: {
8 | Jz: [0, 1],
9 | az: [-0.5, 0.5],
10 | bz: [-0.5, 0.5]
11 | },
12 | inGamut: coords => true,
13 | // Note that XYZ is relative to D65
14 | white: Color.whites.D65,
15 | b: 1.15,
16 | g: 0.66,
17 | n:2610 / (2 ** 14),
18 | ninv: (2 ** 14) / 2610,
19 | c1: 3424 / (2 ** 12),
20 | c2: 2413 / (2 ** 7),
21 | c3: 2392 / (2 ** 7),
22 | p: 1.7 * 2523 / (2 ** 5),
23 | pinv: (2 ** 5) / (1.7 * 2523),
24 | d: -0.56,
25 | d0: 1.6295499532821566E-11,
26 | XYZtoCone_M: [
27 | [ 0.41478972, 0.579999, 0.0146480 ],
28 | [ -0.2015100, 1.120649, 0.0531008 ],
29 | [ -0.0166008, 0.264800, 0.6684799 ]
30 | ],
31 | // XYZtoCone_M inverted
32 | ConetoXYZ_M: [
33 | [ 1.9242264357876067, -1.0047923125953657, 0.037651404030618 ],
34 | [ 0.35031676209499907, 0.7264811939316552, -0.06538442294808501 ],
35 | [ -0.09098281098284752, -0.3127282905230739, 1.5227665613052603 ]
36 | ],
37 | ConetoIab_M: [
38 | [ 0.5, 0.5, 0 ],
39 | [ 3.524000, -4.066708, 0.542708 ],
40 | [ 0.199076, 1.096799, -1.295875 ]
41 | ],
42 | // ConetoIab_M inverted
43 | IabtoCone_M: [
44 | [ 1, 0.1386050432715393, 0.05804731615611886 ],
45 | [ 0.9999999999999999, -0.1386050432715393, -0.05804731615611886 ],
46 | [ 0.9999999999999998, -0.09601924202631895, -0.8118918960560388 ]
47 | ],
48 | fromXYZ (XYZ) {
49 |
50 | const {b, g, n, p, c1, c2, c3, d, d0, XYZtoCone_M, ConetoIab_M} = this;
51 |
52 | // First make XYZ absolute, not relative to media white
53 | // Maximum luminance in PQ is 10,000 cd/m²
54 | // Relative XYZ has Y=1 for media white
55 | // BT.2048 says media white Y=203 at PQ 58
56 |
57 | // console.log({XYZ});
58 |
59 | let [ Xa, Ya, Za ] = Color.spaces.absxyzd65.fromXYZ(XYZ);
60 | // console.log({Xa, Ya, Za});
61 |
62 |
63 | // modify X and Y
64 | let Xm = (b * Xa) - ((b - 1) * Za);
65 | let Ym = (g * Ya) - ((g - 1) * Xa);
66 | // console.log({Xm, Ym, Za});
67 |
68 | // move to LMS cone domain
69 | let LMS = util.multiplyMatrices(XYZtoCone_M, [ Xm, Ym, Za ]);
70 | // console.log({LMS});
71 |
72 | // PQ-encode LMS
73 | let PQLMS = LMS.map (function (val) {
74 | let num = c1 + (c2 * ((val / 10000) ** n));
75 | let denom = 1 + (c3 * ((val / 10000) ** n));
76 | // console.log({val, num, denom});
77 | return (num / denom) ** p;
78 | });
79 | // console.log({PQLMS});
80 |
81 | // almost there, calculate Iz az bz
82 | let [ Iz, az, bz] = util.multiplyMatrices(ConetoIab_M, PQLMS);
83 | // console.log({Iz, az, bz});
84 |
85 | let Jz = ((1 + d) * Iz) / (1 + (d * Iz)) - d0;
86 | return [Jz, az, bz];
87 |
88 | },
89 | toXYZ(Jzazbz) {
90 |
91 | const {b, g, ninv, pinv, c1, c2, c3, d, d0, ConetoXYZ_M, IabtoCone_M} = this;
92 |
93 | let [Jz, az, bz] = Jzazbz;
94 | let Iz = (Jz + d0) / (1 + d - d * (Jz + d0));
95 | // console.log({Iz});
96 |
97 | // bring into LMS cone domain
98 | let PQLMS = util.multiplyMatrices(IabtoCone_M, [ Iz, az, bz ]);
99 | // console.log({PQLMS});
100 |
101 | // convert from PQ-coded to linear-light
102 | let LMS = PQLMS.map(function (val){
103 | let num = (c1 - (val ** pinv));
104 | let denom = (c3 * (val ** pinv)) - c2;
105 | let x = 10000 * ((num / denom) ** ninv);
106 | // console.log({x, num, denom})
107 | return (x); // luminance relative to diffuse white, [0, 70 or so].
108 | });
109 | // console.log({LMS});
110 |
111 | // modified abs XYZ
112 | let [ Xm, Ym, Za ] = util.multiplyMatrices(ConetoXYZ_M, LMS);
113 | // console.log({sXm, Ym, Za});
114 |
115 | // restore standard D50 relative XYZ, relative to media white
116 | let Xa = (Xm + ((b -1) * Za)) / b;
117 | let Ya = (Ym + ((g -1) * Xa)) / g;
118 | return Color.spaces.absxyzd65.toXYZ([ Xa, Ya, Za ]);
119 | },
120 | parse (str, parsed = Color.parseFunction(str)) {
121 | if (parsed && parsed.name === "jzabz") {
122 | return {
123 | spaceId: "jzazbz",
124 | coords: parsed.args.slice(0, 3),
125 | alpha: parsed.args.slice(3)[0]
126 | };
127 | }
128 | },
129 | instance: {
130 | toString ({format, ...rest} = {}) {
131 | return Color.prototype.toString.call(this, {name: "jzazbz", format, ...rest});
132 | }
133 | }
134 | });
135 |
136 | export default Color;
137 | export {util};
138 |
--------------------------------------------------------------------------------
/docs/interpolation.md:
--------------------------------------------------------------------------------
1 | # Interpolation
2 |
3 | ## Ranges
4 |
5 | `color.range()` and `Color.range()` are at the core of Color.js’ interpolation engine.
6 | They give you a function that accepts a number and returns a color.
7 | For numbers in the range 0 to 1, the function _interpolates_; for numbers outside that range, the function _extrapolates_ (and thus, may not return the results you expect):
8 |
9 | ```js
10 | let color = new Color("p3", [0, 1, 0]);
11 | let redgreen = color.range("red", {
12 | space: "lch", // interpolation space
13 | outputSpace: "srgb"
14 | });
15 | redgreen(.5); // midpoint
16 | ```
17 |
18 | Percentages (0 to 100%) should be converted to numbers (0 to 1).
19 |
20 | The `space` parameter controls the [color space](spaces.html)
21 | interpolation occurs in, and defaults to Lab.
22 | Colors do not need to be in that space, they will be converted for interpolation.
23 | The interpolation space can make a big difference in the result:
24 |
25 | ```js
26 | let c1 = new Color("rebeccapurple");
27 | let c2 = new Color("lch", [85, 100, 85]);
28 | c1.range(c2); // lab
29 | c1.range(c2, {space: "lch"});
30 | c1.range(c2, {space: "srgb"}); // gamma encoded sRGB
31 | c1.range(c2, {space: "srgb-linear"}); //linear-light sRGB
32 | c1.range(c2, {space: "xyz"}); // XYZ, same result as linear RGB
33 | c1.range(c2, {space: "hsl"});
34 | c1.range(c2, {space: "hwb"});
35 | ```
36 |
37 | Note that for color spaces with a hue angle there are multiple ways to interpolate, which can produce drastically different results.
38 | The `hue` argument is inspired by [the hue-adjuster in CSS Color 5](https://drafts.csswg.org/css-color-5/#hue-adjuster).
39 |
40 | ```js
41 | let c1 = new Color("rebeccapurple");
42 | c1.lch;
43 | let c2 = new Color("lch", [85, 85, 85 + 720]);
44 | c1.range(c2, {space: "lch", hue: "longer"});
45 | c1.range(c2, {space: "lch", hue: "shorter"});
46 | c1.range(c2, {space: "lch", hue: "increasing"});
47 | c1.range(c2, {space: "lch", hue: "decreasing"});
48 | c1.range(c2, {space: "lch", hue: "raw"});
49 | c1.range(c2, {space: "lch"}); // default is "shorter"
50 | ```
51 |
52 | Range interpolates between colors as they were at the time of its creation.
53 | If you change the colors afterwards, the range will not be affected:
54 |
55 | ```js
56 | let color = new Color("red");
57 | let color2 = new Color("black");
58 | let gradient = color.range(color2);
59 | color.coords[1] = 1;
60 | color2.coords[2] = 1;
61 | gradient(.5);
62 | gradient = color.range(color2);
63 | gradient(.5);
64 | ```
65 |
66 | Interpolating between a coordinate and `NaN` keeps that coordinate constant.
67 | This is useful for achromatic transitions:
68 |
69 | ```js
70 | let lime = new Color("p3", [0, 1, 0]);
71 | let white = new Color("lch", [100, 0, 0]);
72 | let white2 = new Color("lch", [100, 0, NaN]);
73 | let limewhite = lime.range(white, {space: "lch"});
74 | let limewhite2 = lime.range(white2, {space: "lch"});
75 |
76 | // Two kinds of fade out to transparent
77 | lime.range(new Color("transparent"));
78 | lime.range(new Color(lime.space, [NaN, NaN, NaN], 0), {space: lime.space});
79 | ```
80 |
81 | You can use the `progression` parameter to customize the progression and make it non-linear:
82 | ```js
83 | let r = new Color("lch(50 50 0)").range("lch(90 50 20)");
84 | Color.range(r, {progression: p => p ** 3});
85 | ```
86 |
87 | Note that you can use `Color.range(rangeFunction)` to modify a range after it has been created, as you can see in the example above.
88 | This produces a new range, and leaves the old one unaffected.
89 |
90 | ## Interpolation by discrete steps
91 |
92 | `color.steps()` and `Color.steps()` give you an array of discrete steps.
93 |
94 | ```js
95 | let color = new Color("p3", [0, 1, 0]);
96 | color.steps("red", {
97 | space: "lch",
98 | outputSpace: "srgb",
99 | maxDeltaE: 3, // max deltaE between consecutive steps (optional)
100 | steps: 10 // min number of steps
101 | });
102 | ```
103 |
104 | By default, the deltaE76 function is used.
105 |
106 | ## Mixing colors
107 |
108 | Interpolation can be used to create color mixtures,
109 | in any desired proportion,
110 | between two colors.
111 |
112 | Shortcut for specific points in the range:
113 | ```js
114 | let color = new Color("p3", [0, 1, 0]);
115 | let redgreen = color.mix("red", .5, {space: "lch", outputSpace: "srgb"});
116 | let reddishGreen = color.mix("red", .25, {space: "lch", outputSpace: "srgb"});
117 | ```
118 |
119 | Static syntax, for one-off mixing:
120 | ```js
121 | Color.mix("color(display-p3 0 1 0)", "red", .5);
122 | ```
123 |
--------------------------------------------------------------------------------
/api/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | API Documentation
7 |
8 |
9 | @@include('_head.html')
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 | @@include('_header.html', {
21 | "title": "API Documentation"
22 | })
23 |
24 |
25 |
26 |
27 |
28 |
To be able to use import statements in your JS, your <script> element needs type="module"
165 |
166 |
167 |
168 |
169 | @@include('_footer.html')
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/docs/output.md:
--------------------------------------------------------------------------------
1 | # Output
2 |
3 | Eventually, no matter what your calculations are, you will want to display some kind of output, most likely on a web page,
4 | frequently in CSS. This page is all about that.
5 |
6 | ## Getting a string representation of a color
7 |
8 | `color.toString()` is a swiss army knife for many of your serialization needs.
9 | In fact, you may have used it without knowing, since Javascript calls it implicitly with no params when you coerce an object to a string:
10 |
11 | ```js
12 | let magenta = new Color("srgb", [1, 0, .4]);
13 | "I love " + magenta;
14 | ```
15 |
16 | While this may suffice for some uses, in many cases you will want to provide parameters to customize the result.
17 | Here are a few examples.
18 |
19 | ### Disable gamut mapping
20 |
21 | ```js
22 | let funkyMagenta = new Color("p3", [1, 0, .4]);
23 | funkyMagenta = funkyMagenta.to("srgb");
24 | funkyMagenta.toString(); // gamut mapping by default
25 | funkyMagenta.toString({inGamut: false}); // disable gamut mapping
26 | ```
27 |
28 | Note that you cannot disable gamut mapping in `certain color spaces whose conversion math doesn't make sense for out of gamut values.
29 | These are typically the polar forms of gamma-corrected sRGB (HSL, HWB, HSV etc).
30 |
31 | ### Change precision
32 |
33 | By default, values are rounded to 5 significant digits.
34 | You can change that with the `precision` parameter:
35 |
36 | ```js
37 | let pink = new Color("lch", [70, 50, 350]);
38 | pink = pink.to("srgb");
39 | pink.toString();
40 | pink.toString({precision: 1});
41 | pink.toString({precision: 2});
42 | pink.toString({precision: 3});
43 | pink.toString({precision: 21});
44 | ```
45 |
46 | Tip: Building an app that needs high precision? You can change the default precision of 5 globally by setting `Color.defaults.precision`!
47 |
48 | ### Get a CSS color value that is actually supported by the current browser
49 |
50 | When using sRGB or HSL, you can just use the output of `color.toString()` directly in CSS.
51 | However, with many color spaces, and most browsers, this is not the case yet.
52 |
53 | One way to go about with this is to check if the value is supported and convert it if not:
54 |
55 | ```js
56 | let green = new Color("lch", [80, 80, 120]);
57 | let cssColor = green.toString();
58 | if (!CSS.supports("color", cssColor))
59 | cssColor = green.to("srgb").toString();
60 | ```
61 |
62 | This works fairly well, but the browser may support a wider gamut than sRGB, and it forces everything into sRGB.
63 | An iterative approach may be better:
64 |
65 | ```js
66 | let green = new Color("lch", [80, 80, 120]);
67 | let cssColor = green.toString();
68 | if (!CSS.supports("color", cssColor))
69 | cssColor = green.to("p3").toString();
70 | if (!CSS.supports("color", cssColor))
71 | cssColor = green.to("srgb").toString();
72 | cssColor;
73 | ```
74 | As of June 2020, `cssColor` will be sRGB in Chrome and Firefox, and P3 in Safari, providing access to 50% more colors than sRGB!
75 |
76 | So, this works, but the process is a little tedious. Thankfully, Color.js has got your back!
77 | Simply use the `fallback` parameter.
78 | If set to `true` it will use the default set of fallbacks (P3, then sRGB), but you can also provide your own.
79 | Let's rewrite the example above using the `fallback` parameter!
80 |
81 | ```js
82 | let green = new Color("lch", [80, 80, 120]);
83 | let cssColor = green.toString({fallback: true});
84 | let cssColor2 = green.toString({fallback: ["p3", "hsl"]});
85 | ```
86 |
87 | Tip: You can change the default set of fallbacks by setting `Color.defaults.fallbackSpaces`.
88 |
89 | What if you want access to the converted color? For example, you may want to indicate whether it was in gamut or not.
90 | You can access the `color` property on the returned value:
91 |
92 | ```js
93 | let green = new Color("lch", [80, 90, 120]);
94 | let cssColor = green.toString({fallback: true});
95 | cssColor.color.inGamut();
96 | ```
97 |
98 | Note: While `color.toString()` returns a primitive string in most cases, when `fallback` is used it returns a `String` object
99 | so that it can have a property (primitives in Javascript cannot have properties).
100 |
101 | ## Creating a CSS gradient from a range
102 |
103 | When working with [ranges](interpolation), you may often need to display the range as a CSS gradient.
104 | The trick here is to grab as many steps as you need via `color.steps()`, then use them as gradient color stops.
105 | If you don't know how many steps you need, this is what the `maxDeltaE` parameter is for, as it lets you specify the maximum allowed deltaE between consecutive colors.
106 |
107 |
108 |
109 | ```js
110 | let r = Color.range("hsl(330 90% 50%)", "hotpink");
111 | let stops = Color.steps(r, {steps: 5, maxDeltaE: 3});
112 | let element = document.querySelector("#test");
113 | element.style.background = `linear-gradient(to right, ${
114 | stops.join(", ")
115 | })`;
116 | ```
117 |
118 | Play with the parameters above to see what gradient is produced, or use the [gradients demo app](/apps/gradients)!
119 |
120 | Note that in the example above, `color.toString()` is called implicitly with no params due to `array.join()`.
121 | You can also map the colors to strings yourself:
122 |
123 |
124 |
125 | ```js
126 | let r = Color.range("rebeccapurple", "gold");
127 | let stops = Color.steps(r, {steps: 10});
128 | let element = document.querySelector("#test2");
129 | element.style.background = `linear-gradient(to right, ${
130 | stops.map(c => c.toString({precision: 2})).join(", ")
131 | })`;
132 | ```
133 |
--------------------------------------------------------------------------------
/src/interpolation.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./color.js";
2 | import * as angles from "./angles.js";
3 |
4 | let methods = {
5 | range (...args) {
6 | return Color.range(this, ...args);
7 | },
8 |
9 | /**
10 | * Return an intermediate color between two colors
11 | * Signatures: color.mix(color, p, options)
12 | * color.mix(color, options)
13 | * color.mix(color)
14 | */
15 | mix (color, p = .5, o = {}) {
16 | if (util.type(p) === "object") {
17 | [p, o] = [.5, p];
18 | }
19 |
20 | let {space, outputSpace} = o;
21 |
22 | color = Color.get(color);
23 | let range = this.range(color, {space, outputSpace});
24 | return range(p);
25 | },
26 |
27 | /**
28 | * Interpolate to color2 and return an array of colors
29 | * @returns {Array[Color]}
30 | */
31 | steps (...args) {
32 | return Color.steps(this, ...args);
33 | }
34 | };
35 |
36 | Color.steps = function(color1, color2, options = {}) {
37 | let range;
38 |
39 | if (isRange(color1)) {
40 | // Tweaking existing range
41 | [range, options] = [color1, color2];
42 | [color1, color2] = range.rangeArgs.colors;
43 | }
44 |
45 | let {
46 | maxDeltaE, deltaEMethod,
47 | steps = 2, maxSteps = 1000,
48 | ...rangeOptions
49 | } = options;
50 |
51 | if (!range) {
52 | color1 = Color.get(color1);
53 | color2 = Color.get(color2);
54 | range = Color.range(color1, color2, rangeOptions);
55 | }
56 |
57 | let totalDelta = this.deltaE(color2);
58 | let actualSteps = maxDeltaE > 0? Math.max(steps, Math.ceil(totalDelta / maxDeltaE) + 1) : steps;
59 | let ret = [];
60 |
61 | if (maxSteps !== undefined) {
62 | actualSteps = Math.min(actualSteps, maxSteps);
63 | }
64 |
65 | if (actualSteps === 1) {
66 | ret = [{p: .5, color: range(.5)}];
67 | }
68 | else {
69 | let step = 1 / (actualSteps - 1);
70 | ret = Array.from({length: actualSteps}, (_, i) => {
71 | let p = i * step;
72 | return {p, color: range(p)};
73 | });
74 | }
75 |
76 | if (maxDeltaE > 0) {
77 | // Iterate over all stops and find max deltaE
78 | let maxDelta = ret.reduce((acc, cur, i) => {
79 | if (i === 0) {
80 | return 0;
81 | }
82 |
83 | let deltaE = cur.color.deltaE(ret[i - 1].color, deltaEMethod);
84 | return Math.max(acc, deltaE);
85 | }, 0);
86 |
87 | while (maxDelta > maxDeltaE) {
88 | // Insert intermediate stops and measure maxDelta again
89 | // We need to do this for all pairs, otherwise the midpoint shifts
90 | maxDelta = 0;
91 |
92 | for (let i = 1; (i < ret.length) && (ret.length < maxSteps); i++) {
93 | let prev = ret[i - 1];
94 | let cur = ret[i];
95 |
96 | let p = (cur.p + prev.p) / 2;
97 | let color = range(p);
98 | maxDelta = Math.max(maxDelta, color.deltaE(prev.color), color.deltaE(cur.color));
99 | ret.splice(i, 0, {p, color: range(p)});
100 | i++;
101 | }
102 | }
103 | }
104 |
105 | ret = ret.map(a => a.color);
106 |
107 | return ret;
108 | };
109 |
110 | /**
111 | * Interpolate to color2 and return a function that takes a 0-1 percentage
112 | * @returns {Function}
113 | */
114 | Color.range = function(color1, color2, options = {}) {
115 | if (isRange(color1)) {
116 | // Tweaking existing range
117 | let [range, options] = [color1, color2];
118 | return Color.range(...range.rangeArgs.colors, {...range.rangeArgs.options, ...options});
119 | }
120 |
121 | let {space, outputSpace, progression, premultiplied} = options;
122 |
123 | // Make sure we're working on copies of these colors
124 | color1 = new Color(color1);
125 | color2 = new Color(color2);
126 |
127 |
128 | let rangeArgs = {colors: [color1, color2], options};
129 |
130 | if (space) {
131 | space = Color.space(space);
132 | }
133 | else {
134 | space = Color.spaces[Color.defaults.interpolationSpace] || color1.space;
135 | }
136 |
137 | outputSpace = outputSpace? Color.space(outputSpace) : (color1.space || space);
138 |
139 | color1 = color1.to(space).toGamut();
140 | color2 = color2.to(space).toGamut();
141 |
142 | // Handle hue interpolation
143 | // See https://github.com/w3c/csswg-drafts/issues/4735#issuecomment-635741840
144 | if (space.coords.hue && space.coords.hue.isAngle) {
145 | let arc = options.hue = options.hue || "shorter";
146 |
147 | [color1[space.id].hue, color2[space.id].hue] = angles.adjust(arc, [color1[space.id].hue, color2[space.id].hue]);
148 | }
149 |
150 | if (premultiplied) {
151 | // not coping with polar spaces yet
152 | color1.coords = color1.coords.map (c => c * color1.alpha);
153 | color2.coords = color2.coords.map (c => c * color2.alpha);
154 | }
155 |
156 | return Object.assign(p => {
157 | p = progression? progression(p) : p;
158 | let coords = color1.coords.map((start, i) => {
159 | let end = color2.coords[i];
160 | return interpolate(start, end, p);
161 | });
162 | let alpha = interpolate(color1.alpha, color2.alpha, p);
163 | let ret = new Color(space, coords, alpha);
164 |
165 | if (premultiplied) {
166 | // undo premultiplication
167 | ret.coords = ret.coords.map(c => c / alpha);
168 | }
169 |
170 | if (outputSpace !== space) {
171 | ret = ret.to(outputSpace);
172 | }
173 |
174 | return ret;
175 | }, {
176 | rangeArgs
177 | });
178 | };
179 |
180 | export function isRange (val) {
181 | return util.type(val) === "function" && val.rangeArgs;
182 | };
183 |
184 | // Helper
185 | function interpolate(start, end, p) {
186 | if (isNaN(start)) {
187 | return end;
188 | }
189 |
190 | if (isNaN(end)) {
191 | return start;
192 | }
193 |
194 | return start + (end - start) * p;
195 | }
196 |
197 | Object.assign(Color.defaults, {
198 | interpolationSpace: "lab"
199 | });
200 |
201 | Object.assign(Color.prototype, methods);
202 | Color.statify(Object.keys(methods));
203 |
204 | export default Color;
205 |
--------------------------------------------------------------------------------
/src/spaces/ictcp.js:
--------------------------------------------------------------------------------
1 | import Color, {util} from "./rec2020.js";
2 |
3 | const rec2020 = Color.spaces.rec2020;
4 |
5 | Color.defineSpace({
6 | // Only the PQ form of ICtCp is implemented here. There is also an HLG form.
7 | // from Dolby, "WHAT IS ICTCP?"
8 | // https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf
9 | // and
10 | // Dolby, "Perceptual Color Volume
11 | // Measuring the Distinguishable Colors of HDR and WCG Displays"
12 | // https://professional.dolby.com/siteassets/pdfs/dolby-vision-measuring-perceptual-color-volume-v7.1.pdf
13 | id: "ictcp",
14 | name: "ICTCP",
15 | // From BT.2100-2 page 7:
16 | // During production, signal values are expected to exceed the
17 | // range E′ = [0.0 : 1.0]. This provides processing headroom and avoids
18 | // signal degradation during cascaded processing. Such values of E′,
19 | // below 0.0 or exceeding 1.0, should not be clipped during production
20 | // and exchange.
21 | // Values below 0.0 should not be clipped in reference displays (even
22 | // though they represent “negative” light) to allow the black level of
23 | // the signal (LB) to be properly set using test signals known as “PLUGE”
24 | coords: {
25 | I: [0, 1], // Constant luminance
26 | CT: [-0.5, 0.5], // Full BT.2020 gamut in range [-0.5, 0.5]
27 | CP: [-0.5, 0.5]
28 | },
29 | inGamut: coords => true,
30 | // Note that XYZ is relative to D65
31 | white: Color.whites.D65,
32 | c1: 3424 / 4096,
33 | c2: 2413 / 128,
34 | c3: 2392 / 128,
35 | m1: 2610 / 16384,
36 | m2: 2523 / 32,
37 | im1: 16384 / 2610,
38 | im2: 32 / 2523,
39 | // The matrix below includes the 4% crosstalk components
40 | // and is from the Dolby "What is ICtCp" paper"
41 | XYZtoLMS_M: [
42 | [ 0.3592, 0.6976, -0.0358],
43 | [-0.1922, 1.1004, 0.0755],
44 | [ 0.0070, 0.0749, 0.8434]
45 | ],
46 | // linear-light Rec.2020 to LMS, again with crosstalk
47 | // rational terms from Jan Fröhlich,
48 | // Encoding High Dynamic Range andWide Color Gamut Imagery, p.97
49 | // and ITU-R BT.2124-0 p.2
50 | Rec2020toLMS_M: [
51 | [ 1688 / 4096, 2146 / 4096, 262 / 4096 ],
52 | [ 683 / 4096, 2951 / 4096, 462 / 4096 ],
53 | [ 99 / 4096, 309 / 4096, 3688 / 4096 ]
54 | ],
55 | // this includes the Ebner LMS coefficients,
56 | // the rotation, and the scaling to [-0.5,0.5] range
57 | // rational terms from Fröhlich p.97
58 | // and ITU-R BT.2124-0 pp.2-3
59 | LMStoIPT_M: [
60 | [ 2048 / 4096, 2048 / 4096, 0 ],
61 | [ 6610 / 4096, -13613 / 4096, 7003 / 4096 ],
62 | [ 17933 / 4096, -17390 / 4096, -543 / 4096 ]
63 | ],
64 | // inverted matrices, calculated from the above
65 | IPTtoLMS_M: [
66 | [0.99998889656284013833, 0.00860505014728705821, 0.1110343715986164786 ],
67 | [1.0000111034371598616, -0.00860505014728705821, -0.1110343715986164786 ],
68 | [1.000032063391005412, 0.56004913547279000113, -0.32063391005412026469],
69 | ],
70 | LMStoRec2020_M: [
71 | [ 3.4375568932814012112, -2.5072112125095058195, 0.069654319228104608382],
72 | [-0.79142868665644156125, 1.9838372198740089874, -0.19240853321756742626 ],
73 | [-0.025646662911506476363, -0.099240248643945566751, 1.1248869115554520431 ]
74 | ],
75 | LMStoXYZ_M: [
76 | [ 2.0701800566956135096, -1.3264568761030210255, 0.20661600684785517081 ],
77 | [ 0.36498825003265747974, 0.68046736285223514102, -0.045421753075853231409],
78 | [-0.049595542238932107896, -0.049421161186757487412, 1.1879959417328034394 ]
79 | ],
80 | fromXYZ (XYZ) {
81 |
82 | const {XYZtoLMS_M} = this;
83 | // console.log ({c1, c2, c3, m1, m2});
84 |
85 | // Make XYZ absolute, not relative to media white
86 | // Maximum luminance in PQ is 10,000 cd/m²
87 | // Relative XYZ has Y=1 for media white
88 | // BT.2048 says media white Y=203 at PQ 58
89 | // This also does the D50 to D65 adaptation
90 |
91 | let [ Xa, Ya, Za ] = Color.spaces.absxyzd65.fromXYZ(XYZ);
92 | // console.log({Xa, Ya, Za});
93 |
94 | // move to LMS cone domain
95 | let LMS = util.multiplyMatrices(XYZtoLMS_M, [ Xa, Ya, Za ]);
96 | // console.log({LMS});
97 |
98 | return this.LMStoICtCp(LMS);
99 | },
100 | toXYZ (ICtCp) {
101 |
102 | const {LMStoXYZ_M} = this;
103 |
104 | let LMS = this.ICtCptoLMS(ICtCp);
105 |
106 | let XYZa = util.multiplyMatrices(LMStoXYZ_M, LMS);
107 |
108 | // convert from Absolute, D65 XYZ to media white relative, D50 XYZ
109 | return Color.spaces.absxyzd65.toXYZ(XYZa);
110 |
111 | },
112 | LMStoICtCp (LMS) {
113 |
114 | const {LMStoIPT_M, c1, c2, c3, m1, m2} = this;
115 | // console.log ({c1, c2, c3, m1, m2});
116 |
117 | // apply the PQ EOTF
118 | // we can't ever be dividing by zero because of the "1 +" in the denominator
119 | let PQLMS = LMS.map (function (val) {
120 | let num = c1 + (c2 * ((val / 10000) ** m1));
121 | let denom = 1 + (c3 * ((val / 10000) ** m1));
122 | // console.log({val, num, denom});
123 | return (num / denom) ** m2;
124 | });
125 | // console.log({PQLMS});
126 |
127 | // LMS to IPT, with rotation for Y'C'bC'r compatibility
128 | return util.multiplyMatrices(LMStoIPT_M, PQLMS);
129 | },
130 | ICtCptoLMS (ICtCp) {
131 |
132 | const {IPTtoLMS_M, c1, c2, c3, im1, im2} = this;
133 |
134 | let PQLMS = util.multiplyMatrices(IPTtoLMS_M, ICtCp);
135 |
136 | // From BT.2124-0 Annex 2 Conversion 3
137 | let LMS = PQLMS.map (function (val) {
138 | let num = Math.max((val ** im2) - c1, 0);
139 | let denom = (c2 - (c3 * (val ** im2)));
140 | return 10000 * ((num / denom) ** im1);
141 | });
142 |
143 | return LMS;
144 | }
145 | // },
146 | // from: {
147 | // rec2020: function() {
148 |
149 | // }
150 | // },
151 | // to: {
152 | // rec2020: function() {
153 |
154 | // }
155 | // }
156 | });
157 |
158 | export default Color;
159 |
--------------------------------------------------------------------------------
/docs/color-difference.md:
--------------------------------------------------------------------------------
1 | # Color differences
2 |
3 | ## Euclidean distance
4 |
5 | We often need to determine the distance between two colors, for a variety of use cases.
6 | Before most people dive into color science, when they are only familiar with sRGB colors,
7 | their first attempt to do so usually is the Euclidean distance of colors in sRGB,
8 | like so: `sqrt((r₁ - r₂)² + (g₁ - g₂)² + (b₁ - b₂)²)`.
9 | However, since sRGB is not [perceptually uniform](https://programmingdesignsystems.com/color/perceptually-uniform-color-spaces/),
10 | pairs of colors with the same Euclidean distance can have hugely different perceptual differences:
11 |
12 |
13 |
14 |
15 |
16 |
17 | ```js
18 | let color1 = new Color("hsl(30, 100%, 50%)");
19 | let color2 = new Color("hsl(50, 100%, 50%)");
20 | let color3 = new Color("hsl(230, 100%, 50%)");
21 | let color4 = new Color("hsl(260, 100%, 50%)");
22 | color1.distance(color2, "srgb");
23 | color3.distance(color4, "srgb");
24 | ```
25 |
26 | Notice that even though `color3` and `color4` are far closer than `color1` and `color2`, their sRGB Euclidean distance is slightly larger!
27 |
28 | Euclidean distance *can* be very useful in calculating color difference, as long as the measurement is done in a perceptually uniform color space, such as Lab, ICtCp or Jzazbz:
29 |
30 | ```js
31 | let color1 = new Color("hsl(30, 100%, 50%)");
32 | let color2 = new Color("hsl(50, 100%, 50%)");
33 | let color3 = new Color("hsl(230, 100%, 50%)");
34 | let color4 = new Color("hsl(260, 100%, 50%)");
35 | color1.distance(color2, "lab");
36 | color3.distance(color4, "lab");
37 |
38 | color1.distance(color2, "jzazbz");
39 | color3.distance(color4, "jzazbz");
40 | ```
41 |
42 | ## Delta E (ΔE)
43 |
44 | DeltaE (ΔE) is a family of algorithms specifically for calculating the difference (delta) between two colors.
45 | The very first version of DeltaE, [DeltaE 1976](https://en.wikipedia.org/wiki/Color_difference#CIE76) was simply the Euclidean distance of the colors in Lab:
46 |
47 | ```js
48 | let color1 = new Color("hsl(30, 100%, 50%)");
49 | let color2 = new Color("hsl(50, 100%, 50%)");
50 | let color3 = new Color("hsl(230, 100%, 50%)");
51 | let color4 = new Color("hsl(260, 100%, 50%)");
52 |
53 | color1.deltaE76(color2);
54 | color3.deltaE76(color4);
55 | ```
56 |
57 | However, because Lab turned out to not be as perceptually uniform as it was once thought, the algorithm was revised in 1984 (CMC), 1994, and lastly, 2000, with the most accurate and most complicated Lab-based DeltaE algorithm to date.
58 |
59 | Instead of handling the remaining perceptual non-uniformities of Lab in the color difference equation,
60 | another option is to use a better color model and perform a simpler color difference calculation in that space.
61 |
62 | Examples include DeltaEJz (which uses JzCzhz) and deltaEITP (which uses ICtCp). An additional benefit of these two color difference formulae is that, unlike Lab which is mostly tested with medium to low chroma, reflective surface colors, JzCzhz and ICtCp are designed to be used with light-emitting devices (screens), high chroma colors often found in Wide Gamut content, and a much larger range of luminances as found in High Dynamic Range content.
63 |
64 | Color.js supports all the DeltaE algorithms mentioned above except DeltaE 94. Each DeltaE algorithm comes with its own method (e.g. `color1.deltaECMC(color2)`),
65 | as well as a parameterized syntax (e.g. `color1.deltaE(color2, "CMC")`) which falls back to DeltaE 76 when the requested algorithm is not available, or the second argument is missing.
66 |
67 | Note: If you are not using the Color.js bundle that includes everything, you will need to import the modules for DeltaE CMC, DeltaE 2000, DeltaEJz and DeltaEITP manually. DeltaE 76 is supported in the core Color.js.
68 |
69 | ```js
70 | // These are not needed if you're just using the bundle
71 | // and will be omitted in other code examples
72 | import "https://colorjs.io/src/deltaE/deltaECMC.js";
73 | import "https://colorjs.io/src/deltaE/deltaE2000.js";
74 | import "https://colorjs.io/src/deltaE/deltaEITP.js";
75 |
76 | let color1 = new Color("blue");
77 | let color2 = new Color("lab", [30, 30, 30]);
78 | let color3 = new Color("lch", [40, 50, 60]);
79 |
80 | color1.deltaE(color2, "76");
81 | color2.deltaE(color3, "76");
82 |
83 | color1.deltaE(color2, "CMC");
84 | color2.deltaE(color3, "CMC");
85 |
86 | color1.deltaE(color2, "2000");
87 | color2.deltaE(color3, "2000");
88 |
89 | color1.deltaE(color2, "ITP");
90 | color2.deltaE(color3, "ITP");
91 | ```
92 |
93 | For most DeltaE algorithms, 2.3 is considered the "Just Noticeable Difference" (JND).
94 | Can you notice a difference in the two colors below?
95 |
96 | ```js
97 | let color1 = new Color("lch", [40, 50, 60]);
98 | let color2 = new Color("lch", [40, 50, 60]);
99 |
100 | color1.deltaE(color2, "76");
101 | color1.deltaE(color2, "CMC");
102 | color1.deltaE(color2, "2000");
103 | ```
104 |
105 | ## Setting the default DeltaE algorithm
106 |
107 | Notice that even if you include better DeltaE algorithms such as ΔΕ2000,
108 | the default DeltaE algorithm used in every function that accepts a deltaE argument (e.g `Color#steps()`) or `color.deltaE()` with no method parameter will remain DeltaE 1976.
109 | This is because Color.js doesn't necessarily know which DeltaE method is better for your use case.
110 | E.g. for high performance code, you may prefer the speed over accuracy tradeoff of DeltaE 1976.
111 | You can however change this default:
112 |
113 | ```js
114 | let color1 = new Color("blue");
115 | let color2 = new Color("lch", [20, 50, 230]);
116 | color1.deltaE(color2);
117 | Color.defaults.deltaE = "2000";
118 | color1.deltaE(color2);
119 | ```
120 |
--------------------------------------------------------------------------------
/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Color.js
6 | @@include('_head.html')
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
Color.js
21 |
Let's get serious about color
22 |
23 |
24 |
25 |
26 |
Fully color space aware
27 |
Each color belongs to a color space; operations are color space agnostic.
28 | Modules for a wide variety of color spaces, including Lab/LCH, sRGB and friends (HSL/HSV/HWB), Display P3, Jzazbz, REC.2100 and more.
29 |
30 |
31 |
Doesn't gloss over color science
32 |
Actual gamut mapping instead of naïve clipping,
33 | multiple DeltaE methods (76, CMC, 2000, Jz),
34 | multiple chromatic adaptation methods (von Kries, Bradford, CAT02, CAT16),
35 | all with sensible defaults
36 |
37 |
38 |
39 |
Up to date with CSS Color 4
40 |
Every CSS Color 4 format & color space supported for both input and output, whether your browser supports it or not.
41 |
42 |
43 |
Readable, object-oriented API
44 |
Color objects for multiple operations on the same color, and static Color.something() functions for one-off calculations
45 |
46 |
47 |
Modular & Extensible
48 |
Use only what you need, or a bundle. Client-side or Node. Deep extensibility with hooks.
49 |
50 |
51 |
52 |
53 |
54 |
55 | Color.js is currently an unreleased work in progress. Here be dragons.
56 | If you found this website somehow, feel free to try it out and give us feedback, but sshhhh! 🤫
57 | There are more bugs in the live code snippets ("Color Notebook") in the docs than the actual Color.js library, so before reporting a bug, please try to reproduce it outside Color Notebook.
58 |
59 |
60 |
61 |
Reading colors
62 |
63 |
Any color from CSS Color Level 4 should work:
64 |
65 | let color = new Color("slategray");
66 | let color2 = new Color("hwb(60 30% 40%)");
67 | let color3 = new Color("color(display-p3 0 1 0)");
68 | let color4 = new Color("lch(50% 80 30)");
69 |
70 | // CSS variables work too!
71 | let colorjsBlue = new Color("--color-blue");
72 |
You can use properties to modify coordinates
81 | of any color space and convert back
82 |
83 | let color = new Color("slategray");
84 | color.lightness = 80; // LCH coords available directly on colors
85 | color.chroma *= 1.2; // saturate 20%
86 | color.hwb.whiteness += 10; // any other color space also available
87 |
88 |
89 |
Chaining-style modifications are also supported:
90 |
91 | let color = new Color("lch(50% 50 10)");
92 | color = color.set({
93 | hue: h => h + 180,
94 | chroma: 60
95 | }).lighten();
96 |