@tenphi/glaze 0.0.0-snapshot.042c9e4
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/LICENSE +21 -0
- package/README.md +1124 -0
- package/dist/index.cjs +1716 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +553 -0
- package/dist/index.d.mts +553 -0
- package/dist/index.mjs +1699 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1716 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/okhsl-color-math.ts
|
|
4
|
+
const OKLab_to_LMS_M = [
|
|
5
|
+
[
|
|
6
|
+
1,
|
|
7
|
+
.3963377773761749,
|
|
8
|
+
.2158037573099136
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
1,
|
|
12
|
+
-.1055613458156586,
|
|
13
|
+
-.0638541728258133
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
1,
|
|
17
|
+
-.0894841775298119,
|
|
18
|
+
-1.2914855480194092
|
|
19
|
+
]
|
|
20
|
+
];
|
|
21
|
+
const LMS_to_linear_sRGB_M = [
|
|
22
|
+
[
|
|
23
|
+
4.076741636075959,
|
|
24
|
+
-3.307711539258062,
|
|
25
|
+
.2309699031821041
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
-1.2684379732850313,
|
|
29
|
+
2.6097573492876878,
|
|
30
|
+
-.3413193760026569
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
-.004196076138675526,
|
|
34
|
+
-.703418617935936,
|
|
35
|
+
1.7076146940746113
|
|
36
|
+
]
|
|
37
|
+
];
|
|
38
|
+
const linear_sRGB_to_LMS_M = [
|
|
39
|
+
[
|
|
40
|
+
.4122214708,
|
|
41
|
+
.5363325363,
|
|
42
|
+
.0514459929
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
.2119034982,
|
|
46
|
+
.6806995451,
|
|
47
|
+
.1073969566
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
.0883024619,
|
|
51
|
+
.2817188376,
|
|
52
|
+
.6299787005
|
|
53
|
+
]
|
|
54
|
+
];
|
|
55
|
+
const LMS_to_OKLab_M = [
|
|
56
|
+
[
|
|
57
|
+
.2104542553,
|
|
58
|
+
.793617785,
|
|
59
|
+
-.0040720468
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
1.9779984951,
|
|
63
|
+
-2.428592205,
|
|
64
|
+
.4505937099
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
.0259040371,
|
|
68
|
+
.7827717662,
|
|
69
|
+
-.808675766
|
|
70
|
+
]
|
|
71
|
+
];
|
|
72
|
+
const OKLab_to_linear_sRGB_coefficients = [
|
|
73
|
+
[[-1.8817030993265873, -.8093650129914302], [
|
|
74
|
+
1.19086277,
|
|
75
|
+
1.76576728,
|
|
76
|
+
.59662641,
|
|
77
|
+
.75515197,
|
|
78
|
+
.56771245
|
|
79
|
+
]],
|
|
80
|
+
[[1.8144407988010998, -1.194452667805235], [
|
|
81
|
+
.73956515,
|
|
82
|
+
-.45954404,
|
|
83
|
+
.08285427,
|
|
84
|
+
.1254107,
|
|
85
|
+
.14503204
|
|
86
|
+
]],
|
|
87
|
+
[[.13110757611180954, 1.813339709266608], [
|
|
88
|
+
1.35733652,
|
|
89
|
+
-.00915799,
|
|
90
|
+
-1.1513021,
|
|
91
|
+
-.50559606,
|
|
92
|
+
.00692167
|
|
93
|
+
]]
|
|
94
|
+
];
|
|
95
|
+
const TAU = 2 * Math.PI;
|
|
96
|
+
const K1 = .206;
|
|
97
|
+
const K2 = .03;
|
|
98
|
+
const K3 = (1 + K1) / (1 + K2);
|
|
99
|
+
const EPSILON = 1e-10;
|
|
100
|
+
const constrainAngle = (angle) => (angle % 360 + 360) % 360;
|
|
101
|
+
const toe = (x) => .5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
|
|
102
|
+
const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
|
|
103
|
+
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
104
|
+
const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
|
|
105
|
+
const transform = (input, matrix) => [
|
|
106
|
+
dot3(input, matrix[0]),
|
|
107
|
+
dot3(input, matrix[1]),
|
|
108
|
+
dot3(input, matrix[2])
|
|
109
|
+
];
|
|
110
|
+
const cubed3 = (lms) => [
|
|
111
|
+
lms[0] ** 3,
|
|
112
|
+
lms[1] ** 3,
|
|
113
|
+
lms[2] ** 3
|
|
114
|
+
];
|
|
115
|
+
const cbrt3 = (lms) => [
|
|
116
|
+
Math.cbrt(lms[0]),
|
|
117
|
+
Math.cbrt(lms[1]),
|
|
118
|
+
Math.cbrt(lms[2])
|
|
119
|
+
];
|
|
120
|
+
const clampVal = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
121
|
+
const OKLabToLinearSRGB = (lab) => {
|
|
122
|
+
return transform(cubed3(transform(lab, OKLab_to_LMS_M)), LMS_to_linear_sRGB_M);
|
|
123
|
+
};
|
|
124
|
+
const computeMaxSaturationOKLC = (a, b) => {
|
|
125
|
+
const okCoeff = OKLab_to_linear_sRGB_coefficients;
|
|
126
|
+
const lmsToRgb = LMS_to_linear_sRGB_M;
|
|
127
|
+
const tmp2 = [a, b];
|
|
128
|
+
const tmp3 = [
|
|
129
|
+
0,
|
|
130
|
+
a,
|
|
131
|
+
b
|
|
132
|
+
];
|
|
133
|
+
let chnlCoeff;
|
|
134
|
+
let chnlLMS;
|
|
135
|
+
if (dotXY(okCoeff[0][0], tmp2) > 1) {
|
|
136
|
+
chnlCoeff = okCoeff[0][1];
|
|
137
|
+
chnlLMS = lmsToRgb[0];
|
|
138
|
+
} else if (dotXY(okCoeff[1][0], tmp2) > 1) {
|
|
139
|
+
chnlCoeff = okCoeff[1][1];
|
|
140
|
+
chnlLMS = lmsToRgb[1];
|
|
141
|
+
} else {
|
|
142
|
+
chnlCoeff = okCoeff[2][1];
|
|
143
|
+
chnlLMS = lmsToRgb[2];
|
|
144
|
+
}
|
|
145
|
+
const [k0, k1, k2, k3, k4] = chnlCoeff;
|
|
146
|
+
const [wl, wm, ws] = chnlLMS;
|
|
147
|
+
let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;
|
|
148
|
+
const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
|
|
149
|
+
const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
|
|
150
|
+
const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
|
|
151
|
+
const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
|
|
152
|
+
const l_ = 1 + sat * kl;
|
|
153
|
+
const m_ = 1 + sat * km;
|
|
154
|
+
const s_ = 1 + sat * ks;
|
|
155
|
+
const l = l_ ** 3;
|
|
156
|
+
const m = m_ ** 3;
|
|
157
|
+
const s = s_ ** 3;
|
|
158
|
+
const lds = 3 * kl * l_ * l_;
|
|
159
|
+
const mds = 3 * km * m_ * m_;
|
|
160
|
+
const sds = 3 * ks * s_ * s_;
|
|
161
|
+
const lds2 = 6 * kl * kl * l_;
|
|
162
|
+
const mds2 = 6 * km * km * m_;
|
|
163
|
+
const sds2 = 6 * ks * ks * s_;
|
|
164
|
+
const f = wl * l + wm * m + ws * s;
|
|
165
|
+
const f1 = wl * lds + wm * mds + ws * sds;
|
|
166
|
+
const f2 = wl * lds2 + wm * mds2 + ws * sds2;
|
|
167
|
+
sat = sat - f * f1 / (f1 * f1 - .5 * f * f2);
|
|
168
|
+
return sat;
|
|
169
|
+
};
|
|
170
|
+
const findCuspOKLCH = (a, b) => {
|
|
171
|
+
const S_cusp = computeMaxSaturationOKLC(a, b);
|
|
172
|
+
const rgb_at_max = OKLabToLinearSRGB([
|
|
173
|
+
1,
|
|
174
|
+
S_cusp * a,
|
|
175
|
+
S_cusp * b
|
|
176
|
+
]);
|
|
177
|
+
const L_cusp = Math.cbrt(1 / Math.max(Math.max(rgb_at_max[0], rgb_at_max[1]), Math.max(rgb_at_max[2], 0)));
|
|
178
|
+
return [L_cusp, L_cusp * S_cusp];
|
|
179
|
+
};
|
|
180
|
+
const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp) => {
|
|
181
|
+
const lmsToRgb = LMS_to_linear_sRGB_M;
|
|
182
|
+
const tmp3 = [
|
|
183
|
+
0,
|
|
184
|
+
a,
|
|
185
|
+
b
|
|
186
|
+
];
|
|
187
|
+
const floatMax = Number.MAX_VALUE;
|
|
188
|
+
let t;
|
|
189
|
+
const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
|
|
190
|
+
const dotXYZ = (vec, x, y, z) => vec[0] * x + vec[1] * y + vec[2] * z;
|
|
191
|
+
if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0) {
|
|
192
|
+
const denom = c1 * cusp[0] + cusp[1] * (l0 - l1);
|
|
193
|
+
t = denom === 0 ? 0 : cusp[1] * l0 / denom;
|
|
194
|
+
} else {
|
|
195
|
+
const denom = c1 * (cusp[0] - 1) + cusp[1] * (l0 - l1);
|
|
196
|
+
t = denom === 0 ? 0 : cusp[1] * (l0 - 1) / denom;
|
|
197
|
+
const dl = l1 - l0;
|
|
198
|
+
const dc = c1;
|
|
199
|
+
const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
|
|
200
|
+
const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
|
|
201
|
+
const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
|
|
202
|
+
const L = l0 * (1 - t) + t * l1;
|
|
203
|
+
const C = t * c1;
|
|
204
|
+
const l_ = L + C * kl;
|
|
205
|
+
const m_ = L + C * km;
|
|
206
|
+
const s_ = L + C * ks;
|
|
207
|
+
const l = l_ ** 3;
|
|
208
|
+
const m = m_ ** 3;
|
|
209
|
+
const s = s_ ** 3;
|
|
210
|
+
const ldt = 3 * (dl + dc * kl) * l_ * l_;
|
|
211
|
+
const mdt = 3 * (dl + dc * km) * m_ * m_;
|
|
212
|
+
const sdt = 3 * (dl + dc * ks) * s_ * s_;
|
|
213
|
+
const ldt2 = 6 * (dl + dc * kl) ** 2 * l_;
|
|
214
|
+
const mdt2 = 6 * (dl + dc * km) ** 2 * m_;
|
|
215
|
+
const sdt2 = 6 * (dl + dc * ks) ** 2 * s_;
|
|
216
|
+
const r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;
|
|
217
|
+
const r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);
|
|
218
|
+
const r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);
|
|
219
|
+
const ur = r1 / (r1 * r1 - .5 * r_ * r2);
|
|
220
|
+
let tr = -r_ * ur;
|
|
221
|
+
const g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;
|
|
222
|
+
const g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);
|
|
223
|
+
const g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);
|
|
224
|
+
const ug = g1 / (g1 * g1 - .5 * g_ * g2);
|
|
225
|
+
let tg = -g_ * ug;
|
|
226
|
+
const b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;
|
|
227
|
+
const b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);
|
|
228
|
+
const b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);
|
|
229
|
+
const ub = b1 / (b1 * b1 - .5 * b_ * b2);
|
|
230
|
+
let tb = -b_ * ub;
|
|
231
|
+
tr = ur >= 0 ? tr : floatMax;
|
|
232
|
+
tg = ug >= 0 ? tg : floatMax;
|
|
233
|
+
tb = ub >= 0 ? tb : floatMax;
|
|
234
|
+
t += Math.min(tr, Math.min(tg, tb));
|
|
235
|
+
}
|
|
236
|
+
return t;
|
|
237
|
+
};
|
|
238
|
+
const computeSt = (cusp) => [cusp[1] / cusp[0], cusp[1] / (1 - cusp[0])];
|
|
239
|
+
const computeStMid = (a, b) => [.11516993 + 1 / (7.4477897 + 4.1590124 * b + a * (-2.19557347 + 1.75198401 * b + a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a)))), .11239642 + 1 / (1.6132032 - .68124379 * b + a * (.40370612 + .90148123 * b + a * (-.27087943 + .6122399 * b + a * (.00299215 - .45399568 * b - .14661872 * a))))];
|
|
240
|
+
const getCs = (L, a, b, cusp) => {
|
|
241
|
+
const cMax = findGamutIntersectionOKLCH(a, b, L, 1, L, cusp);
|
|
242
|
+
const stMax = computeSt(cusp);
|
|
243
|
+
const k = cMax / Math.min(L * stMax[0], (1 - L) * stMax[1]);
|
|
244
|
+
const stMid = computeStMid(a, b);
|
|
245
|
+
let ca = L * stMid[0];
|
|
246
|
+
let cb = (1 - L) * stMid[1];
|
|
247
|
+
const cMid = .9 * k * Math.sqrt(Math.sqrt(1 / (1 / ca ** 4 + 1 / cb ** 4)));
|
|
248
|
+
ca = L * .4;
|
|
249
|
+
cb = (1 - L) * .8;
|
|
250
|
+
return [
|
|
251
|
+
Math.sqrt(1 / (1 / ca ** 2 + 1 / cb ** 2)),
|
|
252
|
+
cMid,
|
|
253
|
+
cMax
|
|
254
|
+
];
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
|
|
258
|
+
*/
|
|
259
|
+
function okhslToOklab(h, s, l) {
|
|
260
|
+
const L = toeInv(l);
|
|
261
|
+
let a = 0;
|
|
262
|
+
let b = 0;
|
|
263
|
+
const hNorm = constrainAngle(h) / 360;
|
|
264
|
+
if (L !== 0 && L !== 1 && s !== 0) {
|
|
265
|
+
const a_ = Math.cos(TAU * hNorm);
|
|
266
|
+
const b_ = Math.sin(TAU * hNorm);
|
|
267
|
+
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
268
|
+
const mid = .8;
|
|
269
|
+
const midInv = 1.25;
|
|
270
|
+
let t, k0, k1, k2;
|
|
271
|
+
if (s < mid) {
|
|
272
|
+
t = midInv * s;
|
|
273
|
+
k0 = 0;
|
|
274
|
+
k1 = mid * c0;
|
|
275
|
+
k2 = 1 - k1 / cMid;
|
|
276
|
+
} else {
|
|
277
|
+
t = 5 * (s - .8);
|
|
278
|
+
k0 = cMid;
|
|
279
|
+
k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
280
|
+
k2 = 1 - k1 / (cMax - cMid);
|
|
281
|
+
}
|
|
282
|
+
const c = k0 + t * k1 / (1 - k2 * t);
|
|
283
|
+
a = c * a_;
|
|
284
|
+
b = c * b_;
|
|
285
|
+
}
|
|
286
|
+
return [
|
|
287
|
+
L,
|
|
288
|
+
a,
|
|
289
|
+
b
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
|
|
294
|
+
* Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
|
|
295
|
+
*/
|
|
296
|
+
function okhslToLinearSrgb(h, s, l) {
|
|
297
|
+
return OKLabToLinearSRGB(okhslToOklab(h, s, l));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Compute relative luminance Y from linear sRGB channels.
|
|
301
|
+
* Per WCAG 2: Y = 0.2126·R + 0.7152·G + 0.0722·B
|
|
302
|
+
*/
|
|
303
|
+
function relativeLuminanceFromLinearRgb(rgb) {
|
|
304
|
+
return .2126 * rgb[0] + .7152 * rgb[1] + .0722 * rgb[2];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* WCAG 2 contrast ratio from two luminance values.
|
|
308
|
+
*/
|
|
309
|
+
function contrastRatioFromLuminance(yA, yB) {
|
|
310
|
+
const lighter = Math.max(yA, yB);
|
|
311
|
+
const darker = Math.min(yA, yB);
|
|
312
|
+
return (lighter + .05) / (darker + .05);
|
|
313
|
+
}
|
|
314
|
+
const sRGBLinearToGamma = (val) => {
|
|
315
|
+
const sign = val < 0 ? -1 : 1;
|
|
316
|
+
const abs = Math.abs(val);
|
|
317
|
+
return abs > .0031308 ? sign * (1.055 * Math.pow(abs, 1 / 2.4) - .055) : 12.92 * val;
|
|
318
|
+
};
|
|
319
|
+
const sRGBGammaToLinear = (val) => {
|
|
320
|
+
const sign = val < 0 ? -1 : 1;
|
|
321
|
+
const abs = Math.abs(val);
|
|
322
|
+
return abs <= .04045 ? val / 12.92 : sign * Math.pow((abs + .055) / 1.055, 2.4);
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
|
|
326
|
+
*/
|
|
327
|
+
function okhslToSrgb(h, s, l) {
|
|
328
|
+
const lin = okhslToLinearSrgb(h, s, l);
|
|
329
|
+
return [
|
|
330
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(lin[0]))),
|
|
331
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(lin[1]))),
|
|
332
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(lin[2])))
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Compute WCAG 2 relative luminance from linear sRGB, matching the browser
|
|
337
|
+
* rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
|
|
338
|
+
* This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
|
|
339
|
+
*/
|
|
340
|
+
function gamutClampedLuminance(linearRgb) {
|
|
341
|
+
const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
|
|
342
|
+
const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
|
|
343
|
+
const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
|
|
344
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
345
|
+
}
|
|
346
|
+
const linearSrgbToOklab = (rgb) => {
|
|
347
|
+
return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
|
|
348
|
+
};
|
|
349
|
+
const oklabToOkhsl = (lab) => {
|
|
350
|
+
const L = lab[0];
|
|
351
|
+
const a = lab[1];
|
|
352
|
+
const b = lab[2];
|
|
353
|
+
const C = Math.sqrt(a * a + b * b);
|
|
354
|
+
if (C < EPSILON) return [
|
|
355
|
+
0,
|
|
356
|
+
0,
|
|
357
|
+
toe(L)
|
|
358
|
+
];
|
|
359
|
+
const a_ = a / C;
|
|
360
|
+
const b_ = b / C;
|
|
361
|
+
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
362
|
+
h = constrainAngle(h);
|
|
363
|
+
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
364
|
+
const mid = .8;
|
|
365
|
+
const midInv = 1.25;
|
|
366
|
+
let s;
|
|
367
|
+
if (C < cMid) {
|
|
368
|
+
const k1 = mid * c0;
|
|
369
|
+
s = C / (k1 + C * (1 - k1 / cMid)) / midInv;
|
|
370
|
+
} else {
|
|
371
|
+
const k0 = cMid;
|
|
372
|
+
const k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
373
|
+
const k2 = 1 - k1 / (cMax - cMid);
|
|
374
|
+
const cDiff = C - k0;
|
|
375
|
+
s = mid + cDiff / (k1 + cDiff * k2) / 5;
|
|
376
|
+
}
|
|
377
|
+
const l = toe(L);
|
|
378
|
+
return [
|
|
379
|
+
h,
|
|
380
|
+
clampVal(s, 0, 1),
|
|
381
|
+
clampVal(l, 0, 1)
|
|
382
|
+
];
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
|
|
386
|
+
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
387
|
+
*/
|
|
388
|
+
function srgbToOkhsl(rgb) {
|
|
389
|
+
return oklabToOkhsl(linearSrgbToOklab([
|
|
390
|
+
sRGBGammaToLinear(rgb[0]),
|
|
391
|
+
sRGBGammaToLinear(rgb[1]),
|
|
392
|
+
sRGBGammaToLinear(rgb[2])
|
|
393
|
+
]));
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Parse a hex color string (#rgb or #rrggbb) to sRGB [r, g, b] in 0–1 range.
|
|
397
|
+
* Returns null if the string is not a valid hex color.
|
|
398
|
+
*/
|
|
399
|
+
function parseHex(hex) {
|
|
400
|
+
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
401
|
+
if (h.length === 3) {
|
|
402
|
+
const r = parseInt(h[0] + h[0], 16);
|
|
403
|
+
const g = parseInt(h[1] + h[1], 16);
|
|
404
|
+
const b = parseInt(h[2] + h[2], 16);
|
|
405
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
406
|
+
return [
|
|
407
|
+
r / 255,
|
|
408
|
+
g / 255,
|
|
409
|
+
b / 255
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
if (h.length === 6) {
|
|
413
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
414
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
415
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
416
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
417
|
+
return [
|
|
418
|
+
r / 255,
|
|
419
|
+
g / 255,
|
|
420
|
+
b / 255
|
|
421
|
+
];
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function fmt$1(value, decimals) {
|
|
426
|
+
return parseFloat(value.toFixed(decimals)).toString();
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Format OKHSL values as a CSS `okhsl(H S% L%)` string.
|
|
430
|
+
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
431
|
+
*/
|
|
432
|
+
function formatOkhsl(h, s, l) {
|
|
433
|
+
return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
437
|
+
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
438
|
+
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
439
|
+
*/
|
|
440
|
+
function formatRgb(h, s, l) {
|
|
441
|
+
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
442
|
+
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
446
|
+
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
447
|
+
*/
|
|
448
|
+
function formatHsl(h, s, l) {
|
|
449
|
+
const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
|
|
450
|
+
const max = Math.max(r, g, b);
|
|
451
|
+
const min = Math.min(r, g, b);
|
|
452
|
+
const delta = max - min;
|
|
453
|
+
let hh = 0;
|
|
454
|
+
let ss = 0;
|
|
455
|
+
const ll = (max + min) / 2;
|
|
456
|
+
if (delta > 0) {
|
|
457
|
+
ss = ll > .5 ? delta / (2 - max - min) : delta / (max + min);
|
|
458
|
+
if (max === r) hh = ((g - b) / delta + (g < b ? 6 : 0)) * 60;
|
|
459
|
+
else if (max === g) hh = ((b - r) / delta + 2) * 60;
|
|
460
|
+
else hh = ((r - g) / delta + 4) * 60;
|
|
461
|
+
}
|
|
462
|
+
return `hsl(${fmt$1(hh, 2)} ${fmt$1(ss * 100, 2)}% ${fmt$1(ll * 100, 2)}%)`;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
466
|
+
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
467
|
+
*/
|
|
468
|
+
function formatOklch(h, s, l) {
|
|
469
|
+
const [L, a, b] = okhslToOklab(h, s / 100, l / 100);
|
|
470
|
+
const C = Math.sqrt(a * a + b * b);
|
|
471
|
+
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
472
|
+
hh = constrainAngle(hh);
|
|
473
|
+
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/contrast-solver.ts
|
|
478
|
+
/**
|
|
479
|
+
* OKHSL Contrast Solver
|
|
480
|
+
*
|
|
481
|
+
* Finds the closest OKHSL lightness that satisfies a WCAG 2 contrast target
|
|
482
|
+
* against a base color. Used by glaze when resolving dependent colors
|
|
483
|
+
* with `contrast`.
|
|
484
|
+
*/
|
|
485
|
+
const CONTRAST_PRESETS = {
|
|
486
|
+
AA: 4.5,
|
|
487
|
+
AAA: 7,
|
|
488
|
+
"AA-large": 3,
|
|
489
|
+
"AAA-large": 4.5
|
|
490
|
+
};
|
|
491
|
+
function resolveMinContrast(value) {
|
|
492
|
+
if (typeof value === "number") return Math.max(1, value);
|
|
493
|
+
return CONTRAST_PRESETS[value];
|
|
494
|
+
}
|
|
495
|
+
const CACHE_SIZE = 512;
|
|
496
|
+
const luminanceCache = /* @__PURE__ */ new Map();
|
|
497
|
+
const cacheOrder = [];
|
|
498
|
+
function cachedLuminance(h, s, l) {
|
|
499
|
+
const lRounded = Math.round(l * 1e4) / 1e4;
|
|
500
|
+
const key = `${h}|${s}|${lRounded}`;
|
|
501
|
+
const cached = luminanceCache.get(key);
|
|
502
|
+
if (cached !== void 0) return cached;
|
|
503
|
+
const y = gamutClampedLuminance(okhslToLinearSrgb(h, s, lRounded));
|
|
504
|
+
if (luminanceCache.size >= CACHE_SIZE) {
|
|
505
|
+
const evict = cacheOrder.shift();
|
|
506
|
+
luminanceCache.delete(evict);
|
|
507
|
+
}
|
|
508
|
+
luminanceCache.set(key, y);
|
|
509
|
+
cacheOrder.push(key);
|
|
510
|
+
return y;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Binary search one branch [lo, hi] for the nearest passing lightness to `preferred`.
|
|
514
|
+
*/
|
|
515
|
+
function searchBranch(h, s, lo, hi, yBase, target, epsilon, maxIter, preferred) {
|
|
516
|
+
const yLo = cachedLuminance(h, s, lo);
|
|
517
|
+
const yHi = cachedLuminance(h, s, hi);
|
|
518
|
+
const crLo = contrastRatioFromLuminance(yLo, yBase);
|
|
519
|
+
const crHi = contrastRatioFromLuminance(yHi, yBase);
|
|
520
|
+
if (crLo < target && crHi < target) {
|
|
521
|
+
if (crLo >= crHi) return {
|
|
522
|
+
lightness: lo,
|
|
523
|
+
contrast: crLo,
|
|
524
|
+
met: false
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
lightness: hi,
|
|
528
|
+
contrast: crHi,
|
|
529
|
+
met: false
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
let low = lo;
|
|
533
|
+
let high = hi;
|
|
534
|
+
for (let i = 0; i < maxIter; i++) {
|
|
535
|
+
if (high - low < epsilon) break;
|
|
536
|
+
const mid = (low + high) / 2;
|
|
537
|
+
if (contrastRatioFromLuminance(cachedLuminance(h, s, mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
538
|
+
else high = mid;
|
|
539
|
+
else if (mid < preferred) high = mid;
|
|
540
|
+
else low = mid;
|
|
541
|
+
}
|
|
542
|
+
const yLow = cachedLuminance(h, s, low);
|
|
543
|
+
const yHigh = cachedLuminance(h, s, high);
|
|
544
|
+
const crLow = contrastRatioFromLuminance(yLow, yBase);
|
|
545
|
+
const crHigh = contrastRatioFromLuminance(yHigh, yBase);
|
|
546
|
+
const lowPasses = crLow >= target;
|
|
547
|
+
const highPasses = crHigh >= target;
|
|
548
|
+
if (lowPasses && highPasses) {
|
|
549
|
+
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
550
|
+
lightness: low,
|
|
551
|
+
contrast: crLow,
|
|
552
|
+
met: true
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
lightness: high,
|
|
556
|
+
contrast: crHigh,
|
|
557
|
+
met: true
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (lowPasses) return {
|
|
561
|
+
lightness: low,
|
|
562
|
+
contrast: crLow,
|
|
563
|
+
met: true
|
|
564
|
+
};
|
|
565
|
+
if (highPasses) return {
|
|
566
|
+
lightness: high,
|
|
567
|
+
contrast: crHigh,
|
|
568
|
+
met: true
|
|
569
|
+
};
|
|
570
|
+
return coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Fallback coarse scan when binary search is unstable near gamut edges.
|
|
574
|
+
*/
|
|
575
|
+
function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
|
|
576
|
+
const STEPS = 64;
|
|
577
|
+
const step = (hi - lo) / STEPS;
|
|
578
|
+
let bestL = lo;
|
|
579
|
+
let bestCr = 0;
|
|
580
|
+
let bestMet = false;
|
|
581
|
+
for (let i = 0; i <= STEPS; i++) {
|
|
582
|
+
const l = lo + step * i;
|
|
583
|
+
const cr = contrastRatioFromLuminance(cachedLuminance(h, s, l), yBase);
|
|
584
|
+
if (cr >= target && !bestMet) {
|
|
585
|
+
bestL = l;
|
|
586
|
+
bestCr = cr;
|
|
587
|
+
bestMet = true;
|
|
588
|
+
} else if (cr >= target && bestMet) {
|
|
589
|
+
bestL = l;
|
|
590
|
+
bestCr = cr;
|
|
591
|
+
} else if (!bestMet && cr > bestCr) {
|
|
592
|
+
bestL = l;
|
|
593
|
+
bestCr = cr;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (bestMet && bestL > lo + step) {
|
|
597
|
+
let rLo = bestL - step;
|
|
598
|
+
let rHi = bestL;
|
|
599
|
+
for (let i = 0; i < maxIter; i++) {
|
|
600
|
+
if (rHi - rLo < epsilon) break;
|
|
601
|
+
const mid = (rLo + rHi) / 2;
|
|
602
|
+
const cr = contrastRatioFromLuminance(cachedLuminance(h, s, mid), yBase);
|
|
603
|
+
if (cr >= target) {
|
|
604
|
+
rHi = mid;
|
|
605
|
+
bestL = mid;
|
|
606
|
+
bestCr = cr;
|
|
607
|
+
} else rLo = mid;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
lightness: bestL,
|
|
612
|
+
contrast: bestCr,
|
|
613
|
+
met: bestMet
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Find the OKHSL lightness that satisfies a WCAG 2 contrast target
|
|
618
|
+
* against a base color, staying as close to `preferredLightness` as possible.
|
|
619
|
+
*/
|
|
620
|
+
function findLightnessForContrast(options) {
|
|
621
|
+
const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
|
|
622
|
+
const target = resolveMinContrast(contrastInput);
|
|
623
|
+
const searchTarget = target * 1.007;
|
|
624
|
+
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
625
|
+
const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
|
|
626
|
+
if (crPref >= searchTarget) return {
|
|
627
|
+
lightness: preferredLightness,
|
|
628
|
+
contrast: crPref,
|
|
629
|
+
met: true,
|
|
630
|
+
branch: "preferred"
|
|
631
|
+
};
|
|
632
|
+
const [minL, maxL] = lightnessRange;
|
|
633
|
+
const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
|
|
634
|
+
const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
|
|
635
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
636
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
637
|
+
const darkerPasses = darkerResult?.met ?? false;
|
|
638
|
+
const lighterPasses = lighterResult?.met ?? false;
|
|
639
|
+
if (darkerPasses && lighterPasses) {
|
|
640
|
+
if (Math.abs(darkerResult.lightness - preferredLightness) <= Math.abs(lighterResult.lightness - preferredLightness)) return {
|
|
641
|
+
...darkerResult,
|
|
642
|
+
branch: "darker"
|
|
643
|
+
};
|
|
644
|
+
return {
|
|
645
|
+
...lighterResult,
|
|
646
|
+
branch: "lighter"
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (darkerPasses) return {
|
|
650
|
+
...darkerResult,
|
|
651
|
+
branch: "darker"
|
|
652
|
+
};
|
|
653
|
+
if (lighterPasses) return {
|
|
654
|
+
...lighterResult,
|
|
655
|
+
branch: "lighter"
|
|
656
|
+
};
|
|
657
|
+
const candidates = [];
|
|
658
|
+
if (darkerResult) candidates.push({
|
|
659
|
+
...darkerResult,
|
|
660
|
+
branch: "darker"
|
|
661
|
+
});
|
|
662
|
+
if (lighterResult) candidates.push({
|
|
663
|
+
...lighterResult,
|
|
664
|
+
branch: "lighter"
|
|
665
|
+
});
|
|
666
|
+
if (candidates.length === 0) return {
|
|
667
|
+
lightness: preferredLightness,
|
|
668
|
+
contrast: crPref,
|
|
669
|
+
met: false,
|
|
670
|
+
branch: "preferred"
|
|
671
|
+
};
|
|
672
|
+
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
673
|
+
return candidates[0];
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
677
|
+
* to `preferred`.
|
|
678
|
+
*/
|
|
679
|
+
function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
|
|
680
|
+
const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
|
|
681
|
+
const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
|
|
682
|
+
if (crLo < target && crHi < target) {
|
|
683
|
+
if (crLo >= crHi) return {
|
|
684
|
+
lightness: lo,
|
|
685
|
+
contrast: crLo,
|
|
686
|
+
met: false
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
lightness: hi,
|
|
690
|
+
contrast: crHi,
|
|
691
|
+
met: false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
let low = lo;
|
|
695
|
+
let high = hi;
|
|
696
|
+
for (let i = 0; i < maxIter; i++) {
|
|
697
|
+
if (high - low < epsilon) break;
|
|
698
|
+
const mid = (low + high) / 2;
|
|
699
|
+
if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
|
|
700
|
+
else high = mid;
|
|
701
|
+
else if (mid < preferred) high = mid;
|
|
702
|
+
else low = mid;
|
|
703
|
+
}
|
|
704
|
+
const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
|
|
705
|
+
const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
|
|
706
|
+
const lowPasses = crLow >= target;
|
|
707
|
+
const highPasses = crHigh >= target;
|
|
708
|
+
if (lowPasses && highPasses) {
|
|
709
|
+
if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
|
|
710
|
+
lightness: low,
|
|
711
|
+
contrast: crLow,
|
|
712
|
+
met: true
|
|
713
|
+
};
|
|
714
|
+
return {
|
|
715
|
+
lightness: high,
|
|
716
|
+
contrast: crHigh,
|
|
717
|
+
met: true
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (lowPasses) return {
|
|
721
|
+
lightness: low,
|
|
722
|
+
contrast: crLow,
|
|
723
|
+
met: true
|
|
724
|
+
};
|
|
725
|
+
if (highPasses) return {
|
|
726
|
+
lightness: high,
|
|
727
|
+
contrast: crHigh,
|
|
728
|
+
met: true
|
|
729
|
+
};
|
|
730
|
+
return crLow >= crHigh ? {
|
|
731
|
+
lightness: low,
|
|
732
|
+
contrast: crLow,
|
|
733
|
+
met: false
|
|
734
|
+
} : {
|
|
735
|
+
lightness: high,
|
|
736
|
+
contrast: crHigh,
|
|
737
|
+
met: false
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
|
|
742
|
+
* target against a base color, staying as close to `preferredValue` as possible.
|
|
743
|
+
*/
|
|
744
|
+
function findValueForMixContrast(options) {
|
|
745
|
+
const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
746
|
+
const target = resolveMinContrast(contrastInput);
|
|
747
|
+
const searchTarget = target * 1.01;
|
|
748
|
+
const yBase = gamutClampedLuminance(baseLinearRgb);
|
|
749
|
+
const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
|
|
750
|
+
if (crPref >= searchTarget) return {
|
|
751
|
+
value: preferredValue,
|
|
752
|
+
contrast: crPref,
|
|
753
|
+
met: true
|
|
754
|
+
};
|
|
755
|
+
const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
756
|
+
const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
|
|
757
|
+
if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
|
|
758
|
+
if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
|
|
759
|
+
const darkerPasses = darkerResult?.met ?? false;
|
|
760
|
+
const lighterPasses = lighterResult?.met ?? false;
|
|
761
|
+
if (darkerPasses && lighterPasses) {
|
|
762
|
+
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
763
|
+
value: darkerResult.lightness,
|
|
764
|
+
contrast: darkerResult.contrast,
|
|
765
|
+
met: true
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
value: lighterResult.lightness,
|
|
769
|
+
contrast: lighterResult.contrast,
|
|
770
|
+
met: true
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (darkerPasses) return {
|
|
774
|
+
value: darkerResult.lightness,
|
|
775
|
+
contrast: darkerResult.contrast,
|
|
776
|
+
met: true
|
|
777
|
+
};
|
|
778
|
+
if (lighterPasses) return {
|
|
779
|
+
value: lighterResult.lightness,
|
|
780
|
+
contrast: lighterResult.contrast,
|
|
781
|
+
met: true
|
|
782
|
+
};
|
|
783
|
+
const candidates = [];
|
|
784
|
+
if (darkerResult) candidates.push({
|
|
785
|
+
...darkerResult,
|
|
786
|
+
branch: "lower"
|
|
787
|
+
});
|
|
788
|
+
if (lighterResult) candidates.push({
|
|
789
|
+
...lighterResult,
|
|
790
|
+
branch: "upper"
|
|
791
|
+
});
|
|
792
|
+
if (candidates.length === 0) return {
|
|
793
|
+
value: preferredValue,
|
|
794
|
+
contrast: crPref,
|
|
795
|
+
met: false
|
|
796
|
+
};
|
|
797
|
+
candidates.sort((a, b) => b.contrast - a.contrast);
|
|
798
|
+
return {
|
|
799
|
+
value: candidates[0].lightness,
|
|
800
|
+
contrast: candidates[0].contrast,
|
|
801
|
+
met: candidates[0].met
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/glaze.ts
|
|
807
|
+
/**
|
|
808
|
+
* Glaze — OKHSL-based color theme generator.
|
|
809
|
+
*
|
|
810
|
+
* Generates robust light, dark, and high-contrast colors from a hue/saturation
|
|
811
|
+
* seed, preserving contrast for UI pairs via explicit dependencies.
|
|
812
|
+
*/
|
|
813
|
+
let globalConfig = {
|
|
814
|
+
lightLightness: [10, 100],
|
|
815
|
+
darkLightness: [15, 95],
|
|
816
|
+
darkDesaturation: .1,
|
|
817
|
+
darkCurve: .5,
|
|
818
|
+
states: {
|
|
819
|
+
dark: "@dark",
|
|
820
|
+
highContrast: "@high-contrast"
|
|
821
|
+
},
|
|
822
|
+
modes: {
|
|
823
|
+
dark: true,
|
|
824
|
+
highContrast: false
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
function pairNormal(p) {
|
|
828
|
+
return Array.isArray(p) ? p[0] : p;
|
|
829
|
+
}
|
|
830
|
+
function pairHC(p) {
|
|
831
|
+
return Array.isArray(p) ? p[1] : p;
|
|
832
|
+
}
|
|
833
|
+
function isShadowDef(def) {
|
|
834
|
+
return def.type === "shadow";
|
|
835
|
+
}
|
|
836
|
+
function isMixDef(def) {
|
|
837
|
+
return def.type === "mix";
|
|
838
|
+
}
|
|
839
|
+
const DEFAULT_SHADOW_TUNING = {
|
|
840
|
+
saturationFactor: .18,
|
|
841
|
+
maxSaturation: .25,
|
|
842
|
+
lightnessFactor: .25,
|
|
843
|
+
lightnessBounds: [.05, .2],
|
|
844
|
+
minGapTarget: .05,
|
|
845
|
+
alphaMax: 1,
|
|
846
|
+
bgHueBlend: .2
|
|
847
|
+
};
|
|
848
|
+
function resolveShadowTuning(perColor) {
|
|
849
|
+
return {
|
|
850
|
+
...DEFAULT_SHADOW_TUNING,
|
|
851
|
+
...globalConfig.shadowTuning,
|
|
852
|
+
...perColor,
|
|
853
|
+
lightnessBounds: perColor?.lightnessBounds ?? globalConfig.shadowTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function circularLerp(a, b, t) {
|
|
857
|
+
let diff = b - a;
|
|
858
|
+
if (diff > 180) diff -= 360;
|
|
859
|
+
else if (diff < -180) diff += 360;
|
|
860
|
+
return ((a + diff * t) % 360 + 360) % 360;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Compute the canonical max-contrast reference t value for normalization.
|
|
864
|
+
* Uses bg.l=1, fg.l=0, intensity=100 — the theoretical maximum.
|
|
865
|
+
* This is a fixed constant per tuning configuration, ensuring uniform
|
|
866
|
+
* scaling across all bg/fg pairs at low intensities.
|
|
867
|
+
*/
|
|
868
|
+
function computeRefT(tuning) {
|
|
869
|
+
const EPSILON = 1e-6;
|
|
870
|
+
let lShRef = clamp(tuning.lightnessFactor, tuning.lightnessBounds[0], tuning.lightnessBounds[1]);
|
|
871
|
+
lShRef = Math.max(Math.min(lShRef, 1 - tuning.minGapTarget), 0);
|
|
872
|
+
return 1 / Math.max(1 - lShRef, EPSILON);
|
|
873
|
+
}
|
|
874
|
+
function computeShadow(bg, fg, intensity, tuning) {
|
|
875
|
+
const EPSILON = 1e-6;
|
|
876
|
+
const clampedIntensity = clamp(intensity, 0, 100);
|
|
877
|
+
const contrastWeight = fg ? Math.abs(bg.l - fg.l) : 1;
|
|
878
|
+
const deltaL = clampedIntensity / 100 * contrastWeight;
|
|
879
|
+
const h = fg ? circularLerp(fg.h, bg.h, tuning.bgHueBlend) : bg.h;
|
|
880
|
+
const s = fg ? Math.min(fg.s * tuning.saturationFactor, tuning.maxSaturation) : 0;
|
|
881
|
+
let lSh = clamp(bg.l * tuning.lightnessFactor, tuning.lightnessBounds[0], tuning.lightnessBounds[1]);
|
|
882
|
+
lSh = Math.max(Math.min(lSh, bg.l - tuning.minGapTarget), 0);
|
|
883
|
+
const t = deltaL / Math.max(bg.l - lSh, EPSILON);
|
|
884
|
+
const tRef = computeRefT(tuning);
|
|
885
|
+
const norm = Math.tanh(tRef / tuning.alphaMax);
|
|
886
|
+
const alpha = Math.min(tuning.alphaMax * Math.tanh(t / tuning.alphaMax) / norm, tuning.alphaMax);
|
|
887
|
+
return {
|
|
888
|
+
h,
|
|
889
|
+
s,
|
|
890
|
+
l: lSh,
|
|
891
|
+
alpha
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function validateColorDefs(defs) {
|
|
895
|
+
const names = new Set(Object.keys(defs));
|
|
896
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
897
|
+
if (isShadowDef(def)) {
|
|
898
|
+
if (!names.has(def.bg)) throw new Error(`glaze: shadow "${name}" references non-existent bg "${def.bg}".`);
|
|
899
|
+
if (isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
|
|
900
|
+
if (def.fg !== void 0) {
|
|
901
|
+
if (!names.has(def.fg)) throw new Error(`glaze: shadow "${name}" references non-existent fg "${def.fg}".`);
|
|
902
|
+
if (isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
|
|
903
|
+
}
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (isMixDef(def)) {
|
|
907
|
+
if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
|
|
908
|
+
if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
909
|
+
if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
910
|
+
if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const regDef = def;
|
|
914
|
+
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
915
|
+
if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
|
|
916
|
+
if (regDef.base && !names.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
917
|
+
if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
918
|
+
if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
|
|
919
|
+
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived lightness unpredictable.`);
|
|
920
|
+
}
|
|
921
|
+
const visited = /* @__PURE__ */ new Set();
|
|
922
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
923
|
+
function dfs(name) {
|
|
924
|
+
if (inStack.has(name)) throw new Error(`glaze: circular base reference detected involving "${name}".`);
|
|
925
|
+
if (visited.has(name)) return;
|
|
926
|
+
inStack.add(name);
|
|
927
|
+
const def = defs[name];
|
|
928
|
+
if (isShadowDef(def)) {
|
|
929
|
+
dfs(def.bg);
|
|
930
|
+
if (def.fg) dfs(def.fg);
|
|
931
|
+
} else if (isMixDef(def)) {
|
|
932
|
+
dfs(def.base);
|
|
933
|
+
dfs(def.target);
|
|
934
|
+
} else {
|
|
935
|
+
const regDef = def;
|
|
936
|
+
if (regDef.base) dfs(regDef.base);
|
|
937
|
+
}
|
|
938
|
+
inStack.delete(name);
|
|
939
|
+
visited.add(name);
|
|
940
|
+
}
|
|
941
|
+
for (const name of names) dfs(name);
|
|
942
|
+
}
|
|
943
|
+
function topoSort(defs) {
|
|
944
|
+
const result = [];
|
|
945
|
+
const visited = /* @__PURE__ */ new Set();
|
|
946
|
+
function visit(name) {
|
|
947
|
+
if (visited.has(name)) return;
|
|
948
|
+
visited.add(name);
|
|
949
|
+
const def = defs[name];
|
|
950
|
+
if (isShadowDef(def)) {
|
|
951
|
+
visit(def.bg);
|
|
952
|
+
if (def.fg) visit(def.fg);
|
|
953
|
+
} else if (isMixDef(def)) {
|
|
954
|
+
visit(def.base);
|
|
955
|
+
visit(def.target);
|
|
956
|
+
} else {
|
|
957
|
+
const regDef = def;
|
|
958
|
+
if (regDef.base) visit(regDef.base);
|
|
959
|
+
}
|
|
960
|
+
result.push(name);
|
|
961
|
+
}
|
|
962
|
+
for (const name of Object.keys(defs)) visit(name);
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
function lightnessWindow(isHighContrast, kind) {
|
|
966
|
+
if (isHighContrast) return [0, 100];
|
|
967
|
+
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
968
|
+
}
|
|
969
|
+
function mapLightnessLight(l, mode, isHighContrast) {
|
|
970
|
+
if (mode === "static") return l;
|
|
971
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light");
|
|
972
|
+
return l * (hi - lo) / 100 + lo;
|
|
973
|
+
}
|
|
974
|
+
function mobiusCurve(t, beta) {
|
|
975
|
+
if (beta >= 1) return t;
|
|
976
|
+
return t / (t + beta * (1 - t));
|
|
977
|
+
}
|
|
978
|
+
function mapLightnessDark(l, mode, isHighContrast) {
|
|
979
|
+
if (mode === "static") return l;
|
|
980
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
981
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
982
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
983
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
984
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
985
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
986
|
+
}
|
|
987
|
+
function lightMappedToDark(lightL, isHighContrast) {
|
|
988
|
+
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
989
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light");
|
|
990
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark");
|
|
991
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
992
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
993
|
+
}
|
|
994
|
+
function mapSaturationDark(s, mode) {
|
|
995
|
+
if (mode === "static") return s;
|
|
996
|
+
return s * (1 - globalConfig.darkDesaturation);
|
|
997
|
+
}
|
|
998
|
+
function schemeLightnessRange(isDark, mode, isHighContrast) {
|
|
999
|
+
if (mode === "static") return [0, 1];
|
|
1000
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light");
|
|
1001
|
+
return [lo / 100, hi / 100];
|
|
1002
|
+
}
|
|
1003
|
+
function clamp(v, min, max) {
|
|
1004
|
+
return Math.max(min, Math.min(max, v));
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Parse a value that can be absolute (number) or relative (signed string).
|
|
1008
|
+
* Returns the numeric value and whether it's relative.
|
|
1009
|
+
*/
|
|
1010
|
+
function parseRelativeOrAbsolute(value) {
|
|
1011
|
+
if (typeof value === "number") return {
|
|
1012
|
+
value,
|
|
1013
|
+
relative: false
|
|
1014
|
+
};
|
|
1015
|
+
return {
|
|
1016
|
+
value: parseFloat(value),
|
|
1017
|
+
relative: true
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Compute the effective hue for a color, given the theme seed hue
|
|
1022
|
+
* and an optional per-color hue override.
|
|
1023
|
+
*/
|
|
1024
|
+
function resolveEffectiveHue(seedHue, defHue) {
|
|
1025
|
+
if (defHue === void 0) return seedHue;
|
|
1026
|
+
const parsed = parseRelativeOrAbsolute(defHue);
|
|
1027
|
+
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
1028
|
+
return (parsed.value % 360 + 360) % 360;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Check whether a lightness value represents an absolute root definition
|
|
1032
|
+
* (i.e. a number, not a relative string).
|
|
1033
|
+
*/
|
|
1034
|
+
function isAbsoluteLightness(lightness) {
|
|
1035
|
+
if (lightness === void 0) return false;
|
|
1036
|
+
return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
|
|
1037
|
+
}
|
|
1038
|
+
function resolveRootColor(_name, def, _ctx, isHighContrast) {
|
|
1039
|
+
const rawL = def.lightness;
|
|
1040
|
+
return {
|
|
1041
|
+
lightL: clamp(parseRelativeOrAbsolute(isHighContrast ? pairHC(rawL) : pairNormal(rawL)).value, 0, 100),
|
|
1042
|
+
satFactor: clamp(def.saturation ?? 1, 0, 1)
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue) {
|
|
1046
|
+
const baseName = def.base;
|
|
1047
|
+
const baseResolved = ctx.resolved.get(baseName);
|
|
1048
|
+
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
1049
|
+
const mode = def.mode ?? "auto";
|
|
1050
|
+
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
1051
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1052
|
+
const baseL = baseVariant.l * 100;
|
|
1053
|
+
let preferredL;
|
|
1054
|
+
const rawLightness = def.lightness;
|
|
1055
|
+
if (rawLightness === void 0) preferredL = baseL;
|
|
1056
|
+
else {
|
|
1057
|
+
const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
|
|
1058
|
+
if (parsed.relative) {
|
|
1059
|
+
const delta = parsed.value;
|
|
1060
|
+
if (isDark && mode === "auto") preferredL = lightMappedToDark(clamp(getSchemeVariant(baseResolved, false, isHighContrast).l * 100 + delta, 0, 100), isHighContrast);
|
|
1061
|
+
else preferredL = clamp(baseL + delta, 0, 100);
|
|
1062
|
+
} else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
|
|
1063
|
+
else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
|
|
1064
|
+
}
|
|
1065
|
+
const rawContrast = def.contrast;
|
|
1066
|
+
if (rawContrast !== void 0) {
|
|
1067
|
+
const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
|
|
1068
|
+
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1069
|
+
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1070
|
+
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
|
|
1071
|
+
return {
|
|
1072
|
+
l: findLightnessForContrast({
|
|
1073
|
+
hue: effectiveHue,
|
|
1074
|
+
saturation: effectiveSat,
|
|
1075
|
+
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1076
|
+
baseLinearRgb,
|
|
1077
|
+
contrast: minCr,
|
|
1078
|
+
lightnessRange: [0, 1]
|
|
1079
|
+
}).lightness * 100,
|
|
1080
|
+
satFactor
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
l: clamp(preferredL, 0, 100),
|
|
1085
|
+
satFactor
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1089
|
+
if (isDark && isHighContrast) return color.darkContrast;
|
|
1090
|
+
if (isDark) return color.dark;
|
|
1091
|
+
if (isHighContrast) return color.lightContrast;
|
|
1092
|
+
return color.light;
|
|
1093
|
+
}
|
|
1094
|
+
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1095
|
+
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1096
|
+
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
1097
|
+
const regDef = def;
|
|
1098
|
+
const mode = regDef.mode ?? "auto";
|
|
1099
|
+
const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
|
|
1100
|
+
const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
|
|
1101
|
+
let lightL;
|
|
1102
|
+
let satFactor;
|
|
1103
|
+
if (isRoot) {
|
|
1104
|
+
const root = resolveRootColor(name, regDef, ctx, isHighContrast);
|
|
1105
|
+
lightL = root.lightL;
|
|
1106
|
+
satFactor = root.satFactor;
|
|
1107
|
+
} else {
|
|
1108
|
+
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
|
|
1109
|
+
lightL = dep.l;
|
|
1110
|
+
satFactor = dep.satFactor;
|
|
1111
|
+
}
|
|
1112
|
+
let finalL;
|
|
1113
|
+
let finalSat;
|
|
1114
|
+
if (isDark && isRoot) {
|
|
1115
|
+
finalL = mapLightnessDark(lightL, mode, isHighContrast);
|
|
1116
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1117
|
+
} else if (isDark && !isRoot) {
|
|
1118
|
+
finalL = lightL;
|
|
1119
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
1120
|
+
} else if (isRoot) {
|
|
1121
|
+
finalL = mapLightnessLight(lightL, mode, isHighContrast);
|
|
1122
|
+
finalSat = satFactor * ctx.saturation / 100;
|
|
1123
|
+
} else {
|
|
1124
|
+
finalL = lightL;
|
|
1125
|
+
finalSat = satFactor * ctx.saturation / 100;
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
h: effectiveHue,
|
|
1129
|
+
s: clamp(finalSat, 0, 1),
|
|
1130
|
+
l: clamp(finalL / 100, 0, 1),
|
|
1131
|
+
alpha: regDef.opacity ?? 1
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
1135
|
+
const bgVariant = getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast);
|
|
1136
|
+
let fgVariant;
|
|
1137
|
+
if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
|
|
1138
|
+
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1139
|
+
const tuning = resolveShadowTuning(def.tuning);
|
|
1140
|
+
return computeShadow(bgVariant, fgVariant, intensity, tuning);
|
|
1141
|
+
}
|
|
1142
|
+
function variantToLinearRgb(v) {
|
|
1143
|
+
return okhslToLinearSrgb(v.h, v.s, v.l);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
1147
|
+
* When one color has no saturation, its hue is meaningless —
|
|
1148
|
+
* use the hue from the color that has saturation (matches CSS
|
|
1149
|
+
* color-mix "missing component" behavior).
|
|
1150
|
+
*/
|
|
1151
|
+
function mixHue(base, target, t) {
|
|
1152
|
+
const SAT_EPSILON = 1e-6;
|
|
1153
|
+
const baseHasSat = base.s > SAT_EPSILON;
|
|
1154
|
+
const targetHasSat = target.s > SAT_EPSILON;
|
|
1155
|
+
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
1156
|
+
if (targetHasSat) return target.h;
|
|
1157
|
+
return base.h;
|
|
1158
|
+
}
|
|
1159
|
+
function linearSrgbLerp(base, target, t) {
|
|
1160
|
+
return [
|
|
1161
|
+
base[0] + (target[0] - base[0]) * t,
|
|
1162
|
+
base[1] + (target[1] - base[1]) * t,
|
|
1163
|
+
base[2] + (target[2] - base[2]) * t
|
|
1164
|
+
];
|
|
1165
|
+
}
|
|
1166
|
+
function linearRgbToVariant(rgb) {
|
|
1167
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1168
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
|
|
1169
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
|
|
1170
|
+
Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
|
|
1171
|
+
]);
|
|
1172
|
+
return {
|
|
1173
|
+
h,
|
|
1174
|
+
s,
|
|
1175
|
+
l,
|
|
1176
|
+
alpha: 1
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1180
|
+
const baseResolved = ctx.resolved.get(def.base);
|
|
1181
|
+
const targetResolved = ctx.resolved.get(def.target);
|
|
1182
|
+
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1183
|
+
const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
|
|
1184
|
+
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1185
|
+
const blend = def.blend ?? "opaque";
|
|
1186
|
+
const space = def.space ?? "okhsl";
|
|
1187
|
+
const baseLinear = variantToLinearRgb(baseVariant);
|
|
1188
|
+
const targetLinear = variantToLinearRgb(targetVariant);
|
|
1189
|
+
if (def.contrast !== void 0) {
|
|
1190
|
+
const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
|
|
1191
|
+
let luminanceAt;
|
|
1192
|
+
if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1193
|
+
else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1194
|
+
else luminanceAt = (v) => {
|
|
1195
|
+
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1196
|
+
};
|
|
1197
|
+
t = findValueForMixContrast({
|
|
1198
|
+
preferredValue: t,
|
|
1199
|
+
baseLinearRgb: baseLinear,
|
|
1200
|
+
targetLinearRgb: targetLinear,
|
|
1201
|
+
contrast: minCr,
|
|
1202
|
+
luminanceAtValue: luminanceAt
|
|
1203
|
+
}).value;
|
|
1204
|
+
}
|
|
1205
|
+
if (blend === "transparent") return {
|
|
1206
|
+
h: targetVariant.h,
|
|
1207
|
+
s: targetVariant.s,
|
|
1208
|
+
l: targetVariant.l,
|
|
1209
|
+
alpha: clamp(t, 0, 1)
|
|
1210
|
+
};
|
|
1211
|
+
if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
|
|
1212
|
+
return {
|
|
1213
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
1214
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
1215
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
1216
|
+
alpha: 1
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function resolveAllColors(hue, saturation, defs) {
|
|
1220
|
+
validateColorDefs(defs);
|
|
1221
|
+
const order = topoSort(defs);
|
|
1222
|
+
const ctx = {
|
|
1223
|
+
hue,
|
|
1224
|
+
saturation,
|
|
1225
|
+
defs,
|
|
1226
|
+
resolved: /* @__PURE__ */ new Map()
|
|
1227
|
+
};
|
|
1228
|
+
function defMode(def) {
|
|
1229
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1230
|
+
return def.mode ?? "auto";
|
|
1231
|
+
}
|
|
1232
|
+
const lightMap = /* @__PURE__ */ new Map();
|
|
1233
|
+
for (const name of order) {
|
|
1234
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
|
|
1235
|
+
lightMap.set(name, variant);
|
|
1236
|
+
ctx.resolved.set(name, {
|
|
1237
|
+
name,
|
|
1238
|
+
light: variant,
|
|
1239
|
+
dark: variant,
|
|
1240
|
+
lightContrast: variant,
|
|
1241
|
+
darkContrast: variant,
|
|
1242
|
+
mode: defMode(defs[name])
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const lightHCMap = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const name of order) ctx.resolved.set(name, {
|
|
1247
|
+
...ctx.resolved.get(name),
|
|
1248
|
+
lightContrast: lightMap.get(name)
|
|
1249
|
+
});
|
|
1250
|
+
for (const name of order) {
|
|
1251
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, false, true);
|
|
1252
|
+
lightHCMap.set(name, variant);
|
|
1253
|
+
ctx.resolved.set(name, {
|
|
1254
|
+
...ctx.resolved.get(name),
|
|
1255
|
+
lightContrast: variant
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
const darkMap = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const name of order) ctx.resolved.set(name, {
|
|
1260
|
+
name,
|
|
1261
|
+
light: lightMap.get(name),
|
|
1262
|
+
dark: lightMap.get(name),
|
|
1263
|
+
lightContrast: lightHCMap.get(name),
|
|
1264
|
+
darkContrast: lightHCMap.get(name),
|
|
1265
|
+
mode: defMode(defs[name])
|
|
1266
|
+
});
|
|
1267
|
+
for (const name of order) {
|
|
1268
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
|
|
1269
|
+
darkMap.set(name, variant);
|
|
1270
|
+
ctx.resolved.set(name, {
|
|
1271
|
+
...ctx.resolved.get(name),
|
|
1272
|
+
dark: variant
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
const darkHCMap = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const name of order) ctx.resolved.set(name, {
|
|
1277
|
+
...ctx.resolved.get(name),
|
|
1278
|
+
darkContrast: darkMap.get(name)
|
|
1279
|
+
});
|
|
1280
|
+
for (const name of order) {
|
|
1281
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, true, true);
|
|
1282
|
+
darkHCMap.set(name, variant);
|
|
1283
|
+
ctx.resolved.set(name, {
|
|
1284
|
+
...ctx.resolved.get(name),
|
|
1285
|
+
darkContrast: variant
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
const result = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const name of order) result.set(name, {
|
|
1290
|
+
name,
|
|
1291
|
+
light: lightMap.get(name),
|
|
1292
|
+
dark: darkMap.get(name),
|
|
1293
|
+
lightContrast: lightHCMap.get(name),
|
|
1294
|
+
darkContrast: darkHCMap.get(name),
|
|
1295
|
+
mode: defMode(defs[name])
|
|
1296
|
+
});
|
|
1297
|
+
return result;
|
|
1298
|
+
}
|
|
1299
|
+
const formatters = {
|
|
1300
|
+
okhsl: formatOkhsl,
|
|
1301
|
+
rgb: formatRgb,
|
|
1302
|
+
hsl: formatHsl,
|
|
1303
|
+
oklch: formatOklch
|
|
1304
|
+
};
|
|
1305
|
+
function fmt(value, decimals) {
|
|
1306
|
+
return parseFloat(value.toFixed(decimals)).toString();
|
|
1307
|
+
}
|
|
1308
|
+
function formatVariant(v, format = "okhsl") {
|
|
1309
|
+
const base = formatters[format](v.h, v.s * 100, v.l * 100);
|
|
1310
|
+
if (v.alpha >= 1) return base;
|
|
1311
|
+
const closing = base.lastIndexOf(")");
|
|
1312
|
+
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
1313
|
+
}
|
|
1314
|
+
function resolveModes(override) {
|
|
1315
|
+
return {
|
|
1316
|
+
dark: override?.dark ?? globalConfig.modes.dark,
|
|
1317
|
+
highContrast: override?.highContrast ?? globalConfig.modes.highContrast
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
|
|
1321
|
+
const tokens = {};
|
|
1322
|
+
for (const [name, color] of resolved) {
|
|
1323
|
+
const key = `#${prefix}${name}`;
|
|
1324
|
+
const entry = { "": formatVariant(color.light, format) };
|
|
1325
|
+
if (modes.dark) entry[states.dark] = formatVariant(color.dark, format);
|
|
1326
|
+
if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format);
|
|
1327
|
+
if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format);
|
|
1328
|
+
tokens[key] = entry;
|
|
1329
|
+
}
|
|
1330
|
+
return tokens;
|
|
1331
|
+
}
|
|
1332
|
+
function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl") {
|
|
1333
|
+
const result = { light: {} };
|
|
1334
|
+
if (modes.dark) result.dark = {};
|
|
1335
|
+
if (modes.highContrast) result.lightContrast = {};
|
|
1336
|
+
if (modes.dark && modes.highContrast) result.darkContrast = {};
|
|
1337
|
+
for (const [name, color] of resolved) {
|
|
1338
|
+
const key = `${prefix}${name}`;
|
|
1339
|
+
result.light[key] = formatVariant(color.light, format);
|
|
1340
|
+
if (modes.dark) result.dark[key] = formatVariant(color.dark, format);
|
|
1341
|
+
if (modes.highContrast) result.lightContrast[key] = formatVariant(color.lightContrast, format);
|
|
1342
|
+
if (modes.dark && modes.highContrast) result.darkContrast[key] = formatVariant(color.darkContrast, format);
|
|
1343
|
+
}
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
function buildJsonMap(resolved, modes, format = "okhsl") {
|
|
1347
|
+
const result = {};
|
|
1348
|
+
for (const [name, color] of resolved) {
|
|
1349
|
+
const entry = { light: formatVariant(color.light, format) };
|
|
1350
|
+
if (modes.dark) entry.dark = formatVariant(color.dark, format);
|
|
1351
|
+
if (modes.highContrast) entry.lightContrast = formatVariant(color.lightContrast, format);
|
|
1352
|
+
if (modes.dark && modes.highContrast) entry.darkContrast = formatVariant(color.darkContrast, format);
|
|
1353
|
+
result[name] = entry;
|
|
1354
|
+
}
|
|
1355
|
+
return result;
|
|
1356
|
+
}
|
|
1357
|
+
function buildCssMap(resolved, prefix, suffix, format) {
|
|
1358
|
+
const lines = {
|
|
1359
|
+
light: [],
|
|
1360
|
+
dark: [],
|
|
1361
|
+
lightContrast: [],
|
|
1362
|
+
darkContrast: []
|
|
1363
|
+
};
|
|
1364
|
+
for (const [name, color] of resolved) {
|
|
1365
|
+
const prop = `--${prefix}${name}${suffix}`;
|
|
1366
|
+
lines.light.push(`${prop}: ${formatVariant(color.light, format)};`);
|
|
1367
|
+
lines.dark.push(`${prop}: ${formatVariant(color.dark, format)};`);
|
|
1368
|
+
lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format)};`);
|
|
1369
|
+
lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format)};`);
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
light: lines.light.join("\n"),
|
|
1373
|
+
dark: lines.dark.join("\n"),
|
|
1374
|
+
lightContrast: lines.lightContrast.join("\n"),
|
|
1375
|
+
darkContrast: lines.darkContrast.join("\n")
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function createTheme(hue, saturation, initialColors) {
|
|
1379
|
+
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
1380
|
+
return {
|
|
1381
|
+
get hue() {
|
|
1382
|
+
return hue;
|
|
1383
|
+
},
|
|
1384
|
+
get saturation() {
|
|
1385
|
+
return saturation;
|
|
1386
|
+
},
|
|
1387
|
+
colors(defs) {
|
|
1388
|
+
colorDefs = {
|
|
1389
|
+
...colorDefs,
|
|
1390
|
+
...defs
|
|
1391
|
+
};
|
|
1392
|
+
},
|
|
1393
|
+
color(name, def) {
|
|
1394
|
+
if (def === void 0) return colorDefs[name];
|
|
1395
|
+
colorDefs[name] = def;
|
|
1396
|
+
},
|
|
1397
|
+
remove(names) {
|
|
1398
|
+
const list = Array.isArray(names) ? names : [names];
|
|
1399
|
+
for (const name of list) delete colorDefs[name];
|
|
1400
|
+
},
|
|
1401
|
+
has(name) {
|
|
1402
|
+
return name in colorDefs;
|
|
1403
|
+
},
|
|
1404
|
+
list() {
|
|
1405
|
+
return Object.keys(colorDefs);
|
|
1406
|
+
},
|
|
1407
|
+
reset() {
|
|
1408
|
+
colorDefs = {};
|
|
1409
|
+
},
|
|
1410
|
+
export() {
|
|
1411
|
+
return {
|
|
1412
|
+
hue,
|
|
1413
|
+
saturation,
|
|
1414
|
+
colors: { ...colorDefs }
|
|
1415
|
+
};
|
|
1416
|
+
},
|
|
1417
|
+
extend(options) {
|
|
1418
|
+
return createTheme(options.hue ?? hue, options.saturation ?? saturation, options.colors ? {
|
|
1419
|
+
...colorDefs,
|
|
1420
|
+
...options.colors
|
|
1421
|
+
} : { ...colorDefs });
|
|
1422
|
+
},
|
|
1423
|
+
resolve() {
|
|
1424
|
+
return resolveAllColors(hue, saturation, colorDefs);
|
|
1425
|
+
},
|
|
1426
|
+
tokens(options) {
|
|
1427
|
+
return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
|
|
1428
|
+
},
|
|
1429
|
+
tasty(options) {
|
|
1430
|
+
return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
|
|
1431
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1432
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1433
|
+
}, resolveModes(options?.modes), options?.format);
|
|
1434
|
+
},
|
|
1435
|
+
json(options) {
|
|
1436
|
+
return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
|
|
1437
|
+
},
|
|
1438
|
+
css(options) {
|
|
1439
|
+
return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1444
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
1445
|
+
if (prefix === true) return `${themeName}-`;
|
|
1446
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1447
|
+
return "";
|
|
1448
|
+
}
|
|
1449
|
+
function validatePrimaryTheme(primary, themes) {
|
|
1450
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
1451
|
+
const available = Object.keys(themes).join(", ");
|
|
1452
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function createPalette(themes) {
|
|
1456
|
+
return {
|
|
1457
|
+
tokens(options) {
|
|
1458
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1459
|
+
const modes = resolveModes(options?.modes);
|
|
1460
|
+
const allTokens = {};
|
|
1461
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1462
|
+
const resolved = theme.resolve();
|
|
1463
|
+
const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
|
|
1464
|
+
for (const variant of Object.keys(tokens)) {
|
|
1465
|
+
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1466
|
+
Object.assign(allTokens[variant], tokens[variant]);
|
|
1467
|
+
}
|
|
1468
|
+
if (themeName === options?.primary) {
|
|
1469
|
+
const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
|
|
1470
|
+
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return allTokens;
|
|
1474
|
+
},
|
|
1475
|
+
tasty(options) {
|
|
1476
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1477
|
+
const states = {
|
|
1478
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1479
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1480
|
+
};
|
|
1481
|
+
const modes = resolveModes(options?.modes);
|
|
1482
|
+
const allTokens = {};
|
|
1483
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1484
|
+
const resolved = theme.resolve();
|
|
1485
|
+
const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
|
|
1486
|
+
Object.assign(allTokens, tokens);
|
|
1487
|
+
if (themeName === options?.primary) {
|
|
1488
|
+
const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
|
|
1489
|
+
Object.assign(allTokens, unprefixed);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return allTokens;
|
|
1493
|
+
},
|
|
1494
|
+
json(options) {
|
|
1495
|
+
const modes = resolveModes(options?.modes);
|
|
1496
|
+
const result = {};
|
|
1497
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
1498
|
+
return result;
|
|
1499
|
+
},
|
|
1500
|
+
css(options) {
|
|
1501
|
+
validatePrimaryTheme(options?.primary, themes);
|
|
1502
|
+
const suffix = options?.suffix ?? "-color";
|
|
1503
|
+
const format = options?.format ?? "rgb";
|
|
1504
|
+
const allLines = {
|
|
1505
|
+
light: [],
|
|
1506
|
+
dark: [],
|
|
1507
|
+
lightContrast: [],
|
|
1508
|
+
darkContrast: []
|
|
1509
|
+
};
|
|
1510
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1511
|
+
const resolved = theme.resolve();
|
|
1512
|
+
const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
|
|
1513
|
+
for (const key of [
|
|
1514
|
+
"light",
|
|
1515
|
+
"dark",
|
|
1516
|
+
"lightContrast",
|
|
1517
|
+
"darkContrast"
|
|
1518
|
+
]) if (css[key]) allLines[key].push(css[key]);
|
|
1519
|
+
if (themeName === options?.primary) {
|
|
1520
|
+
const unprefixed = buildCssMap(resolved, "", suffix, format);
|
|
1521
|
+
for (const key of [
|
|
1522
|
+
"light",
|
|
1523
|
+
"dark",
|
|
1524
|
+
"lightContrast",
|
|
1525
|
+
"darkContrast"
|
|
1526
|
+
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
light: allLines.light.join("\n"),
|
|
1531
|
+
dark: allLines.dark.join("\n"),
|
|
1532
|
+
lightContrast: allLines.lightContrast.join("\n"),
|
|
1533
|
+
darkContrast: allLines.darkContrast.join("\n")
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function createColorToken(input) {
|
|
1539
|
+
const defs = { __color__: {
|
|
1540
|
+
lightness: input.lightness,
|
|
1541
|
+
saturation: input.saturationFactor,
|
|
1542
|
+
mode: input.mode
|
|
1543
|
+
} };
|
|
1544
|
+
return {
|
|
1545
|
+
resolve() {
|
|
1546
|
+
return resolveAllColors(input.hue, input.saturation, defs).get("__color__");
|
|
1547
|
+
},
|
|
1548
|
+
token(options) {
|
|
1549
|
+
return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
|
|
1550
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1551
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1552
|
+
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
1553
|
+
},
|
|
1554
|
+
tasty(options) {
|
|
1555
|
+
return buildTokenMap(resolveAllColors(input.hue, input.saturation, defs), "", {
|
|
1556
|
+
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1557
|
+
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1558
|
+
}, resolveModes(options?.modes), options?.format)["#__color__"];
|
|
1559
|
+
},
|
|
1560
|
+
json(options) {
|
|
1561
|
+
return buildJsonMap(resolveAllColors(input.hue, input.saturation, defs), resolveModes(options?.modes), options?.format)["__color__"];
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Create a single-hue glaze theme.
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* ```ts
|
|
1570
|
+
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
1571
|
+
* // or shorthand:
|
|
1572
|
+
* const primary = glaze(280, 80);
|
|
1573
|
+
* ```
|
|
1574
|
+
*/
|
|
1575
|
+
function glaze(hueOrOptions, saturation) {
|
|
1576
|
+
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
|
|
1577
|
+
return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Configure global glaze settings.
|
|
1581
|
+
*/
|
|
1582
|
+
glaze.configure = function configure(config) {
|
|
1583
|
+
globalConfig = {
|
|
1584
|
+
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
1585
|
+
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
1586
|
+
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
1587
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
1588
|
+
states: {
|
|
1589
|
+
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
1590
|
+
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
1591
|
+
},
|
|
1592
|
+
modes: {
|
|
1593
|
+
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
1594
|
+
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
1595
|
+
},
|
|
1596
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
1597
|
+
};
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* Compose multiple themes into a palette.
|
|
1601
|
+
*/
|
|
1602
|
+
glaze.palette = function palette(themes) {
|
|
1603
|
+
return createPalette(themes);
|
|
1604
|
+
};
|
|
1605
|
+
/**
|
|
1606
|
+
* Create a theme from a serialized export.
|
|
1607
|
+
*/
|
|
1608
|
+
glaze.from = function from(data) {
|
|
1609
|
+
return createTheme(data.hue, data.saturation, data.colors);
|
|
1610
|
+
};
|
|
1611
|
+
/**
|
|
1612
|
+
* Create a standalone single-color token.
|
|
1613
|
+
*/
|
|
1614
|
+
glaze.color = function color(input) {
|
|
1615
|
+
return createColorToken(input);
|
|
1616
|
+
};
|
|
1617
|
+
/**
|
|
1618
|
+
* Compute a shadow color from a bg/fg pair and intensity.
|
|
1619
|
+
*/
|
|
1620
|
+
glaze.shadow = function shadow(input) {
|
|
1621
|
+
const bg = parseOkhslInput(input.bg);
|
|
1622
|
+
const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
|
|
1623
|
+
const tuning = resolveShadowTuning(input.tuning);
|
|
1624
|
+
return computeShadow({
|
|
1625
|
+
...bg,
|
|
1626
|
+
alpha: 1
|
|
1627
|
+
}, fg ? {
|
|
1628
|
+
...fg,
|
|
1629
|
+
alpha: 1
|
|
1630
|
+
} : void 0, input.intensity, tuning);
|
|
1631
|
+
};
|
|
1632
|
+
/**
|
|
1633
|
+
* Format a resolved color variant as a CSS string.
|
|
1634
|
+
*/
|
|
1635
|
+
glaze.format = function format(variant, colorFormat) {
|
|
1636
|
+
return formatVariant(variant, colorFormat);
|
|
1637
|
+
};
|
|
1638
|
+
function parseOkhslInput(input) {
|
|
1639
|
+
if (typeof input === "string") {
|
|
1640
|
+
const rgb = parseHex(input);
|
|
1641
|
+
if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1642
|
+
const [h, s, l] = srgbToOkhsl(rgb);
|
|
1643
|
+
return {
|
|
1644
|
+
h,
|
|
1645
|
+
s,
|
|
1646
|
+
l
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
return input;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Create a theme from a hex color string.
|
|
1653
|
+
* Extracts hue and saturation from the color.
|
|
1654
|
+
*/
|
|
1655
|
+
glaze.fromHex = function fromHex(hex) {
|
|
1656
|
+
const rgb = parseHex(hex);
|
|
1657
|
+
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
1658
|
+
const [h, s] = srgbToOkhsl(rgb);
|
|
1659
|
+
return createTheme(h, s * 100);
|
|
1660
|
+
};
|
|
1661
|
+
/**
|
|
1662
|
+
* Create a theme from RGB values (0–255).
|
|
1663
|
+
* Extracts hue and saturation from the color.
|
|
1664
|
+
*/
|
|
1665
|
+
glaze.fromRgb = function fromRgb(r, g, b) {
|
|
1666
|
+
const [h, s] = srgbToOkhsl([
|
|
1667
|
+
r / 255,
|
|
1668
|
+
g / 255,
|
|
1669
|
+
b / 255
|
|
1670
|
+
]);
|
|
1671
|
+
return createTheme(h, s * 100);
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Get the current global configuration (for testing/debugging).
|
|
1675
|
+
*/
|
|
1676
|
+
glaze.getConfig = function getConfig() {
|
|
1677
|
+
return { ...globalConfig };
|
|
1678
|
+
};
|
|
1679
|
+
/**
|
|
1680
|
+
* Reset global configuration to defaults.
|
|
1681
|
+
*/
|
|
1682
|
+
glaze.resetConfig = function resetConfig() {
|
|
1683
|
+
globalConfig = {
|
|
1684
|
+
lightLightness: [10, 100],
|
|
1685
|
+
darkLightness: [15, 95],
|
|
1686
|
+
darkDesaturation: .1,
|
|
1687
|
+
darkCurve: .5,
|
|
1688
|
+
states: {
|
|
1689
|
+
dark: "@dark",
|
|
1690
|
+
highContrast: "@high-contrast"
|
|
1691
|
+
},
|
|
1692
|
+
modes: {
|
|
1693
|
+
dark: true,
|
|
1694
|
+
highContrast: false
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
//#endregion
|
|
1700
|
+
exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
|
|
1701
|
+
exports.findLightnessForContrast = findLightnessForContrast;
|
|
1702
|
+
exports.findValueForMixContrast = findValueForMixContrast;
|
|
1703
|
+
exports.formatHsl = formatHsl;
|
|
1704
|
+
exports.formatOkhsl = formatOkhsl;
|
|
1705
|
+
exports.formatOklch = formatOklch;
|
|
1706
|
+
exports.formatRgb = formatRgb;
|
|
1707
|
+
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
1708
|
+
exports.glaze = glaze;
|
|
1709
|
+
exports.okhslToLinearSrgb = okhslToLinearSrgb;
|
|
1710
|
+
exports.okhslToOklab = okhslToOklab;
|
|
1711
|
+
exports.okhslToSrgb = okhslToSrgb;
|
|
1712
|
+
exports.parseHex = parseHex;
|
|
1713
|
+
exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
|
|
1714
|
+
exports.resolveMinContrast = resolveMinContrast;
|
|
1715
|
+
exports.srgbToOkhsl = srgbToOkhsl;
|
|
1716
|
+
//# sourceMappingURL=index.cjs.map
|