tweakit 0.6.0 → 0.7.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/AGENTS.md +5 -5
- package/README.md +3 -3
- package/dist/tweaks/auto.js +1 -1
- package/dist/tweaks/auto.js.map +1 -1
- package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
- package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
- package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
- package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
- package/dist/tweaks/chunk-ETB35KFV.js +2 -0
- package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
- package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
- package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
- package/dist/tweaks/clip-A3VI53BK.js +2 -0
- package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
- package/dist/tweaks/color-UGYMAF3W.js +2 -0
- package/dist/tweaks/core.js +1 -1
- package/dist/tweaks/core.js.map +3 -3
- package/dist/tweaks/fold-GY47HIQA.js +2 -0
- package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
- package/dist/tweaks/gradient-BTX25WSH.js +2 -0
- package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
- package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
- package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
- package/dist/tweaks/interval-BHPI4LRN.js +2 -0
- package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
- package/dist/tweaks/markup-QM53TQJZ.js +2 -0
- package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
- package/dist/tweaks/monitor-722WIYGW.js +3 -0
- package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
- package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
- package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
- package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
- package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
- package/dist/tweaks/point-XDXKMDXH.js +2 -0
- package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
- package/dist/tweaks/presets-EUWNSCUW.js +2 -0
- package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
- package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
- package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
- package/dist/tweaks/sides-JWIURDA4.js +2 -0
- package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
- package/dist/tweaks/spring-BF5GBTPM.js +2 -0
- package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +5 -4
- package/dist/tweaks.js.map +3 -3
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/types.d.ts +278 -72
- package/package.json +5 -2
- package/src/tweaks/auto.ts +9 -5
- package/src/tweaks/clip.ts +84 -22
- package/src/tweaks/controls/basic.ts +463 -139
- package/src/tweaks/controls/bezier.ts +152 -47
- package/src/tweaks/controls/color.ts +610 -161
- package/src/tweaks/controls/gradient.ts +256 -77
- package/src/tweaks/controls/image.ts +388 -132
- package/src/tweaks/controls/interval.ts +126 -41
- package/src/tweaks/controls/monitor.ts +142 -36
- package/src/tweaks/controls/motion.ts +231 -56
- package/src/tweaks/controls/plot.ts +321 -101
- package/src/tweaks/controls/point.ts +134 -45
- package/src/tweaks/controls/shadow.ts +103 -30
- package/src/tweaks/controls/sides.ts +150 -40
- package/src/tweaks/controls/spring.ts +220 -60
- package/src/tweaks/controls/tabs.ts +39 -18
- package/src/tweaks/core.ts +1 -1
- package/src/tweaks/easing.ts +45 -20
- package/src/tweaks/enhance.ts +21 -6
- package/src/tweaks/feedback.ts +133 -34
- package/src/tweaks/fold.ts +22 -10
- package/src/tweaks/heavy.ts +461 -137
- package/src/tweaks/icons.ts +12 -5
- package/src/tweaks/image-store.ts +131 -43
- package/src/tweaks/lazy.ts +36 -19
- package/src/tweaks/markup.ts +255 -92
- package/src/tweaks/panel.ts +1262 -438
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +213 -51
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +802 -190
- package/src/tweaks/standalone.ts +120 -30
- package/src/tweaks/types.ts +644 -143
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +556 -550
- package/src/wide-gamut.ts +473 -125
- package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
- package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
- package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
- package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
- package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
- package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
- package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
- package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
- package/dist/tweaks/clip-WW7JYO4X.js +0 -2
- package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
- package/dist/tweaks/color-YMMFURP6.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
- package/dist/tweaks/gradient-JER7PW4D.js +0 -2
- package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
- package/dist/tweaks/image-H6QSLGO3.js +0 -4
- package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
- package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
- package/dist/tweaks/interval-TEUHQGZN.js +0 -2
- package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
- package/dist/tweaks/markup-NIBJD6E2.js +0 -2
- package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
- package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
- package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
- package/dist/tweaks/motion-FQPUBKKM.js +0 -2
- package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
- package/dist/tweaks/plot-2HBNBZSG.js +0 -3
- package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
- package/dist/tweaks/point-VV6XV3I6.js +0 -2
- package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
- package/dist/tweaks/shadow-5E4E55I2.js +0 -2
- package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
- package/dist/tweaks/sides-P3STEVYY.js +0 -2
- package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
- package/dist/tweaks/spring-HUVSXNVN.js +0 -2
- package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
- package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
- package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
- /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
- /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
package/src/wide-gamut.ts
CHANGED
|
@@ -6,11 +6,31 @@ import type { ColorMode, GradientInterpolation } from "./tweaks/types.js";
|
|
|
6
6
|
export type Vec3 = [number, number, number];
|
|
7
7
|
type Mat3 = [Vec3, Vec3, Vec3];
|
|
8
8
|
/** The spaces convert() hubs between, through CIE XYZ. */
|
|
9
|
-
export type Space =
|
|
9
|
+
export type Space =
|
|
10
|
+
| "srgb"
|
|
11
|
+
| "p3"
|
|
12
|
+
| "rec2020"
|
|
13
|
+
| "prophoto-rgb"
|
|
14
|
+
| "a98-rgb"
|
|
15
|
+
| "srgb-linear"
|
|
16
|
+
| "xyz-d65"
|
|
17
|
+
| "xyz-d50"
|
|
18
|
+
| "oklab"
|
|
19
|
+
| "oklch"
|
|
20
|
+
| "lab"
|
|
21
|
+
| "lch"
|
|
22
|
+
| "hsl"
|
|
23
|
+
| "hwb";
|
|
10
24
|
/** The RGB gamuts a color can be mapped into. */
|
|
11
25
|
export type Gamut = "srgb" | "p3" | "rec2020";
|
|
12
26
|
/** One editable channel of a mode: label, display range and step, engine-to-display scale. */
|
|
13
|
-
export interface Channel {
|
|
27
|
+
export interface Channel {
|
|
28
|
+
k: string;
|
|
29
|
+
min: number;
|
|
30
|
+
max: number;
|
|
31
|
+
step: number;
|
|
32
|
+
scale: number;
|
|
33
|
+
}
|
|
14
34
|
|
|
15
35
|
const DEG = 180 / Math.PI;
|
|
16
36
|
const RAD = Math.PI / 180;
|
|
@@ -20,8 +40,13 @@ const mul = (m: Mat3, v: Vec3): Vec3 => [
|
|
|
20
40
|
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
|
|
21
41
|
];
|
|
22
42
|
const mulMat = (a: Mat3, b: Mat3): Mat3 => {
|
|
23
|
-
const o: Mat3 = [
|
|
24
|
-
|
|
43
|
+
const o: Mat3 = [
|
|
44
|
+
[0, 0, 0],
|
|
45
|
+
[0, 0, 0],
|
|
46
|
+
[0, 0, 0],
|
|
47
|
+
];
|
|
48
|
+
for (let i = 0; i < 3; i++)
|
|
49
|
+
for (let j = 0; j < 3; j++) o[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
|
|
25
50
|
return o;
|
|
26
51
|
};
|
|
27
52
|
// Non-finite channels (null, NaN, ±Infinity) fall to 0; an infinite chroma would hang toGamut's bisection.
|
|
@@ -29,113 +54,272 @@ export const num = (x: number) => (Number.isFinite(x) ? x : 0);
|
|
|
29
54
|
const clamp = (x: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, x));
|
|
30
55
|
|
|
31
56
|
// ── transfer functions ──
|
|
32
|
-
const srgbLin = (c: number) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
57
|
+
const srgbLin = (c: number) => {
|
|
58
|
+
const a = Math.abs(c);
|
|
59
|
+
return a <= 0.04045 ? c / 12.92 : Math.sign(c) * ((a + 0.055) / 1.055) ** 2.4;
|
|
60
|
+
};
|
|
61
|
+
const srgbGam = (c: number) => {
|
|
62
|
+
const a = Math.abs(c);
|
|
63
|
+
return a <= 0.0031308 ? c * 12.92 : Math.sign(c) * (1.055 * a ** (1 / 2.4) - 0.055);
|
|
64
|
+
};
|
|
65
|
+
const REC_A = 1.09929682680944,
|
|
66
|
+
REC_B = 0.018053968510807; // BT.2020 piecewise OETF constants, not a pure gamma
|
|
67
|
+
const rec2020Lin = (c: number) => {
|
|
68
|
+
const a = Math.abs(c);
|
|
69
|
+
return a < REC_B * 4.5 ? c / 4.5 : Math.sign(c) * ((a + REC_A - 1) / REC_A) ** (1 / 0.45);
|
|
70
|
+
};
|
|
71
|
+
const rec2020Gam = (c: number) => {
|
|
72
|
+
const a = Math.abs(c);
|
|
73
|
+
return a < REC_B ? c * 4.5 : Math.sign(c) * (REC_A * a ** 0.45 - (REC_A - 1));
|
|
74
|
+
};
|
|
37
75
|
const PRO_ET = 1 / 512;
|
|
38
|
-
const prophotoLin = (c: number) => {
|
|
39
|
-
const
|
|
76
|
+
const prophotoLin = (c: number) => {
|
|
77
|
+
const a = Math.abs(c);
|
|
78
|
+
return a <= PRO_ET * 16 ? c / 16 : Math.sign(c) * a ** 1.8;
|
|
79
|
+
};
|
|
80
|
+
const prophotoGam = (c: number) => {
|
|
81
|
+
const a = Math.abs(c);
|
|
82
|
+
return a >= PRO_ET ? Math.sign(c) * a ** (1 / 1.8) : 16 * c;
|
|
83
|
+
};
|
|
40
84
|
|
|
41
85
|
// ── matrices (D65 unless noted) ──
|
|
42
|
-
const LIN_SRGB_TO_XYZ: Mat3 = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
const LIN_SRGB_TO_XYZ: Mat3 = [
|
|
87
|
+
[0.41239079926595934, 0.357584339383878, 0.1804807884018343],
|
|
88
|
+
[0.21263900587151027, 0.715168678767756, 0.07219231536073371],
|
|
89
|
+
[0.01933081871559182, 0.11919477979462598, 0.9505321522496607],
|
|
90
|
+
];
|
|
91
|
+
const XYZ_TO_LIN_SRGB: Mat3 = [
|
|
92
|
+
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
93
|
+
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
94
|
+
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786],
|
|
95
|
+
];
|
|
96
|
+
const LIN_P3_TO_XYZ: Mat3 = [
|
|
97
|
+
[0.4865709486482162, 0.26566769316909306, 0.19821728523436247],
|
|
98
|
+
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
99
|
+
[0, 0.04511338185890264, 1.043944368900976],
|
|
100
|
+
];
|
|
101
|
+
const XYZ_TO_LIN_P3: Mat3 = [
|
|
102
|
+
[2.493496911941425, -0.9313836179191239, -0.40271078445071684],
|
|
103
|
+
[-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
|
|
104
|
+
[0.03584583024378447, -0.07617238926804182, 0.9568845240076872],
|
|
105
|
+
];
|
|
106
|
+
const LIN_REC2020_TO_XYZ: Mat3 = [
|
|
107
|
+
[0.6369580483012914, 0.14461690358620832, 0.16888097516417205],
|
|
108
|
+
[0.2627002120112671, 0.6779980715188708, 0.05930171646986196],
|
|
109
|
+
[0, 0.028072693049087428, 1.060985057710791],
|
|
110
|
+
];
|
|
111
|
+
const XYZ_TO_LIN_REC2020: Mat3 = [
|
|
112
|
+
[1.7166511879712674, -0.35567078377639233, -0.25336628137365974],
|
|
113
|
+
[-0.6666843518324892, 1.6164812366349395, 0.01576854581391113],
|
|
114
|
+
[0.017639857445310783, -0.042770613257808524, 0.9421031212354738],
|
|
115
|
+
];
|
|
116
|
+
const LIN_PRO_TO_XYZ_D50: Mat3 = [
|
|
117
|
+
[0.7977604896723027, 0.13518583717574031, 0.0313493495815248],
|
|
118
|
+
[0.2880711282292934, 0.7118432178101014, 0.00008565396060525902],
|
|
119
|
+
[0, 0, 0.8251046025104601],
|
|
120
|
+
];
|
|
121
|
+
const XYZ_D50_TO_LIN_PRO: Mat3 = [
|
|
122
|
+
[1.3457989731028281, -0.25558010007997534, -0.05110628506753401],
|
|
123
|
+
[-0.5446224939028347, 1.5082327413132781, 0.02053603239147973],
|
|
124
|
+
[0, 0, 1.2119675456389454],
|
|
125
|
+
];
|
|
50
126
|
// CSS Color 4's a98-rgb (Adobe RGB 1998): a pure 563/256 gamma over these primaries, D65.
|
|
51
|
-
const A98_GAMMA = 563 / 256,
|
|
52
|
-
|
|
53
|
-
|
|
127
|
+
const A98_GAMMA = 563 / 256,
|
|
128
|
+
a98Lin = (c: number) => Math.sign(c) * Math.abs(c) ** A98_GAMMA,
|
|
129
|
+
a98Gam = (c: number) => Math.sign(c) * Math.abs(c) ** (1 / A98_GAMMA);
|
|
130
|
+
const LIN_A98_TO_XYZ: Mat3 = [
|
|
131
|
+
[573536 / 994567, 263643 / 1420810, 187206 / 994567],
|
|
132
|
+
[591459 / 1989134, 6239551 / 9945670, 374412 / 4972835],
|
|
133
|
+
[53769 / 1989134, 351524 / 4972835, 4929758 / 4972835],
|
|
134
|
+
];
|
|
135
|
+
const XYZ_TO_LIN_A98: Mat3 = [
|
|
136
|
+
[1829569 / 896150, -506331 / 896150, -308931 / 896150],
|
|
137
|
+
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
|
|
138
|
+
[16779 / 1248040, -147721 / 1248040, 1266979 / 1248040],
|
|
139
|
+
];
|
|
54
140
|
const same = (c: number) => c;
|
|
55
|
-
const XYZ_D65_TO_D50: Mat3 = [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
141
|
+
const XYZ_D65_TO_D50: Mat3 = [
|
|
142
|
+
[1.0479298208405488, 0.022946793341019088, -0.05019222954313557],
|
|
143
|
+
[0.029627815688159344, 0.990434484573249, -0.01707382502938514],
|
|
144
|
+
[-0.009243058152591178, 0.015055144896577895, 0.7518742899580008],
|
|
145
|
+
];
|
|
146
|
+
const XYZ_D50_TO_D65: Mat3 = [
|
|
147
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
148
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
149
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753],
|
|
150
|
+
];
|
|
151
|
+
const XYZ_TO_LMS: Mat3 = [
|
|
152
|
+
[0.819022437996703, 0.3619062600528904, -0.1288737815209879],
|
|
153
|
+
[0.0329836539323885, 0.9292868615863434, 0.0361446663506424],
|
|
154
|
+
[0.0481771893596242, 0.2642395317527308, 0.6335478284694309],
|
|
155
|
+
];
|
|
156
|
+
const LMS_TO_XYZ: Mat3 = [
|
|
157
|
+
[1.2268798758459243, -0.5578149944602171, 0.2813910456659647],
|
|
158
|
+
[-0.0405757452148008, 1.112286803280317, -0.0717110580655164],
|
|
159
|
+
[-0.0763729366746601, -0.4214933324022432, 1.5869240198367816],
|
|
160
|
+
];
|
|
161
|
+
const LMS_TO_OKLAB: Mat3 = [
|
|
162
|
+
[0.210454268309314, 0.7936177747023054, -0.0040720430116193],
|
|
163
|
+
[1.9779985324311684, -2.42859224204858, 0.450593709617411],
|
|
164
|
+
[0.0259040424655478, 0.7827717124575296, -0.8086757549230774],
|
|
165
|
+
];
|
|
166
|
+
const OKLAB_TO_LMS: Mat3 = [
|
|
167
|
+
[1.0, 0.3963377773761749, 0.2158037573099136],
|
|
168
|
+
[1.0, -0.1055613458156586, -0.0638541728258133],
|
|
169
|
+
[1.0, -0.0894841775298119, -1.2914855480194092],
|
|
170
|
+
];
|
|
171
|
+
const LAB_E = 216 / 24389,
|
|
172
|
+
LAB_K = 24389 / 27;
|
|
62
173
|
const WHITE_D50: Vec3 = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585];
|
|
63
174
|
|
|
64
175
|
const rgbToXyz = (c: Vec3, lin: (c: number) => number, m: Mat3) => mul(m, [lin(c[0]), lin(c[1]), lin(c[2])]);
|
|
65
|
-
const xyzToRgb = (xyz: Vec3, gam: (c: number) => number, m: Mat3): Vec3 => {
|
|
66
|
-
const
|
|
67
|
-
|
|
176
|
+
const xyzToRgb = (xyz: Vec3, gam: (c: number) => number, m: Mat3): Vec3 => {
|
|
177
|
+
const l = mul(m, xyz);
|
|
178
|
+
return [gam(l[0]), gam(l[1]), gam(l[2])];
|
|
179
|
+
};
|
|
180
|
+
const oklabToXyz = (lab: Vec3) => {
|
|
181
|
+
const p = mul(OKLAB_TO_LMS, lab);
|
|
182
|
+
return mul(LMS_TO_XYZ, [p[0] ** 3, p[1] ** 3, p[2] ** 3]);
|
|
183
|
+
};
|
|
184
|
+
const xyzToOklab = (xyz: Vec3) => {
|
|
185
|
+
const lms = mul(XYZ_TO_LMS, xyz);
|
|
186
|
+
return mul(LMS_TO_OKLAB, [Math.cbrt(lms[0]), Math.cbrt(lms[1]), Math.cbrt(lms[2])]);
|
|
187
|
+
};
|
|
68
188
|
function labToXyz(lab: Vec3): Vec3 {
|
|
69
|
-
const [L, a, b] = lab;
|
|
189
|
+
const [L, a, b] = lab;
|
|
190
|
+
const fy = (L + 16) / 116,
|
|
191
|
+
fx = a / 500 + fy,
|
|
192
|
+
fz = fy - b / 200;
|
|
70
193
|
const x = fx ** 3 > LAB_E ? fx ** 3 : (116 * fx - 16) / LAB_K;
|
|
71
194
|
const y = L > LAB_K * LAB_E ? fy ** 3 : L / LAB_K;
|
|
72
195
|
const z = fz ** 3 > LAB_E ? fz ** 3 : (116 * fz - 16) / LAB_K;
|
|
73
196
|
return mul(XYZ_D50_TO_D65, [x * WHITE_D50[0], y * WHITE_D50[1], z * WHITE_D50[2]]);
|
|
74
197
|
}
|
|
75
198
|
function xyzToLab(xyz: Vec3): Vec3 {
|
|
76
|
-
const d50 = mul(XYZ_D65_TO_D50, xyz);
|
|
77
|
-
const
|
|
199
|
+
const d50 = mul(XYZ_D65_TO_D50, xyz);
|
|
200
|
+
const f = (t: number) => (t > LAB_E ? Math.cbrt(t) : (LAB_K * t + 16) / 116);
|
|
201
|
+
const fx = f(d50[0] / WHITE_D50[0]),
|
|
202
|
+
fy = f(d50[1] / WHITE_D50[1]),
|
|
203
|
+
fz = f(d50[2] / WHITE_D50[2]);
|
|
78
204
|
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
79
205
|
}
|
|
80
|
-
const toPolar = (r: Vec3): Vec3 => {
|
|
206
|
+
const toPolar = (r: Vec3): Vec3 => {
|
|
207
|
+
const C = Math.hypot(r[1], r[2]);
|
|
208
|
+
let h = Math.atan2(r[2], r[1]) * DEG;
|
|
209
|
+
if (h < 0) h += 360;
|
|
210
|
+
return [r[0], C, h];
|
|
211
|
+
};
|
|
81
212
|
const toRect = (p: Vec3): Vec3 => [p[0], p[1] * Math.cos(p[2] * RAD), p[1] * Math.sin(p[2] * RAD)];
|
|
82
213
|
function hslToSrgb(hsl: Vec3): Vec3 {
|
|
83
|
-
const h = (((Number.isNaN(hsl[0]) ? 0 : hsl[0]) % 360) + 360) % 360,
|
|
84
|
-
|
|
214
|
+
const h = (((Number.isNaN(hsl[0]) ? 0 : hsl[0]) % 360) + 360) % 360,
|
|
215
|
+
s = hsl[1] / 100,
|
|
216
|
+
l = hsl[2] / 100;
|
|
217
|
+
const f = (n: number) => {
|
|
218
|
+
const k = (n + h / 30) % 12,
|
|
219
|
+
a = s * Math.min(l, 1 - l);
|
|
220
|
+
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
221
|
+
};
|
|
85
222
|
return [f(0), f(8), f(4)];
|
|
86
223
|
}
|
|
87
224
|
function srgbHue(rgb: Vec3) {
|
|
88
|
-
const max = Math.max(...rgb),
|
|
225
|
+
const max = Math.max(...rgb),
|
|
226
|
+
min = Math.min(...rgb),
|
|
227
|
+
d = max - min;
|
|
89
228
|
if (d < 1e-9) return NaN; // a gray; the hue would be float noise
|
|
90
|
-
let h: number;
|
|
229
|
+
let h: number;
|
|
230
|
+
if (max === rgb[0]) h = (rgb[1] - rgb[2]) / d + (rgb[1] < rgb[2] ? 6 : 0);
|
|
231
|
+
else if (max === rgb[1]) h = (rgb[2] - rgb[0]) / d + 2;
|
|
232
|
+
else h = (rgb[0] - rgb[1]) / d + 4;
|
|
91
233
|
return h * 60;
|
|
92
234
|
}
|
|
93
235
|
function srgbToHsl(rgb: Vec3): Vec3 {
|
|
94
|
-
const max = Math.max(...rgb),
|
|
95
|
-
|
|
96
|
-
|
|
236
|
+
const max = Math.max(...rgb),
|
|
237
|
+
min = Math.min(...rgb),
|
|
238
|
+
l = (min + max) / 2;
|
|
239
|
+
let h = srgbHue(rgb),
|
|
240
|
+
s = max === min || l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);
|
|
241
|
+
if (s < 0) {
|
|
242
|
+
h += 180;
|
|
243
|
+
s = -s;
|
|
244
|
+
}
|
|
245
|
+
if (h >= 360) h -= 360;
|
|
97
246
|
return [h, s * 100, l * 100];
|
|
98
247
|
}
|
|
99
248
|
function hwbToSrgb(hwb: Vec3): Vec3 {
|
|
100
|
-
const w = hwb[1] / 100,
|
|
101
|
-
|
|
249
|
+
const w = hwb[1] / 100,
|
|
250
|
+
b = hwb[2] / 100;
|
|
251
|
+
if (w + b >= 1) {
|
|
252
|
+
const g = w / (w + b);
|
|
253
|
+
return [g, g, g];
|
|
254
|
+
}
|
|
255
|
+
const rgb = hslToSrgb([hwb[0], 100, 50]),
|
|
256
|
+
scale = 1 - w - b;
|
|
257
|
+
return [rgb[0] * scale + w, rgb[1] * scale + w, rgb[2] * scale + w];
|
|
102
258
|
}
|
|
103
259
|
const srgbToHwb = (rgb: Vec3): Vec3 => [srgbHue(rgb), Math.min(...rgb) * 100, (1 - Math.max(...rgb)) * 100];
|
|
104
260
|
|
|
105
261
|
function toXyz(c: Vec3, space: Space): Vec3 {
|
|
106
262
|
switch (space) {
|
|
107
|
-
case "srgb":
|
|
108
|
-
|
|
109
|
-
case "
|
|
110
|
-
|
|
111
|
-
case "
|
|
112
|
-
|
|
113
|
-
case "
|
|
114
|
-
|
|
115
|
-
case "
|
|
116
|
-
|
|
117
|
-
case "
|
|
118
|
-
|
|
119
|
-
case "
|
|
120
|
-
|
|
263
|
+
case "srgb":
|
|
264
|
+
return rgbToXyz(c, srgbLin, LIN_SRGB_TO_XYZ);
|
|
265
|
+
case "p3":
|
|
266
|
+
return rgbToXyz(c, srgbLin, LIN_P3_TO_XYZ);
|
|
267
|
+
case "rec2020":
|
|
268
|
+
return rgbToXyz(c, rec2020Lin, LIN_REC2020_TO_XYZ);
|
|
269
|
+
case "prophoto-rgb":
|
|
270
|
+
return mul(XYZ_D50_TO_D65, rgbToXyz(c, prophotoLin, LIN_PRO_TO_XYZ_D50));
|
|
271
|
+
case "a98-rgb":
|
|
272
|
+
return rgbToXyz(c, a98Lin, LIN_A98_TO_XYZ);
|
|
273
|
+
case "srgb-linear":
|
|
274
|
+
return mul(LIN_SRGB_TO_XYZ, c);
|
|
275
|
+
case "xyz-d65":
|
|
276
|
+
return [c[0], c[1], c[2]];
|
|
277
|
+
case "xyz-d50":
|
|
278
|
+
return mul(XYZ_D50_TO_D65, c);
|
|
279
|
+
case "oklab":
|
|
280
|
+
return oklabToXyz(c);
|
|
281
|
+
case "oklch":
|
|
282
|
+
return oklabToXyz(toRect(c));
|
|
283
|
+
case "lab":
|
|
284
|
+
return labToXyz(c);
|
|
285
|
+
case "lch":
|
|
286
|
+
return labToXyz(toRect(c));
|
|
287
|
+
case "hsl":
|
|
288
|
+
return rgbToXyz(hslToSrgb(c), srgbLin, LIN_SRGB_TO_XYZ);
|
|
289
|
+
case "hwb":
|
|
290
|
+
return rgbToXyz(hwbToSrgb(c), srgbLin, LIN_SRGB_TO_XYZ);
|
|
121
291
|
}
|
|
122
292
|
}
|
|
123
293
|
function fromXyz(xyz: Vec3, space: Space): Vec3 {
|
|
124
294
|
switch (space) {
|
|
125
|
-
case "srgb":
|
|
126
|
-
|
|
127
|
-
case "
|
|
128
|
-
|
|
129
|
-
case "
|
|
130
|
-
|
|
131
|
-
case "
|
|
132
|
-
|
|
133
|
-
case "
|
|
134
|
-
|
|
135
|
-
case "
|
|
136
|
-
|
|
137
|
-
case "
|
|
138
|
-
|
|
295
|
+
case "srgb":
|
|
296
|
+
return xyzToRgb(xyz, srgbGam, XYZ_TO_LIN_SRGB);
|
|
297
|
+
case "p3":
|
|
298
|
+
return xyzToRgb(xyz, srgbGam, XYZ_TO_LIN_P3);
|
|
299
|
+
case "rec2020":
|
|
300
|
+
return xyzToRgb(xyz, rec2020Gam, XYZ_TO_LIN_REC2020);
|
|
301
|
+
case "prophoto-rgb":
|
|
302
|
+
return xyzToRgb(mul(XYZ_D65_TO_D50, xyz), prophotoGam, XYZ_D50_TO_LIN_PRO);
|
|
303
|
+
case "a98-rgb":
|
|
304
|
+
return xyzToRgb(xyz, a98Gam, XYZ_TO_LIN_A98);
|
|
305
|
+
case "srgb-linear":
|
|
306
|
+
return xyzToRgb(xyz, same, XYZ_TO_LIN_SRGB);
|
|
307
|
+
case "xyz-d65":
|
|
308
|
+
return [xyz[0], xyz[1], xyz[2]];
|
|
309
|
+
case "xyz-d50":
|
|
310
|
+
return mul(XYZ_D65_TO_D50, xyz);
|
|
311
|
+
case "oklab":
|
|
312
|
+
return xyzToOklab(xyz);
|
|
313
|
+
case "oklch":
|
|
314
|
+
return toPolar(xyzToOklab(xyz));
|
|
315
|
+
case "lab":
|
|
316
|
+
return xyzToLab(xyz);
|
|
317
|
+
case "lch":
|
|
318
|
+
return toPolar(xyzToLab(xyz));
|
|
319
|
+
case "hsl":
|
|
320
|
+
return srgbToHsl(xyzToRgb(xyz, srgbGam, XYZ_TO_LIN_SRGB));
|
|
321
|
+
case "hwb":
|
|
322
|
+
return srgbToHwb(xyzToRgb(xyz, srgbGam, XYZ_TO_LIN_SRGB));
|
|
139
323
|
}
|
|
140
324
|
}
|
|
141
325
|
export function convert(coords: Vec3, from: Space, to: Space): Vec3 {
|
|
@@ -151,38 +335,70 @@ export function convert(coords: Vec3, from: Space, to: Space): Vec3 {
|
|
|
151
335
|
const SLACK = 0.000075;
|
|
152
336
|
const inRange = (rgb: Vec3) => rgb.every((c) => c >= -SLACK && c <= 1 + SLACK);
|
|
153
337
|
const inGamut = (oklch: Vec3, gamut: Gamut) => inRange(convert(oklch, "oklch", gamut));
|
|
154
|
-
|
|
338
|
+
// map() on a tuple reads back as an array
|
|
339
|
+
const clip = (rgb: Vec3) => rgb.map((c) => Math.min(1, Math.max(0, c))) as Vec3;
|
|
155
340
|
const deltaEOK = (a: Vec3, b: Vec3) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
156
341
|
/** Map OKLCH into an RGB gamut (CSS Color 4 chroma reduction + local clip). */
|
|
157
342
|
export function toGamut(oklch: Vec3, dest: Gamut): Vec3 {
|
|
158
343
|
if (inGamut(oklch, dest)) return convert(oklch, "oklch", dest);
|
|
159
|
-
const L = oklch[0];
|
|
344
|
+
const L = oklch[0];
|
|
345
|
+
if (L >= 1) return [1, 1, 1];
|
|
346
|
+
if (L <= 0) return [0, 0, 0];
|
|
160
347
|
// An unbounded chroma can't be bisected; fall to the achromatic color at this lightness.
|
|
161
348
|
if (!Number.isFinite(oklch[1])) return clip(convert([L, 0, num(oklch[2])], "oklch", dest));
|
|
162
|
-
const JND = 0.02,
|
|
163
|
-
|
|
349
|
+
const JND = 0.02,
|
|
350
|
+
EPS = 0.0001;
|
|
351
|
+
const cur: Vec3 = [oklch[0], oklch[1], oklch[2]];
|
|
352
|
+
let min = 0,
|
|
353
|
+
max = oklch[1],
|
|
354
|
+
minIn = true,
|
|
355
|
+
clipped = clip(convert(cur, "oklch", dest));
|
|
164
356
|
if (deltaEOK(convert(clipped, dest, "oklab"), convert(oklch, "oklch", "oklab")) < JND) return clipped;
|
|
165
357
|
while (max - min > EPS) {
|
|
166
|
-
const chroma = (min + max) / 2;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
358
|
+
const chroma = (min + max) / 2;
|
|
359
|
+
cur[1] = chroma;
|
|
360
|
+
const inDest = convert(cur, "oklch", dest);
|
|
361
|
+
if (minIn && inRange(inDest)) {
|
|
362
|
+
min = chroma;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
clipped = clip(inDest);
|
|
366
|
+
const e = deltaEOK(convert(clipped, dest, "oklab"), convert(cur, "oklch", "oklab"));
|
|
367
|
+
if (e < JND) {
|
|
368
|
+
if (JND - e < EPS) return clipped;
|
|
369
|
+
minIn = false;
|
|
370
|
+
min = chroma;
|
|
371
|
+
} else max = chroma;
|
|
170
372
|
}
|
|
171
373
|
return clipped;
|
|
172
374
|
}
|
|
173
375
|
// At a fixed hue OKLab→LMS collapses to p_i = L + C·d_i and LMS→XYZ→RGB to one matrix F, so the
|
|
174
376
|
// two per-pixel helpers run trig- and allocation-free; identical to convert([L,C,hue], "oklch", gamut).
|
|
175
377
|
const hueFold = (hue: number, gamut: Gamut) => {
|
|
176
|
-
const h = hue * RAD,
|
|
378
|
+
const h = hue * RAD,
|
|
379
|
+
cos = Math.cos(h),
|
|
380
|
+
sin = Math.sin(h);
|
|
177
381
|
const d = OKLAB_TO_LMS.map((row) => cos * row[1] + sin * row[2]);
|
|
178
|
-
const F = mulMat(
|
|
382
|
+
const F = mulMat(
|
|
383
|
+
gamut === "p3" ? XYZ_TO_LIN_P3 : gamut === "rec2020" ? XYZ_TO_LIN_REC2020 : XYZ_TO_LIN_SRGB,
|
|
384
|
+
LMS_TO_XYZ,
|
|
385
|
+
);
|
|
179
386
|
return { d, F, gam: gamut === "rec2020" ? rec2020Gam : srgbGam };
|
|
180
387
|
};
|
|
181
388
|
/** OKLCH→RGB at a fixed hue for per-pixel rasterizing; `(L, C, out)` writes into `out`. */
|
|
182
389
|
export function oklchToRgbFn(hue: number, gamut: Gamut) {
|
|
183
|
-
const {
|
|
390
|
+
const {
|
|
391
|
+
d: [d0, d1, d2],
|
|
392
|
+
F,
|
|
393
|
+
gam,
|
|
394
|
+
} = hueFold(hue, gamut);
|
|
184
395
|
return (L: number, C: number, out: number[]) => {
|
|
185
|
-
const p0 = L + C * d0,
|
|
396
|
+
const p0 = L + C * d0,
|
|
397
|
+
p1 = L + C * d1,
|
|
398
|
+
p2 = L + C * d2,
|
|
399
|
+
c0 = p0 ** 3,
|
|
400
|
+
c1 = p1 ** 3,
|
|
401
|
+
c2 = p2 ** 3;
|
|
186
402
|
out[0] = gam(F[0][0] * c0 + F[0][1] * c1 + F[0][2] * c2);
|
|
187
403
|
out[1] = gam(F[1][0] * c0 + F[1][1] * c1 + F[1][2] * c2);
|
|
188
404
|
out[2] = gam(F[2][0] * c0 + F[2][1] * c1 + F[2][2] * c2);
|
|
@@ -190,69 +406,173 @@ export function oklchToRgbFn(hue: number, gamut: Gamut) {
|
|
|
190
406
|
}
|
|
191
407
|
/** Fast in-gamut probe at a fixed hue: `(L, C) => inside?`. */
|
|
192
408
|
export function oklchGamutProbe(hue: number, gamut: Gamut) {
|
|
193
|
-
const toRgb = oklchToRgbFn(hue, gamut),
|
|
194
|
-
|
|
409
|
+
const toRgb = oklchToRgbFn(hue, gamut),
|
|
410
|
+
rgb = [0, 0, 0],
|
|
411
|
+
lo = -SLACK,
|
|
412
|
+
hi = 1 + SLACK,
|
|
413
|
+
inside = (v: number) => v >= lo && v <= hi;
|
|
414
|
+
return (L: number, C: number) => {
|
|
415
|
+
toRgb(L, C, rgb);
|
|
416
|
+
return rgb.every(inside);
|
|
417
|
+
};
|
|
195
418
|
}
|
|
196
419
|
/** Bisect the chroma ceiling (highest in-gamut C) at lightness L for an `oklchGamutProbe`. */
|
|
197
420
|
export function chromaCeil(probe: (L: number, C: number) => boolean, L: number, hi = 0.5) {
|
|
198
421
|
if (!probe(L, 0)) return 0;
|
|
199
|
-
let lo = 0;
|
|
422
|
+
let lo = 0;
|
|
423
|
+
for (let k = 0; k < 16; k++) {
|
|
424
|
+
const m = (lo + hi) / 2;
|
|
425
|
+
probe(L, m) ? (lo = m) : (hi = m);
|
|
426
|
+
}
|
|
200
427
|
return lo;
|
|
201
428
|
}
|
|
202
429
|
|
|
203
430
|
// ── hex ──
|
|
204
431
|
/** One 0–1 channel → a 2-digit hex byte (also the alpha suffix for #RRGGBBAA), upper-case as the readout shows it. */
|
|
205
|
-
export const hexByte = (n: number) =>
|
|
432
|
+
export const hexByte = (n: number) =>
|
|
433
|
+
Math.round(Math.min(1, Math.max(0, n)) * 255)
|
|
434
|
+
.toString(16)
|
|
435
|
+
.padStart(2, "0")
|
|
436
|
+
.toUpperCase();
|
|
206
437
|
const rgbToHex = (rgb: Vec3) => `#${hexByte(rgb[0])}${hexByte(rgb[1])}${hexByte(rgb[2])}`;
|
|
207
438
|
export const oklchToHex = (L: number, C: number, H: number) => rgbToHex(toGamut([L, C, H], "srgb"));
|
|
208
439
|
export function hexToOklch(hex: string): Vec3 {
|
|
209
440
|
let h = String(hex).replace("#", "").trim();
|
|
210
|
-
if (h.length === 3)
|
|
441
|
+
if (h.length === 3)
|
|
442
|
+
h = h
|
|
443
|
+
.split("")
|
|
444
|
+
.map((c) => c + c)
|
|
445
|
+
.join("");
|
|
211
446
|
if (h.length < 6) return [0.7, 0.1, 280];
|
|
212
|
-
const rgb: Vec3 = [
|
|
447
|
+
const rgb: Vec3 = [
|
|
448
|
+
parseInt(h.slice(0, 2), 16) / 255,
|
|
449
|
+
parseInt(h.slice(2, 4), 16) / 255,
|
|
450
|
+
parseInt(h.slice(4, 6), 16) / 255,
|
|
451
|
+
];
|
|
213
452
|
return convert(rgb, "srgb", "oklch").map(num) as Vec3;
|
|
214
453
|
}
|
|
215
454
|
|
|
216
455
|
// ── mode model ──
|
|
217
|
-
export const EDIT_MODES: readonly ColorMode[] = [
|
|
218
|
-
|
|
456
|
+
export const EDIT_MODES: readonly ColorMode[] = [
|
|
457
|
+
"hex",
|
|
458
|
+
"srgb",
|
|
459
|
+
"css",
|
|
460
|
+
"hsl",
|
|
461
|
+
"hwb",
|
|
462
|
+
"oklch",
|
|
463
|
+
"oklab",
|
|
464
|
+
"lch",
|
|
465
|
+
"lab",
|
|
466
|
+
"p3",
|
|
467
|
+
"rec2020",
|
|
468
|
+
];
|
|
469
|
+
export const MODE_LABELS: Record<ColorMode, string> = {
|
|
470
|
+
hex: "HEX",
|
|
471
|
+
srgb: "RGB",
|
|
472
|
+
css: "CSS",
|
|
473
|
+
hsl: "HSL",
|
|
474
|
+
hwb: "HWB",
|
|
475
|
+
oklch: "OKLCH",
|
|
476
|
+
oklab: "OKLab",
|
|
477
|
+
lch: "LCH",
|
|
478
|
+
lab: "Lab",
|
|
479
|
+
p3: "P3",
|
|
480
|
+
rec2020: "Rec2020",
|
|
481
|
+
};
|
|
219
482
|
const SRGB_BOUND: readonly ColorMode[] = ["srgb", "css", "hsl", "hwb", "hex"];
|
|
220
483
|
export const showsGamutBoundary = (mode: ColorMode) => !SRGB_BOUND.includes(mode);
|
|
221
484
|
const modeSpaceId = (mode: ColorMode): Space => (mode === "hex" || mode === "css" ? "srgb" : mode);
|
|
222
485
|
// The CSS <color-interpolation-method> a gradient authored in each mode blends through.
|
|
223
486
|
// Null-prototype, so a mode named "constructor" misses and falls to the default.
|
|
224
|
-
const MODE_INTERP: Record<string, GradientInterpolation> = Object.assign(Object.create(null), {
|
|
487
|
+
const MODE_INTERP: Record<string, GradientInterpolation> = Object.assign(Object.create(null), {
|
|
488
|
+
hex: "srgb",
|
|
489
|
+
srgb: "srgb",
|
|
490
|
+
css: "srgb",
|
|
491
|
+
hsl: "hsl",
|
|
492
|
+
hwb: "hwb",
|
|
493
|
+
oklch: "oklch",
|
|
494
|
+
oklab: "oklab",
|
|
495
|
+
lch: "lch",
|
|
496
|
+
lab: "lab",
|
|
497
|
+
p3: "display-p3",
|
|
498
|
+
rec2020: "rec2020",
|
|
499
|
+
});
|
|
225
500
|
export const modeInterpolation = (mode: string) => MODE_INTERP[mode] || "oklch";
|
|
226
501
|
// The reverse: the mode that produced a stored gradient's blend; unknown falls to OKLCH.
|
|
227
|
-
const INTERP_MODE: Record<string, ColorMode> = Object.assign(Object.create(null), {
|
|
502
|
+
const INTERP_MODE: Record<string, ColorMode> = Object.assign(Object.create(null), {
|
|
503
|
+
srgb: "srgb",
|
|
504
|
+
hsl: "hsl",
|
|
505
|
+
hwb: "hwb",
|
|
506
|
+
oklch: "oklch",
|
|
507
|
+
oklab: "oklab",
|
|
508
|
+
lch: "lch",
|
|
509
|
+
lab: "lab",
|
|
510
|
+
"display-p3": "p3",
|
|
511
|
+
rec2020: "rec2020",
|
|
512
|
+
});
|
|
228
513
|
export const interpolationMode = (interp: string) => INTERP_MODE[interp] || "oklch";
|
|
229
514
|
/** The picker's chroma ceiling: the C field's max and the parser's cap on any chroma it reads. */
|
|
230
515
|
export const MAX_CHROMA = 0.5;
|
|
231
516
|
// Each triple is shared by two modes and frozen, so a one-mode tweak can't leak into its twin.
|
|
232
|
-
const rgbTriple = (max: number, step: number, scale: number): readonly Channel[] =>
|
|
233
|
-
|
|
517
|
+
const rgbTriple = (max: number, step: number, scale: number): readonly Channel[] =>
|
|
518
|
+
Object.freeze(["R", "G", "B"].map((k) => Object.freeze({ k, min: 0, max, step, scale })));
|
|
519
|
+
const RGB_255 = rgbTriple(255, 1, 255),
|
|
520
|
+
RGB_UNIT = rgbTriple(1, 0.01, 1);
|
|
234
521
|
export const MODE_CHANNELS: Partial<Record<ColorMode, readonly Channel[]>> = {
|
|
235
|
-
oklch: [
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
522
|
+
oklch: [
|
|
523
|
+
{ k: "L", min: 0, max: 100, step: 1, scale: 100 },
|
|
524
|
+
{ k: "C", min: 0, max: MAX_CHROMA, step: 0.01, scale: 1 },
|
|
525
|
+
{ k: "H", min: 0, max: 360, step: 1, scale: 1 },
|
|
526
|
+
],
|
|
527
|
+
oklab: [
|
|
528
|
+
{ k: "L", min: 0, max: 100, step: 1, scale: 100 },
|
|
529
|
+
{ k: "a", min: -0.4, max: 0.4, step: 0.01, scale: 1 },
|
|
530
|
+
{ k: "b", min: -0.4, max: 0.4, step: 0.01, scale: 1 },
|
|
531
|
+
],
|
|
532
|
+
lch: [
|
|
533
|
+
{ k: "L", min: 0, max: 100, step: 1, scale: 1 },
|
|
534
|
+
{ k: "C", min: 0, max: 150, step: 1, scale: 1 },
|
|
535
|
+
{ k: "H", min: 0, max: 360, step: 1, scale: 1 },
|
|
536
|
+
],
|
|
537
|
+
lab: [
|
|
538
|
+
{ k: "L", min: 0, max: 100, step: 1, scale: 1 },
|
|
539
|
+
{ k: "a", min: -125, max: 125, step: 1, scale: 1 },
|
|
540
|
+
{ k: "b", min: -125, max: 125, step: 1, scale: 1 },
|
|
541
|
+
],
|
|
542
|
+
srgb: RGB_255,
|
|
543
|
+
css: RGB_255,
|
|
544
|
+
hsl: [
|
|
545
|
+
{ k: "H", min: 0, max: 360, step: 1, scale: 1 },
|
|
546
|
+
{ k: "S", min: 0, max: 100, step: 1, scale: 1 },
|
|
547
|
+
{ k: "L", min: 0, max: 100, step: 1, scale: 1 },
|
|
548
|
+
],
|
|
549
|
+
hwb: [
|
|
550
|
+
{ k: "H", min: 0, max: 360, step: 1, scale: 1 },
|
|
551
|
+
{ k: "W", min: 0, max: 100, step: 1, scale: 1 },
|
|
552
|
+
{ k: "B", min: 0, max: 100, step: 1, scale: 1 },
|
|
553
|
+
],
|
|
554
|
+
p3: RGB_UNIT,
|
|
555
|
+
rec2020: RGB_UNIT,
|
|
243
556
|
};
|
|
244
557
|
const digitsFor = (step: number) => (step >= 1 ? 0 : step >= 0.1 ? 1 : step >= 0.01 ? 2 : 3);
|
|
245
558
|
|
|
246
559
|
// Display coords for `mode` from canonical OKLCH. sRGB-bound modes read off the same
|
|
247
560
|
// chroma-reduced sRGB hex uses, so every narrow-gamut mode agrees on the color.
|
|
248
561
|
const modeCoords = (oklch: Vec3, sid: Space, mode: ColorMode): Vec3 =>
|
|
249
|
-
sid === "oklch"
|
|
250
|
-
|
|
251
|
-
|
|
562
|
+
sid === "oklch"
|
|
563
|
+
? [oklch[0], oklch[1], oklch[2]]
|
|
564
|
+
: SRGB_BOUND.includes(mode)
|
|
565
|
+
? sid === "srgb"
|
|
566
|
+
? toGamut(oklch, "srgb")
|
|
567
|
+
: convert(toGamut(oklch, "srgb"), "srgb", sid)
|
|
568
|
+
: convert(oklch, "oklch", sid);
|
|
252
569
|
/** Display-unit channel values for `mode` from canonical OKLCH. */
|
|
253
570
|
export function channelValues(oklch: Vec3, mode: ColorMode) {
|
|
254
571
|
const c = modeCoords(oklch, modeSpaceId(mode), mode);
|
|
255
|
-
return MODE_CHANNELS[mode].map((ch, i) => {
|
|
572
|
+
return MODE_CHANNELS[mode].map((ch, i) => {
|
|
573
|
+
const v = num(c[i]) * ch.scale;
|
|
574
|
+
return clamp(Math.abs(v) < 0.5 * ch.step ? 0 : v, ch.min, ch.max);
|
|
575
|
+
});
|
|
256
576
|
}
|
|
257
577
|
/** New OKLCH with channel `index` of `mode` set to `displayValue`. */
|
|
258
578
|
export function withChannel(oklch: Vec3, mode: ColorMode, index: number, displayValue: number): Vec3 {
|
|
@@ -273,28 +593,55 @@ export function gamutLabel(oklch: Vec3, mode: ColorMode) {
|
|
|
273
593
|
}
|
|
274
594
|
// The three channels in `mode`'s own syntax: commas for legacy css, a % on lightness or on the last two of hsl/hwb.
|
|
275
595
|
const joinChannels = (mode: ColorMode, s: (i: number) => string) =>
|
|
276
|
-
mode === "css"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
596
|
+
mode === "css"
|
|
597
|
+
? `${s(0)}, ${s(1)}, ${s(2)}`
|
|
598
|
+
: mode === "oklch" || mode === "oklab" || mode === "lch" || mode === "lab"
|
|
599
|
+
? `${s(0)}% ${s(1)} ${s(2)}`
|
|
600
|
+
: mode === "hsl" || mode === "hwb"
|
|
601
|
+
? `${s(0)} ${s(1)}% ${s(2)}%`
|
|
602
|
+
: `${s(0)} ${s(1)} ${s(2)}`; // srgb / p3 / rec2020
|
|
280
603
|
/** The collapsed-row value string for `mode` (bare channels w/ units, or hex). */
|
|
281
604
|
export function readout(oklch: Vec3, mode: ColorMode) {
|
|
282
605
|
if (mode === "hex") return oklchToHex(oklch[0], oklch[1], oklch[2]);
|
|
283
|
-
const chans = MODE_CHANNELS[mode],
|
|
284
|
-
|
|
606
|
+
const chans = MODE_CHANNELS[mode],
|
|
607
|
+
v = channelValues(oklch, mode);
|
|
608
|
+
// 360 is 0; only 0 reads back as itself
|
|
609
|
+
return joinChannels(mode, (i) => {
|
|
610
|
+
const t = v[i].toFixed(digitsFor(chans[i].step));
|
|
611
|
+
return chans[i].k === "H" && +t === 360 ? (0).toFixed(digitsFor(chans[i].step)) : t;
|
|
612
|
+
});
|
|
285
613
|
}
|
|
286
614
|
// css is legacy comma rgba with an explicit alpha slot; the rest take modern space syntax.
|
|
287
615
|
const wrapCss = (t: string, mode: ColorMode) =>
|
|
288
|
-
mode === "hex"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
616
|
+
mode === "hex"
|
|
617
|
+
? t
|
|
618
|
+
: mode === "srgb"
|
|
619
|
+
? `rgb(${t})`
|
|
620
|
+
: mode === "css"
|
|
621
|
+
? `rgba(${t}, 1)`
|
|
622
|
+
: mode === "hsl"
|
|
623
|
+
? `hsl(${t})`
|
|
624
|
+
: mode === "hwb"
|
|
625
|
+
? `hwb(${t})`
|
|
626
|
+
: mode === "p3"
|
|
627
|
+
? `color(display-p3 ${t})`
|
|
628
|
+
: mode === "rec2020"
|
|
629
|
+
? `color(rec2020 ${t})`
|
|
630
|
+
: `${mode}(${t})`;
|
|
631
|
+
// The value's channels, a digit finer than the readout (L 63.2%, C 0.257, H 29.2): rounded to the readout, 98% of
|
|
632
|
+
// colors came back different (#939796's faint green read as a neutral gray). A powerless hue is `none`, as CSS Color 4
|
|
633
|
+
// writes it: a float-noise hue on white (180, cyan) or a gray bent every oklch gradient and color-mix() through it.
|
|
634
|
+
// HSL's black and white are written S 0 and hue none: S divides by min(L, 1 - L), so next to either end it read noise
|
|
635
|
+
// as 100% and a hue.
|
|
295
636
|
function precise(oklch: Vec3, mode: ColorMode) {
|
|
296
|
-
const chans = MODE_CHANNELS[mode],
|
|
297
|
-
|
|
637
|
+
const chans = MODE_CHANNELS[mode],
|
|
638
|
+
c = modeCoords(oklch, modeSpaceId(mode), mode);
|
|
639
|
+
// + 0: no -0
|
|
640
|
+
const v = chans.map((ch, i) => {
|
|
641
|
+
const d = digitsFor(ch.step) + 1,
|
|
642
|
+
x = +clamp(num(c[i]) * ch.scale, ch.min, ch.max).toFixed(d);
|
|
643
|
+
return ch.k === "H" && x === 360 ? 0 : x + 0;
|
|
644
|
+
});
|
|
298
645
|
if (mode === "hsl" && (v[2] === 0 || v[2] === 100)) v[1] = 0;
|
|
299
646
|
const gray = mode === "oklch" || mode === "lch" || mode === "hsl" ? v[1] === 0 : mode === "hwb" && v[1] + v[2] >= 100;
|
|
300
647
|
return joinChannels(mode, (i) => (gray && chans[i].k === "H" ? "none" : String(v[i])));
|
|
@@ -306,5 +653,6 @@ export function serialize(oklch: Vec3, mode: ColorMode, alpha: number) {
|
|
|
306
653
|
const t = precise(oklch, mode);
|
|
307
654
|
if (a >= 0.999) return wrapCss(t, mode);
|
|
308
655
|
const av = +a.toFixed(3);
|
|
309
|
-
|
|
656
|
+
// comma alpha for legacy rgba, slash otherwise
|
|
657
|
+
return mode === "css" ? `rgba(${t}, ${av})` : wrapCss(`${t} / ${av}`, mode);
|
|
310
658
|
}
|