@luntta/swatch 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/CONTRIBUTING.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +189 -0
- package/README.md +463 -0
- package/package.json +57 -0
- package/scripts/pack-check.mjs +18 -0
- package/src/bootstrap.js +159 -0
- package/src/core/registry.js +81 -0
- package/src/core/state.js +36 -0
- package/src/core/swatch-class.js +524 -0
- package/src/data/cvd-matrices.js +179 -0
- package/src/data/named-colors.js +157 -0
- package/src/format/css.js +256 -0
- package/src/operations/accessibility.js +103 -0
- package/src/operations/apca.js +80 -0
- package/src/operations/blend.js +72 -0
- package/src/operations/channels.js +123 -0
- package/src/operations/cvd.js +119 -0
- package/src/operations/deltaE.js +207 -0
- package/src/operations/gamut.js +206 -0
- package/src/operations/image.js +192 -0
- package/src/operations/manipulation.js +100 -0
- package/src/operations/mix.js +129 -0
- package/src/operations/naming.js +158 -0
- package/src/operations/palette.js +133 -0
- package/src/operations/random.js +75 -0
- package/src/operations/temperature.js +126 -0
- package/src/operations/tint-shade.js +42 -0
- package/src/palettes/colorbrewer.js +232 -0
- package/src/palettes/index.js +58 -0
- package/src/palettes/viridis.js +59 -0
- package/src/parse/css.js +241 -0
- package/src/parse/hex.js +38 -0
- package/src/parse/index.js +43 -0
- package/src/parse/legacy.js +88 -0
- package/src/parse/named.js +11 -0
- package/src/parse/objects.js +125 -0
- package/src/scale/index.js +382 -0
- package/src/scale/interpolators.js +83 -0
- package/src/spaces/a98.js +55 -0
- package/src/spaces/cmyk.js +75 -0
- package/src/spaces/display-p3.js +50 -0
- package/src/spaces/hsl.js +93 -0
- package/src/spaces/hsluv.js +211 -0
- package/src/spaces/hsv.js +78 -0
- package/src/spaces/hwb.js +48 -0
- package/src/spaces/lab.js +70 -0
- package/src/spaces/lch.js +65 -0
- package/src/spaces/oklab.js +79 -0
- package/src/spaces/oklch.js +53 -0
- package/src/spaces/prophoto.js +72 -0
- package/src/spaces/rec2020.js +65 -0
- package/src/spaces/srgb.js +85 -0
- package/src/spaces/xyz.js +71 -0
- package/src/swatch.js +57 -0
- package/src/util/math.js +53 -0
- package/src/util/matrix.js +92 -0
- package/src/util/suggest.js +66 -0
- package/types/swatch.d.ts +664 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// CIE LCh — polar form of CIE Lab.
|
|
2
|
+
//
|
|
3
|
+
// Two variants are registered matching the Lab variants:
|
|
4
|
+
// lch (D65, our default)
|
|
5
|
+
// lch-d50 (CSS Color 4 variant)
|
|
6
|
+
|
|
7
|
+
import { registerSpace, convert } from "../core/registry.js";
|
|
8
|
+
import { labToLchPolar, lchToLabRect } from "../util/math.js";
|
|
9
|
+
|
|
10
|
+
function lchToXyz(coords, labSpaceId) {
|
|
11
|
+
const lab = lchToLabRect({ l: coords[0], c: coords[1], h: coords[2] });
|
|
12
|
+
return convert([lab.l, lab.a, lab.b], labSpaceId, "xyz");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function xyzToLch(xyz, labSpaceId) {
|
|
16
|
+
const [l, a, b] = convert(xyz, "xyz", labSpaceId);
|
|
17
|
+
const lch = labToLchPolar({ l, a, b });
|
|
18
|
+
return [lch.l, lch.c, lch.h];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
registerSpace({
|
|
22
|
+
id: "lch",
|
|
23
|
+
channels: ["l", "c", "h"],
|
|
24
|
+
ranges: [
|
|
25
|
+
[0, 100],
|
|
26
|
+
[0, 150],
|
|
27
|
+
[0, 360]
|
|
28
|
+
],
|
|
29
|
+
white: "D65",
|
|
30
|
+
toXYZ: (coords) => lchToXyz(coords, "lab"),
|
|
31
|
+
fromXYZ: (xyz) => xyzToLch(xyz, "lab"),
|
|
32
|
+
shortcuts: {
|
|
33
|
+
lab: (coords) => {
|
|
34
|
+
const { l, a, b } = lchToLabRect({
|
|
35
|
+
l: coords[0],
|
|
36
|
+
c: coords[1],
|
|
37
|
+
h: coords[2]
|
|
38
|
+
});
|
|
39
|
+
return [l, a, b];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
registerSpace({
|
|
45
|
+
id: "lch-d50",
|
|
46
|
+
channels: ["l", "c", "h"],
|
|
47
|
+
ranges: [
|
|
48
|
+
[0, 100],
|
|
49
|
+
[0, 150],
|
|
50
|
+
[0, 360]
|
|
51
|
+
],
|
|
52
|
+
white: "D50",
|
|
53
|
+
toXYZ: (coords) => lchToXyz(coords, "lab-d50"),
|
|
54
|
+
fromXYZ: (xyz) => xyzToLch(xyz, "lab-d50"),
|
|
55
|
+
shortcuts: {
|
|
56
|
+
"lab-d50": (coords) => {
|
|
57
|
+
const { l, a, b } = lchToLabRect({
|
|
58
|
+
l: coords[0],
|
|
59
|
+
c: coords[1],
|
|
60
|
+
h: coords[2]
|
|
61
|
+
});
|
|
62
|
+
return [l, a, b];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// OKLab — Björn Ottosson (2020), "A perceptual color space for image processing".
|
|
2
|
+
//
|
|
3
|
+
// L is on 0..1 (roughly maps to L* 0..100 perceptually). a and b are signed,
|
|
4
|
+
// typically in [-0.4, 0.4].
|
|
5
|
+
//
|
|
6
|
+
// The transform is defined from linear sRGB directly, so we register a
|
|
7
|
+
// direct shortcut `srgb-linear ↔ oklab` that bypasses the XYZ hub for
|
|
8
|
+
// accuracy and speed. v2's `_linearRGBToOklab` (src/swatch.js:1495-1516)
|
|
9
|
+
// and `_oklabToRGB` (src/swatch.js:1131-1150) supply the exact same
|
|
10
|
+
// coefficients.
|
|
11
|
+
|
|
12
|
+
import { registerSpace, getSpace } from "../core/registry.js";
|
|
13
|
+
|
|
14
|
+
// Linear sRGB → OKLab.
|
|
15
|
+
export function linearSrgbToOklab(lin) {
|
|
16
|
+
const l =
|
|
17
|
+
0.4122214708 * lin[0] +
|
|
18
|
+
0.5363325363 * lin[1] +
|
|
19
|
+
0.0514459929 * lin[2];
|
|
20
|
+
const m =
|
|
21
|
+
0.2119034982 * lin[0] +
|
|
22
|
+
0.6806995451 * lin[1] +
|
|
23
|
+
0.1073969566 * lin[2];
|
|
24
|
+
const s =
|
|
25
|
+
0.0883024619 * lin[0] +
|
|
26
|
+
0.2817188376 * lin[1] +
|
|
27
|
+
0.6299787005 * lin[2];
|
|
28
|
+
const lp = Math.cbrt(l);
|
|
29
|
+
const mp = Math.cbrt(m);
|
|
30
|
+
const sp = Math.cbrt(s);
|
|
31
|
+
return [
|
|
32
|
+
0.2104542553 * lp + 0.793617785 * mp - 0.0040720468 * sp,
|
|
33
|
+
1.9779984951 * lp - 2.428592205 * mp + 0.4505937099 * sp,
|
|
34
|
+
0.0259040371 * lp + 0.7827717662 * mp - 0.808675766 * sp
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// OKLab → linear sRGB. Inverse of the transform above.
|
|
39
|
+
export function oklabToLinearSrgb(ok) {
|
|
40
|
+
const lp = ok[0] + 0.3963377774 * ok[1] + 0.2158037573 * ok[2];
|
|
41
|
+
const mp = ok[0] - 0.1055613458 * ok[1] - 0.0638541728 * ok[2];
|
|
42
|
+
const sp = ok[0] - 0.0894841775 * ok[1] - 1.291485548 * ok[2];
|
|
43
|
+
const l = lp * lp * lp;
|
|
44
|
+
const m = mp * mp * mp;
|
|
45
|
+
const s = sp * sp * sp;
|
|
46
|
+
return [
|
|
47
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
48
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
49
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerSpace({
|
|
54
|
+
id: "oklab",
|
|
55
|
+
channels: ["l", "a", "b"],
|
|
56
|
+
ranges: [
|
|
57
|
+
[0, 1],
|
|
58
|
+
[-0.4, 0.4],
|
|
59
|
+
[-0.4, 0.4]
|
|
60
|
+
],
|
|
61
|
+
white: "D65",
|
|
62
|
+
toXYZ: (coords) => {
|
|
63
|
+
const lin = oklabToLinearSrgb(coords);
|
|
64
|
+
return getSpace("srgb-linear").toXYZ(lin);
|
|
65
|
+
},
|
|
66
|
+
fromXYZ: (xyz) => {
|
|
67
|
+
const lin = getSpace("srgb-linear").fromXYZ(xyz);
|
|
68
|
+
return linearSrgbToOklab(lin);
|
|
69
|
+
},
|
|
70
|
+
shortcuts: {
|
|
71
|
+
"srgb-linear": (coords) => oklabToLinearSrgb(coords)
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Add the reverse shortcut on srgb-linear. We can't do this at the
|
|
76
|
+
// srgb-linear registration site without a circular dependency, so we
|
|
77
|
+
// patch it here after both spaces exist.
|
|
78
|
+
getSpace("srgb-linear").shortcuts.oklab = (coords) =>
|
|
79
|
+
linearSrgbToOklab(coords);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// OKLCh — polar form of OKLab. Direct shortcut converters to/from OKLab
|
|
2
|
+
// avoid a round-trip through XYZ or linear sRGB.
|
|
3
|
+
|
|
4
|
+
import { registerSpace } from "../core/registry.js";
|
|
5
|
+
import { labToLchPolar, lchToLabRect } from "../util/math.js";
|
|
6
|
+
import { linearSrgbToOklab, oklabToLinearSrgb } from "./oklab.js";
|
|
7
|
+
import { getSpace } from "../core/registry.js";
|
|
8
|
+
|
|
9
|
+
registerSpace({
|
|
10
|
+
id: "oklch",
|
|
11
|
+
channels: ["l", "c", "h"],
|
|
12
|
+
ranges: [
|
|
13
|
+
[0, 1],
|
|
14
|
+
[0, 0.4],
|
|
15
|
+
[0, 360]
|
|
16
|
+
],
|
|
17
|
+
white: "D65",
|
|
18
|
+
toXYZ: (coords) => {
|
|
19
|
+
const { l, a, b } = lchToLabRect({
|
|
20
|
+
l: coords[0],
|
|
21
|
+
c: coords[1],
|
|
22
|
+
h: coords[2]
|
|
23
|
+
});
|
|
24
|
+
const lin = oklabToLinearSrgb([l, a, b]);
|
|
25
|
+
return getSpace("srgb-linear").toXYZ(lin);
|
|
26
|
+
},
|
|
27
|
+
fromXYZ: (xyz) => {
|
|
28
|
+
const lin = getSpace("srgb-linear").fromXYZ(xyz);
|
|
29
|
+
const ok = linearSrgbToOklab(lin);
|
|
30
|
+
const { l, c, h } = labToLchPolar({ l: ok[0], a: ok[1], b: ok[2] });
|
|
31
|
+
return [l, c, h];
|
|
32
|
+
},
|
|
33
|
+
shortcuts: {
|
|
34
|
+
oklab: (coords) => {
|
|
35
|
+
const { l, a, b } = lchToLabRect({
|
|
36
|
+
l: coords[0],
|
|
37
|
+
c: coords[1],
|
|
38
|
+
h: coords[2]
|
|
39
|
+
});
|
|
40
|
+
return [l, a, b];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Reverse shortcut from oklab → oklch.
|
|
46
|
+
getSpace("oklab").shortcuts.oklch = (coords) => {
|
|
47
|
+
const { l, c, h } = labToLchPolar({
|
|
48
|
+
l: coords[0],
|
|
49
|
+
a: coords[1],
|
|
50
|
+
b: coords[2]
|
|
51
|
+
});
|
|
52
|
+
return [l, c, h];
|
|
53
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ProPhoto RGB color space.
|
|
2
|
+
//
|
|
3
|
+
// D50 white with a piecewise transfer function (linear near zero,
|
|
4
|
+
// gamma 1.8 above). ProPhoto has the widest gamut of the common RGB
|
|
5
|
+
// spaces — large enough to cover colors outside the human visual
|
|
6
|
+
// system, which is why it ships with a caveat in pipelines.
|
|
7
|
+
//
|
|
8
|
+
// Because ProPhoto is natively D50 we apply the Bradford chromatic
|
|
9
|
+
// adaptation transform on the way to/from the canonical D65 XYZ hub.
|
|
10
|
+
//
|
|
11
|
+
// Matrices from the CSS Color 4 sample code; transfer function from
|
|
12
|
+
// the ROMM RGB spec (ISO 22028-2).
|
|
13
|
+
|
|
14
|
+
import { multiplyMatrixVector } from "../util/matrix.js";
|
|
15
|
+
import { registerSpace } from "../core/registry.js";
|
|
16
|
+
import { adaptD50ToD65, adaptD65ToD50 } from "./xyz.js";
|
|
17
|
+
|
|
18
|
+
const ET = 1 / 512;
|
|
19
|
+
const ET_LINEAR = 16 / 512;
|
|
20
|
+
|
|
21
|
+
export function prophotoToLinear(coords) {
|
|
22
|
+
const f = (v) => {
|
|
23
|
+
const abs = Math.abs(v);
|
|
24
|
+
if (abs < ET_LINEAR) return v / 16;
|
|
25
|
+
return Math.sign(v) * Math.pow(abs, 1.8);
|
|
26
|
+
};
|
|
27
|
+
return [f(coords[0]), f(coords[1]), f(coords[2])];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function linearToProphoto(lin) {
|
|
31
|
+
const f = (v) => {
|
|
32
|
+
const abs = Math.abs(v);
|
|
33
|
+
if (abs < ET) return 16 * v;
|
|
34
|
+
return Math.sign(v) * Math.pow(abs, 1 / 1.8);
|
|
35
|
+
};
|
|
36
|
+
return [f(lin[0]), f(lin[1]), f(lin[2])];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Linear ProPhoto → XYZ D50.
|
|
40
|
+
export const LINEAR_PROPHOTO_TO_XYZ_D50 = [
|
|
41
|
+
[0.7977666449006423, 0.13518129740053308, 0.0313477341283922],
|
|
42
|
+
[0.2880748288194013, 0.711835234241873, 0.00008993693872564],
|
|
43
|
+
[0.0, 0.0, 0.8251046025104602]
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// XYZ D50 → Linear ProPhoto.
|
|
47
|
+
export const XYZ_D50_TO_LINEAR_PROPHOTO = [
|
|
48
|
+
[1.3457868816471585, -0.25557208737979464, -0.05110186497554526],
|
|
49
|
+
[-0.5446307051249019, 1.5082477428451468, 0.02052744743642139],
|
|
50
|
+
[0.0, 0.0, 1.2119675456389452]
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function linearProphotoToXyzD50(lin) {
|
|
54
|
+
return multiplyMatrixVector(LINEAR_PROPHOTO_TO_XYZ_D50, lin);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function xyzD50ToLinearProphoto(xyz) {
|
|
58
|
+
return multiplyMatrixVector(XYZ_D50_TO_LINEAR_PROPHOTO, xyz);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
registerSpace({
|
|
62
|
+
id: "prophoto",
|
|
63
|
+
channels: ["r", "g", "b"],
|
|
64
|
+
ranges: [
|
|
65
|
+
[0, 1],
|
|
66
|
+
[0, 1],
|
|
67
|
+
[0, 1]
|
|
68
|
+
],
|
|
69
|
+
white: "D50",
|
|
70
|
+
toXYZ: (coords) => adaptD50ToD65(linearProphotoToXyzD50(prophotoToLinear(coords))),
|
|
71
|
+
fromXYZ: (xyz) => linearToProphoto(xyzD50ToLinearProphoto(adaptD65ToD50(xyz)))
|
|
72
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ITU-R BT.2020 (Rec2020) wide-gamut space.
|
|
2
|
+
//
|
|
3
|
+
// Primaries + D65 white, with its own transfer function (α ≈ 1.09929,
|
|
4
|
+
// β ≈ 0.01806). Rec2020 is the UHDTV standard and the reference
|
|
5
|
+
// wide-gamut space for video/HDR workflows.
|
|
6
|
+
//
|
|
7
|
+
// Matrices from the CSS Color 4 sample code; transfer function from
|
|
8
|
+
// ITU-R BT.2020-2.
|
|
9
|
+
|
|
10
|
+
import { multiplyMatrixVector } from "../util/matrix.js";
|
|
11
|
+
import { registerSpace } from "../core/registry.js";
|
|
12
|
+
|
|
13
|
+
const ALPHA = 1.09929682680944;
|
|
14
|
+
const BETA = 0.018053968510807;
|
|
15
|
+
|
|
16
|
+
export function rec2020ToLinear(coords) {
|
|
17
|
+
const f = (v) => {
|
|
18
|
+
const abs = Math.abs(v);
|
|
19
|
+
if (abs < BETA * 4.5) return v / 4.5;
|
|
20
|
+
return Math.sign(v) * Math.pow((abs + ALPHA - 1) / ALPHA, 1 / 0.45);
|
|
21
|
+
};
|
|
22
|
+
return [f(coords[0]), f(coords[1]), f(coords[2])];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function linearToRec2020(lin) {
|
|
26
|
+
const f = (v) => {
|
|
27
|
+
const abs = Math.abs(v);
|
|
28
|
+
if (abs < BETA) return 4.5 * v;
|
|
29
|
+
return Math.sign(v) * (ALPHA * Math.pow(abs, 0.45) - (ALPHA - 1));
|
|
30
|
+
};
|
|
31
|
+
return [f(lin[0]), f(lin[1]), f(lin[2])];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const LINEAR_REC2020_TO_XYZ_D65 = [
|
|
35
|
+
[0.6369580483012914, 0.14461690358620832, 0.1688809751641721],
|
|
36
|
+
[0.26270021201126703, 0.6779980715188708, 0.05930171646986196],
|
|
37
|
+
[0.0, 0.028072693049087428, 1.060985057710791]
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const XYZ_D65_TO_LINEAR_REC2020 = [
|
|
41
|
+
[1.7166511879712674, -0.35567078377639233, -0.25336628137365974],
|
|
42
|
+
[-0.6666843518324892, 1.6164812366349395, 0.01576854581391113],
|
|
43
|
+
[0.017639857445310783, -0.042770613257808524, 0.9421031212354738]
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export function linearRec2020ToXyz(lin) {
|
|
47
|
+
return multiplyMatrixVector(LINEAR_REC2020_TO_XYZ_D65, lin);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function xyzToLinearRec2020(xyz) {
|
|
51
|
+
return multiplyMatrixVector(XYZ_D65_TO_LINEAR_REC2020, xyz);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerSpace({
|
|
55
|
+
id: "rec2020",
|
|
56
|
+
channels: ["r", "g", "b"],
|
|
57
|
+
ranges: [
|
|
58
|
+
[0, 1],
|
|
59
|
+
[0, 1],
|
|
60
|
+
[0, 1]
|
|
61
|
+
],
|
|
62
|
+
white: "D65",
|
|
63
|
+
toXYZ: (coords) => linearRec2020ToXyz(rec2020ToLinear(coords)),
|
|
64
|
+
fromXYZ: (xyz) => linearToRec2020(xyzToLinearRec2020(xyz))
|
|
65
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// sRGB and linear-light sRGB spaces.
|
|
2
|
+
//
|
|
3
|
+
// Two spaces are registered:
|
|
4
|
+
//
|
|
5
|
+
// srgb — gamma-encoded sRGB, coords in [0, 1] (not 0..255).
|
|
6
|
+
// srgb-linear — linear-light sRGB (gamma removed), coords in [0, 1].
|
|
7
|
+
//
|
|
8
|
+
// Keeping the coords in [0, 1] rather than 0..255 is the CSS Color 4 /
|
|
9
|
+
// colorjs.io convention; 0..255 is a *view* exposed by the legacy `.rgb`
|
|
10
|
+
// getter on the Swatch class, not the canonical storage.
|
|
11
|
+
//
|
|
12
|
+
// The sRGB transfer function uses the piecewise approximation with the
|
|
13
|
+
// standard 0.03928 knee. The primaries matrix (sRGB linear → XYZ D65) is the
|
|
14
|
+
// BT.709 reference, identical to what v2 used in _tMatrixRGBToXYZ.
|
|
15
|
+
|
|
16
|
+
import { multiplyMatrixVector, invertMatrix } from "../util/matrix.js";
|
|
17
|
+
import { registerSpace } from "../core/registry.js";
|
|
18
|
+
|
|
19
|
+
// sRGB inverse EOTF: gamma-encoded → linear light.
|
|
20
|
+
export function srgbToLinear(rgb) {
|
|
21
|
+
const f = (v) =>
|
|
22
|
+
Math.abs(v) <= 0.04045
|
|
23
|
+
? v / 12.92
|
|
24
|
+
: Math.sign(v) * Math.pow((Math.abs(v) + 0.055) / 1.055, 2.4);
|
|
25
|
+
return [f(rgb[0]), f(rgb[1]), f(rgb[2])];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// sRGB EOTF: linear light → gamma-encoded.
|
|
29
|
+
export function linearToSrgb(lin) {
|
|
30
|
+
const f = (v) =>
|
|
31
|
+
Math.abs(v) <= 0.0031308
|
|
32
|
+
? 12.92 * v
|
|
33
|
+
: Math.sign(v) * (1.055 * Math.pow(Math.abs(v), 1 / 2.4) - 0.055);
|
|
34
|
+
return [f(lin[0]), f(lin[1]), f(lin[2])];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Linear sRGB → CIE XYZ D65. BT.709 primaries, same values as v2's
|
|
38
|
+
// _tMatrixRGBToXYZ (src/swatch.js:1737-1743).
|
|
39
|
+
export const LINEAR_SRGB_TO_XYZ_D65 = [
|
|
40
|
+
[0.4124564, 0.3575761, 0.1804375],
|
|
41
|
+
[0.2126729, 0.7151522, 0.072175],
|
|
42
|
+
[0.0193339, 0.119192, 0.9503041]
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const XYZ_D65_TO_LINEAR_SRGB = invertMatrix(LINEAR_SRGB_TO_XYZ_D65);
|
|
46
|
+
|
|
47
|
+
export function linearSrgbToXyz(lin) {
|
|
48
|
+
return multiplyMatrixVector(LINEAR_SRGB_TO_XYZ_D65, lin);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function xyzToLinearSrgb(xyz) {
|
|
52
|
+
return multiplyMatrixVector(XYZ_D65_TO_LINEAR_SRGB, xyz);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
registerSpace({
|
|
56
|
+
id: "srgb-linear",
|
|
57
|
+
channels: ["r", "g", "b"],
|
|
58
|
+
ranges: [
|
|
59
|
+
[0, 1],
|
|
60
|
+
[0, 1],
|
|
61
|
+
[0, 1]
|
|
62
|
+
],
|
|
63
|
+
white: "D65",
|
|
64
|
+
toXYZ: (coords) => linearSrgbToXyz(coords),
|
|
65
|
+
fromXYZ: (xyz) => xyzToLinearSrgb(xyz),
|
|
66
|
+
shortcuts: {
|
|
67
|
+
srgb: (coords) => linearToSrgb(coords)
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
registerSpace({
|
|
72
|
+
id: "srgb",
|
|
73
|
+
channels: ["r", "g", "b"],
|
|
74
|
+
ranges: [
|
|
75
|
+
[0, 1],
|
|
76
|
+
[0, 1],
|
|
77
|
+
[0, 1]
|
|
78
|
+
],
|
|
79
|
+
white: "D65",
|
|
80
|
+
toXYZ: (coords) => linearSrgbToXyz(srgbToLinear(coords)),
|
|
81
|
+
fromXYZ: (xyz) => linearToSrgb(xyzToLinearSrgb(xyz)),
|
|
82
|
+
shortcuts: {
|
|
83
|
+
"srgb-linear": (coords) => srgbToLinear(coords)
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// CIE XYZ color space and chromatic adaptation transforms.
|
|
2
|
+
//
|
|
3
|
+
// Two whites are registered:
|
|
4
|
+
//
|
|
5
|
+
// xyz / xyz-d65 — D65 reference white. This is the canonical hub for the
|
|
6
|
+
// conversion graph. All other spaces convert to/from this.
|
|
7
|
+
// xyz-d50 — D50 reference white. Used by spaces that are natively
|
|
8
|
+
// defined under D50 (CIE Lab D50, ProPhoto). The Bradford
|
|
9
|
+
// chromatic adaptation transform converts between D50 and
|
|
10
|
+
// D65 with very low error.
|
|
11
|
+
//
|
|
12
|
+
// Reference white tristimulus values (Y normalized to 1):
|
|
13
|
+
// D65 ≈ (0.95047, 1.0, 1.08883)
|
|
14
|
+
// D50 ≈ (0.96422, 1.0, 0.82521)
|
|
15
|
+
|
|
16
|
+
import { multiplyMatrixVector } from "../util/matrix.js";
|
|
17
|
+
import { registerSpace } from "../core/registry.js";
|
|
18
|
+
|
|
19
|
+
export const D65 = [0.95047, 1.0, 1.08883];
|
|
20
|
+
export const D50 = [0.96422, 1.0, 0.82521];
|
|
21
|
+
|
|
22
|
+
// Bradford chromatic adaptation transforms.
|
|
23
|
+
//
|
|
24
|
+
// Source: https://www.w3.org/TR/css-color-4/#color-conversion-code
|
|
25
|
+
// (the W3C-recommended Bradford CAT used in CSS Color 4 conversions).
|
|
26
|
+
export const BRADFORD_D50_TO_D65 = [
|
|
27
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
28
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
29
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const BRADFORD_D65_TO_D50 = [
|
|
33
|
+
[1.0479298208405488, 0.022946793341019434, -0.05019222954313557],
|
|
34
|
+
[0.029627815688159608, 0.990434484573249, -0.01707382502938514],
|
|
35
|
+
[-0.009243058152591178, 0.015055144896577895, 0.7518742899580008]
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function adaptD50ToD65(xyz) {
|
|
39
|
+
return multiplyMatrixVector(BRADFORD_D50_TO_D65, xyz);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function adaptD65ToD50(xyz) {
|
|
43
|
+
return multiplyMatrixVector(BRADFORD_D65_TO_D50, xyz);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// XYZ D65 — the hub. toXYZ / fromXYZ are identity.
|
|
47
|
+
registerSpace({
|
|
48
|
+
id: "xyz",
|
|
49
|
+
channels: ["x", "y", "z"],
|
|
50
|
+
white: "D65",
|
|
51
|
+
toXYZ: (coords) => [coords[0], coords[1], coords[2]],
|
|
52
|
+
fromXYZ: (xyz) => [xyz[0], xyz[1], xyz[2]]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Alias.
|
|
56
|
+
registerSpace({
|
|
57
|
+
id: "xyz-d65",
|
|
58
|
+
channels: ["x", "y", "z"],
|
|
59
|
+
white: "D65",
|
|
60
|
+
toXYZ: (coords) => [coords[0], coords[1], coords[2]],
|
|
61
|
+
fromXYZ: (xyz) => [xyz[0], xyz[1], xyz[2]]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// XYZ D50 — Bradford-adapted on the way to/from the hub.
|
|
65
|
+
registerSpace({
|
|
66
|
+
id: "xyz-d50",
|
|
67
|
+
channels: ["x", "y", "z"],
|
|
68
|
+
white: "D50",
|
|
69
|
+
toXYZ: (coords) => adaptD50ToD65(coords),
|
|
70
|
+
fromXYZ: (xyz) => adaptD65ToD50(xyz)
|
|
71
|
+
});
|
package/src/swatch.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// swatch — main entry point.
|
|
2
|
+
//
|
|
3
|
+
// Importing this module has two effects:
|
|
4
|
+
//
|
|
5
|
+
// 1. Side effects from `./bootstrap.js`: every registered color space
|
|
6
|
+
// is loaded, the CSS Color 4 parser is attached, and the operations
|
|
7
|
+
// (channels, gamut, manipulation, tint/shade, mix, blend, ΔE,
|
|
8
|
+
// naming, temperature, accessibility, APCA, CVD, scales, palettes)
|
|
9
|
+
// are wired onto the Swatch prototype and the factory.
|
|
10
|
+
//
|
|
11
|
+
// 2. Re-exports: the `swatch` factory function (callable, with statics
|
|
12
|
+
// for temperature, random, scale, bezier, cubehelix, palettes,
|
|
13
|
+
// contrast, isReadable, ensureContrast, apcaContrast, simulate,
|
|
14
|
+
// daltonize, checkPalette, nearestDistinguishable, mostReadable),
|
|
15
|
+
// the `Swatch` class, and the default export.
|
|
16
|
+
//
|
|
17
|
+
// Typical usage:
|
|
18
|
+
//
|
|
19
|
+
// import swatch from "@luntta/swatch";
|
|
20
|
+
// swatch("oklch(0.7 0.15 240)").lighten(0.05).toString({ format: "hex" });
|
|
21
|
+
|
|
22
|
+
import "./bootstrap.js";
|
|
23
|
+
import { swatch, Swatch } from "./core/swatch-class.js";
|
|
24
|
+
|
|
25
|
+
export { swatch, Swatch };
|
|
26
|
+
export default swatch;
|
|
27
|
+
|
|
28
|
+
// Named re-exports of the factory statics, so bundlers can tree-shake unused
|
|
29
|
+
// helpers and editors can autocomplete them directly:
|
|
30
|
+
//
|
|
31
|
+
// import { scale, contrast, spaces } from "@luntta/swatch";
|
|
32
|
+
//
|
|
33
|
+
// `swatch.try` stays on the factory only — `try` is a reserved word and
|
|
34
|
+
// cannot be a named export.
|
|
35
|
+
export const {
|
|
36
|
+
temperature,
|
|
37
|
+
random,
|
|
38
|
+
contrast,
|
|
39
|
+
isReadable,
|
|
40
|
+
ensureContrast,
|
|
41
|
+
apcaContrast,
|
|
42
|
+
simulate,
|
|
43
|
+
daltonize,
|
|
44
|
+
simulateImageData,
|
|
45
|
+
daltonizeImageData,
|
|
46
|
+
image,
|
|
47
|
+
checkPalette,
|
|
48
|
+
nearestDistinguishable,
|
|
49
|
+
mostReadable,
|
|
50
|
+
scale,
|
|
51
|
+
bezier,
|
|
52
|
+
cubehelix,
|
|
53
|
+
palettes,
|
|
54
|
+
isColor,
|
|
55
|
+
spaces,
|
|
56
|
+
cvd
|
|
57
|
+
} = swatch;
|
package/src/util/math.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Generic numeric helpers used by multiple modules.
|
|
2
|
+
|
|
3
|
+
export function clamp(value, lo, hi) {
|
|
4
|
+
return value < lo ? lo : value > hi ? hi : value;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function lerp(a, b, t) {
|
|
8
|
+
return a + (b - a) * t;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Shortest-arc hue interpolation in degrees.
|
|
12
|
+
export function lerpHue(a, b, t) {
|
|
13
|
+
let dh = b - a;
|
|
14
|
+
if (dh > 180) dh -= 360;
|
|
15
|
+
else if (dh < -180) dh += 360;
|
|
16
|
+
let h = a + dh * t;
|
|
17
|
+
if (h < 0) h += 360;
|
|
18
|
+
else if (h >= 360) h -= 360;
|
|
19
|
+
return h;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function degToRad(deg) {
|
|
23
|
+
return (deg * Math.PI) / 180;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function radToDeg(rad) {
|
|
27
|
+
return (rad * 180) / Math.PI;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wrap a hue into [0, 360).
|
|
31
|
+
export function normalizeHue(h) {
|
|
32
|
+
h = h % 360;
|
|
33
|
+
if (h < 0) h += 360;
|
|
34
|
+
return h;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Lab-style {l, a, b} → polar {l, c, h}. Used by both CIE LCh and OKLCh.
|
|
38
|
+
export function labToLchPolar(lab) {
|
|
39
|
+
const c = Math.hypot(lab.a, lab.b);
|
|
40
|
+
let h = Math.atan2(lab.b, lab.a) * (180 / Math.PI);
|
|
41
|
+
if (h < 0) h += 360;
|
|
42
|
+
return { l: lab.l, c, h };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Polar {l, c, h} → {l, a, b}.
|
|
46
|
+
export function lchToLabRect(lch) {
|
|
47
|
+
const hRad = (lch.h * Math.PI) / 180;
|
|
48
|
+
return {
|
|
49
|
+
l: lch.l,
|
|
50
|
+
a: lch.c * Math.cos(hRad),
|
|
51
|
+
b: lch.c * Math.sin(hRad)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// 3×3 matrix helpers extracted from src/swatch.js (the v2 monolith).
|
|
2
|
+
// Generic in dimension; the consumers in v3 only ever use 3×3.
|
|
3
|
+
|
|
4
|
+
export function multiplyMatrices(mA, mB) {
|
|
5
|
+
const result = new Array(mA.length)
|
|
6
|
+
.fill(0)
|
|
7
|
+
.map(() => new Array(mB[0].length).fill(0));
|
|
8
|
+
|
|
9
|
+
return result.map((row, i) =>
|
|
10
|
+
row.map((_, j) => mA[i].reduce((sum, _v, k) => sum + mA[i][k] * mB[k][j], 0))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Apply a 3×3 matrix to a length-3 vector.
|
|
15
|
+
export function multiplyMatrixVector(M, v) {
|
|
16
|
+
return [
|
|
17
|
+
M[0][0] * v[0] + M[0][1] * v[1] + M[0][2] * v[2],
|
|
18
|
+
M[1][0] * v[0] + M[1][1] * v[1] + M[1][2] * v[2],
|
|
19
|
+
M[2][0] * v[0] + M[2][1] * v[1] + M[2][2] * v[2]
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Square-matrix inversion via Gauss–Jordan.
|
|
24
|
+
// Adapted from Andrew Ippoliti, http://blog.acipo.com/matrix-inversion-in-javascript/
|
|
25
|
+
export function invertMatrix(matrix) {
|
|
26
|
+
if (matrix.length !== matrix[0].length) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
let i = 0,
|
|
30
|
+
ii = 0,
|
|
31
|
+
j = 0,
|
|
32
|
+
e = 0;
|
|
33
|
+
const dim = matrix.length;
|
|
34
|
+
const I = [];
|
|
35
|
+
const C = [];
|
|
36
|
+
for (i = 0; i < dim; i += 1) {
|
|
37
|
+
I[I.length] = [];
|
|
38
|
+
C[C.length] = [];
|
|
39
|
+
for (j = 0; j < dim; j += 1) {
|
|
40
|
+
I[i][j] = i === j ? 1 : 0;
|
|
41
|
+
C[i][j] = matrix[i][j];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (i = 0; i < dim; i += 1) {
|
|
46
|
+
e = C[i][i];
|
|
47
|
+
|
|
48
|
+
if (e === 0) {
|
|
49
|
+
for (ii = i + 1; ii < dim; ii += 1) {
|
|
50
|
+
if (C[ii][i] !== 0) {
|
|
51
|
+
for (j = 0; j < dim; j++) {
|
|
52
|
+
e = C[i][j];
|
|
53
|
+
C[i][j] = C[ii][j];
|
|
54
|
+
C[ii][j] = e;
|
|
55
|
+
e = I[i][j];
|
|
56
|
+
I[i][j] = I[ii][j];
|
|
57
|
+
I[ii][j] = e;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
e = C[i][i];
|
|
63
|
+
if (e === 0) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (j = 0; j < dim; j++) {
|
|
69
|
+
C[i][j] = C[i][j] / e;
|
|
70
|
+
I[i][j] = I[i][j] / e;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (ii = 0; ii < dim; ii++) {
|
|
74
|
+
if (ii === i) continue;
|
|
75
|
+
e = C[ii][i];
|
|
76
|
+
for (j = 0; j < dim; j++) {
|
|
77
|
+
C[ii][j] -= e * C[i][j];
|
|
78
|
+
I[ii][j] -= e * I[i][j];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return I;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function identity3() {
|
|
87
|
+
return [
|
|
88
|
+
[1, 0, 0],
|
|
89
|
+
[0, 1, 0],
|
|
90
|
+
[0, 0, 1]
|
|
91
|
+
];
|
|
92
|
+
}
|