saturon 0.2.3 → 0.3.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/LICENSE +1 -1
- package/README.md +5 -4
- package/dist/Color.d.ts +50 -47
- package/dist/Color.js +122 -229
- package/dist/converters.d.ts +35 -141
- package/dist/converters.js +8 -422
- package/dist/engine.d.ts +148 -0
- package/dist/engine.js +1900 -0
- package/dist/index.umd.js +1774 -1300
- package/dist/index.umd.min.js +143 -1
- package/dist/math.d.ts +2 -12
- package/dist/math.js +1 -11
- package/dist/tests/Color.test.d.ts +17 -0
- package/dist/tests/Color.test.js +1062 -0
- package/dist/tests/wpt.test.js +2129 -0
- package/dist/types.d.ts +62 -72
- package/dist/utils.d.ts +10 -141
- package/dist/utils.js +108 -1052
- package/package.json +10 -11
- package/dist/temp.js +0 -3
- /package/dist/{temp.d.ts → tests/wpt.test.d.ts} +0 -0
package/dist/index.umd.js
CHANGED
|
@@ -126,53 +126,32 @@ var Saturon = (() => {
|
|
|
126
126
|
return product.map((x) => x[0]);
|
|
127
127
|
return product;
|
|
128
128
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
expression += input[i];
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (input[i] === "(") {
|
|
143
|
-
expression += "(";
|
|
144
|
-
i++;
|
|
145
|
-
depth = 1;
|
|
146
|
-
while (i < input.length && depth > 0) {
|
|
147
|
-
const char = input[i];
|
|
148
|
-
if (char === "(")
|
|
149
|
-
depth++;
|
|
150
|
-
else if (char === ")")
|
|
151
|
-
depth--;
|
|
152
|
-
if (depth > 0)
|
|
153
|
-
expression += char;
|
|
154
|
-
i++;
|
|
155
|
-
}
|
|
156
|
-
expression += ")";
|
|
157
|
-
}
|
|
158
|
-
return { expression, end: i };
|
|
129
|
+
function normalize(component, value) {
|
|
130
|
+
const [min2, max2] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
|
|
131
|
+
if (Number.isNaN(component))
|
|
132
|
+
return 0;
|
|
133
|
+
if (component === Infinity)
|
|
134
|
+
return max2;
|
|
135
|
+
if (component === -Infinity)
|
|
136
|
+
return min2;
|
|
137
|
+
return typeof component === "number" ? component : 0;
|
|
159
138
|
}
|
|
160
139
|
function fit(coords, model, options = {}) {
|
|
161
140
|
const { method = config.defaults.fit, precision } = options;
|
|
162
141
|
const { components } = colorModels[model];
|
|
163
|
-
const
|
|
142
|
+
const defs = Object.values(components).reduce((arr, props) => (arr[props.index] = props, arr), []);
|
|
164
143
|
let clipped;
|
|
165
|
-
if (method === "none")
|
|
144
|
+
if (method === "none")
|
|
166
145
|
clipped = coords;
|
|
167
|
-
|
|
146
|
+
else if (method === "clip") {
|
|
168
147
|
clipped = coords.map((v, i) => {
|
|
169
|
-
const prop =
|
|
148
|
+
const prop = defs[i];
|
|
170
149
|
if (!prop)
|
|
171
150
|
throw new Error(`Missing component properties for index ${i}.`);
|
|
172
151
|
if (prop.value === "hue")
|
|
173
152
|
return (v % 360 + 360) % 360;
|
|
174
|
-
const [
|
|
175
|
-
return Math.min(
|
|
153
|
+
const [min2, max2] = Array.isArray(prop.value) ? prop.value : [0, 100];
|
|
154
|
+
return Math.min(max2, Math.max(min2, v));
|
|
176
155
|
});
|
|
177
156
|
} else {
|
|
178
157
|
const fn = fitMethods[method];
|
|
@@ -186,618 +165,21 @@ var Saturon = (() => {
|
|
|
186
165
|
if (typeof precision === "number" || precision === null)
|
|
187
166
|
p = precision;
|
|
188
167
|
else if (typeof precision === "undefined")
|
|
189
|
-
p =
|
|
168
|
+
p = defs[i]?.precision ?? 3;
|
|
190
169
|
else
|
|
191
170
|
throw new TypeError(`Invalid precision value: ${precision}.`);
|
|
192
171
|
return p === null ? v : Number(v.toFixed(p));
|
|
193
172
|
});
|
|
194
173
|
}
|
|
195
|
-
function modelConverterToColorConverter(name, converter) {
|
|
196
|
-
const evaluateComponent = (token, value, base = {}, commaSeparated = false, relative = false) => {
|
|
197
|
-
const parsePercent = (str, min2, max2) => {
|
|
198
|
-
const percent = parseFloat(str);
|
|
199
|
-
if (isNaN(percent))
|
|
200
|
-
throw new Error(`Invalid percentage value: '${str}'.`);
|
|
201
|
-
if (value === "percentage")
|
|
202
|
-
return percent;
|
|
203
|
-
if (min2 < 0 && max2 > 0)
|
|
204
|
-
return percent / 100 * (max2 - min2) / 2;
|
|
205
|
-
return percent / 100 * (max2 - min2) + min2;
|
|
206
|
-
};
|
|
207
|
-
const parseHue = (token2) => {
|
|
208
|
-
const value2 = parseFloat(token2);
|
|
209
|
-
if (isNaN(value2))
|
|
210
|
-
throw new Error(`Invalid hue value: '${token2}'.`);
|
|
211
|
-
if (token2.slice(-3) === "deg")
|
|
212
|
-
return value2;
|
|
213
|
-
if (token2.slice(-3) === "rad")
|
|
214
|
-
return value2 * (180 / pi);
|
|
215
|
-
if (token2.slice(-4) === "grad")
|
|
216
|
-
return value2 * 0.9;
|
|
217
|
-
if (token2.slice(-4) === "turn")
|
|
218
|
-
return value2 * 360;
|
|
219
|
-
return value2;
|
|
220
|
-
};
|
|
221
|
-
const parseCalc = (token2, _min, _max) => {
|
|
222
|
-
const tokenize2 = (s) => {
|
|
223
|
-
const out = [];
|
|
224
|
-
for (let i = 0; i < s.length; ) {
|
|
225
|
-
const c = s[i];
|
|
226
|
-
if (/\s/.test(c)) {
|
|
227
|
-
i++;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (s.slice(i, i + 2) === "**") {
|
|
231
|
-
out.push({ type: "operator", value: "**" });
|
|
232
|
-
i += 2;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (/[0-9]/.test(c) || c === "." && /[0-9]/.test(s[i + 1] || "")) {
|
|
236
|
-
let num = "";
|
|
237
|
-
while (i < s.length && /[0-9.]/.test(s[i]))
|
|
238
|
-
num += s[i++];
|
|
239
|
-
if (i < s.length && /[eE]/.test(s[i])) {
|
|
240
|
-
num += s[i++];
|
|
241
|
-
if (/[+-]/.test(s[i]))
|
|
242
|
-
num += s[i++];
|
|
243
|
-
while (i < s.length && /[0-9]/.test(s[i]))
|
|
244
|
-
num += s[i++];
|
|
245
|
-
}
|
|
246
|
-
out.push({ type: "number", value: parseFloat(num) });
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (/[a-zA-Z_]/.test(c)) {
|
|
250
|
-
let id = "";
|
|
251
|
-
while (i < s.length && /[a-zA-Z0-9_]/.test(s[i]))
|
|
252
|
-
id += s[i++];
|
|
253
|
-
out.push({ type: "identifier", value: id });
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if ("+-*/%(),".includes(c)) {
|
|
257
|
-
out.push({ type: "operator", value: c });
|
|
258
|
-
i++;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
throw new Error(`Unexpected character: ${c}`);
|
|
262
|
-
}
|
|
263
|
-
return out;
|
|
264
|
-
};
|
|
265
|
-
const parse = (tokens) => {
|
|
266
|
-
let pos = 0;
|
|
267
|
-
const cur = () => pos < tokens.length ? tokens[pos] : null;
|
|
268
|
-
const nxt = () => {
|
|
269
|
-
if (pos >= tokens.length)
|
|
270
|
-
throw new Error("Unexpected end of input");
|
|
271
|
-
return tokens[pos++];
|
|
272
|
-
};
|
|
273
|
-
const expect = (v) => {
|
|
274
|
-
const t = cur();
|
|
275
|
-
if (!t || t.value !== v)
|
|
276
|
-
throw new Error(`Expected "${v}" but got "${t ? t.value : "end of input"}`);
|
|
277
|
-
nxt();
|
|
278
|
-
};
|
|
279
|
-
const parsePrimary = () => {
|
|
280
|
-
const t = cur();
|
|
281
|
-
if (!t)
|
|
282
|
-
throw new Error("Unexpected end of input");
|
|
283
|
-
if (t.type === "number") {
|
|
284
|
-
nxt();
|
|
285
|
-
return { type: "number", value: t.value };
|
|
286
|
-
}
|
|
287
|
-
if (t.type === "identifier") {
|
|
288
|
-
nxt();
|
|
289
|
-
if (cur() && cur().value === "(") {
|
|
290
|
-
nxt();
|
|
291
|
-
const args = [];
|
|
292
|
-
if (cur() && cur().value !== ")") {
|
|
293
|
-
args.push(parseAdd());
|
|
294
|
-
while (cur() && cur().value === ",") {
|
|
295
|
-
nxt();
|
|
296
|
-
args.push(parseAdd());
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
expect(")");
|
|
300
|
-
return { type: "call", func: t.value, args };
|
|
301
|
-
}
|
|
302
|
-
return { type: "var", name: t.value };
|
|
303
|
-
}
|
|
304
|
-
if (t.value === "(") {
|
|
305
|
-
nxt();
|
|
306
|
-
const e2 = parseAdd();
|
|
307
|
-
expect(")");
|
|
308
|
-
return e2;
|
|
309
|
-
}
|
|
310
|
-
throw new Error(`Unexpected token: ${t.value}`);
|
|
311
|
-
};
|
|
312
|
-
const parseUnary = () => {
|
|
313
|
-
if (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
314
|
-
const op = nxt().value;
|
|
315
|
-
return { type: "unary", op, arg: parseUnary() };
|
|
316
|
-
}
|
|
317
|
-
return parsePrimary();
|
|
318
|
-
};
|
|
319
|
-
const parsePower = () => {
|
|
320
|
-
let left = parseUnary();
|
|
321
|
-
while (cur() && cur().value === "**") {
|
|
322
|
-
const op = nxt().value;
|
|
323
|
-
left = { type: "binary", op, left, right: parseUnary() };
|
|
324
|
-
}
|
|
325
|
-
return left;
|
|
326
|
-
};
|
|
327
|
-
const parseMul = () => {
|
|
328
|
-
let left = parsePower();
|
|
329
|
-
while (cur() && ["*", "/", "%"].includes(String(cur().value))) {
|
|
330
|
-
const op = nxt().value;
|
|
331
|
-
left = { type: "binary", op, left, right: parsePower() };
|
|
332
|
-
}
|
|
333
|
-
return left;
|
|
334
|
-
};
|
|
335
|
-
const parseAdd = () => {
|
|
336
|
-
let left = parseMul();
|
|
337
|
-
while (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
338
|
-
const op = nxt().value;
|
|
339
|
-
left = { type: "binary", op, left, right: parseMul() };
|
|
340
|
-
}
|
|
341
|
-
return left;
|
|
342
|
-
};
|
|
343
|
-
const ast = parseAdd();
|
|
344
|
-
if (pos < tokens.length)
|
|
345
|
-
throw new Error(`Extra tokens after expression: ${tokens.slice(pos).map((t) => t.value).join(" ")}`);
|
|
346
|
-
return ast;
|
|
347
|
-
};
|
|
348
|
-
const evaluate = (ast, env) => {
|
|
349
|
-
switch (ast.type) {
|
|
350
|
-
case "number":
|
|
351
|
-
return ast.value;
|
|
352
|
-
case "var": {
|
|
353
|
-
const v = env[ast.name];
|
|
354
|
-
if (v === void 0)
|
|
355
|
-
throw new Error(`Unknown variable: ${ast.name}`);
|
|
356
|
-
if (typeof v === "function")
|
|
357
|
-
throw new Error(`Expected variable but found function: ${ast.name}`);
|
|
358
|
-
return v;
|
|
359
|
-
}
|
|
360
|
-
case "binary": {
|
|
361
|
-
const L = evaluate(ast.left, env), R = evaluate(ast.right, env);
|
|
362
|
-
switch (ast.op) {
|
|
363
|
-
case "+":
|
|
364
|
-
return L + R;
|
|
365
|
-
case "-":
|
|
366
|
-
return L - R;
|
|
367
|
-
case "*":
|
|
368
|
-
return L * R;
|
|
369
|
-
case "/":
|
|
370
|
-
return L / R;
|
|
371
|
-
case "%":
|
|
372
|
-
return L % R;
|
|
373
|
-
case "**":
|
|
374
|
-
return L ** R;
|
|
375
|
-
default:
|
|
376
|
-
throw new Error(`Unknown binary operator: ${ast.op}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
case "unary": {
|
|
380
|
-
const v = evaluate(ast.arg, env);
|
|
381
|
-
switch (ast.op) {
|
|
382
|
-
case "+":
|
|
383
|
-
return +v;
|
|
384
|
-
case "-":
|
|
385
|
-
return -v;
|
|
386
|
-
default:
|
|
387
|
-
throw new Error(`Unknown unary operator: ${ast.op}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
case "call": {
|
|
391
|
-
const fn = env[ast.func];
|
|
392
|
-
if (typeof fn !== "function")
|
|
393
|
-
throw new Error(`Unknown function: ${ast.func}`);
|
|
394
|
-
return fn(...ast.args.map((a) => evaluate(a, env)));
|
|
395
|
-
}
|
|
396
|
-
default: {
|
|
397
|
-
throw new Error(`Unknown AST node type: ${ast.type}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
let inner = token2.slice(5, -1).trim();
|
|
402
|
-
if (inner === "infinity")
|
|
403
|
-
return Infinity;
|
|
404
|
-
if (inner === "-infinity")
|
|
405
|
-
return -Infinity;
|
|
406
|
-
if (inner === "NaN")
|
|
407
|
-
return NaN;
|
|
408
|
-
inner = inner.replace(/(\d+(\.\d+)?)%/g, (m) => {
|
|
409
|
-
if (relative === true)
|
|
410
|
-
throw new Error("<hue> and <percentage> values are converted to <number> in relative syntax.");
|
|
411
|
-
const r = parsePercent(m, _min, _max);
|
|
412
|
-
return r !== void 0 ? String(r) : "0";
|
|
413
|
-
});
|
|
414
|
-
inner = inner.replace(/(\d+(\.\d+)?)(deg|rad|grad|turn)/g, (_, num, __, unit) => {
|
|
415
|
-
if (relative === true)
|
|
416
|
-
throw new Error("<hue> and <percentage> values are converted to <number> in relative syntax.");
|
|
417
|
-
return String(parseHue(`${parseFloat(num)}${unit}`));
|
|
418
|
-
});
|
|
419
|
-
const caclEnv = {
|
|
420
|
-
...base,
|
|
421
|
-
pi,
|
|
422
|
-
e,
|
|
423
|
-
tau: pi * 2,
|
|
424
|
-
pow,
|
|
425
|
-
sqrt,
|
|
426
|
-
sin,
|
|
427
|
-
cos,
|
|
428
|
-
tan,
|
|
429
|
-
asin,
|
|
430
|
-
acos,
|
|
431
|
-
atan,
|
|
432
|
-
atan2,
|
|
433
|
-
exp,
|
|
434
|
-
log,
|
|
435
|
-
log10,
|
|
436
|
-
log2,
|
|
437
|
-
abs,
|
|
438
|
-
min,
|
|
439
|
-
max,
|
|
440
|
-
hypot,
|
|
441
|
-
round,
|
|
442
|
-
ceil,
|
|
443
|
-
floor,
|
|
444
|
-
sign,
|
|
445
|
-
trunc,
|
|
446
|
-
random
|
|
447
|
-
};
|
|
448
|
-
try {
|
|
449
|
-
const tokens = tokenize2(inner);
|
|
450
|
-
const ast = parse(tokens);
|
|
451
|
-
const result = evaluate(ast, caclEnv);
|
|
452
|
-
return result;
|
|
453
|
-
} catch (err) {
|
|
454
|
-
throw new Error(`Evaluation error: ${err}`);
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
const evaluateHue = () => {
|
|
458
|
-
if (/^-?(?:\d+|\d*\.\d+)(?:deg|rad|grad|turn)$/.test(token)) {
|
|
459
|
-
return parseHue(token);
|
|
460
|
-
}
|
|
461
|
-
if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
|
|
462
|
-
return parseFloat(token);
|
|
463
|
-
}
|
|
464
|
-
const [min2, max2] = [0, 360];
|
|
465
|
-
if (token.slice(0, 5) === "calc(") {
|
|
466
|
-
return parseCalc(token, min2, max2);
|
|
467
|
-
}
|
|
468
|
-
throw new Error(`Invalid hue value: '${token}'. Must be a number, or a number with a unit (deg, rad, grad, turn).`);
|
|
469
|
-
};
|
|
470
|
-
const evaluatePercent = () => {
|
|
471
|
-
if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
|
|
472
|
-
if (commaSeparated && supportsLegacy === true) {
|
|
473
|
-
throw new Error("The legacy color syntax does not allow numbers for <percentage> components.");
|
|
474
|
-
}
|
|
475
|
-
return parseFloat(token);
|
|
476
|
-
}
|
|
477
|
-
const [min2, max2] = [0, 100];
|
|
478
|
-
if (token[token.length - 1] === "%") {
|
|
479
|
-
return parsePercent(token, min2, max2);
|
|
480
|
-
}
|
|
481
|
-
if (token.slice(0, 5) === "calc(") {
|
|
482
|
-
return parseCalc(token, min2, max2);
|
|
483
|
-
}
|
|
484
|
-
throw new Error(`Invalid percentage value: '${token}'. Must be a percentage or a number.`);
|
|
485
|
-
};
|
|
486
|
-
const evaluateNumber = () => {
|
|
487
|
-
if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
|
|
488
|
-
return parseFloat(token);
|
|
489
|
-
}
|
|
490
|
-
const [min2, max2] = value;
|
|
491
|
-
if (token[token.length - 1] === "%") {
|
|
492
|
-
return parsePercent(token, min2, max2);
|
|
493
|
-
}
|
|
494
|
-
if (token.slice(0, 5) === "calc(") {
|
|
495
|
-
return parseCalc(token, min2, max2);
|
|
496
|
-
}
|
|
497
|
-
throw new Error(`Invalid number value: '${token}'. Must be a number${relative === false ? " or a percentage" : ""}.`);
|
|
498
|
-
};
|
|
499
|
-
if (token === "none")
|
|
500
|
-
return NaN;
|
|
501
|
-
if (token in base)
|
|
502
|
-
return base[token];
|
|
503
|
-
if (value === "hue")
|
|
504
|
-
return evaluateHue();
|
|
505
|
-
if (value === "percentage")
|
|
506
|
-
return evaluatePercent();
|
|
507
|
-
if (Array.isArray(value))
|
|
508
|
-
return evaluateNumber();
|
|
509
|
-
throw new Error(`Unable to parse component token: ${token}`);
|
|
510
|
-
};
|
|
511
|
-
const parseAST = (ast) => {
|
|
512
|
-
const { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated } = ast;
|
|
513
|
-
const { components: components2, supportsLegacy: supportsLegacy2 } = converter;
|
|
514
|
-
components2.alpha = { index: 3, value: [0, 1], precision: 3 };
|
|
515
|
-
if (commaSeparated && supportsLegacy2 !== true) {
|
|
516
|
-
throw new Error(`<${fn}()> does not support comma-separated syntax.`);
|
|
517
|
-
}
|
|
518
|
-
const sorted = Object.entries(components2).sort((a, b) => a[1].index - b[1].index);
|
|
519
|
-
if (fromOrigin) {
|
|
520
|
-
let colorSpace;
|
|
521
|
-
if (fn === "color") {
|
|
522
|
-
colorSpace = space;
|
|
523
|
-
} else if (fn in colorModels) {
|
|
524
|
-
colorSpace = fn;
|
|
525
|
-
} else {
|
|
526
|
-
for (const model in colorModels) {
|
|
527
|
-
if (colorModels[model].alphaVariant === fn) {
|
|
528
|
-
colorSpace = model;
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const originComponents = Color.from(fromOrigin).in(colorSpace).toObject({ fit: "none", precision: null });
|
|
534
|
-
const evaluatedComponents = [c1, c2, c3, alpha].map((token, i) => {
|
|
535
|
-
const [, meta] = sorted[i];
|
|
536
|
-
return evaluateComponent(token, meta.value, originComponents, commaSeparated, true);
|
|
537
|
-
});
|
|
538
|
-
return evaluatedComponents.slice(0, 4);
|
|
539
|
-
} else {
|
|
540
|
-
const result = [];
|
|
541
|
-
const percentFlags = [];
|
|
542
|
-
const tokens = [c1, c2, c3, alpha];
|
|
543
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
544
|
-
const [, meta] = sorted[i];
|
|
545
|
-
const token = tokens[i];
|
|
546
|
-
if (commaSeparated && token === "none") {
|
|
547
|
-
throw new Error(`${fn}() cannot use "none" in comma-separated syntax.`);
|
|
548
|
-
}
|
|
549
|
-
if (meta.index !== 3 && meta.value !== "hue" && meta.value !== "percentage" && token.slice(0, 5) !== "calc(") {
|
|
550
|
-
percentFlags.push(token.trim()[token.length - 1] === "%");
|
|
551
|
-
}
|
|
552
|
-
if (token) {
|
|
553
|
-
const value = evaluateComponent(token, meta.value, {}, commaSeparated);
|
|
554
|
-
result[meta.index] = value;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (commaSeparated && percentFlags.length > 1) {
|
|
558
|
-
const allPercent = percentFlags.every(Boolean);
|
|
559
|
-
const nonePercent = percentFlags.every((f) => !f);
|
|
560
|
-
if (!allPercent && !nonePercent) {
|
|
561
|
-
throw new Error(`${fn}()'s <number> components must all be numbers or all percentages.`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return result.slice(0, 4);
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
const getAST = (tokens) => {
|
|
568
|
-
const getAlpha = (index, separator = "/") => {
|
|
569
|
-
if (tokens[index] !== void 0) {
|
|
570
|
-
if (tokens[index] === separator) {
|
|
571
|
-
return { value: tokens[index + 1], hasAlpha: true };
|
|
572
|
-
}
|
|
573
|
-
throw new Error("Invalid alpha separator");
|
|
574
|
-
}
|
|
575
|
-
return { value: "1", hasAlpha: false };
|
|
576
|
-
};
|
|
577
|
-
let fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated = false, expectedLength;
|
|
578
|
-
if (tokens[0] === "color") {
|
|
579
|
-
fn = "color";
|
|
580
|
-
if (tokens[1] === "from") {
|
|
581
|
-
space = tokens[3];
|
|
582
|
-
fromOrigin = tokens[2];
|
|
583
|
-
c1 = tokens[4];
|
|
584
|
-
c2 = tokens[5];
|
|
585
|
-
c3 = tokens[6];
|
|
586
|
-
const { value, hasAlpha } = getAlpha(7);
|
|
587
|
-
expectedLength = hasAlpha ? 9 : 7;
|
|
588
|
-
alpha = value;
|
|
589
|
-
} else {
|
|
590
|
-
space = tokens[1];
|
|
591
|
-
fromOrigin = null;
|
|
592
|
-
c1 = tokens[2];
|
|
593
|
-
c2 = tokens[3];
|
|
594
|
-
c3 = tokens[4];
|
|
595
|
-
const { value, hasAlpha } = getAlpha(5);
|
|
596
|
-
expectedLength = hasAlpha ? 7 : 5;
|
|
597
|
-
alpha = value;
|
|
598
|
-
}
|
|
599
|
-
} else {
|
|
600
|
-
fn = tokens[0];
|
|
601
|
-
space = null;
|
|
602
|
-
if (tokens[1] === "from") {
|
|
603
|
-
fromOrigin = tokens[2];
|
|
604
|
-
c1 = tokens[3];
|
|
605
|
-
c2 = tokens[4];
|
|
606
|
-
c3 = tokens[5];
|
|
607
|
-
const { value, hasAlpha } = getAlpha(6);
|
|
608
|
-
expectedLength = hasAlpha ? 8 : 6;
|
|
609
|
-
alpha = value;
|
|
610
|
-
} else {
|
|
611
|
-
fromOrigin = null;
|
|
612
|
-
c1 = tokens[1];
|
|
613
|
-
if (tokens[2] === "," && tokens[4] === ",") {
|
|
614
|
-
commaSeparated = true;
|
|
615
|
-
c2 = tokens[3];
|
|
616
|
-
c3 = tokens[5];
|
|
617
|
-
const { value, hasAlpha } = getAlpha(6, ",");
|
|
618
|
-
expectedLength = hasAlpha ? 8 : 6;
|
|
619
|
-
if (hasAlpha && tokens[6] !== ",") {
|
|
620
|
-
throw new Error("Comma optional syntax requires no commas at all.");
|
|
621
|
-
}
|
|
622
|
-
alpha = value;
|
|
623
|
-
} else {
|
|
624
|
-
c2 = tokens[2];
|
|
625
|
-
c3 = tokens[3];
|
|
626
|
-
const { value, hasAlpha } = getAlpha(4);
|
|
627
|
-
expectedLength = hasAlpha ? 6 : 4;
|
|
628
|
-
alpha = value;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
if (tokens.length !== expectedLength) {
|
|
633
|
-
throw new Error(`Invalid number of tokens for ${fn}(): expected ${expectedLength} but got ${tokens.length}.`);
|
|
634
|
-
}
|
|
635
|
-
return { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated };
|
|
636
|
-
};
|
|
637
|
-
const tokenize = (str) => {
|
|
638
|
-
const tokens = [];
|
|
639
|
-
let i = 0;
|
|
640
|
-
let funcName = "";
|
|
641
|
-
while (i < str.length && str[i] !== "(") {
|
|
642
|
-
funcName += str[i];
|
|
643
|
-
i++;
|
|
644
|
-
}
|
|
645
|
-
funcName = funcName.trim();
|
|
646
|
-
tokens.push(funcName);
|
|
647
|
-
const innerStart = str.indexOf("(") + 1;
|
|
648
|
-
const innerStr = str.slice(innerStart, -1).trim();
|
|
649
|
-
i = 0;
|
|
650
|
-
if (innerStr.slice(0, 5) === "from ") {
|
|
651
|
-
tokens.push("from");
|
|
652
|
-
i += 5;
|
|
653
|
-
while (i < innerStr.length && innerStr[i] === " ")
|
|
654
|
-
i++;
|
|
655
|
-
const colorStart = i;
|
|
656
|
-
while (i < innerStr.length && innerStr[i] !== " ")
|
|
657
|
-
i++;
|
|
658
|
-
const colorStr = innerStr.slice(colorStart, i);
|
|
659
|
-
if (colorStr.includes("(")) {
|
|
660
|
-
const { expression, end } = extractBalancedExpression(innerStr, colorStart);
|
|
661
|
-
tokens.push(expression);
|
|
662
|
-
i = end;
|
|
663
|
-
} else {
|
|
664
|
-
tokens.push(colorStr);
|
|
665
|
-
}
|
|
666
|
-
while (i < innerStr.length && innerStr[i] === " ")
|
|
667
|
-
i++;
|
|
668
|
-
}
|
|
669
|
-
if (tokens[0] === "color" && i < innerStr.length) {
|
|
670
|
-
const spaceStart = i;
|
|
671
|
-
while (i < innerStr.length && innerStr[i] !== " ")
|
|
672
|
-
i++;
|
|
673
|
-
tokens.push(innerStr.slice(spaceStart, i));
|
|
674
|
-
while (i < innerStr.length && innerStr[i] === " ")
|
|
675
|
-
i++;
|
|
676
|
-
}
|
|
677
|
-
while (i < innerStr.length) {
|
|
678
|
-
const char = innerStr[i];
|
|
679
|
-
if (char === ",") {
|
|
680
|
-
tokens.push(",");
|
|
681
|
-
i++;
|
|
682
|
-
if (innerStr[i] === " ")
|
|
683
|
-
i++;
|
|
684
|
-
} else if (char === "/") {
|
|
685
|
-
tokens.push("/");
|
|
686
|
-
i++;
|
|
687
|
-
if (innerStr[i] === " ")
|
|
688
|
-
i++;
|
|
689
|
-
} else if (char === " ") {
|
|
690
|
-
i++;
|
|
691
|
-
} else if (/[a-zA-Z#]/.test(char)) {
|
|
692
|
-
const identStart = i;
|
|
693
|
-
let ident = "";
|
|
694
|
-
while (i < innerStr.length && /[a-zA-Z0-9-%#]/.test(innerStr[i])) {
|
|
695
|
-
ident += innerStr[i];
|
|
696
|
-
i++;
|
|
697
|
-
}
|
|
698
|
-
if (i < innerStr.length && innerStr[i] === "(") {
|
|
699
|
-
const { expression, end } = extractBalancedExpression(innerStr, identStart);
|
|
700
|
-
tokens.push(expression);
|
|
701
|
-
i = end;
|
|
702
|
-
} else {
|
|
703
|
-
tokens.push(ident);
|
|
704
|
-
}
|
|
705
|
-
} else if (/[\d.-]/.test(char)) {
|
|
706
|
-
let num = "";
|
|
707
|
-
while (i < innerStr.length && /[\d.eE+-]/.test(innerStr[i])) {
|
|
708
|
-
num += innerStr[i];
|
|
709
|
-
i++;
|
|
710
|
-
}
|
|
711
|
-
if (i < innerStr.length && innerStr[i] === "%") {
|
|
712
|
-
num += "%";
|
|
713
|
-
i++;
|
|
714
|
-
tokens.push(num);
|
|
715
|
-
} else if (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
|
|
716
|
-
let unit = "";
|
|
717
|
-
while (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
|
|
718
|
-
unit += innerStr[i];
|
|
719
|
-
i++;
|
|
720
|
-
}
|
|
721
|
-
tokens.push(num + unit);
|
|
722
|
-
} else {
|
|
723
|
-
tokens.push(num);
|
|
724
|
-
}
|
|
725
|
-
} else {
|
|
726
|
-
throw new Error(`Unexpected character: ${char}`);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return tokens;
|
|
730
|
-
};
|
|
731
|
-
const validateRelativeColorSpace = (str, name2) => {
|
|
732
|
-
const prefix = "color(from ";
|
|
733
|
-
if (str.slice(0, 11) !== prefix || str[str.length - 1] !== ")") {
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
const innerStr = str.slice(prefix.length, -1).trim();
|
|
737
|
-
const { expression, end } = extractBalancedExpression(innerStr, 0);
|
|
738
|
-
if (!expression) {
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
const rest = innerStr.slice(end).trim();
|
|
742
|
-
const parts = rest.split(/\s+/);
|
|
743
|
-
if (parts.length < 1) {
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
const colorSpace = parts[0];
|
|
747
|
-
return colorSpace === name2;
|
|
748
|
-
};
|
|
749
|
-
const { components, bridge, fromBridge, toBridge, alphaVariant, supportsLegacy } = converter;
|
|
750
|
-
const { PI: pi, E: e, pow, sqrt, sin, cos, tan, asin, acos, atan, atan2, exp, log, log10, log2, abs, min, max, hypot, round, ceil, floor, sign, trunc, random } = Math;
|
|
751
|
-
return {
|
|
752
|
-
isValid: (str) => {
|
|
753
|
-
const { alphaVariant: alphaVariant2 = name } = converter;
|
|
754
|
-
if (name in colorSpaces) {
|
|
755
|
-
const startsWithColor = str.slice(0, `color(${name} `.length) === `color(${name} `;
|
|
756
|
-
const startsWithFrom = str.slice(0, "color(from".length) === "color(from" && validateRelativeColorSpace(str, name);
|
|
757
|
-
return (startsWithColor || startsWithFrom) && str[str.length - 1] === ")";
|
|
758
|
-
}
|
|
759
|
-
return (str.slice(0, `${name}(`.length) === `${name}(` || str.slice(0, `${alphaVariant2}(`.length) === `${alphaVariant2}(`) && str[str.length - 1] === ")";
|
|
760
|
-
},
|
|
761
|
-
bridge,
|
|
762
|
-
toBridge: (coords) => [...toBridge(coords.slice(0, 3)), coords[3] ?? 1],
|
|
763
|
-
parse: (str) => {
|
|
764
|
-
const tokens = tokenize(str);
|
|
765
|
-
const ast = getAST(tokens);
|
|
766
|
-
const coords = parseAST(ast);
|
|
767
|
-
return [...coords.slice(0, 3), coords[3] ?? 1];
|
|
768
|
-
},
|
|
769
|
-
fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
|
|
770
|
-
format: ([c1, c2, c3, a = 1], options = {}) => {
|
|
771
|
-
const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
|
|
772
|
-
const clipped = fit([c1, c2, c3], name, { method: fitMethod, precision });
|
|
773
|
-
const alpha = Number(min(max(a, 0), 1).toFixed(3)).toString();
|
|
774
|
-
const formatted = clipped.map((v, index) => {
|
|
775
|
-
v = isNaN(v) ? 0 : v;
|
|
776
|
-
if ((units || legacy) && components) {
|
|
777
|
-
const def = Object.values(components).find((comp) => comp.index === index);
|
|
778
|
-
if (def?.value === "percentage")
|
|
779
|
-
return `${v}%`;
|
|
780
|
-
if (def?.value === "hue" && units)
|
|
781
|
-
return `${v}deg`;
|
|
782
|
-
}
|
|
783
|
-
return v.toString();
|
|
784
|
-
});
|
|
785
|
-
if (name in colorSpaces) {
|
|
786
|
-
return `color(${name} ${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
787
|
-
}
|
|
788
|
-
if (legacy && supportsLegacy) {
|
|
789
|
-
return a === 1 ? `${name}(${formatted.join(", ")})` : `${alphaVariant || name}(${formatted.join(", ")}, ${alpha})`;
|
|
790
|
-
}
|
|
791
|
-
return `${name}(${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
174
|
function spaceConverterToModelConverter(name, converter) {
|
|
796
175
|
const { fromLinear = (c) => c, toLinear = (c) => c, toBridgeMatrix, fromBridgeMatrix } = converter;
|
|
797
176
|
return {
|
|
798
177
|
supportsLegacy: false,
|
|
799
178
|
targetGamut: converter.targetGamut === null ? null : name,
|
|
800
|
-
components:
|
|
179
|
+
components: {
|
|
180
|
+
...Object.fromEntries(converter.components.map((comp, index) => [comp, { index, value: [0, 1], precision: 5 }])),
|
|
181
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
182
|
+
},
|
|
801
183
|
bridge: converter.bridge,
|
|
802
184
|
toBridge: (coords) => {
|
|
803
185
|
return multiplyMatrices(toBridgeMatrix, coords.map((c) => toLinear(c)));
|
|
@@ -805,6 +187,88 @@ var Saturon = (() => {
|
|
|
805
187
|
fromBridge: (coords) => multiplyMatrices(fromBridgeMatrix, coords).map((c) => fromLinear(c))
|
|
806
188
|
};
|
|
807
189
|
}
|
|
190
|
+
function hueDelta(a, b) {
|
|
191
|
+
const d = ((b - a) % 360 + 360) % 360;
|
|
192
|
+
return d > 180 ? d - 360 : d;
|
|
193
|
+
}
|
|
194
|
+
function hueDeltaLong(a, b) {
|
|
195
|
+
const d = hueDelta(a, b);
|
|
196
|
+
return d >= 0 ? d - 360 : d + 360;
|
|
197
|
+
}
|
|
198
|
+
function interpHue(a, b, t, method) {
|
|
199
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
200
|
+
return NaN;
|
|
201
|
+
switch (method) {
|
|
202
|
+
case "shorter":
|
|
203
|
+
return ((a + t * hueDelta(a, b)) % 360 + 360) % 360;
|
|
204
|
+
case "longer":
|
|
205
|
+
return ((a + t * hueDeltaLong(a, b)) % 360 + 360) % 360;
|
|
206
|
+
case "increasing":
|
|
207
|
+
return ((a * (1 - t) + (b < a ? b + 360 : b) * t) % 360 + 360) % 360;
|
|
208
|
+
case "decreasing":
|
|
209
|
+
return ((a * (1 - t) + (b > a ? b - 360 : b) * t) % 360 + 360) % 360;
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`Invalid hue interpolation: ${method}`);
|
|
212
|
+
}
|
|
213
|
+
function mixTwo(colorA, colorB, t, model = "oklab", hue = "shorter") {
|
|
214
|
+
const A = colorA.in(model).coords.slice();
|
|
215
|
+
const B = colorB.in(model).coords.slice();
|
|
216
|
+
const { components } = colorModels[model];
|
|
217
|
+
if (!components)
|
|
218
|
+
throw new Error(`Model ${model} does not have defined components.`);
|
|
219
|
+
const norm = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
|
|
220
|
+
const repair = (a, b, v) => {
|
|
221
|
+
const [min2, max2] = norm(v);
|
|
222
|
+
const fixInf = (x) => x === Infinity ? max2 : x === -Infinity ? min2 : x;
|
|
223
|
+
a = fixInf(a);
|
|
224
|
+
b = fixInf(b);
|
|
225
|
+
const aNaN = Number.isNaN(a);
|
|
226
|
+
const bNaN = Number.isNaN(b);
|
|
227
|
+
if (aNaN && bNaN)
|
|
228
|
+
return { a: NaN, b: NaN };
|
|
229
|
+
if (aNaN)
|
|
230
|
+
return { a: b, b };
|
|
231
|
+
if (bNaN)
|
|
232
|
+
return { a, b: a };
|
|
233
|
+
return { a, b, ok: true };
|
|
234
|
+
};
|
|
235
|
+
const hueIndex = components.h?.index ?? -1;
|
|
236
|
+
for (const key in components) {
|
|
237
|
+
const { index, value } = components[key];
|
|
238
|
+
if (index > 3)
|
|
239
|
+
continue;
|
|
240
|
+
const r = repair(A[index], B[index], value);
|
|
241
|
+
if (r.ok) {
|
|
242
|
+
const [min2, max2] = norm(value);
|
|
243
|
+
const fixInf = (x) => x === Infinity ? max2 : x === -Infinity ? min2 : x;
|
|
244
|
+
A[index] = fixInf(A[index]);
|
|
245
|
+
B[index] = fixInf(B[index]);
|
|
246
|
+
} else {
|
|
247
|
+
A[index] = r.a;
|
|
248
|
+
B[index] = r.b;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
t = Math.min(1, Math.max(0, t));
|
|
252
|
+
if (t === 0)
|
|
253
|
+
return new Color(model, [...A]);
|
|
254
|
+
if (t === 1)
|
|
255
|
+
return new Color(model, [...B]);
|
|
256
|
+
const aA = A[3] ?? 1;
|
|
257
|
+
const aB = B[3] ?? 1;
|
|
258
|
+
const resolvedA = A.slice(0, 3);
|
|
259
|
+
const resolvedB = B.slice(0, 3);
|
|
260
|
+
if (aA < 1 || aB < 1) {
|
|
261
|
+
const premixed = resolvedA.map((a2, i) => {
|
|
262
|
+
const b = resolvedB[i];
|
|
263
|
+
return i === hueIndex ? interpHue(a2, b, t, hue) : a2 * aA * (1 - t) + b * aB * t;
|
|
264
|
+
});
|
|
265
|
+
const a = aA * (1 - t) + aB * t;
|
|
266
|
+
const out = a > 0 ? premixed.map((v, i) => i === hueIndex ? v : v / a) : resolvedA.map((_, i) => i === hueIndex ? premixed[i] : 0);
|
|
267
|
+
return new Color(model, [...out, a]);
|
|
268
|
+
}
|
|
269
|
+
const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], t, hue) : a + (resolvedB[i] - a) * t);
|
|
270
|
+
return new Color(model, [...mixed, 1]);
|
|
271
|
+
}
|
|
808
272
|
|
|
809
273
|
// dist/math.js
|
|
810
274
|
var EPSILON = 1e-5;
|
|
@@ -890,15 +354,6 @@ var Saturon = (() => {
|
|
|
890
354
|
[1, -0.0894841775298119, -1.2914855480194092]
|
|
891
355
|
]
|
|
892
356
|
};
|
|
893
|
-
var EASINGS = {
|
|
894
|
-
linear: (t) => t,
|
|
895
|
-
"ease-in": (t) => t * t,
|
|
896
|
-
"ease-out": (t) => t * (2 - t),
|
|
897
|
-
"ease-in-out": (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
898
|
-
"ease-in-cubic": (t) => t * t * t,
|
|
899
|
-
"ease-out-cubic": (t) => --t * t * t + 1,
|
|
900
|
-
"ease-in-out-cubic": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
|
|
901
|
-
};
|
|
902
357
|
var fitMethods = {
|
|
903
358
|
"chroma-reduction": (coords, model) => {
|
|
904
359
|
const color = new Color(model, coords);
|
|
@@ -914,7 +369,7 @@ var Saturon = (() => {
|
|
|
914
369
|
let C_low = 0;
|
|
915
370
|
let C_high = 1;
|
|
916
371
|
const epsilon = 1e-6;
|
|
917
|
-
let clipped
|
|
372
|
+
let clipped;
|
|
918
373
|
while (C_high - C_low > epsilon) {
|
|
919
374
|
const C_mid = (C_low + C_high) / 2;
|
|
920
375
|
const candidate_color = new Color("oklch", [L_clipped, C_mid, H]);
|
|
@@ -963,14 +418,14 @@ var Saturon = (() => {
|
|
|
963
418
|
const E = current.deltaEOK(initialClippedColor);
|
|
964
419
|
if (E < JND)
|
|
965
420
|
return clipped;
|
|
966
|
-
let
|
|
967
|
-
let
|
|
421
|
+
let min2 = 0;
|
|
422
|
+
let max2 = C;
|
|
968
423
|
let min_inGamut = true;
|
|
969
|
-
while (
|
|
970
|
-
const chroma = (
|
|
424
|
+
while (max2 - min2 > epsilon) {
|
|
425
|
+
const chroma = (min2 + max2) / 2;
|
|
971
426
|
const candidate = new Color("oklch", [L, chroma, H]);
|
|
972
427
|
if (min_inGamut && candidate.inGamut(targetGamut, 1e-5))
|
|
973
|
-
|
|
428
|
+
min2 = chroma;
|
|
974
429
|
else {
|
|
975
430
|
const clippedCoords = fit(candidate.in(model).toArray({ fit: "none", precision: null }).slice(0, 3), model, { method: "clip" });
|
|
976
431
|
clipped = clippedCoords;
|
|
@@ -981,10 +436,10 @@ var Saturon = (() => {
|
|
|
981
436
|
return clipped;
|
|
982
437
|
else {
|
|
983
438
|
min_inGamut = false;
|
|
984
|
-
|
|
439
|
+
min2 = chroma;
|
|
985
440
|
}
|
|
986
441
|
} else
|
|
987
|
-
|
|
442
|
+
max2 = chroma;
|
|
988
443
|
}
|
|
989
444
|
}
|
|
990
445
|
return clipped;
|
|
@@ -993,16 +448,16 @@ var Saturon = (() => {
|
|
|
993
448
|
function RGB_to_XYZD65(rgb) {
|
|
994
449
|
const lin_sRGB = rgb.map((v) => {
|
|
995
450
|
const n = v / 255;
|
|
996
|
-
const
|
|
997
|
-
return
|
|
451
|
+
const sign2 = n < 0 ? -1 : 1, abs2 = Math.abs(n);
|
|
452
|
+
return abs2 <= 0.04045 ? n / 12.92 : sign2 * ((abs2 + 0.055) / 1.055) ** 2.4;
|
|
998
453
|
});
|
|
999
454
|
return multiplyMatrices(MATRICES.SRGB_to_XYZD65, lin_sRGB);
|
|
1000
455
|
}
|
|
1001
456
|
function XYZD65_to_RGB(xyz) {
|
|
1002
457
|
const linRGB = multiplyMatrices(MATRICES.XYZD65_to_SRGB, xyz);
|
|
1003
458
|
const gammaRGB = linRGB.map((v) => {
|
|
1004
|
-
const
|
|
1005
|
-
return
|
|
459
|
+
const sign2 = v < 0 ? -1 : 1, abs2 = Math.abs(v);
|
|
460
|
+
return abs2 > 31308e-7 ? sign2 * (1.055 * abs2 ** (1 / 2.4) - 0.055) : 12.92 * v;
|
|
1006
461
|
});
|
|
1007
462
|
return gammaRGB.map((v) => v * 255);
|
|
1008
463
|
}
|
|
@@ -1018,13 +473,13 @@ var Saturon = (() => {
|
|
|
1018
473
|
}
|
|
1019
474
|
function RGB_to_HSL(rgb) {
|
|
1020
475
|
const [r, g, b] = rgb.map((v) => v / 255);
|
|
1021
|
-
const
|
|
1022
|
-
const L = (
|
|
1023
|
-
const d =
|
|
476
|
+
const max2 = Math.max(r, g, b), min2 = Math.min(r, g, b);
|
|
477
|
+
const L = (max2 + min2) / 2;
|
|
478
|
+
const d = max2 - min2;
|
|
1024
479
|
let H = NaN, S = 0;
|
|
1025
480
|
if (d > 0) {
|
|
1026
481
|
S = d / (1 - Math.abs(2 * L - 1));
|
|
1027
|
-
H =
|
|
482
|
+
H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
1028
483
|
H = H * 60 % 360;
|
|
1029
484
|
}
|
|
1030
485
|
const S_out = S * 100, L_out = L * 100;
|
|
@@ -1043,14 +498,14 @@ var Saturon = (() => {
|
|
|
1043
498
|
}
|
|
1044
499
|
function RGB_to_HWB(rgb) {
|
|
1045
500
|
const [r, g, b] = rgb.map((v) => v / 255);
|
|
1046
|
-
const
|
|
1047
|
-
const d =
|
|
501
|
+
const max2 = Math.max(r, g, b), min2 = Math.min(r, g, b);
|
|
502
|
+
const d = max2 - min2;
|
|
1048
503
|
let H = NaN;
|
|
1049
504
|
if (d > EPSILON) {
|
|
1050
|
-
H =
|
|
505
|
+
H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
1051
506
|
H = H * 60 % 360;
|
|
1052
507
|
}
|
|
1053
|
-
return [H,
|
|
508
|
+
return [H, min2 * 100, (1 - max2) * 100];
|
|
1054
509
|
}
|
|
1055
510
|
function LAB_to_XYZD50([L, a, b]) {
|
|
1056
511
|
const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
@@ -1257,18 +712,18 @@ var Saturon = (() => {
|
|
|
1257
712
|
components: ["r", "g", "b"],
|
|
1258
713
|
bridge: "xyz-d65",
|
|
1259
714
|
toLinear: (c) => {
|
|
1260
|
-
const
|
|
1261
|
-
const
|
|
1262
|
-
if (
|
|
1263
|
-
return
|
|
1264
|
-
return
|
|
715
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
716
|
+
const abs2 = Math.abs(c);
|
|
717
|
+
if (abs2 <= 0.04045)
|
|
718
|
+
return sign2 * (abs2 / 12.92);
|
|
719
|
+
return sign2 * Math.pow((abs2 + 0.055) / 1.055, 2.4);
|
|
1265
720
|
},
|
|
1266
721
|
fromLinear: (c) => {
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
1269
|
-
if (
|
|
1270
|
-
return
|
|
1271
|
-
return
|
|
722
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
723
|
+
const abs2 = Math.abs(c);
|
|
724
|
+
if (abs2 > 31308e-7)
|
|
725
|
+
return sign2 * (1.055 * Math.pow(abs2, 1 / 2.4) - 0.055);
|
|
726
|
+
return sign2 * (12.92 * abs2);
|
|
1272
727
|
},
|
|
1273
728
|
toBridgeMatrix: MATRICES.SRGB_to_XYZD65,
|
|
1274
729
|
fromBridgeMatrix: MATRICES.XYZD65_to_SRGB
|
|
@@ -1283,18 +738,18 @@ var Saturon = (() => {
|
|
|
1283
738
|
components: ["r", "g", "b"],
|
|
1284
739
|
bridge: "xyz-d65",
|
|
1285
740
|
toLinear: (c) => {
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
if (
|
|
1289
|
-
return
|
|
1290
|
-
return
|
|
741
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
742
|
+
const abs2 = Math.abs(c);
|
|
743
|
+
if (abs2 <= 0.04045)
|
|
744
|
+
return sign2 * (abs2 / 12.92);
|
|
745
|
+
return sign2 * Math.pow((abs2 + 0.055) / 1.055, 2.4);
|
|
1291
746
|
},
|
|
1292
747
|
fromLinear: (c) => {
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
1295
|
-
if (
|
|
1296
|
-
return
|
|
1297
|
-
return
|
|
748
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
749
|
+
const abs2 = Math.abs(c);
|
|
750
|
+
if (abs2 > 31308e-7)
|
|
751
|
+
return sign2 * (1.055 * Math.pow(abs2, 1 / 2.4) - 0.055);
|
|
752
|
+
return sign2 * (12.92 * abs2);
|
|
1298
753
|
},
|
|
1299
754
|
toBridgeMatrix: MATRICES.P3_to_XYZD65,
|
|
1300
755
|
fromBridgeMatrix: MATRICES.XYZD65_to_P3
|
|
@@ -1305,20 +760,20 @@ var Saturon = (() => {
|
|
|
1305
760
|
toLinear: (c) => {
|
|
1306
761
|
const \u03B1 = 1.09929682680944;
|
|
1307
762
|
const \u03B2 = 0.018053968510807;
|
|
1308
|
-
const
|
|
1309
|
-
const
|
|
1310
|
-
if (
|
|
1311
|
-
return
|
|
1312
|
-
return
|
|
763
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
764
|
+
const abs2 = Math.abs(c);
|
|
765
|
+
if (abs2 < \u03B2 * 4.5)
|
|
766
|
+
return sign2 * (abs2 / 4.5);
|
|
767
|
+
return sign2 * Math.pow((abs2 + \u03B1 - 1) / \u03B1, 1 / 0.45);
|
|
1313
768
|
},
|
|
1314
769
|
fromLinear: (c) => {
|
|
1315
770
|
const \u03B1 = 1.09929682680944;
|
|
1316
771
|
const \u03B2 = 0.018053968510807;
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
if (
|
|
1320
|
-
return
|
|
1321
|
-
return
|
|
772
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
773
|
+
const abs2 = Math.abs(c);
|
|
774
|
+
if (abs2 > \u03B2)
|
|
775
|
+
return sign2 * (\u03B1 * Math.pow(abs2, 0.45) - (\u03B1 - 1));
|
|
776
|
+
return sign2 * (4.5 * abs2);
|
|
1322
777
|
},
|
|
1323
778
|
toBridgeMatrix: MATRICES.REC2020_to_XYZD65,
|
|
1324
779
|
fromBridgeMatrix: MATRICES.XYZD65_to_REC2020
|
|
@@ -1327,14 +782,14 @@ var Saturon = (() => {
|
|
|
1327
782
|
components: ["r", "g", "b"],
|
|
1328
783
|
bridge: "xyz-d65",
|
|
1329
784
|
toLinear: (c) => {
|
|
1330
|
-
const
|
|
1331
|
-
const
|
|
1332
|
-
return
|
|
785
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
786
|
+
const abs2 = Math.abs(c);
|
|
787
|
+
return sign2 * Math.pow(abs2, 563 / 256);
|
|
1333
788
|
},
|
|
1334
789
|
fromLinear: (c) => {
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
return
|
|
790
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
791
|
+
const abs2 = Math.abs(c);
|
|
792
|
+
return sign2 * Math.pow(abs2, 256 / 563);
|
|
1338
793
|
},
|
|
1339
794
|
toBridgeMatrix: MATRICES.A98_to_XYZD65,
|
|
1340
795
|
fromBridgeMatrix: MATRICES.XYZD65_to_A98
|
|
@@ -1344,19 +799,19 @@ var Saturon = (() => {
|
|
|
1344
799
|
bridge: "xyz-d50",
|
|
1345
800
|
toLinear: (c) => {
|
|
1346
801
|
const Et2 = 16 / 512;
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
if (
|
|
1350
|
-
return
|
|
1351
|
-
return
|
|
802
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
803
|
+
const abs2 = Math.abs(c);
|
|
804
|
+
if (abs2 <= Et2)
|
|
805
|
+
return sign2 * (abs2 / 16);
|
|
806
|
+
return sign2 * Math.pow(abs2, 1.8);
|
|
1352
807
|
},
|
|
1353
808
|
fromLinear: (c) => {
|
|
1354
809
|
const Et = 1 / 512;
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1357
|
-
if (
|
|
1358
|
-
return
|
|
1359
|
-
return
|
|
810
|
+
const sign2 = c < 0 ? -1 : 1;
|
|
811
|
+
const abs2 = Math.abs(c);
|
|
812
|
+
if (abs2 >= Et)
|
|
813
|
+
return sign2 * Math.pow(abs2, 1 / 1.8);
|
|
814
|
+
return sign2 * (16 * abs2);
|
|
1360
815
|
},
|
|
1361
816
|
toBridgeMatrix: MATRICES.ProPhoto_to_XYZD50,
|
|
1362
817
|
fromBridgeMatrix: MATRICES.XYZD50_to_ProPhoto
|
|
@@ -1406,7 +861,8 @@ var Saturon = (() => {
|
|
|
1406
861
|
components: {
|
|
1407
862
|
r: { index: 0, value: [0, 255], precision: 0 },
|
|
1408
863
|
g: { index: 1, value: [0, 255], precision: 0 },
|
|
1409
|
-
b: { index: 2, value: [0, 255], precision: 0 }
|
|
864
|
+
b: { index: 2, value: [0, 255], precision: 0 },
|
|
865
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1410
866
|
},
|
|
1411
867
|
bridge: "xyz-d65",
|
|
1412
868
|
toBridge: RGB_to_XYZD65,
|
|
@@ -1418,7 +874,8 @@ var Saturon = (() => {
|
|
|
1418
874
|
components: {
|
|
1419
875
|
h: { index: 0, value: "hue", precision: 0 },
|
|
1420
876
|
s: { index: 1, value: "percentage", precision: 0 },
|
|
1421
|
-
l: { index: 2, value: "percentage", precision: 0 }
|
|
877
|
+
l: { index: 2, value: "percentage", precision: 0 },
|
|
878
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1422
879
|
},
|
|
1423
880
|
bridge: "rgb",
|
|
1424
881
|
toBridge: HSL_to_RGB,
|
|
@@ -1428,7 +885,8 @@ var Saturon = (() => {
|
|
|
1428
885
|
components: {
|
|
1429
886
|
h: { index: 0, value: "hue", precision: 0 },
|
|
1430
887
|
w: { index: 1, value: "percentage", precision: 0 },
|
|
1431
|
-
b: { index: 2, value: "percentage", precision: 0 }
|
|
888
|
+
b: { index: 2, value: "percentage", precision: 0 },
|
|
889
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1432
890
|
},
|
|
1433
891
|
bridge: "rgb",
|
|
1434
892
|
toBridge: HWB_to_RGB,
|
|
@@ -1439,7 +897,8 @@ var Saturon = (() => {
|
|
|
1439
897
|
components: {
|
|
1440
898
|
l: { index: 0, value: "percentage", precision: 5 },
|
|
1441
899
|
a: { index: 1, value: [-125, 125], precision: 5 },
|
|
1442
|
-
b: { index: 2, value: [-125, 125], precision: 5 }
|
|
900
|
+
b: { index: 2, value: [-125, 125], precision: 5 },
|
|
901
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1443
902
|
},
|
|
1444
903
|
bridge: "xyz-d50",
|
|
1445
904
|
toBridge: LAB_to_XYZD50,
|
|
@@ -1450,7 +909,8 @@ var Saturon = (() => {
|
|
|
1450
909
|
components: {
|
|
1451
910
|
l: { index: 0, value: "percentage", precision: 5 },
|
|
1452
911
|
c: { index: 1, value: [0, 150], precision: 5 },
|
|
1453
|
-
h: { index: 2, value: "hue", precision: 5 }
|
|
912
|
+
h: { index: 2, value: "hue", precision: 5 },
|
|
913
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1454
914
|
},
|
|
1455
915
|
bridge: "lab",
|
|
1456
916
|
toBridge: LCH_to_LAB,
|
|
@@ -1461,7 +921,8 @@ var Saturon = (() => {
|
|
|
1461
921
|
components: {
|
|
1462
922
|
l: { index: 0, value: [0, 1], precision: 5 },
|
|
1463
923
|
a: { index: 1, value: [-0.4, 0.4], precision: 5 },
|
|
1464
|
-
b: { index: 2, value: [-0.4, 0.4], precision: 5 }
|
|
924
|
+
b: { index: 2, value: [-0.4, 0.4], precision: 5 },
|
|
925
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1465
926
|
},
|
|
1466
927
|
bridge: "xyz-d65",
|
|
1467
928
|
toBridge: OKLAB_to_XYZD65,
|
|
@@ -1472,7 +933,8 @@ var Saturon = (() => {
|
|
|
1472
933
|
components: {
|
|
1473
934
|
l: { index: 0, value: [0, 1], precision: 5 },
|
|
1474
935
|
c: { index: 1, value: [0, 0.4], precision: 5 },
|
|
1475
|
-
h: { index: 2, value: "hue", precision: 5 }
|
|
936
|
+
h: { index: 2, value: "hue", precision: 5 },
|
|
937
|
+
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
1476
938
|
},
|
|
1477
939
|
bridge: "oklab",
|
|
1478
940
|
toBridge: OKLCH_to_OKLAB,
|
|
@@ -1480,396 +942,1516 @@ var Saturon = (() => {
|
|
|
1480
942
|
},
|
|
1481
943
|
...colorSpaces
|
|
1482
944
|
};
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
945
|
+
|
|
946
|
+
// dist/engine.js
|
|
947
|
+
var grammar = `
|
|
948
|
+
/* ==========================================================================
|
|
949
|
+
Core Color Types
|
|
950
|
+
========================================================================== */
|
|
951
|
+
<color> = <color-base> | currentColor | <system-color> | <contrast-color()> | <device-cmyk()> | <light-dark-color>
|
|
952
|
+
|
|
953
|
+
<color-base> = <hex-color> | <color-function> | <named-color> | <color-mix()> | transparent
|
|
954
|
+
|
|
955
|
+
<color-function> = <rgb()> | <rgba()> | <hsl()> | <hsla()> | <hwb()> | <lab()> | <lch()> | <oklab()> | <oklch()> | <alpha()> | <color()> | <custom-color-function>
|
|
956
|
+
|
|
957
|
+
/* ==========================================================================
|
|
958
|
+
RGB & RGBA Syntax
|
|
959
|
+
========================================================================== */
|
|
960
|
+
<rgb()> = [ <legacy-rgb-syntax> | <modern-rgb-syntax> | <relative-rgb-syntax> ]
|
|
961
|
+
<rgba()> = [ <legacy-rgba-syntax> | <modern-rgba-syntax> | <relative-rgba-syntax> ]
|
|
962
|
+
|
|
963
|
+
<legacy-rgb-syntax> = rgb( <percentage>#{3} [ , <alpha-value> ]? ) | rgb( <number>#{3} [ , <alpha-value> ]? )
|
|
964
|
+
<legacy-rgba-syntax> = rgba( <percentage>#{3} [ , <alpha-value> ]? ) | rgba( <number>#{3} [ , <alpha-value> ]? )
|
|
965
|
+
|
|
966
|
+
<modern-rgb-syntax> = rgb( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
|
|
967
|
+
<modern-rgba-syntax> = rgba( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
|
|
968
|
+
<rgb-absolute-component> = <number> | <percentage> | none
|
|
969
|
+
<alpha-absolute-value> = <alpha-value> | none
|
|
970
|
+
|
|
971
|
+
<relative-rgb-syntax> = rgb( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
972
|
+
<relative-rgba-syntax> = rgba( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
973
|
+
|
|
974
|
+
/* ==========================================================================
|
|
975
|
+
HSL & HSLA Syntax
|
|
976
|
+
========================================================================== */
|
|
977
|
+
<hsl()> = [ <legacy-hsl-syntax> | <modern-hsl-syntax> | <relative-hsl-syntax> ]
|
|
978
|
+
<hsla()> = [ <legacy-hsla-syntax> | <modern-hsla-syntax> | <relative-hsla-syntax> ]
|
|
979
|
+
|
|
980
|
+
<legacy-hsl-syntax> = hsl( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
|
|
981
|
+
<legacy-hsla-syntax> = hsla( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
|
|
982
|
+
|
|
983
|
+
<modern-hsl-syntax> = hsl( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
984
|
+
<modern-hsla-syntax> = hsla( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
985
|
+
|
|
986
|
+
<relative-hsl-syntax> = hsl( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
|
|
987
|
+
<relative-hsla-syntax> = hsla( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
|
|
988
|
+
<hsl-relative-hue> = <hue> | none | h | s | l | alpha
|
|
989
|
+
<hsl-relative-component> = <percentage> | <number> | none | h | s | l | alpha
|
|
990
|
+
<hsl-relative-alpha> = <alpha-value> | none | h | s | l | alpha
|
|
991
|
+
|
|
992
|
+
/* ==========================================================================
|
|
993
|
+
HWB Syntax
|
|
994
|
+
========================================================================== */
|
|
995
|
+
<hwb()> = [ <absolute-hwb-syntax> | <relative-hwb-syntax> ]
|
|
996
|
+
<absolute-hwb-syntax> = hwb( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
997
|
+
<relative-hwb-syntax> = hwb( from <color> <hwb-relative-hue> <hwb-relative-component>{2} [ / <hwb-relative-alpha> ]? )
|
|
998
|
+
<hwb-relative-hue> = <hue> | none | h | w | b | alpha
|
|
999
|
+
<hwb-relative-component> = <percentage> | <number> | none | h | w | b | alpha
|
|
1000
|
+
<hwb-relative-alpha> = <alpha-value> | none | h | w | b | alpha
|
|
1001
|
+
|
|
1002
|
+
/* ==========================================================================
|
|
1003
|
+
LAB & LCH Syntax
|
|
1004
|
+
========================================================================== */
|
|
1005
|
+
<lab()> = [ <absolute-lab-syntax> | <relative-lab-syntax> ]
|
|
1006
|
+
<absolute-lab-syntax> = lab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
|
|
1007
|
+
<relative-lab-syntax> = lab( from <color> <lab-relative-component>{3} [ / <lab-relative-alpha> ]? )
|
|
1008
|
+
<lab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
|
|
1009
|
+
<lab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
|
|
1010
|
+
|
|
1011
|
+
<lch()> = [ <absolute-lch-syntax> | <relative-lch-syntax> ]
|
|
1012
|
+
<absolute-lch-syntax> = lch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
|
|
1013
|
+
<relative-lch-syntax> = lch( from <color> <lch-relative-component>{2} <lch-relative-hue> [ / <lch-relative-alpha> ]? )
|
|
1014
|
+
<lch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
|
|
1015
|
+
<lch-relative-hue> = <hue> | none | l | c | h | alpha
|
|
1016
|
+
<lch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
|
|
1017
|
+
|
|
1018
|
+
/* ==========================================================================
|
|
1019
|
+
OKLAB & OKLCH Syntax
|
|
1020
|
+
========================================================================== */
|
|
1021
|
+
<oklab()> = [ <absolute-oklab-syntax> | <relative-oklab-syntax> ]
|
|
1022
|
+
<absolute-oklab-syntax> = oklab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
|
|
1023
|
+
<relative-oklab-syntax> = oklab( from <color> <oklab-relative-component>{3} [ / <oklab-relative-alpha> ]? )
|
|
1024
|
+
<oklab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
|
|
1025
|
+
<oklab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
|
|
1026
|
+
|
|
1027
|
+
<oklch()> = [ <absolute-oklch-syntax> | <relative-oklch-syntax> ]
|
|
1028
|
+
<absolute-oklch-syntax> = oklch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
|
|
1029
|
+
<relative-oklch-syntax> = oklch( from <color> <oklch-relative-component>{2} <oklch-relative-hue> [ / <oklch-relative-alpha> ]? )
|
|
1030
|
+
<oklch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
|
|
1031
|
+
<oklch-relative-hue> = <hue> | none | l | c | h | alpha
|
|
1032
|
+
<oklch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
|
|
1033
|
+
|
|
1034
|
+
/* ==========================================================================
|
|
1035
|
+
ALPHA Syntax
|
|
1036
|
+
========================================================================== */
|
|
1037
|
+
<alpha()> = alpha( from <color> [ / [ <alpha-value> | none | alpha ] ]? )
|
|
1038
|
+
|
|
1039
|
+
/* ==========================================================================
|
|
1040
|
+
COLOR Space Function Syntax
|
|
1041
|
+
========================================================================== */
|
|
1042
|
+
<color()> = [ <absolute-color-syntax> | <relative-color-syntax> ]
|
|
1043
|
+
<absolute-color-syntax> = color( [ <predefined-rgb> | <xyz-space> | <custom-color-space> ] [ <number> | <percentage> | none ]{3} [ / [ <alpha-value> | none ] ]? )
|
|
1044
|
+
<relative-color-syntax> = [ <relative-rgb-color-syntax> | <relative-xyz-color-syntax> | <relative-custom-color-syntax> ]
|
|
1045
|
+
|
|
1046
|
+
<relative-rgb-color-syntax> = color( from <color> <predefined-rgb> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
1047
|
+
|
|
1048
|
+
<relative-xyz-color-syntax> = color( from <color> <xyz-space> <xyz-relative-component>{3} [ / <xyz-relative-alpha> ]? )
|
|
1049
|
+
<xyz-relative-component> = <number> | <percentage> | none | x | y | z | alpha
|
|
1050
|
+
<xyz-relative-alpha> = <alpha-value> | none | x | y | z | alpha
|
|
1051
|
+
|
|
1052
|
+
<predefined-rgb> = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020
|
|
1053
|
+
<xyz-space> = xyz | xyz-d50 | xyz-d65
|
|
1054
|
+
|
|
1055
|
+
<relative-custom-color-syntax> = %unregistered-sentinal-value%
|
|
1056
|
+
|
|
1057
|
+
/* ==========================================================================
|
|
1058
|
+
Functional Utilities (Light/Dark, Contrast, CMYK, Mix)
|
|
1059
|
+
========================================================================== */
|
|
1060
|
+
<light-dark-color> = light-dark( <color> , <color> )
|
|
1061
|
+
|
|
1062
|
+
<contrast-color()> = contrast-color( <color> )
|
|
1063
|
+
|
|
1064
|
+
<device-cmyk()> = <legacy-device-cmyk-syntax> | <modern-device-cmyk-syntax>
|
|
1065
|
+
<legacy-device-cmyk-syntax> = device-cmyk( <number>#{4} )
|
|
1066
|
+
<modern-device-cmyk-syntax> = device-cmyk( <cmyk-component>{4} [ / [ <alpha-value> | none ] ]? )
|
|
1067
|
+
<cmyk-component> = <number> | <percentage> | none
|
|
1068
|
+
|
|
1069
|
+
<color-mix()> = color-mix( [ <color-interpolation-method> , ]? [ <color> && <percentage [0,100]>? ]# )
|
|
1070
|
+
<color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
|
|
1071
|
+
<color-space> = <rectangular-color-space> | <polar-color-space>
|
|
1072
|
+
<rectangular-color-space> = srgb | srgb-linear | display-p3 | display-p3-linear | a98-rgb | prophoto-rgb | rec2020 | lab | oklab | <xyz-space> | <custom-color-space> | <custom-rectangular-space>
|
|
1073
|
+
<polar-color-space> = hsl | hwb | lch | oklch | <custom-polar-space>
|
|
1074
|
+
<hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
|
|
1075
|
+
|
|
1076
|
+
<custom-color-function> = %unregistered-sentinel-value%
|
|
1077
|
+
<custom-rectangular-space> = %unregistered-sentinel-value%
|
|
1078
|
+
<custom-polar-space> = %unregistered-sentinel-value%
|
|
1079
|
+
|
|
1080
|
+
/* ==========================================================================
|
|
1081
|
+
Shared Value Components
|
|
1082
|
+
========================================================================== */
|
|
1083
|
+
<alpha-value> = <number> | <percentage>
|
|
1084
|
+
<hue> = <number> | <angle>
|
|
1085
|
+
<rgb-relative-component> = <number> | <percentage> | none | r | g | b | alpha
|
|
1086
|
+
<rgb-relative-alpha> = <alpha-value> | none | r | g | b | alpha
|
|
1087
|
+
<custom-color-space> = %unregistered-sentinel-value%
|
|
1088
|
+
`;
|
|
1089
|
+
var RULE_START_RE = /^<[^>]+>\s*=/;
|
|
1090
|
+
var HASH_REPEAT_RE = /^(.*)#\{(\d+)\}$/;
|
|
1091
|
+
var REPEAT_RE = /^(.*)\{(\d+)\}$/;
|
|
1092
|
+
var REF_RE = /^<([^>\s]+)(?:\s+([^>]+))?>$/;
|
|
1093
|
+
var ARG_RE = /\[[^\]]+\]|[^\s]+/g;
|
|
1094
|
+
var grammarMap = parseGrammar(grammar);
|
|
1095
|
+
function concatNodes(a, b) {
|
|
1096
|
+
const lenA = a.length;
|
|
1097
|
+
const lenB = b.length;
|
|
1098
|
+
const result = new Array(lenA + lenB);
|
|
1099
|
+
for (let i = 0; i < lenA; i++)
|
|
1100
|
+
result[i] = a[i];
|
|
1101
|
+
for (let i = 0; i < lenB; i++)
|
|
1102
|
+
result[lenA + i] = b[i];
|
|
1103
|
+
return result;
|
|
1104
|
+
}
|
|
1105
|
+
function parseCmykComponent(n) {
|
|
1106
|
+
if (!n?.value)
|
|
1107
|
+
return 0;
|
|
1108
|
+
const raw = Array.isArray(n.value) ? n.value[0]?.value : n.value;
|
|
1109
|
+
return parseFloat(raw.trim());
|
|
1110
|
+
}
|
|
1111
|
+
function getLiteralValue(n) {
|
|
1112
|
+
if (!n)
|
|
1113
|
+
return "";
|
|
1114
|
+
if (n.type === "literal")
|
|
1115
|
+
return n.value;
|
|
1116
|
+
if (Array.isArray(n.value))
|
|
1117
|
+
return getLiteralValue(n.value[0]);
|
|
1118
|
+
return "";
|
|
1119
|
+
}
|
|
1120
|
+
function validateTokens(tokens, rootRule) {
|
|
1121
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1122
|
+
const rulesToTest = rootRule ? [rootRule] : Object.keys(grammarMap);
|
|
1123
|
+
for (let i = 0; i < rulesToTest.length; i++) {
|
|
1124
|
+
const ruleName = rulesToTest[i];
|
|
1125
|
+
const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
|
|
1126
|
+
for (let j = 0; j < results.length; j++) {
|
|
1127
|
+
const result = results[j];
|
|
1128
|
+
if (result.success && result.nextIndex === tokens.length) {
|
|
1129
|
+
return result.nodes[0];
|
|
1527
1130
|
}
|
|
1528
1131
|
}
|
|
1529
|
-
}
|
|
1530
|
-
"
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
const { expression: expr, end: e } = extractBalancedExpression(remaining, 0);
|
|
1549
|
-
if (expr) {
|
|
1550
|
-
colorExpression = expr;
|
|
1551
|
-
rest = remaining.slice(e).trim();
|
|
1552
|
-
} else {
|
|
1553
|
-
const m = remaining.match(/^([^\s]+)(.*)$/);
|
|
1554
|
-
colorExpression = m ? m[1] : remaining;
|
|
1555
|
-
rest = m ? m[2].trim() : "";
|
|
1556
|
-
}
|
|
1557
|
-
} else {
|
|
1558
|
-
const m = remaining.match(/^([^\s]+)(.*)$/);
|
|
1559
|
-
colorExpression = m ? m[1] : remaining;
|
|
1560
|
-
rest = m ? m[2].trim() : "";
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error("Invalid syntax");
|
|
1134
|
+
}
|
|
1135
|
+
function matchRule(ruleName, tokens, index, grammar2, memo, currentFunction, args) {
|
|
1136
|
+
const key = `${ruleName}:${index}:${currentFunction || ""}:${args ? args.join(",") : ""}`;
|
|
1137
|
+
const cached = memo.get(key);
|
|
1138
|
+
if (cached)
|
|
1139
|
+
return cached;
|
|
1140
|
+
const token = tokens[index];
|
|
1141
|
+
const validator = validators[ruleName];
|
|
1142
|
+
if (validator) {
|
|
1143
|
+
if (!token)
|
|
1144
|
+
return [];
|
|
1145
|
+
if (validator(token, args)) {
|
|
1146
|
+
const result = [
|
|
1147
|
+
{
|
|
1148
|
+
success: true,
|
|
1149
|
+
nextIndex: index + 1,
|
|
1150
|
+
nodes: [{ type: ruleName, value: token }]
|
|
1561
1151
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1152
|
+
];
|
|
1153
|
+
memo.set(key, result);
|
|
1154
|
+
return result;
|
|
1155
|
+
}
|
|
1156
|
+
memo.set(key, []);
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
const rule = grammar2[ruleName];
|
|
1160
|
+
if (!rule) {
|
|
1161
|
+
memo.set(key, []);
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
const results = matchNode(rule, tokens, index, grammar2, memo, currentFunction);
|
|
1165
|
+
const wrapped = new Array(results.length);
|
|
1166
|
+
for (let i = 0; i < results.length; i++) {
|
|
1167
|
+
wrapped[i] = {
|
|
1168
|
+
success: true,
|
|
1169
|
+
nextIndex: results[i].nextIndex,
|
|
1170
|
+
nodes: [{ type: ruleName, value: results[i].nodes }]
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
memo.set(key, wrapped);
|
|
1174
|
+
return wrapped;
|
|
1175
|
+
}
|
|
1176
|
+
function matchNode(node, tokens, index, grammar2, memo, currentFunction) {
|
|
1177
|
+
switch (node.kind) {
|
|
1178
|
+
case "literal": {
|
|
1179
|
+
const token = tokens[index];
|
|
1180
|
+
if (!token || token !== node.value.toLowerCase())
|
|
1181
|
+
return [];
|
|
1182
|
+
return [
|
|
1183
|
+
{
|
|
1184
|
+
success: true,
|
|
1185
|
+
nextIndex: index + 1,
|
|
1186
|
+
nodes: [
|
|
1187
|
+
{
|
|
1188
|
+
type: token === "(" || token === ")" || token === "," || token === "/" ? "symbol" : "literal",
|
|
1189
|
+
value: token
|
|
1577
1190
|
}
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1191
|
+
]
|
|
1580
1192
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
if (weight12 + weight22 <= 0) {
|
|
1598
|
-
throw new Error("Sum of percengates cannot be 0%.");
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
const totalWeight = weight12 + weight22;
|
|
1602
|
-
let alphaMultiplier2;
|
|
1603
|
-
if (totalWeight > 1) {
|
|
1604
|
-
weight12 /= totalWeight;
|
|
1605
|
-
weight22 /= totalWeight;
|
|
1606
|
-
} else {
|
|
1607
|
-
weight12 /= totalWeight;
|
|
1608
|
-
weight22 /= totalWeight;
|
|
1609
|
-
alphaMultiplier2 = totalWeight;
|
|
1193
|
+
];
|
|
1194
|
+
}
|
|
1195
|
+
case "ref": {
|
|
1196
|
+
const token = tokens[index];
|
|
1197
|
+
if (!token)
|
|
1198
|
+
return [];
|
|
1199
|
+
if (currentFunction && colorModels[currentFunction]) {
|
|
1200
|
+
const component = colorModels[currentFunction].components[token];
|
|
1201
|
+
if (component && component.value === node.name) {
|
|
1202
|
+
return [
|
|
1203
|
+
{
|
|
1204
|
+
success: true,
|
|
1205
|
+
nextIndex: index + 1,
|
|
1206
|
+
nodes: [{ type: node.name, value: token }]
|
|
1207
|
+
}
|
|
1208
|
+
];
|
|
1610
1209
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
let current = "";
|
|
1621
|
-
while (i < inner.length) {
|
|
1622
|
-
const char = inner[i];
|
|
1623
|
-
if (char === ",") {
|
|
1624
|
-
parts.push(current.trim());
|
|
1625
|
-
current = "";
|
|
1626
|
-
i++;
|
|
1627
|
-
continue;
|
|
1210
|
+
}
|
|
1211
|
+
return matchRule(node.name, tokens, index, grammar2, memo, currentFunction, node.args);
|
|
1212
|
+
}
|
|
1213
|
+
case "choice": {
|
|
1214
|
+
const results = [];
|
|
1215
|
+
for (let i = 0; i < node.nodes.length; i++) {
|
|
1216
|
+
const childMatches = matchNode(node.nodes[i], tokens, index, grammar2, memo, currentFunction);
|
|
1217
|
+
for (let j = 0; j < childMatches.length; j++) {
|
|
1218
|
+
results.push(childMatches[j]);
|
|
1628
1219
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1220
|
+
}
|
|
1221
|
+
return results;
|
|
1222
|
+
}
|
|
1223
|
+
case "sequence": {
|
|
1224
|
+
let states = [{ success: true, nextIndex: index, nodes: [] }];
|
|
1225
|
+
for (let i = 0; i < node.nodes.length; i++) {
|
|
1226
|
+
const child = node.nodes[i];
|
|
1227
|
+
const nextStates = [];
|
|
1228
|
+
for (let j = 0; j < states.length; j++) {
|
|
1229
|
+
const state = states[j];
|
|
1230
|
+
const matches = matchNode(child, tokens, state.nextIndex, grammar2, memo, currentFunction);
|
|
1231
|
+
for (let k = 0; k < matches.length; k++) {
|
|
1232
|
+
nextStates.push({
|
|
1233
|
+
success: true,
|
|
1234
|
+
nextIndex: matches[k].nextIndex,
|
|
1235
|
+
nodes: concatNodes(state.nodes, matches[k].nodes)
|
|
1236
|
+
});
|
|
1635
1237
|
}
|
|
1636
1238
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
parts.push(current.trim());
|
|
1641
|
-
if (parts.length !== 3) {
|
|
1642
|
-
throw new Error("color-mix must have three comma-separated parts.");
|
|
1239
|
+
states = nextStates;
|
|
1240
|
+
if (states.length === 0)
|
|
1241
|
+
return [];
|
|
1643
1242
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1243
|
+
return states;
|
|
1244
|
+
}
|
|
1245
|
+
case "optional": {
|
|
1246
|
+
const matches = matchNode(node.node, tokens, index, grammar2, memo, currentFunction);
|
|
1247
|
+
const results = new Array(matches.length + 1);
|
|
1248
|
+
results[0] = { success: true, nextIndex: index, nodes: [] };
|
|
1249
|
+
for (let i = 0; i < matches.length; i++) {
|
|
1250
|
+
results[i + 1] = matches[i];
|
|
1648
1251
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1252
|
+
return results;
|
|
1253
|
+
}
|
|
1254
|
+
case "repeat": {
|
|
1255
|
+
let states = [{ success: true, nextIndex: index, nodes: [] }];
|
|
1256
|
+
for (let i = 0; i < node.min; i++) {
|
|
1257
|
+
const nextStates = [];
|
|
1258
|
+
for (let j = 0; j < states.length; j++) {
|
|
1259
|
+
const state = states[j];
|
|
1260
|
+
const matches = matchNode(node.node, tokens, state.nextIndex, grammar2, memo, currentFunction);
|
|
1261
|
+
for (let k = 0; k < matches.length; k++) {
|
|
1262
|
+
nextStates.push({
|
|
1263
|
+
success: true,
|
|
1264
|
+
nextIndex: matches[k].nextIndex,
|
|
1265
|
+
nodes: concatNodes(state.nodes, matches[k].nodes)
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1661
1268
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1269
|
+
states = nextStates;
|
|
1270
|
+
if (states.length === 0)
|
|
1271
|
+
return [];
|
|
1664
1272
|
}
|
|
1665
|
-
|
|
1666
|
-
const { color: color2, weight: weight2 } = extractColorAndWeight(parts[2]);
|
|
1667
|
-
const { amount, alphaMultiplier = 1 } = getWeight2Prime(weight1, weight2);
|
|
1668
|
-
return color1.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false).in(model).mix(color2.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false), { amount, hue }).in("rgb").coords;
|
|
1669
|
-
}
|
|
1670
|
-
},
|
|
1671
|
-
transparent: {
|
|
1672
|
-
isValid: (str) => str === "transparent",
|
|
1673
|
-
bridge: "rgb",
|
|
1674
|
-
toBridge: (coords) => coords,
|
|
1675
|
-
parse: (str) => [NaN, NaN, NaN, 0]
|
|
1676
|
-
// eslint-disable-line no-unused-vars
|
|
1677
|
-
}
|
|
1678
|
-
};
|
|
1679
|
-
var colorTypes = {
|
|
1680
|
-
...colorBases,
|
|
1681
|
-
currentColor: {
|
|
1682
|
-
isValid: (str) => str === "currentcolor",
|
|
1683
|
-
bridge: "rgb",
|
|
1684
|
-
toBridge: (coords) => coords,
|
|
1685
|
-
parse: (str) => [0, 0, 0, 1]
|
|
1686
|
-
// eslint-disable-line no-unused-vars
|
|
1687
|
-
},
|
|
1688
|
-
"system-color": {
|
|
1689
|
-
isValid: (str) => Object.keys(config.systemColors).some((key) => key.toLowerCase() === str),
|
|
1690
|
-
bridge: "rgb",
|
|
1691
|
-
toBridge: (coords) => coords,
|
|
1692
|
-
parse: (str) => {
|
|
1693
|
-
const { systemColors: systemColors2 } = config;
|
|
1694
|
-
const key = Object.keys(systemColors2).find((k) => k.toLowerCase() === str);
|
|
1695
|
-
const rgbArr = systemColors2[key][config.theme === "light" ? 0 : 1];
|
|
1696
|
-
return [...rgbArr, 1];
|
|
1697
|
-
}
|
|
1698
|
-
},
|
|
1699
|
-
"contrast-color": {
|
|
1700
|
-
isValid: (str) => str.slice(0, 15) === "contrast-color(" && str[str.length - 1] === ")",
|
|
1701
|
-
bridge: "rgb",
|
|
1702
|
-
toBridge: (coords) => coords,
|
|
1703
|
-
parse: (str) => {
|
|
1704
|
-
const inner = str.slice(15, -1);
|
|
1705
|
-
const [, luminance] = Color.from(inner).in("xyz-d65").coords;
|
|
1706
|
-
return luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1];
|
|
1273
|
+
return states;
|
|
1707
1274
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const char = content[i];
|
|
1726
|
-
if (char === " ") {
|
|
1727
|
-
i++;
|
|
1728
|
-
continue;
|
|
1729
|
-
}
|
|
1730
|
-
if (char === "(" || /[a-zA-Z-]/.test(char)) {
|
|
1731
|
-
const { expression: expr, end } = extractBalancedExpression(content, i);
|
|
1732
|
-
if (expr) {
|
|
1733
|
-
tokens.push(expr);
|
|
1734
|
-
i = end;
|
|
1735
|
-
continue;
|
|
1275
|
+
case "permutation": {
|
|
1276
|
+
const numNodes = node.nodes.length;
|
|
1277
|
+
const fullMask = (1 << numNodes) - 1;
|
|
1278
|
+
let states = [{ nextIndex: index, nodes: [], remainingMask: fullMask }];
|
|
1279
|
+
const finalResults = [];
|
|
1280
|
+
while (states.length > 0) {
|
|
1281
|
+
const nextStates = [];
|
|
1282
|
+
for (let i = 0; i < states.length; i++) {
|
|
1283
|
+
const state = states[i];
|
|
1284
|
+
let allRemainingOptional = true;
|
|
1285
|
+
for (let bit = 0; bit < numNodes; bit++) {
|
|
1286
|
+
if ((state.remainingMask & 1 << bit) !== 0) {
|
|
1287
|
+
if (node.nodes[bit].kind !== "optional") {
|
|
1288
|
+
allRemainingOptional = false;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1736
1292
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1293
|
+
if (allRemainingOptional) {
|
|
1294
|
+
finalResults.push({
|
|
1295
|
+
success: true,
|
|
1296
|
+
nextIndex: state.nextIndex,
|
|
1297
|
+
nodes: state.nodes
|
|
1298
|
+
});
|
|
1743
1299
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1300
|
+
for (let bit = 0; bit < numNodes; bit++) {
|
|
1301
|
+
if ((state.remainingMask & 1 << bit) !== 0) {
|
|
1302
|
+
const child = node.nodes[bit];
|
|
1303
|
+
const nodeToMatch = child.kind === "optional" ? child.node : child;
|
|
1304
|
+
const matches = matchNode(nodeToMatch, tokens, state.nextIndex, grammar2, memo, currentFunction);
|
|
1305
|
+
const nextMask = state.remainingMask & ~(1 << bit);
|
|
1306
|
+
for (let m = 0; m < matches.length; m++) {
|
|
1307
|
+
nextStates.push({
|
|
1308
|
+
nextIndex: matches[m].nextIndex,
|
|
1309
|
+
nodes: concatNodes(state.nodes, matches[m].nodes),
|
|
1310
|
+
remainingMask: nextMask
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1751
1313
|
}
|
|
1752
1314
|
}
|
|
1753
|
-
tokens.push(num);
|
|
1754
|
-
continue;
|
|
1755
1315
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1316
|
+
states = nextStates;
|
|
1317
|
+
}
|
|
1318
|
+
return finalResults;
|
|
1319
|
+
}
|
|
1320
|
+
case "hashList": {
|
|
1321
|
+
let currentStates = matchNode(node.node, tokens, index, grammar2, memo, currentFunction);
|
|
1322
|
+
const allSuccessfulStates = [];
|
|
1323
|
+
while (currentStates.length > 0) {
|
|
1324
|
+
const nextStates = [];
|
|
1325
|
+
for (let i = 0; i < currentStates.length; i++) {
|
|
1326
|
+
const state = currentStates[i];
|
|
1327
|
+
allSuccessfulStates.push(state);
|
|
1328
|
+
const commaIdx = state.nextIndex;
|
|
1329
|
+
if (commaIdx < tokens.length && tokens[commaIdx] === ",") {
|
|
1330
|
+
const commaNode = { type: "symbol", value: "," };
|
|
1331
|
+
const matches = matchNode(node.node, tokens, commaIdx + 1, grammar2, memo, currentFunction);
|
|
1332
|
+
for (let m = 0; m < matches.length; m++) {
|
|
1333
|
+
const match = matches[m];
|
|
1334
|
+
if (match.nextIndex > commaIdx) {
|
|
1335
|
+
const newNodes = new Array(state.nodes.length + 1 + match.nodes.length);
|
|
1336
|
+
let idx = 0;
|
|
1337
|
+
for (let n = 0; n < state.nodes.length; n++)
|
|
1338
|
+
newNodes[idx++] = state.nodes[n];
|
|
1339
|
+
newNodes[idx++] = commaNode;
|
|
1340
|
+
for (let n = 0; n < match.nodes.length; n++)
|
|
1341
|
+
newNodes[idx++] = match.nodes[n];
|
|
1342
|
+
nextStates.push({
|
|
1343
|
+
success: true,
|
|
1344
|
+
nextIndex: match.nextIndex,
|
|
1345
|
+
nodes: newNodes
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
currentStates = nextStates;
|
|
1352
|
+
}
|
|
1353
|
+
return allSuccessfulStates;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function tokenize(input) {
|
|
1358
|
+
const lower = input.toLowerCase();
|
|
1359
|
+
const tokens = [];
|
|
1360
|
+
let i = 0;
|
|
1361
|
+
while (i < lower.length) {
|
|
1362
|
+
if (lower.slice(i, i + 2) === "/*") {
|
|
1363
|
+
const endIndex = lower.indexOf("*/", i + 2);
|
|
1364
|
+
if (endIndex !== -1) {
|
|
1365
|
+
i = endIndex + 2;
|
|
1366
|
+
} else {
|
|
1367
|
+
i = lower.length;
|
|
1368
|
+
}
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
const ch = lower[i];
|
|
1372
|
+
if (/\s/.test(ch)) {
|
|
1373
|
+
i++;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (lower.slice(i, i + 5) === "calc(") {
|
|
1377
|
+
const start2 = i;
|
|
1378
|
+
let parenCount = 0;
|
|
1379
|
+
while (i < lower.length) {
|
|
1380
|
+
if (lower[i] === "(")
|
|
1381
|
+
parenCount++;
|
|
1382
|
+
else if (lower[i] === ")") {
|
|
1383
|
+
parenCount--;
|
|
1384
|
+
if (parenCount === 0) {
|
|
1385
|
+
i++;
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
i++;
|
|
1390
|
+
}
|
|
1391
|
+
tokens.push(lower.slice(start2, i));
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if ("(),/".includes(ch)) {
|
|
1395
|
+
tokens.push(ch);
|
|
1396
|
+
i++;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const start = i;
|
|
1400
|
+
while (i < lower.length && !/\s/.test(lower[i]) && !"(),/".includes(lower[i])) {
|
|
1401
|
+
i++;
|
|
1402
|
+
}
|
|
1403
|
+
tokens.push(lower.slice(start, i));
|
|
1404
|
+
}
|
|
1405
|
+
return tokens;
|
|
1406
|
+
}
|
|
1407
|
+
function parseGrammar(grammar2) {
|
|
1408
|
+
const rules = {};
|
|
1409
|
+
const lines = grammar2.replace(/\/\*[\s\S]*?\*\//g, "").split("\n");
|
|
1410
|
+
let currentRuleName = "";
|
|
1411
|
+
let currentDef = "";
|
|
1412
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1413
|
+
const line = lines[i].trim();
|
|
1414
|
+
if (!line)
|
|
1415
|
+
continue;
|
|
1416
|
+
if (RULE_START_RE.test(line)) {
|
|
1417
|
+
if (currentRuleName)
|
|
1418
|
+
rules[currentRuleName] = parseExpression(currentDef);
|
|
1419
|
+
const eqIndex = line.indexOf("=");
|
|
1420
|
+
currentRuleName = line.slice(0, eqIndex).trim();
|
|
1421
|
+
currentDef = line.slice(eqIndex + 1).trim();
|
|
1422
|
+
} else if (currentRuleName) {
|
|
1423
|
+
currentDef += " " + line;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (currentRuleName)
|
|
1427
|
+
rules[currentRuleName] = parseExpression(currentDef);
|
|
1428
|
+
return rules;
|
|
1429
|
+
}
|
|
1430
|
+
function parseExpression(expr) {
|
|
1431
|
+
const parts = splitTopLevel(expr, "|");
|
|
1432
|
+
if (parts.length > 1)
|
|
1433
|
+
return { kind: "choice", nodes: parts.map(parsePermutation) };
|
|
1434
|
+
return parsePermutation(expr);
|
|
1435
|
+
}
|
|
1436
|
+
function splitTopLevel(str, separator) {
|
|
1437
|
+
const result = [];
|
|
1438
|
+
let depth = 0;
|
|
1439
|
+
let start = 0;
|
|
1440
|
+
const sepLen = separator.length;
|
|
1441
|
+
const len = str.length;
|
|
1442
|
+
for (let i = 0; i < len; i++) {
|
|
1443
|
+
const ch = str.charCodeAt(i);
|
|
1444
|
+
if (ch === 91 || ch === 40) {
|
|
1445
|
+
depth++;
|
|
1446
|
+
} else if (ch === 93 || ch === 41) {
|
|
1447
|
+
depth--;
|
|
1448
|
+
} else if (depth === 0 && str.startsWith(separator, i)) {
|
|
1449
|
+
const part = str.slice(start, i).trim();
|
|
1450
|
+
if (part)
|
|
1451
|
+
result.push(part);
|
|
1452
|
+
i += sepLen - 1;
|
|
1453
|
+
start = i + 1;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const last = str.slice(start).trim();
|
|
1457
|
+
if (last)
|
|
1458
|
+
result.push(last);
|
|
1459
|
+
return result;
|
|
1460
|
+
}
|
|
1461
|
+
function splitSequence(str) {
|
|
1462
|
+
const result = [];
|
|
1463
|
+
let i = 0;
|
|
1464
|
+
const len = str.length;
|
|
1465
|
+
while (i < len) {
|
|
1466
|
+
const ch = str.charCodeAt(i);
|
|
1467
|
+
if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
|
|
1468
|
+
i++;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (ch === 40 || ch === 41 || ch === 44 || ch === 47 || ch === 124) {
|
|
1472
|
+
result.push(str[i]);
|
|
1473
|
+
i++;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const start = i;
|
|
1477
|
+
if (ch === 60) {
|
|
1478
|
+
while (i < len && str.charCodeAt(i) !== 62)
|
|
1479
|
+
i++;
|
|
1480
|
+
i++;
|
|
1481
|
+
while (i < len) {
|
|
1482
|
+
const c = str.charCodeAt(i);
|
|
1483
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
|
|
1484
|
+
break;
|
|
1485
|
+
i++;
|
|
1486
|
+
}
|
|
1487
|
+
result.push(str.slice(start, i));
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (ch === 91) {
|
|
1491
|
+
let depth = 1;
|
|
1492
|
+
i++;
|
|
1493
|
+
while (i < len && depth > 0) {
|
|
1494
|
+
const c = str.charCodeAt(i);
|
|
1495
|
+
if (c === 91)
|
|
1496
|
+
depth++;
|
|
1497
|
+
else if (c === 93)
|
|
1498
|
+
depth--;
|
|
1499
|
+
i++;
|
|
1500
|
+
}
|
|
1501
|
+
while (i < len) {
|
|
1502
|
+
const c = str.charCodeAt(i);
|
|
1503
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
|
|
1504
|
+
break;
|
|
1505
|
+
i++;
|
|
1506
|
+
}
|
|
1507
|
+
result.push(str.slice(start, i));
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
while (i < len) {
|
|
1511
|
+
const c = str.charCodeAt(i);
|
|
1512
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
|
|
1513
|
+
break;
|
|
1514
|
+
i++;
|
|
1515
|
+
}
|
|
1516
|
+
result.push(str.slice(start, i));
|
|
1517
|
+
}
|
|
1518
|
+
return result;
|
|
1519
|
+
}
|
|
1520
|
+
function parsePermutation(targetExpr) {
|
|
1521
|
+
const parts = splitTopLevel(targetExpr, "&&");
|
|
1522
|
+
if (parts.length > 1)
|
|
1523
|
+
return { kind: "permutation", nodes: parts.map(parseSequence) };
|
|
1524
|
+
return parseSequence(targetExpr);
|
|
1525
|
+
}
|
|
1526
|
+
function parseSequence(targetExpr) {
|
|
1527
|
+
const tokens = splitSequence(targetExpr);
|
|
1528
|
+
return { kind: "sequence", nodes: tokens.map(parseTerm) };
|
|
1529
|
+
}
|
|
1530
|
+
function parseTerm(term) {
|
|
1531
|
+
term = term.trim();
|
|
1532
|
+
const len = term.length;
|
|
1533
|
+
if (len === 1) {
|
|
1534
|
+
const c = term.charCodeAt(0);
|
|
1535
|
+
if (c === 40 || c === 41 || c === 44 || c === 47) {
|
|
1536
|
+
return { kind: "literal", value: term };
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const lastChar = term.charCodeAt(len - 1);
|
|
1540
|
+
if (lastChar === 35 && term.indexOf("{") === -1) {
|
|
1541
|
+
return { kind: "hashList", node: parseTerm(term.slice(0, -1)) };
|
|
1542
|
+
}
|
|
1543
|
+
if (lastChar === 63) {
|
|
1544
|
+
return { kind: "optional", node: parseTerm(term.slice(0, -1)) };
|
|
1545
|
+
}
|
|
1546
|
+
const hashRepeatMatch = HASH_REPEAT_RE.exec(term);
|
|
1547
|
+
if (hashRepeatMatch) {
|
|
1548
|
+
const innerNode = parseTerm(hashRepeatMatch[1]);
|
|
1549
|
+
const count = Number(hashRepeatMatch[2]);
|
|
1550
|
+
const commaNode = { kind: "literal", value: "," };
|
|
1551
|
+
const nodes = [];
|
|
1552
|
+
for (let i = 0; i < count; i++) {
|
|
1553
|
+
nodes.push(innerNode);
|
|
1554
|
+
if (i < count - 1)
|
|
1555
|
+
nodes.push(commaNode);
|
|
1556
|
+
}
|
|
1557
|
+
return { kind: "sequence", nodes };
|
|
1558
|
+
}
|
|
1559
|
+
const repeatMatch = REPEAT_RE.exec(term);
|
|
1560
|
+
if (repeatMatch) {
|
|
1561
|
+
const num = Number(repeatMatch[2]);
|
|
1562
|
+
return {
|
|
1563
|
+
kind: "repeat",
|
|
1564
|
+
node: parseTerm(repeatMatch[1]),
|
|
1565
|
+
min: num,
|
|
1566
|
+
max: num
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const firstChar = term.charCodeAt(0);
|
|
1570
|
+
if (firstChar === 91 && lastChar === 93) {
|
|
1571
|
+
return parseExpression(term.slice(1, -1));
|
|
1572
|
+
}
|
|
1573
|
+
if (firstChar === 60 && lastChar === 62) {
|
|
1574
|
+
const match = REF_RE.exec(term);
|
|
1575
|
+
if (match) {
|
|
1576
|
+
let argsArray = void 0;
|
|
1577
|
+
if (match[2]) {
|
|
1578
|
+
const argMatches = match[2].match(ARG_RE);
|
|
1579
|
+
if (argMatches) {
|
|
1580
|
+
argsArray = argMatches.map((arg) => arg.trim());
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return { kind: "ref", name: `<${match[1]}>`, args: argsArray };
|
|
1584
|
+
}
|
|
1585
|
+
return { kind: "ref", name: term };
|
|
1586
|
+
}
|
|
1587
|
+
return { kind: "literal", value: term };
|
|
1588
|
+
}
|
|
1589
|
+
var { PI: pi, E: e, pow, sqrt, sin, cos, tan, asin, acos, atan, atan2, exp, log, log10, log2, abs, min, max, hypot, round, ceil, floor, sign, trunc, random } = Math;
|
|
1590
|
+
var BASE_MATH_ENV = {
|
|
1591
|
+
pi,
|
|
1592
|
+
e,
|
|
1593
|
+
tau: pi * 2,
|
|
1594
|
+
pow,
|
|
1595
|
+
sqrt,
|
|
1596
|
+
sin,
|
|
1597
|
+
cos,
|
|
1598
|
+
tan,
|
|
1599
|
+
asin,
|
|
1600
|
+
acos,
|
|
1601
|
+
atan,
|
|
1602
|
+
atan2,
|
|
1603
|
+
exp,
|
|
1604
|
+
log,
|
|
1605
|
+
log10,
|
|
1606
|
+
log2,
|
|
1607
|
+
abs,
|
|
1608
|
+
min,
|
|
1609
|
+
max,
|
|
1610
|
+
hypot,
|
|
1611
|
+
round,
|
|
1612
|
+
ceil,
|
|
1613
|
+
floor,
|
|
1614
|
+
sign,
|
|
1615
|
+
trunc,
|
|
1616
|
+
random
|
|
1617
|
+
};
|
|
1618
|
+
function extractToken(n) {
|
|
1619
|
+
if (typeof n === "string")
|
|
1620
|
+
return n;
|
|
1621
|
+
if (n.type === "<percentage>")
|
|
1622
|
+
return String(n.value || "");
|
|
1623
|
+
if (Array.isArray(n.value))
|
|
1624
|
+
return extractToken(n.value[0]);
|
|
1625
|
+
return String(n.value || "");
|
|
1626
|
+
}
|
|
1627
|
+
function parsePercent(str, type, min2, max2) {
|
|
1628
|
+
const percent = parseFloat(str);
|
|
1629
|
+
if (isNaN(percent))
|
|
1630
|
+
throw new Error(`Invalid percentage: '${str}'.`);
|
|
1631
|
+
if (type === "percentage")
|
|
1632
|
+
return percent;
|
|
1633
|
+
if (min2 < 0 && max2 > 0)
|
|
1634
|
+
return percent / 100 * (max2 - min2) / 2;
|
|
1635
|
+
return percent / 100 * (max2 - min2) + min2;
|
|
1636
|
+
}
|
|
1637
|
+
function parseHue(str) {
|
|
1638
|
+
const val = parseFloat(str);
|
|
1639
|
+
if (isNaN(val))
|
|
1640
|
+
throw new Error(`Invalid hue: '${str}'.`);
|
|
1641
|
+
if (str[str.length - 1] === "deg")
|
|
1642
|
+
return val;
|
|
1643
|
+
if (str[str.length - 1] === "rad")
|
|
1644
|
+
return val * (180 / pi);
|
|
1645
|
+
if (str[str.length - 1] === "grad")
|
|
1646
|
+
return val * 0.9;
|
|
1647
|
+
if (str[str.length - 1] === "turn")
|
|
1648
|
+
return val * 360;
|
|
1649
|
+
return val;
|
|
1650
|
+
}
|
|
1651
|
+
function parseCalcExpression(expr, type, base, min2, max2) {
|
|
1652
|
+
if (expr === "infinity")
|
|
1653
|
+
return Infinity;
|
|
1654
|
+
if (expr === "-infinity")
|
|
1655
|
+
return -Infinity;
|
|
1656
|
+
if (expr === "nan")
|
|
1657
|
+
return NaN;
|
|
1658
|
+
const UNIT_NUM = 1;
|
|
1659
|
+
const UNIT_PCT = 2;
|
|
1660
|
+
const UNIT_ANG = 4;
|
|
1661
|
+
const getUnitName = (u) => u === UNIT_ANG ? "angle" : u === UNIT_PCT ? "percent" : "number";
|
|
1662
|
+
const tokenize2 = (s) => {
|
|
1663
|
+
const out = [];
|
|
1664
|
+
for (let i = 0; i < s.length; ) {
|
|
1665
|
+
const c = s[i];
|
|
1666
|
+
if (/\s/.test(c)) {
|
|
1667
|
+
i++;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (s.slice(i, i + 2) === "**") {
|
|
1671
|
+
out.push({ type: "operator", value: "**" });
|
|
1672
|
+
i += 2;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (/[0-9]/.test(c) || c === "." && /[0-9]/.test(s[i + 1] || "")) {
|
|
1676
|
+
let numStr = "";
|
|
1677
|
+
while (i < s.length && /[0-9.]/.test(s[i]))
|
|
1678
|
+
numStr += s[i++];
|
|
1679
|
+
if (i < s.length && /[eE]/.test(s[i])) {
|
|
1680
|
+
numStr += s[i++];
|
|
1681
|
+
if (/[+-]/.test(s[i]))
|
|
1682
|
+
numStr += s[i++];
|
|
1683
|
+
while (i < s.length && /[0-9]/.test(s[i]))
|
|
1684
|
+
numStr += s[i++];
|
|
1685
|
+
}
|
|
1686
|
+
let unitType = UNIT_NUM;
|
|
1687
|
+
let rawUnit = "";
|
|
1688
|
+
if (s[i] === "%") {
|
|
1689
|
+
unitType = UNIT_PCT;
|
|
1690
|
+
rawUnit = "%";
|
|
1758
1691
|
i++;
|
|
1759
|
-
|
|
1692
|
+
} else if (s.slice(i, i + 3) === "deg") {
|
|
1693
|
+
unitType = UNIT_ANG;
|
|
1694
|
+
rawUnit = "deg";
|
|
1695
|
+
i += 3;
|
|
1696
|
+
} else if (s.slice(i, i + 3) === "rad") {
|
|
1697
|
+
unitType = UNIT_ANG;
|
|
1698
|
+
rawUnit = "rad";
|
|
1699
|
+
i += 3;
|
|
1700
|
+
} else if (s.slice(i, i + 4) === "grad") {
|
|
1701
|
+
unitType = UNIT_ANG;
|
|
1702
|
+
rawUnit = "grad";
|
|
1703
|
+
i += 4;
|
|
1704
|
+
} else if (s.slice(i, i + 4) === "turn") {
|
|
1705
|
+
unitType = UNIT_ANG;
|
|
1706
|
+
rawUnit = "turn";
|
|
1707
|
+
i += 4;
|
|
1760
1708
|
}
|
|
1761
|
-
|
|
1709
|
+
let val = parseFloat(numStr);
|
|
1710
|
+
if (unitType === UNIT_PCT) {
|
|
1711
|
+
val = parsePercent(numStr + rawUnit, type, min2, max2);
|
|
1712
|
+
} else if (unitType === UNIT_ANG) {
|
|
1713
|
+
val = parseHue(numStr + rawUnit);
|
|
1714
|
+
}
|
|
1715
|
+
out.push({ type: "number", value: val, unit: unitType });
|
|
1716
|
+
continue;
|
|
1762
1717
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1718
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
1719
|
+
let id = "";
|
|
1720
|
+
while (i < s.length && /[a-zA-Z0-9_]/.test(s[i]))
|
|
1721
|
+
id += s[i++];
|
|
1722
|
+
out.push({ type: "identifier", value: id });
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if ("+-*/%(),".includes(c)) {
|
|
1726
|
+
out.push({ type: "operator", value: c });
|
|
1727
|
+
i++;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
throw new Error(`Unexpected character: ${c}`);
|
|
1731
|
+
}
|
|
1732
|
+
return out;
|
|
1733
|
+
};
|
|
1734
|
+
const parse = (tokens) => {
|
|
1735
|
+
let pos = 0;
|
|
1736
|
+
const cur = () => pos < tokens.length ? tokens[pos] : null;
|
|
1737
|
+
const nxt = () => {
|
|
1738
|
+
if (pos >= tokens.length)
|
|
1739
|
+
throw new Error("Unexpected end of input");
|
|
1740
|
+
return tokens[pos++];
|
|
1741
|
+
};
|
|
1742
|
+
const expect = (v) => {
|
|
1743
|
+
const t = cur();
|
|
1744
|
+
if (!t || t.value !== v) {
|
|
1745
|
+
throw new Error(`Expected "${v}" but got "${t ? t.value : "end of input"}"`);
|
|
1746
|
+
}
|
|
1747
|
+
nxt();
|
|
1748
|
+
};
|
|
1749
|
+
const parsePrimary = () => {
|
|
1750
|
+
const t = cur();
|
|
1751
|
+
if (!t)
|
|
1752
|
+
throw new Error("Unexpected end of input");
|
|
1753
|
+
if (t.type === "number") {
|
|
1754
|
+
nxt();
|
|
1755
|
+
return { type: "number", value: t.value, unit: t.unit };
|
|
1756
|
+
}
|
|
1757
|
+
if (t.type === "identifier") {
|
|
1758
|
+
nxt();
|
|
1759
|
+
if (cur() && cur().value === "(") {
|
|
1760
|
+
nxt();
|
|
1761
|
+
const args = [];
|
|
1762
|
+
if (cur() && cur().value !== ")") {
|
|
1763
|
+
args.push(parseAdd());
|
|
1764
|
+
while (cur() && cur().value === ",") {
|
|
1765
|
+
nxt();
|
|
1766
|
+
args.push(parseAdd());
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
expect(")");
|
|
1770
|
+
return { type: "call", func: t.value, args };
|
|
1774
1771
|
}
|
|
1775
|
-
|
|
1772
|
+
return { type: "var", name: t.value };
|
|
1776
1773
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1774
|
+
if (t.value === "(") {
|
|
1775
|
+
nxt();
|
|
1776
|
+
const e2 = parseAdd();
|
|
1777
|
+
expect(")");
|
|
1778
|
+
return e2;
|
|
1782
1779
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1780
|
+
throw new Error(`Unexpected token: ${t.value}`);
|
|
1781
|
+
};
|
|
1782
|
+
const parseUnary = () => {
|
|
1783
|
+
if (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
1784
|
+
const op = nxt().value;
|
|
1785
|
+
return { type: "unary", op, arg: parseUnary() };
|
|
1785
1786
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
idx++;
|
|
1787
|
+
return parsePrimary();
|
|
1788
|
+
};
|
|
1789
|
+
const parsePower = () => {
|
|
1790
|
+
let left = parseUnary();
|
|
1791
|
+
while (cur() && cur().value === "**") {
|
|
1792
|
+
const op = nxt().value;
|
|
1793
|
+
left = { type: "binary", op, left, right: parseUnary() };
|
|
1794
1794
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1795
|
+
return left;
|
|
1796
|
+
};
|
|
1797
|
+
const parseMul = () => {
|
|
1798
|
+
let left = parsePower();
|
|
1799
|
+
while (cur() && ["*", "/", "%"].includes(String(cur().value))) {
|
|
1800
|
+
const op = nxt().value;
|
|
1801
|
+
left = { type: "binary", op, left, right: parsePower() };
|
|
1799
1802
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1803
|
+
return left;
|
|
1804
|
+
};
|
|
1805
|
+
const parseAdd = () => {
|
|
1806
|
+
let left = parseMul();
|
|
1807
|
+
while (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
1808
|
+
const op = nxt().value;
|
|
1809
|
+
left = { type: "binary", op, left, right: parseMul() };
|
|
1810
|
+
}
|
|
1811
|
+
return left;
|
|
1812
|
+
};
|
|
1813
|
+
const ast = parseAdd();
|
|
1814
|
+
if (pos < tokens.length) {
|
|
1815
|
+
throw new Error(`Extra tokens after expression: ${tokens.slice(pos).map((t) => t.value).join(" ")}`);
|
|
1816
|
+
}
|
|
1817
|
+
return ast;
|
|
1818
|
+
};
|
|
1819
|
+
const evaluate = (ast, env) => {
|
|
1820
|
+
switch (ast.type) {
|
|
1821
|
+
case "number":
|
|
1822
|
+
return { value: ast.value, unit: ast.unit };
|
|
1823
|
+
case "var": {
|
|
1824
|
+
const v = env[ast.name];
|
|
1825
|
+
if (v === void 0)
|
|
1826
|
+
throw new Error(`Unknown variable: ${ast.name}`);
|
|
1827
|
+
if (typeof v === "function") {
|
|
1828
|
+
throw new Error(`Expected variable but found function: ${ast.name}`);
|
|
1829
|
+
}
|
|
1830
|
+
return { value: v, unit: UNIT_NUM };
|
|
1831
|
+
}
|
|
1832
|
+
case "binary": {
|
|
1833
|
+
const L = evaluate(ast.left, env);
|
|
1834
|
+
const R = evaluate(ast.right, env);
|
|
1835
|
+
switch (ast.op) {
|
|
1836
|
+
case "+":
|
|
1837
|
+
case "-": {
|
|
1838
|
+
const val = ast.op === "+" ? L.value + R.value : L.value - R.value;
|
|
1839
|
+
const unitMask = L.unit | R.unit;
|
|
1840
|
+
if (L.unit === R.unit)
|
|
1841
|
+
return { value: val, unit: L.unit };
|
|
1842
|
+
if (unitMask === 5)
|
|
1843
|
+
return { value: val, unit: UNIT_ANG };
|
|
1844
|
+
throw new Error(`Cannot ${ast.op} mismatched units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
|
|
1845
|
+
}
|
|
1846
|
+
case "*": {
|
|
1847
|
+
const val = L.value * R.value;
|
|
1848
|
+
if (L.unit === UNIT_NUM)
|
|
1849
|
+
return { value: val, unit: R.unit };
|
|
1850
|
+
if (R.unit === UNIT_NUM)
|
|
1851
|
+
return { value: val, unit: L.unit };
|
|
1852
|
+
throw new Error(`Cannot multiply units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
|
|
1853
|
+
}
|
|
1854
|
+
case "/": {
|
|
1855
|
+
if (R.unit === UNIT_NUM)
|
|
1856
|
+
return { value: L.value / R.value, unit: L.unit };
|
|
1857
|
+
if (L.unit === R.unit)
|
|
1858
|
+
return { value: L.value / R.value, unit: UNIT_NUM };
|
|
1859
|
+
throw new Error(`Cannot divide units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
|
|
1860
|
+
}
|
|
1861
|
+
case "%": {
|
|
1862
|
+
if (R.unit === UNIT_NUM)
|
|
1863
|
+
return { value: L.value % R.value, unit: L.unit };
|
|
1864
|
+
if (L.unit === R.unit)
|
|
1865
|
+
return { value: L.value % R.value, unit: L.unit };
|
|
1866
|
+
throw new Error(`Cannot modulo units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
|
|
1867
|
+
}
|
|
1868
|
+
case "**": {
|
|
1869
|
+
if (R.unit !== UNIT_NUM)
|
|
1870
|
+
throw new Error(`Exponent must be a number, got ${getUnitName(R.unit)}`);
|
|
1871
|
+
return { value: L.value ** R.value, unit: L.unit };
|
|
1872
|
+
}
|
|
1873
|
+
default:
|
|
1874
|
+
throw new Error(`Unknown binary operator: ${ast.op}`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
case "unary": {
|
|
1878
|
+
const res = evaluate(ast.arg, env);
|
|
1879
|
+
switch (ast.op) {
|
|
1880
|
+
case "+":
|
|
1881
|
+
return { value: +res.value, unit: res.unit };
|
|
1882
|
+
case "-":
|
|
1883
|
+
return { value: -res.value, unit: res.unit };
|
|
1884
|
+
default:
|
|
1885
|
+
throw new Error(`Unknown unary operator: ${ast.op}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
case "call": {
|
|
1889
|
+
const fn = env[ast.func];
|
|
1890
|
+
if (typeof fn !== "function")
|
|
1891
|
+
throw new Error(`Unknown function: ${ast.func}`);
|
|
1892
|
+
const evaluatedArgs = ast.args.map((a) => evaluate(a, env));
|
|
1893
|
+
const numArgs = evaluatedArgs.map((a) => a.value);
|
|
1894
|
+
const val = fn(...numArgs);
|
|
1895
|
+
const preserveUnitFuncs = ["min", "max", "round", "ceil", "floor", "abs", "sign", "trunc"];
|
|
1896
|
+
if (preserveUnitFuncs.includes(ast.func)) {
|
|
1897
|
+
let combinedMask = 0;
|
|
1898
|
+
for (let i = 0; i < evaluatedArgs.length; i++) {
|
|
1899
|
+
combinedMask |= evaluatedArgs[i].unit;
|
|
1900
|
+
}
|
|
1901
|
+
if ((combinedMask & 6) === 6)
|
|
1902
|
+
throw new Error(`Function ${ast.func} cannot mix angle and percent`);
|
|
1903
|
+
if ((combinedMask & 3) === 3)
|
|
1904
|
+
throw new Error(`Function ${ast.func} cannot mix number and percent`);
|
|
1905
|
+
const finalUnit = combinedMask & UNIT_ANG ? UNIT_ANG : combinedMask & UNIT_PCT ? UNIT_PCT : UNIT_NUM;
|
|
1906
|
+
return { value: val, unit: finalUnit };
|
|
1907
|
+
}
|
|
1908
|
+
return { value: val, unit: UNIT_NUM };
|
|
1909
|
+
}
|
|
1910
|
+
default: {
|
|
1911
|
+
throw new Error(`Unknown AST node type: ${ast.type}`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
const calcEnv = Object.assign({}, BASE_MATH_ENV, base);
|
|
1916
|
+
return evaluate(parse(tokenize2(expr)), calcEnv).value;
|
|
1917
|
+
}
|
|
1918
|
+
function evaluateComponent(node, expectedType, base = {}) {
|
|
1919
|
+
if (!node)
|
|
1920
|
+
return NaN;
|
|
1921
|
+
const token = extractToken(node);
|
|
1922
|
+
return evaluateCSSValue(token, expectedType, base);
|
|
1923
|
+
}
|
|
1924
|
+
function evaluateCSSValue(tokenString, expectedType, base = {}) {
|
|
1925
|
+
const token = tokenString.trim();
|
|
1926
|
+
if (token === "none")
|
|
1927
|
+
return NaN;
|
|
1928
|
+
if (token in base)
|
|
1929
|
+
return base[token];
|
|
1930
|
+
const [tMin, tMax] = Array.isArray(expectedType) ? expectedType : expectedType === "percentage" ? [0, 100] : [0, 1];
|
|
1931
|
+
if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
|
|
1932
|
+
return parseFloat(token);
|
|
1933
|
+
}
|
|
1934
|
+
if (token[token.length - 1] === "%" && /^-?(?:\d+|\d*\.\d+)%$/.test(token)) {
|
|
1935
|
+
return parsePercent(token, expectedType, tMin, tMax);
|
|
1936
|
+
}
|
|
1937
|
+
if (/^-?(?:\d+|\d*\.\d+)(deg|rad|grad|turn)$/.test(token)) {
|
|
1938
|
+
return parseHue(token);
|
|
1939
|
+
}
|
|
1940
|
+
if (token.slice(0, 5) === "calc(" && token[token.length - 1] === ")") {
|
|
1941
|
+
return parseCalcExpression(token.slice(5, -1).trim(), expectedType, base, tMin, tMax);
|
|
1942
|
+
}
|
|
1943
|
+
throw new Error(`Unable to parse component token: ${token}`);
|
|
1944
|
+
}
|
|
1945
|
+
function createLegacyColorParser(model, componentKeys) {
|
|
1946
|
+
return (node) => {
|
|
1947
|
+
const t = node.value;
|
|
1948
|
+
const config2 = colorModels[model].components;
|
|
1949
|
+
const c1 = evaluateComponent(t[2], config2[componentKeys[0]].value);
|
|
1950
|
+
const c2 = evaluateComponent(t[4], config2[componentKeys[1]].value);
|
|
1951
|
+
const c3 = evaluateComponent(t[6], config2[componentKeys[2]].value);
|
|
1952
|
+
let alpha = 1;
|
|
1953
|
+
if (t[7]?.type === "symbol" && t[7]?.value === ",")
|
|
1954
|
+
alpha = evaluateComponent(t[8], [0, 1]);
|
|
1955
|
+
return { model, coords: [c1, c2, c3, alpha] };
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function createModernColorParser(model, componentKeys) {
|
|
1959
|
+
return (node) => {
|
|
1960
|
+
const t = node.value;
|
|
1961
|
+
const config2 = colorModels[model].components;
|
|
1962
|
+
const c1 = evaluateComponent(t[2], config2[componentKeys[0]].value);
|
|
1963
|
+
const c2 = evaluateComponent(t[3], config2[componentKeys[1]].value);
|
|
1964
|
+
const c3 = evaluateComponent(t[4], config2[componentKeys[2]].value);
|
|
1965
|
+
let alpha = 1;
|
|
1966
|
+
if (t[5]?.type === "symbol" && t[5]?.value === "/")
|
|
1967
|
+
alpha = evaluateComponent(t[6], [0, 1]);
|
|
1968
|
+
return { model, coords: [c1, c2, c3, alpha] };
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function createRelativeColorParser(model, componentKeys) {
|
|
1972
|
+
return (node) => {
|
|
1973
|
+
const t = node.value;
|
|
1974
|
+
const config2 = colorModels[model].components;
|
|
1975
|
+
const originColorNode = t[3];
|
|
1976
|
+
const originParsed = parseNode(originColorNode);
|
|
1977
|
+
const originInstance = new Color(originParsed.model, originParsed.coords).in(model);
|
|
1978
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
1979
|
+
const c1 = evaluateComponent(t[4], config2[componentKeys[0]].value, baseEnv);
|
|
1980
|
+
const c2 = evaluateComponent(t[5], config2[componentKeys[1]].value, baseEnv);
|
|
1981
|
+
const c3 = evaluateComponent(t[6], config2[componentKeys[2]].value, baseEnv);
|
|
1982
|
+
let alpha = 1;
|
|
1983
|
+
if (t[7]?.type === "symbol" && t[7]?.value === "/") {
|
|
1984
|
+
alpha = evaluateComponent(t[8], [0, 1], baseEnv);
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
model,
|
|
1988
|
+
coords: [c1, c2, c3, alpha]
|
|
1989
|
+
};
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function createRelativeColorFunctionParser(node) {
|
|
1993
|
+
const t = node.value;
|
|
1994
|
+
const { model, coords } = parseNode(t[3]);
|
|
1995
|
+
const spaceNode = t[4];
|
|
1996
|
+
const targetModel = Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value;
|
|
1997
|
+
const originInstance = new Color(model, coords).in(targetModel);
|
|
1998
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
1999
|
+
const c1 = evaluateComponent(t[5], [0, 1], baseEnv);
|
|
2000
|
+
const c2 = evaluateComponent(t[6], [0, 1], baseEnv);
|
|
2001
|
+
const c3 = evaluateComponent(t[7], [0, 1], baseEnv);
|
|
2002
|
+
let alpha = 1;
|
|
2003
|
+
if (t[8]?.type === "symbol" && t[8]?.value === "/") {
|
|
2004
|
+
alpha = evaluateComponent(t[9], [0, 1], baseEnv);
|
|
2005
|
+
}
|
|
2006
|
+
return {
|
|
2007
|
+
model: targetModel,
|
|
2008
|
+
coords: [c1, c2, c3, alpha]
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
var validators = (() => {
|
|
2012
|
+
const NUMBER_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
2013
|
+
const PERCENT_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)%$/;
|
|
2014
|
+
const ANGLE_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/;
|
|
2015
|
+
const RANGE_REGEX = /^\[([^,]+),([^\]]+)\]$/;
|
|
2016
|
+
const systemColorsSet = new Set(Object.keys(systemColors).map((key) => key.toLowerCase()));
|
|
2017
|
+
const rangeCache = /* @__PURE__ */ new Map();
|
|
2018
|
+
const getRange = (args) => {
|
|
2019
|
+
if (!args)
|
|
2020
|
+
return void 0;
|
|
2021
|
+
for (let i = 0; i < args.length; i++) {
|
|
2022
|
+
if (RANGE_REGEX.test(args[i]))
|
|
2023
|
+
return args[i];
|
|
2024
|
+
}
|
|
2025
|
+
return void 0;
|
|
2026
|
+
};
|
|
2027
|
+
const checkRange = (val, rangeStr) => {
|
|
2028
|
+
let bounds = rangeCache.get(rangeStr);
|
|
2029
|
+
if (!bounds) {
|
|
2030
|
+
const match = rangeStr.match(RANGE_REGEX);
|
|
2031
|
+
bounds = {
|
|
2032
|
+
min: match[1].trim() === "-INF" ? -Infinity : parseFloat(match[1]),
|
|
2033
|
+
max: match[2].trim() === "INF" || match[2].trim() === "+INF" ? Infinity : parseFloat(match[2])
|
|
1815
2034
|
};
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
2035
|
+
rangeCache.set(rangeStr, bounds);
|
|
2036
|
+
}
|
|
2037
|
+
if (bounds.min !== -Infinity && val < bounds.min)
|
|
2038
|
+
return false;
|
|
2039
|
+
if (bounds.max !== Infinity && val > bounds.max)
|
|
2040
|
+
return false;
|
|
2041
|
+
return true;
|
|
2042
|
+
};
|
|
2043
|
+
return {
|
|
2044
|
+
"<named-color>": (str) => Object.hasOwn(namedColors, str),
|
|
2045
|
+
"<system-color>": (str) => systemColorsSet.has(str),
|
|
2046
|
+
"<hex-color>": (str) => {
|
|
2047
|
+
return str[0] === "#" && (str.length === 4 || str.length === 5 || str.length === 7 || str.length === 9) && /^[0-9a-fA-F]+$/.test(str.slice(1));
|
|
2048
|
+
},
|
|
2049
|
+
"<number>": (str, args) => {
|
|
2050
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
2051
|
+
return true;
|
|
2052
|
+
if (!NUMBER_REGEX.test(str))
|
|
2053
|
+
return false;
|
|
2054
|
+
if (args !== void 0) {
|
|
2055
|
+
const rangeArg = getRange(args);
|
|
2056
|
+
if (rangeArg)
|
|
2057
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
1823
2058
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
2059
|
+
return true;
|
|
2060
|
+
},
|
|
2061
|
+
"<percentage>": (str, args) => {
|
|
2062
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
2063
|
+
return true;
|
|
2064
|
+
if (!PERCENT_REGEX.test(str))
|
|
2065
|
+
return false;
|
|
2066
|
+
if (args !== void 0) {
|
|
2067
|
+
const rangeArg = getRange(args);
|
|
2068
|
+
if (rangeArg)
|
|
2069
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
2070
|
+
}
|
|
2071
|
+
return true;
|
|
2072
|
+
},
|
|
2073
|
+
"<angle>": (str, args) => {
|
|
2074
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
2075
|
+
return true;
|
|
2076
|
+
if (!ANGLE_REGEX.test(str))
|
|
2077
|
+
return false;
|
|
2078
|
+
if (args !== void 0) {
|
|
2079
|
+
const rangeArg = getRange(args);
|
|
2080
|
+
if (rangeArg)
|
|
2081
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
2082
|
+
}
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
})();
|
|
2087
|
+
var parsers = {
|
|
2088
|
+
"<color>": (node) => {
|
|
2089
|
+
const child = node.value[0];
|
|
2090
|
+
const { type, value } = child;
|
|
2091
|
+
if (type === "literal") {
|
|
2092
|
+
if (value === "currentcolor")
|
|
2093
|
+
return { model: "rgb", coords: [0, 0, 0, 1] };
|
|
2094
|
+
else
|
|
2095
|
+
throw new Error(`Unknown <color>: ${value}`);
|
|
1826
2096
|
}
|
|
2097
|
+
return parseNode(child);
|
|
1827
2098
|
},
|
|
1828
|
-
"
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2099
|
+
"<color-base>": (node) => {
|
|
2100
|
+
const child = node.value[0];
|
|
2101
|
+
const { type, value } = child;
|
|
2102
|
+
if (type === "literal") {
|
|
2103
|
+
if (value === "transparent")
|
|
2104
|
+
return { model: "rgb", coords: [0, 0, 0, 0] };
|
|
2105
|
+
else
|
|
2106
|
+
throw new Error(`Unknown <color-base>: ${value}`);
|
|
2107
|
+
}
|
|
2108
|
+
return parseNode(child);
|
|
2109
|
+
},
|
|
2110
|
+
"<named-color>": (node) => {
|
|
2111
|
+
const { value: name } = node;
|
|
2112
|
+
const rgb = namedColors[name];
|
|
2113
|
+
if (!rgb)
|
|
2114
|
+
throw new Error(`Unknown <named-color>: ${name}`);
|
|
2115
|
+
return { model: "rgb", coords: [...rgb, 1] };
|
|
2116
|
+
},
|
|
2117
|
+
"<system-color>": (node) => {
|
|
2118
|
+
const { value: name } = node;
|
|
2119
|
+
const index = config.theme === "light" ? 0 : 1;
|
|
2120
|
+
const key = Object.keys(systemColors).find((k) => k.toLowerCase() === name);
|
|
2121
|
+
const rgb = systemColors[key][index];
|
|
2122
|
+
if (!rgb)
|
|
2123
|
+
throw new Error(`Unknown <system-color>: ${name}`);
|
|
2124
|
+
return { model: "rgb", coords: [...rgb, 1] };
|
|
2125
|
+
},
|
|
2126
|
+
"<contrast-color()>": (node) => {
|
|
2127
|
+
const colorNode = node.value[2];
|
|
2128
|
+
const { model, coords } = parseNode(colorNode);
|
|
2129
|
+
const [, luminance] = new Color(model, coords).in("xyz-d65").coords;
|
|
2130
|
+
return { model: "rgb", coords: luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1] };
|
|
2131
|
+
},
|
|
2132
|
+
"<light-dark-color>": (node) => {
|
|
2133
|
+
const children = node.value;
|
|
2134
|
+
const lightNode = children[2];
|
|
2135
|
+
const darkNode = children[4];
|
|
2136
|
+
const { model, coords } = parseNode(config?.theme === "light" ? lightNode : darkNode);
|
|
2137
|
+
const { coords: rgbCoords } = new Color(model, coords).in("rgb");
|
|
2138
|
+
return { model: "rgb", coords: rgbCoords };
|
|
2139
|
+
},
|
|
2140
|
+
"<hex-color>": (node) => {
|
|
2141
|
+
const hex = node.value;
|
|
2142
|
+
const len = hex.length;
|
|
2143
|
+
let r = 0, g = 0, b = 0, a = 1;
|
|
2144
|
+
if (len === 4 || len === 5) {
|
|
2145
|
+
const rCh = hex.charCodeAt(1);
|
|
2146
|
+
const gCh = hex.charCodeAt(2);
|
|
2147
|
+
const bCh = hex.charCodeAt(3);
|
|
2148
|
+
const rVal = rCh > 64 ? (rCh & 7) + 9 : rCh & 15;
|
|
2149
|
+
const gVal = gCh > 64 ? (gCh & 7) + 9 : gCh & 15;
|
|
2150
|
+
const bVal = bCh > 64 ? (bCh & 7) + 9 : bCh & 15;
|
|
2151
|
+
r = rVal << 4 | rVal;
|
|
2152
|
+
g = gVal << 4 | gVal;
|
|
2153
|
+
b = bVal << 4 | bVal;
|
|
2154
|
+
if (len === 5) {
|
|
2155
|
+
const aCh = hex.charCodeAt(4);
|
|
2156
|
+
const aVal = aCh > 64 ? (aCh & 7) + 9 : aCh & 15;
|
|
2157
|
+
a = (aVal << 4 | aVal) / 255;
|
|
2158
|
+
}
|
|
2159
|
+
} else {
|
|
2160
|
+
const num = parseInt(hex.slice(1), 16);
|
|
2161
|
+
if (len === 7) {
|
|
2162
|
+
r = num >> 16 & 255;
|
|
2163
|
+
g = num >> 8 & 255;
|
|
2164
|
+
b = num & 255;
|
|
2165
|
+
} else if (len === 9) {
|
|
2166
|
+
r = num >> 24 & 255;
|
|
2167
|
+
g = num >> 16 & 255;
|
|
2168
|
+
b = num >> 8 & 255;
|
|
2169
|
+
a = (num & 255) / 255;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return {
|
|
2173
|
+
model: "rgb",
|
|
2174
|
+
coords: [r, g, b, a]
|
|
2175
|
+
};
|
|
2176
|
+
},
|
|
2177
|
+
"<device-cmyk()>": (node) => parseNode(node.value[0]),
|
|
2178
|
+
"<legacy-device-cmyk-syntax>": (node) => {
|
|
2179
|
+
const t = node.value;
|
|
2180
|
+
const [c, m, y, k] = [t[2], t[4], t[6], t[8]].map(parseCmykComponent);
|
|
2181
|
+
return {
|
|
2182
|
+
model: "rgb",
|
|
2183
|
+
coords: [(1 - (c * (1 - k) + k)) * 255, (1 - (m * (1 - k) + k)) * 255, (1 - (y * (1 - k) + k)) * 255, 1]
|
|
2184
|
+
};
|
|
2185
|
+
},
|
|
2186
|
+
"<modern-device-cmyk-syntax>": (node) => {
|
|
2187
|
+
const t = node.value;
|
|
2188
|
+
const c = evaluateComponent(t[2], [0, 1]);
|
|
2189
|
+
const m = evaluateComponent(t[3], [0, 1]);
|
|
2190
|
+
const y = evaluateComponent(t[4], [0, 1]);
|
|
2191
|
+
const k = evaluateComponent(t[5], [0, 1]);
|
|
2192
|
+
const alphaNode = t.find((c2, idx) => idx === 7 && (c2.type === "<alpha-value>" || c2.type === "literal" && c2.value === "none"));
|
|
2193
|
+
const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
|
|
2194
|
+
const cMath = normalize(c, [0, 1]);
|
|
2195
|
+
const mMath = normalize(m, [0, 1]);
|
|
2196
|
+
const yMath = normalize(y, [0, 1]);
|
|
2197
|
+
const kMath = normalize(k, [0, 1]);
|
|
2198
|
+
const alphaMath = isNaN(alpha) ? alpha : Math.max(0, Math.min(1, alpha));
|
|
2199
|
+
return {
|
|
2200
|
+
model: "rgb",
|
|
2201
|
+
coords: [
|
|
2202
|
+
(1 - (cMath * (1 - kMath) + kMath)) * 255,
|
|
2203
|
+
(1 - (mMath * (1 - kMath) + kMath)) * 255,
|
|
2204
|
+
(1 - (yMath * (1 - kMath) + kMath)) * 255,
|
|
2205
|
+
alphaMath
|
|
2206
|
+
]
|
|
2207
|
+
};
|
|
2208
|
+
},
|
|
2209
|
+
"<color-mix()>": (node) => {
|
|
2210
|
+
let model = "oklab";
|
|
2211
|
+
let hue = "shorter";
|
|
2212
|
+
const children = node.value;
|
|
2213
|
+
let i = 2;
|
|
2214
|
+
if (children[i]?.type === "<color-interpolation-method>") {
|
|
2215
|
+
const methodParts = children[i].value;
|
|
2216
|
+
i++;
|
|
2217
|
+
if (methodParts[1]) {
|
|
2218
|
+
model = getLiteralValue(methodParts[1]);
|
|
2219
|
+
}
|
|
2220
|
+
if (methodParts[2] && methodParts[2].type !== "symbol") {
|
|
2221
|
+
hue = getLiteralValue(methodParts[2]);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const items = [];
|
|
2225
|
+
while (i < children.length) {
|
|
2226
|
+
const current = children[i];
|
|
2227
|
+
if (current.type === "symbol" && (current.value === "," || current.value === ")")) {
|
|
2228
|
+
i++;
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
let percentage = void 0;
|
|
2232
|
+
let colorNode;
|
|
2233
|
+
if (current.type === "<percentage>") {
|
|
2234
|
+
const tokenStr = String(current.value || "");
|
|
2235
|
+
percentage = evaluateCSSValue(tokenStr, "percentage");
|
|
2236
|
+
i++;
|
|
2237
|
+
colorNode = children[i];
|
|
2238
|
+
} else {
|
|
2239
|
+
colorNode = current;
|
|
2240
|
+
const nextNode = children[i + 1];
|
|
2241
|
+
if (nextNode?.type === "<percentage>") {
|
|
2242
|
+
const tokenStr = String(nextNode.value || "");
|
|
2243
|
+
percentage = evaluateCSSValue(tokenStr, "percentage");
|
|
1849
2244
|
i++;
|
|
1850
|
-
continue;
|
|
1851
2245
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2246
|
+
}
|
|
2247
|
+
const colorData = colorNode ? parseNode(colorNode) : null;
|
|
2248
|
+
if (colorData && colorData.coords) {
|
|
2249
|
+
i++;
|
|
2250
|
+
items.push({
|
|
2251
|
+
color: new Color(colorData.model, colorData.coords),
|
|
2252
|
+
percentage
|
|
2253
|
+
});
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
i++;
|
|
2257
|
+
}
|
|
2258
|
+
if (items.length === 0) {
|
|
2259
|
+
throw new Error("color-mix() must contain at least one color component.");
|
|
2260
|
+
}
|
|
2261
|
+
const mixedResult = Color.mix(items, { in: model, hue });
|
|
2262
|
+
return {
|
|
2263
|
+
model: mixedResult.model,
|
|
2264
|
+
coords: mixedResult.coords
|
|
2265
|
+
};
|
|
2266
|
+
},
|
|
2267
|
+
"<color-function>": (node) => parseNode(node.value[0]),
|
|
2268
|
+
"<custom-color-function>": (node) => parseNode(node.value[0]),
|
|
2269
|
+
"<rgb()>": (node) => parseNode(node.value[0]),
|
|
2270
|
+
"<rgba()>": (node) => parseNode(node.value[0]),
|
|
2271
|
+
"<hsl()>": (node) => parseNode(node.value[0]),
|
|
2272
|
+
"<hsla()>": (node) => parseNode(node.value[0]),
|
|
2273
|
+
"<hwb()>": (node) => parseNode(node.value[0]),
|
|
2274
|
+
"<lab()>": (node) => parseNode(node.value[0]),
|
|
2275
|
+
"<lch()>": (node) => parseNode(node.value[0]),
|
|
2276
|
+
"<oklab()>": (node) => parseNode(node.value[0]),
|
|
2277
|
+
"<oklch()>": (node) => parseNode(node.value[0]),
|
|
2278
|
+
"<legacy-rgb-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
|
|
2279
|
+
"<legacy-rgba-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
|
|
2280
|
+
"<legacy-hsl-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
|
|
2281
|
+
"<legacy-hsla-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
|
|
2282
|
+
"<modern-rgb-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
|
|
2283
|
+
"<modern-rgba-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
|
|
2284
|
+
"<modern-hsl-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
|
|
2285
|
+
"<modern-hsla-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
|
|
2286
|
+
"<absolute-hwb-syntax>": createModernColorParser("hwb", ["h", "w", "b"]),
|
|
2287
|
+
"<absolute-lab-syntax>": createModernColorParser("lab", ["l", "a", "b"]),
|
|
2288
|
+
"<absolute-lch-syntax>": createModernColorParser("lch", ["l", "c", "h"]),
|
|
2289
|
+
"<absolute-oklab-syntax>": createModernColorParser("oklab", ["l", "a", "b"]),
|
|
2290
|
+
"<absolute-oklch-syntax>": createModernColorParser("oklch", ["l", "c", "h"]),
|
|
2291
|
+
"<relative-rgb-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
|
|
2292
|
+
"<relative-rgba-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
|
|
2293
|
+
"<relative-hsl-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
|
|
2294
|
+
"<relative-hsla-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
|
|
2295
|
+
"<relative-hwb-syntax>": createRelativeColorParser("hwb", ["h", "w", "b"]),
|
|
2296
|
+
"<relative-lab-syntax>": createRelativeColorParser("lab", ["l", "a", "b"]),
|
|
2297
|
+
"<relative-lch-syntax>": createRelativeColorParser("lch", ["l", "c", "h"]),
|
|
2298
|
+
"<relative-oklab-syntax>": createRelativeColorParser("oklab", ["l", "a", "b"]),
|
|
2299
|
+
"<relative-oklch-syntax>": createRelativeColorParser("oklch", ["l", "c", "h"]),
|
|
2300
|
+
"<alpha()>": (node) => {
|
|
2301
|
+
const children = node.value;
|
|
2302
|
+
const colorNode = children[3];
|
|
2303
|
+
const originColor = parseNode(colorNode);
|
|
2304
|
+
if (!originColor)
|
|
2305
|
+
throw new Error("Missing or invalid origin color in alpha() function");
|
|
2306
|
+
const originInstance = new Color(originColor.model, originColor.coords);
|
|
2307
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
2308
|
+
const alphaNode = children.find((c, idx) => c.type === "<alpha-value>" || idx === 5 && c.type === "literal" && c.value === "none");
|
|
2309
|
+
const targetAlpha = alphaNode ? evaluateComponent(alphaNode, [0, 1], baseEnv) : originColor.coords[3];
|
|
2310
|
+
return {
|
|
2311
|
+
model: originColor.model,
|
|
2312
|
+
coords: [...originColor.coords.slice(0, 3), targetAlpha]
|
|
2313
|
+
};
|
|
2314
|
+
},
|
|
2315
|
+
"<color()>": (node) => parseNode(node.value[0]),
|
|
2316
|
+
"<relative-color-syntax>": (node) => parseNode(node.value[0]),
|
|
2317
|
+
"<absolute-color-syntax>": (node) => {
|
|
2318
|
+
const t = node.value;
|
|
2319
|
+
const spaceNode = t[2];
|
|
2320
|
+
const model = Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value;
|
|
2321
|
+
const c1 = evaluateComponent(t[3], [0, 1]);
|
|
2322
|
+
const c2 = evaluateComponent(t[4], [0, 1]);
|
|
2323
|
+
const c3 = evaluateComponent(t[5], [0, 1]);
|
|
2324
|
+
const alphaNode = t.find((c, idx) => idx > 5 && (c.type === "<alpha-value>" || c.type === "literal" && c.value === "none"));
|
|
2325
|
+
const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
|
|
2326
|
+
return {
|
|
2327
|
+
model,
|
|
2328
|
+
coords: [c1, c2, c3, alpha]
|
|
2329
|
+
};
|
|
2330
|
+
},
|
|
2331
|
+
"<relative-rgb-color-syntax>": createRelativeColorFunctionParser,
|
|
2332
|
+
"<relative-xyz-color-syntax>": createRelativeColorFunctionParser,
|
|
2333
|
+
"<relative-custom-color-syntax>": (node) => parseNode(node.value[0])
|
|
2334
|
+
};
|
|
2335
|
+
var formatters = (() => {
|
|
2336
|
+
const colorFunctionFormatters = (() => {
|
|
2337
|
+
const out = {};
|
|
2338
|
+
for (const model of Object.keys(colorModels)) {
|
|
2339
|
+
const { bridge, fromBridge, supportsLegacy, components, alphaVariant } = colorModels[model];
|
|
2340
|
+
out[model] = {
|
|
2341
|
+
bridge,
|
|
2342
|
+
fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
|
|
2343
|
+
format: ([c1, c2, c3, a = 1], options = {}) => {
|
|
2344
|
+
const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
|
|
2345
|
+
const fitted = fit([c1, c2, c3], model, { method: fitMethod, precision });
|
|
2346
|
+
const formatted = [...fitted, a].map((c, index) => {
|
|
2347
|
+
const norm = normalize(c, Object.values(components)[index].value);
|
|
2348
|
+
if ((units || legacy) && components) {
|
|
2349
|
+
const def = Object.values(components).find((comp) => comp.index === index);
|
|
2350
|
+
if (def?.value === "percentage")
|
|
2351
|
+
return `${norm}%`;
|
|
2352
|
+
if (def?.value === "hue" && units)
|
|
2353
|
+
return `${norm}deg`;
|
|
2354
|
+
}
|
|
2355
|
+
return norm.toString();
|
|
2356
|
+
});
|
|
2357
|
+
const f = formatted.slice(0, 3);
|
|
2358
|
+
const alpha = formatted[3];
|
|
2359
|
+
if (model in colorSpaces)
|
|
2360
|
+
return `color(${model} ${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
2361
|
+
if (legacy && supportsLegacy) {
|
|
2362
|
+
return a === 1 ? `${model}(${f.join(", ")})` : `${alphaVariant || model}(${f.join(", ")}, ${alpha})`;
|
|
1858
2363
|
}
|
|
2364
|
+
return `${model}(${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
1859
2365
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
return out;
|
|
2369
|
+
})();
|
|
2370
|
+
return {
|
|
2371
|
+
...colorFunctionFormatters,
|
|
2372
|
+
"hex-color": {
|
|
2373
|
+
bridge: "rgb",
|
|
2374
|
+
fromBridge: (coords) => coords,
|
|
2375
|
+
format: ([r, g, b, a = 1]) => {
|
|
2376
|
+
const toHex = (v) => v.toString(16).padStart(2, "0");
|
|
2377
|
+
const hex = [r, g, b].map((v) => toHex(Math.round(Math.max(0, Math.min(255, v))))).join("");
|
|
2378
|
+
return `#${hex}${a < 1 ? toHex(Math.round(a * 255)) : ""}`;
|
|
1862
2379
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2380
|
+
},
|
|
2381
|
+
"named-color": {
|
|
2382
|
+
bridge: "rgb",
|
|
2383
|
+
fromBridge: (coords) => coords,
|
|
2384
|
+
format: (rgb) => {
|
|
2385
|
+
const [r, g, b] = rgb.map((v, i) => i < 3 ? Math.round(Math.min(255, Math.max(0, v))) : v);
|
|
2386
|
+
for (const [name, [nr, ng, nb]] of Object.entries(namedColors)) {
|
|
2387
|
+
if (r === nr && g === ng && b === nb)
|
|
2388
|
+
return name;
|
|
2389
|
+
}
|
|
1866
2390
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2391
|
+
},
|
|
2392
|
+
"device-cmyk": {
|
|
2393
|
+
bridge: "rgb",
|
|
2394
|
+
fromBridge: (coords) => coords,
|
|
2395
|
+
format: ([red, green, blue, alpha = 1], options = {}) => {
|
|
2396
|
+
const { legacy = false, precision = 3, fit: fitMethod = config.defaults.fit } = options;
|
|
2397
|
+
const [fr, fg, fb] = fit([red, green, blue], "rgb", { method: fitMethod });
|
|
2398
|
+
const r = fr / 255;
|
|
2399
|
+
const g = fg / 255;
|
|
2400
|
+
const b = fb / 255;
|
|
2401
|
+
const k = 1 - Math.max(r, g, b);
|
|
2402
|
+
const formatComponent = (value) => {
|
|
2403
|
+
return Number(precision === null ? value : value.toFixed(precision)).toString();
|
|
2404
|
+
};
|
|
2405
|
+
const c = formatComponent(k === 1 ? 0 : (1 - r - k) / (1 - k));
|
|
2406
|
+
const m = formatComponent(k === 1 ? 0 : (1 - g - k) / (1 - k));
|
|
2407
|
+
const y = formatComponent(k === 1 ? 0 : (1 - b - k) / (1 - k));
|
|
2408
|
+
const kFormatted = formatComponent(k);
|
|
2409
|
+
const alphaFormatted = Number(alpha.toFixed(3)).toString();
|
|
2410
|
+
if (legacy) {
|
|
2411
|
+
return `device-cmyk(${c}, ${m}, ${y}, ${kFormatted}${alpha < 1 ? `, ${alphaFormatted}` : ""})`;
|
|
2412
|
+
}
|
|
2413
|
+
const rgbPart = colorFunctionFormatters.rgb.format?.([fr, fg, fb, alpha], options);
|
|
2414
|
+
return `device-cmyk(${c} ${m} ${y} ${kFormatted}${alpha < 1 ? ` / ${alphaFormatted}` : ""}, ${rgbPart})`;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
})();
|
|
2419
|
+
function parseNode(node) {
|
|
2420
|
+
const parser = parsers[node.type];
|
|
2421
|
+
if (!parser)
|
|
2422
|
+
throw new Error(`No parser for rule: ${node.type}`);
|
|
2423
|
+
return parser(node);
|
|
2424
|
+
}
|
|
2425
|
+
function isValid(input, rule) {
|
|
2426
|
+
const cleaned = input.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
2427
|
+
const tokens = tokenize(cleaned);
|
|
2428
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2429
|
+
if (rule) {
|
|
2430
|
+
if (grammarMap[rule]) {
|
|
2431
|
+
const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
|
|
2432
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
2433
|
+
}
|
|
2434
|
+
if (validators[rule]) {
|
|
2435
|
+
const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
|
|
2436
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
2437
|
+
}
|
|
2438
|
+
try {
|
|
2439
|
+
const inlineNode = parseExpression(rule);
|
|
2440
|
+
const results = matchNode(inlineNode, tokens, 0, grammarMap, memo, null);
|
|
2441
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
throw new Error(`Failed to resolve rule or parse inline expression: "${rule}"`, { cause: error });
|
|
1870
2444
|
}
|
|
1871
2445
|
}
|
|
1872
|
-
|
|
2446
|
+
const allRules = /* @__PURE__ */ new Set([...Object.keys(grammarMap), ...Object.keys(validators)]);
|
|
2447
|
+
for (const ruleName of allRules) {
|
|
2448
|
+
const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
|
|
2449
|
+
const isMatch = results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
2450
|
+
if (isMatch)
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
1873
2455
|
|
|
1874
2456
|
// dist/Color.js
|
|
1875
2457
|
var Color = class _Color {
|
|
@@ -1887,59 +2469,26 @@ var Saturon = (() => {
|
|
|
1887
2469
|
this.coords = c;
|
|
1888
2470
|
}
|
|
1889
2471
|
static from(color) {
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const {
|
|
1894
|
-
if (!isValid(c))
|
|
1895
|
-
continue;
|
|
1896
|
-
const parsed = parse(c);
|
|
1897
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
1898
|
-
const model = t in colorModels ? t : bridge;
|
|
2472
|
+
const tokens = tokenize(color);
|
|
2473
|
+
const tree = validateTokens(tokens);
|
|
2474
|
+
try {
|
|
2475
|
+
const { model, coords } = parseNode(tree);
|
|
1899
2476
|
return new _Color(model, coords);
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
throw new Error(`Failed to parse color string '${color}'`, { cause: error });
|
|
1900
2479
|
}
|
|
1901
|
-
throw new Error(`Unsupported or invalid color format: '${color}'.`);
|
|
1902
2480
|
}
|
|
1903
2481
|
/**
|
|
1904
|
-
*
|
|
2482
|
+
* Validates a color string.
|
|
1905
2483
|
*
|
|
1906
|
-
* @param color - Color string to
|
|
1907
|
-
* @param
|
|
2484
|
+
* @param color - Color string to check.
|
|
2485
|
+
* @param options - Validation options.
|
|
2486
|
+
* @returns `true` if valid, otherwise `false`.
|
|
1908
2487
|
*/
|
|
1909
|
-
static
|
|
1910
|
-
const
|
|
1911
|
-
for (const type in colorTypes) {
|
|
1912
|
-
const t = type;
|
|
1913
|
-
const { isValid, bridge, parse, toBridge } = colorTypes[t];
|
|
1914
|
-
if (!isValid(c))
|
|
1915
|
-
continue;
|
|
1916
|
-
if (!strict)
|
|
1917
|
-
return t;
|
|
1918
|
-
try {
|
|
1919
|
-
const parsed = parse(c);
|
|
1920
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
1921
|
-
const model = type in colorModels ? type : bridge;
|
|
1922
|
-
return typeof new _Color(model, coords) === "object" ? t : void 0;
|
|
1923
|
-
} catch {
|
|
1924
|
-
return void 0;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
return void 0;
|
|
1928
|
-
}
|
|
1929
|
-
static isValid(color, type) {
|
|
2488
|
+
static isValid(color, options = {}) {
|
|
2489
|
+
const { rule } = options;
|
|
1930
2490
|
try {
|
|
1931
|
-
|
|
1932
|
-
const t = type?.trim().toLowerCase();
|
|
1933
|
-
const c = clean(color);
|
|
1934
|
-
const { isValid, bridge, parse, toBridge } = colorTypes[t];
|
|
1935
|
-
if (!isValid(c))
|
|
1936
|
-
return false;
|
|
1937
|
-
const parsed = parse(c);
|
|
1938
|
-
const coords = t in colorModels ? parsed : toBridge(parsed);
|
|
1939
|
-
const model = t in colorModels ? t : bridge;
|
|
1940
|
-
return !!new _Color(model, coords);
|
|
1941
|
-
}
|
|
1942
|
-
return !!_Color.from(color);
|
|
2491
|
+
return isValid(color, rule) && !!_Color.from(color);
|
|
1943
2492
|
} catch {
|
|
1944
2493
|
return false;
|
|
1945
2494
|
}
|
|
@@ -1956,7 +2505,7 @@ var Saturon = (() => {
|
|
|
1956
2505
|
const models = Object.keys(colorModels);
|
|
1957
2506
|
const model = options.model ?? models[Math.floor(Math.random() * models.length)];
|
|
1958
2507
|
const { components } = colorModels[model];
|
|
1959
|
-
const valid =
|
|
2508
|
+
const valid = new Set(Object.keys(components));
|
|
1960
2509
|
for (const section of ["limits", "bias", "base", "deviation"]) {
|
|
1961
2510
|
const record = options[section];
|
|
1962
2511
|
if (!record)
|
|
@@ -1977,36 +2526,91 @@ var Saturon = (() => {
|
|
|
1977
2526
|
const v = Math.random() || 1e-9;
|
|
1978
2527
|
value = base + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * dev;
|
|
1979
2528
|
} else {
|
|
1980
|
-
let [
|
|
2529
|
+
let [min2, max2] = comp.value === "hue" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
|
|
1981
2530
|
const limits = options.limits?.[name];
|
|
1982
2531
|
if (limits) {
|
|
1983
2532
|
const [lMin, lMax] = limits;
|
|
1984
|
-
|
|
1985
|
-
|
|
2533
|
+
min2 = Math.max(min2, lMin);
|
|
2534
|
+
max2 = Math.min(max2, lMax);
|
|
1986
2535
|
}
|
|
1987
2536
|
let r = Math.random();
|
|
1988
2537
|
const biasFn = options.bias?.[name];
|
|
1989
2538
|
if (biasFn)
|
|
1990
2539
|
r = biasFn(r);
|
|
1991
|
-
value =
|
|
2540
|
+
value = min2 + r * (max2 - min2);
|
|
1992
2541
|
}
|
|
1993
2542
|
if (comp.value === "hue")
|
|
1994
2543
|
value = (value % 360 + 360) % 360;
|
|
1995
2544
|
else if (comp.value === "percentage")
|
|
1996
2545
|
value = Math.min(100, Math.max(0, value));
|
|
1997
2546
|
else if (Array.isArray(comp.value)) {
|
|
1998
|
-
const [
|
|
1999
|
-
value = Math.min(
|
|
2547
|
+
const [min2, max2] = comp.value;
|
|
2548
|
+
value = Math.min(max2, Math.max(min2, value));
|
|
2000
2549
|
} else
|
|
2001
2550
|
throw new Error(`Invalid component value definition for "${name}".`);
|
|
2002
2551
|
coords[comp.index] = value;
|
|
2003
2552
|
}
|
|
2004
2553
|
return new _Color(model, coords);
|
|
2005
2554
|
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Statically mixes multiple colors together by specified percentages.
|
|
2557
|
+
* Fully compliant with CSS Color Module Level 5 specification constraints.
|
|
2558
|
+
*
|
|
2559
|
+
* @param colors - Array of MixItems containing colors and optional percentages.
|
|
2560
|
+
* @param options - Options containing the interpolation space and hue policy.
|
|
2561
|
+
* @returns A new `Color` instance representing the mixed color.
|
|
2562
|
+
*/
|
|
2563
|
+
static mix(colors, options) {
|
|
2564
|
+
const { in: model = "oklab", hue = "shorter" } = options;
|
|
2565
|
+
if (!colors || colors.length === 0) {
|
|
2566
|
+
throw new Error("At least one color must be provided in the colors array.");
|
|
2567
|
+
}
|
|
2568
|
+
if (colors.length === 1)
|
|
2569
|
+
return colors[0].color.in(model);
|
|
2570
|
+
const weights = colors.map((c) => c.percentage !== void 0 ? c.percentage / 100 : void 0);
|
|
2571
|
+
const definedSum = weights.reduce((sum, w) => sum + (w || 0), 0);
|
|
2572
|
+
const missingCount = weights.filter((w) => w === void 0).length;
|
|
2573
|
+
let alphaMult = 1;
|
|
2574
|
+
if (weights.length > 0 && weights.every((w) => w === 0)) {
|
|
2575
|
+
alphaMult = 0;
|
|
2576
|
+
for (let i = 0; i < weights.length; i++)
|
|
2577
|
+
weights[i] = 1 / weights.length;
|
|
2578
|
+
} else if (missingCount === 0 && definedSum < 1) {
|
|
2579
|
+
alphaMult = definedSum;
|
|
2580
|
+
}
|
|
2581
|
+
if (alphaMult > 0) {
|
|
2582
|
+
if (missingCount > 0) {
|
|
2583
|
+
const remaining = Math.max(0, 1 - definedSum);
|
|
2584
|
+
const share = remaining / missingCount;
|
|
2585
|
+
for (let i = 0; i < weights.length; i++) {
|
|
2586
|
+
if (weights[i] === void 0)
|
|
2587
|
+
weights[i] = share;
|
|
2588
|
+
}
|
|
2589
|
+
} else if (definedSum > 0 && definedSum !== 1) {
|
|
2590
|
+
for (let i = 0; i < weights.length; i++) {
|
|
2591
|
+
weights[i] = weights[i] / definedSum;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
let accColor = colors[0].color.in(model);
|
|
2596
|
+
let accWeight = weights[0];
|
|
2597
|
+
for (let i = 1; i < colors.length; i++) {
|
|
2598
|
+
const currentWeight = weights[i];
|
|
2599
|
+
accWeight += currentWeight;
|
|
2600
|
+
const t = accWeight === 0 ? 0 : currentWeight / accWeight;
|
|
2601
|
+
accColor = mixTwo(accColor, colors[i].color.in(model), t, model, hue);
|
|
2602
|
+
}
|
|
2603
|
+
if (alphaMult !== 1) {
|
|
2604
|
+
const finalCoords = [...accColor.coords];
|
|
2605
|
+
finalCoords[3] = (finalCoords[3] ?? 1) * alphaMult;
|
|
2606
|
+
return new _Color(model, finalCoords);
|
|
2607
|
+
}
|
|
2608
|
+
return accColor;
|
|
2609
|
+
}
|
|
2006
2610
|
to(type, options = {}) {
|
|
2007
2611
|
const t = type.toLowerCase();
|
|
2008
2612
|
const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
|
|
2009
|
-
const conv =
|
|
2613
|
+
const conv = formatters[t];
|
|
2010
2614
|
if (!conv)
|
|
2011
2615
|
throw new Error(`Unsupported color type: '${t}'.`);
|
|
2012
2616
|
const { fromBridge, bridge, format } = conv;
|
|
@@ -2084,7 +2688,7 @@ var Saturon = (() => {
|
|
|
2084
2688
|
* @returns The formatted color string.
|
|
2085
2689
|
*/
|
|
2086
2690
|
toString(options = {}) {
|
|
2087
|
-
const { format } =
|
|
2691
|
+
const { format } = formatters[this.model];
|
|
2088
2692
|
const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
|
|
2089
2693
|
return format?.(this.coords, { legacy, fit: fit2, precision, units });
|
|
2090
2694
|
}
|
|
@@ -2092,20 +2696,17 @@ var Saturon = (() => {
|
|
|
2092
2696
|
* Returns the color as an object of component values.
|
|
2093
2697
|
*
|
|
2094
2698
|
* @param options - Optional retrieval options.
|
|
2095
|
-
* @returns An object mapping each component
|
|
2699
|
+
* @returns An object mapping each component to its numeric value.
|
|
2096
2700
|
* @throws If the model has no defined components.
|
|
2097
2701
|
*/
|
|
2702
|
+
// eslint-disable-next-line no-unused-vars
|
|
2098
2703
|
toObject(options = {}) {
|
|
2099
2704
|
const coords = this.toArray(options);
|
|
2100
2705
|
const { components } = colorModels[this.model];
|
|
2101
2706
|
if (!components)
|
|
2102
2707
|
throw new Error(`Model ${this.model} does not have defined components.`);
|
|
2103
|
-
const fullComponents = {
|
|
2104
|
-
...components,
|
|
2105
|
-
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
2106
|
-
};
|
|
2107
2708
|
const result = {};
|
|
2108
|
-
for (const [name, { index }] of Object.entries(
|
|
2709
|
+
for (const [name, { index }] of Object.entries(components))
|
|
2109
2710
|
result[name] = coords[index];
|
|
2110
2711
|
return result;
|
|
2111
2712
|
}
|
|
@@ -2113,7 +2714,7 @@ var Saturon = (() => {
|
|
|
2113
2714
|
* Returns the color as an array of component values, optionally normalized and fitted.
|
|
2114
2715
|
*
|
|
2115
2716
|
* @param options - Conversion configuration.
|
|
2116
|
-
* @returns An array of normalized color components
|
|
2717
|
+
* @returns An array of normalized color components.
|
|
2117
2718
|
* @throws If the model has no defined components.
|
|
2118
2719
|
*/
|
|
2119
2720
|
toArray(options = {}) {
|
|
@@ -2122,22 +2723,7 @@ var Saturon = (() => {
|
|
|
2122
2723
|
const { components } = colorModels[model];
|
|
2123
2724
|
if (!components)
|
|
2124
2725
|
throw new Error(`Model ${model} does not have defined components.`);
|
|
2125
|
-
const
|
|
2126
|
-
...components,
|
|
2127
|
-
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
2128
|
-
};
|
|
2129
|
-
const normalize = (c, i) => {
|
|
2130
|
-
const v = Object.values(defs)[i]?.value;
|
|
2131
|
-
const [min, max] = Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
|
|
2132
|
-
if (Number.isNaN(c))
|
|
2133
|
-
return 0;
|
|
2134
|
-
if (c === Infinity)
|
|
2135
|
-
return max;
|
|
2136
|
-
if (c === -Infinity)
|
|
2137
|
-
return min;
|
|
2138
|
-
return typeof c === "number" ? c : 0;
|
|
2139
|
-
};
|
|
2140
|
-
const norm = coords.slice(0, 3).map(normalize);
|
|
2726
|
+
const norm = coords.slice(0, 3).map((c, i) => normalize(c, Object.values(components)[i].value));
|
|
2141
2727
|
const fitted = fit(norm, model, {
|
|
2142
2728
|
method,
|
|
2143
2729
|
precision
|
|
@@ -2189,44 +2775,28 @@ var Saturon = (() => {
|
|
|
2189
2775
|
* - A partial object mapping component names to numbers or update functions
|
|
2190
2776
|
* - A function that receives current components and returns partial updates or an array of values
|
|
2191
2777
|
* - An array of new values corresponding to component indices
|
|
2192
|
-
* @param
|
|
2778
|
+
* @param normalized - Whether to normalize component values to their valid ranges. Defaults to `true`.
|
|
2193
2779
|
* When `false`, values are not clamped or validated against their ranges.
|
|
2194
2780
|
* @returns A new Color instance with the updated component values
|
|
2195
2781
|
*/
|
|
2196
2782
|
/* eslint-disable no-unused-vars */
|
|
2197
|
-
with(values
|
|
2198
|
-
const normRange = (defValue) => Array.isArray(defValue) ? defValue : defValue === "hue" ? [0, 360] : [0, 100];
|
|
2199
|
-
const normalizeComponent = (c, defValue) => {
|
|
2200
|
-
if (normalize === false)
|
|
2201
|
-
return c;
|
|
2202
|
-
const [min, max] = normRange(defValue);
|
|
2203
|
-
if (Number.isNaN(c))
|
|
2204
|
-
return 0;
|
|
2205
|
-
if (c === Infinity)
|
|
2206
|
-
return max;
|
|
2207
|
-
if (c === -Infinity)
|
|
2208
|
-
return min;
|
|
2209
|
-
return c;
|
|
2210
|
-
};
|
|
2783
|
+
with(values) {
|
|
2211
2784
|
const { model } = this;
|
|
2212
2785
|
const coords = this.coords.slice();
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
2217
|
-
};
|
|
2218
|
-
const names = Object.keys(defs);
|
|
2786
|
+
const { components } = colorModels[model];
|
|
2787
|
+
const names = Object.keys(components);
|
|
2788
|
+
const componentDefs = Object.values(components);
|
|
2219
2789
|
const newValues = typeof values === "function" ? values(Object.fromEntries(names.map((k) => {
|
|
2220
|
-
const def =
|
|
2221
|
-
return [k,
|
|
2790
|
+
const def = components[k];
|
|
2791
|
+
return [k, normalize(coords[def.index], def.value)];
|
|
2222
2792
|
}))) : values;
|
|
2223
2793
|
if (Array.isArray(newValues)) {
|
|
2224
2794
|
const adjusted = coords.map((curr, i) => {
|
|
2225
2795
|
const incoming = newValues[i];
|
|
2226
2796
|
if (typeof incoming !== "number")
|
|
2227
2797
|
return curr;
|
|
2228
|
-
const def =
|
|
2229
|
-
return
|
|
2798
|
+
const def = componentDefs.find((d) => d.index === i);
|
|
2799
|
+
return normalize(incoming, def.value);
|
|
2230
2800
|
});
|
|
2231
2801
|
return new _Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
|
|
2232
2802
|
}
|
|
@@ -2234,107 +2804,24 @@ var Saturon = (() => {
|
|
|
2234
2804
|
for (const name of names) {
|
|
2235
2805
|
if (!(name in newValues))
|
|
2236
2806
|
continue;
|
|
2237
|
-
const { index, value } =
|
|
2807
|
+
const { index, value } = components[name];
|
|
2238
2808
|
const raw = newValues[name];
|
|
2239
|
-
const prev =
|
|
2240
|
-
const val = typeof raw === "function" ?
|
|
2809
|
+
const prev = normalize(coords[index], value);
|
|
2810
|
+
const val = typeof raw === "function" ? normalize(raw(prev), value) : typeof raw === "number" ? normalize(raw, value) : coords[index];
|
|
2241
2811
|
next[index] = val;
|
|
2242
2812
|
}
|
|
2243
2813
|
return new _Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
|
|
2244
2814
|
}
|
|
2245
2815
|
/**
|
|
2246
|
-
*
|
|
2816
|
+
* Creates a new Color instance with values fitted to the color model's gamut.
|
|
2247
2817
|
*
|
|
2248
|
-
* @param
|
|
2249
|
-
* @
|
|
2250
|
-
* @returns A new `Color` instance representing the mixed color.
|
|
2251
|
-
* @throws If the color model does not have defined components.
|
|
2818
|
+
* @param options - Configuration options for fitting
|
|
2819
|
+
* @returns A new Color instance with fitted values
|
|
2252
2820
|
*/
|
|
2253
|
-
|
|
2254
|
-
const {
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
const { components } = colorModels[model];
|
|
2258
|
-
if (!components)
|
|
2259
|
-
throw new Error(`Model ${model} does not have defined components.`);
|
|
2260
|
-
const defs = {
|
|
2261
|
-
...components,
|
|
2262
|
-
alpha: { index: 3, value: [0, 1], precision: 3 }
|
|
2263
|
-
};
|
|
2264
|
-
const normRange = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
|
|
2265
|
-
const repairPair = (a, b, v) => {
|
|
2266
|
-
const [min, max] = normRange(v);
|
|
2267
|
-
const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
|
|
2268
|
-
a = fixInf(a);
|
|
2269
|
-
b = fixInf(b);
|
|
2270
|
-
const aNaN = Number.isNaN(a);
|
|
2271
|
-
const bNaN = Number.isNaN(b);
|
|
2272
|
-
if (aNaN && bNaN)
|
|
2273
|
-
return { a: 0, b: 0 };
|
|
2274
|
-
if (aNaN)
|
|
2275
|
-
return { a: b, b };
|
|
2276
|
-
if (bNaN)
|
|
2277
|
-
return { a, b: a };
|
|
2278
|
-
return { a, b, ok: true };
|
|
2279
|
-
};
|
|
2280
|
-
const hueIndex = defs.h?.index ?? -1;
|
|
2281
|
-
for (const key in defs) {
|
|
2282
|
-
const { index, value } = defs[key];
|
|
2283
|
-
if (index > 3)
|
|
2284
|
-
continue;
|
|
2285
|
-
const r = repairPair(A[index], B[index], value);
|
|
2286
|
-
if (r.ok) {
|
|
2287
|
-
const [min, max] = normRange(value);
|
|
2288
|
-
const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
|
|
2289
|
-
A[index] = fixInf(A[index]);
|
|
2290
|
-
B[index] = fixInf(B[index]);
|
|
2291
|
-
} else {
|
|
2292
|
-
A[index] = r.a;
|
|
2293
|
-
B[index] = r.b;
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1 } = options;
|
|
2297
|
-
const ease = typeof easing === "function" ? easing : EASINGS[easing];
|
|
2298
|
-
const t = ease(Math.min(1, Math.max(0, amount)));
|
|
2299
|
-
const tt = Math.pow(t, 1 / gamma);
|
|
2300
|
-
const wrap = (v) => (v % 360 + 360) % 360;
|
|
2301
|
-
const hueDelta = (a, b) => {
|
|
2302
|
-
const d = wrap(b - a);
|
|
2303
|
-
return d > 180 ? d - 360 : d;
|
|
2304
|
-
};
|
|
2305
|
-
const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
|
|
2306
|
-
const interpHue = (a, b, t2, method) => {
|
|
2307
|
-
switch (method) {
|
|
2308
|
-
case "shorter":
|
|
2309
|
-
return wrap(a + t2 * hueDelta(a, b));
|
|
2310
|
-
case "longer":
|
|
2311
|
-
return wrap(a + t2 * hueDeltaLong(a, b));
|
|
2312
|
-
case "increasing":
|
|
2313
|
-
return wrap(a * (1 - t2) + (b < a ? b + 360 : b) * t2);
|
|
2314
|
-
case "decreasing":
|
|
2315
|
-
return wrap(a * (1 - t2) + (b > a ? b - 360 : b) * t2);
|
|
2316
|
-
}
|
|
2317
|
-
throw new Error(`Invalid hue interpolation: ${method}`);
|
|
2318
|
-
};
|
|
2319
|
-
if (tt === 0)
|
|
2320
|
-
return new _Color(model, [...A]);
|
|
2321
|
-
if (tt === 1)
|
|
2322
|
-
return new _Color(model, [...B]);
|
|
2323
|
-
const aA = A[3];
|
|
2324
|
-
const aB = B[3];
|
|
2325
|
-
const resolvedA = A.slice(0, 3);
|
|
2326
|
-
const resolvedB = B.slice(0, 3);
|
|
2327
|
-
if (aA < 1 || aB < 1) {
|
|
2328
|
-
const premixed = resolvedA.map((a2, i) => {
|
|
2329
|
-
const b = resolvedB[i];
|
|
2330
|
-
return i === hueIndex ? interpHue(a2, b, tt, hue) : a2 * aA * (1 - tt) + b * aB * tt;
|
|
2331
|
-
});
|
|
2332
|
-
const a = aA * (1 - tt) + aB * tt;
|
|
2333
|
-
const out = a > 0 ? premixed.map((v, i) => i === hueIndex ? v : v / a) : resolvedA.map((_, i) => i === hueIndex ? premixed[i] : 0);
|
|
2334
|
-
return new _Color(model, [...out, a]);
|
|
2335
|
-
}
|
|
2336
|
-
const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], tt, hue) : a + (resolvedB[i] - a) * tt);
|
|
2337
|
-
return new _Color(model, [...mixed, 1]);
|
|
2821
|
+
fit(options = {}) {
|
|
2822
|
+
const { fit: method = config.defaults.fit, precision } = options;
|
|
2823
|
+
const fitted = this.toArray({ fit: method, precision });
|
|
2824
|
+
return new _Color(this.model, fitted);
|
|
2338
2825
|
}
|
|
2339
2826
|
/**
|
|
2340
2827
|
* Fits this color within the specified gamut using a given method.
|
|
@@ -2351,21 +2838,10 @@ var Saturon = (() => {
|
|
|
2351
2838
|
const fitted = this.in(g).toArray({ fit: method, precision: null });
|
|
2352
2839
|
return new _Color(g, fitted).in(this.model);
|
|
2353
2840
|
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Creates a new Color instance with values fitted to the color model's gamut.
|
|
2356
|
-
*
|
|
2357
|
-
* @param options - Configuration options for fitting
|
|
2358
|
-
* @returns A new Color instance with fitted values
|
|
2359
|
-
*/
|
|
2360
|
-
fit(options = {}) {
|
|
2361
|
-
const { fit: method = config.defaults.fit, precision } = options;
|
|
2362
|
-
const fitted = this.toArray({ fit: method, precision });
|
|
2363
|
-
return new _Color(this.model, fitted);
|
|
2364
|
-
}
|
|
2365
2841
|
/**
|
|
2366
2842
|
* Calculates the WCAG 2.1 contrast ratio between this color and another.
|
|
2367
2843
|
*
|
|
2368
|
-
* @param other - The comparison
|
|
2844
|
+
* @param other - The comparison Color instance.
|
|
2369
2845
|
* @returns Contrast ratio from 1 to 21.
|
|
2370
2846
|
*
|
|
2371
2847
|
* @remarks
|
|
@@ -2373,15 +2849,14 @@ var Saturon = (() => {
|
|
|
2373
2849
|
* - For perceptual accuracy, consider using APCA instead.
|
|
2374
2850
|
*/
|
|
2375
2851
|
contrast(other) {
|
|
2376
|
-
const o = typeof other === "string" ? _Color.from(other) : other;
|
|
2377
2852
|
const [, L1] = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
2378
|
-
const [, L2] =
|
|
2853
|
+
const [, L2] = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
2379
2854
|
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
|
|
2380
2855
|
}
|
|
2381
2856
|
/**
|
|
2382
2857
|
* Calculates the color difference (ΔEOK) between the current color and another color using the OKLAB color space.
|
|
2383
2858
|
*
|
|
2384
|
-
* @param other - The other
|
|
2859
|
+
* @param other - The other Color instance to compare against.
|
|
2385
2860
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
2386
2861
|
*
|
|
2387
2862
|
* @remarks
|
|
@@ -2392,7 +2867,7 @@ var Saturon = (() => {
|
|
|
2392
2867
|
deltaEOK(other) {
|
|
2393
2868
|
const coordsOptions = { fit: "none", precision: null };
|
|
2394
2869
|
const [L1, a1, b1] = this.in("oklab").toArray(coordsOptions);
|
|
2395
|
-
const [L2, a2, b2] =
|
|
2870
|
+
const [L2, a2, b2] = other.in("oklab").toArray(coordsOptions);
|
|
2396
2871
|
const \u0394L = L1 - L2;
|
|
2397
2872
|
const \u0394A = a1 - a2;
|
|
2398
2873
|
const \u0394B = b1 - b2;
|
|
@@ -2403,13 +2878,13 @@ var Saturon = (() => {
|
|
|
2403
2878
|
* Calculates the color difference (ΔE) between two colors using the CIE76 formula.
|
|
2404
2879
|
* This is a simple Euclidean distance in LAB color space.
|
|
2405
2880
|
*
|
|
2406
|
-
* @param other - The other
|
|
2881
|
+
* @param other - The other Color instance to compare against.
|
|
2407
2882
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
2408
2883
|
*/
|
|
2409
2884
|
deltaE76(other) {
|
|
2410
2885
|
const coordsOptions = { fit: "none", precision: null };
|
|
2411
2886
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
2412
|
-
const [L2, a2, b2] =
|
|
2887
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
2413
2888
|
const \u0394L = L1 - L2;
|
|
2414
2889
|
const \u0394A = a1 - a2;
|
|
2415
2890
|
const \u0394B = b1 - b2;
|
|
@@ -2419,13 +2894,13 @@ var Saturon = (() => {
|
|
|
2419
2894
|
* Calculates the color difference (ΔE) between two colors using the CIE94 formula.
|
|
2420
2895
|
* This method improves perceptual accuracy over CIE76 by applying weighting factors.
|
|
2421
2896
|
*
|
|
2422
|
-
* @param other - The other
|
|
2897
|
+
* @param other - The other Color instance to compare against.
|
|
2423
2898
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
2424
2899
|
*/
|
|
2425
2900
|
deltaE94(other) {
|
|
2426
2901
|
const coordsOptions = { fit: "none", precision: null };
|
|
2427
2902
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
2428
|
-
const [L2, a2, b2] =
|
|
2903
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
2429
2904
|
const \u0394L = L1 - L2;
|
|
2430
2905
|
const \u0394A = a1 - a2;
|
|
2431
2906
|
const \u0394B = b1 - b2;
|
|
@@ -2443,13 +2918,13 @@ var Saturon = (() => {
|
|
|
2443
2918
|
* Calculates the color difference (ΔE) between two colors using the CIEDE2000 formula.
|
|
2444
2919
|
* This is the most perceptually accurate method, accounting for interactions between hue, chroma, and lightness.
|
|
2445
2920
|
*
|
|
2446
|
-
* @param other - The other
|
|
2921
|
+
* @param other - The other Color instance to compare against.
|
|
2447
2922
|
* @returns A number in range (0-1) (smaller indicates more similar colors).
|
|
2448
2923
|
*/
|
|
2449
2924
|
deltaE2000(other) {
|
|
2450
2925
|
const coordsOptions = { fit: "none", precision: null };
|
|
2451
2926
|
const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
|
|
2452
|
-
const [L2, a2, b2] =
|
|
2927
|
+
const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
|
|
2453
2928
|
const \u03C0 = Math.PI, d2r = \u03C0 / 180, r2d = 180 / \u03C0;
|
|
2454
2929
|
const C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
|
|
2455
2930
|
const C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
|
|
@@ -2487,7 +2962,7 @@ var Saturon = (() => {
|
|
|
2487
2962
|
const Cdash = (Cdash1 + Cdash2) / 2;
|
|
2488
2963
|
const Cdash7 = Math.pow(Cdash, 7);
|
|
2489
2964
|
const hsum = h1 + h2;
|
|
2490
|
-
let hdash
|
|
2965
|
+
let hdash;
|
|
2491
2966
|
if (Cdash1 === 0 && Cdash2 === 0) {
|
|
2492
2967
|
hdash = hsum;
|
|
2493
2968
|
} else if (habs <= 180) {
|
|
@@ -2518,8 +2993,8 @@ var Saturon = (() => {
|
|
|
2518
2993
|
/**
|
|
2519
2994
|
* Checks numeric equality with another color within a tolerance.
|
|
2520
2995
|
*
|
|
2521
|
-
* @param other - Color
|
|
2522
|
-
* @param epsilon - Allowed floating-point difference (
|
|
2996
|
+
* @param other - Color instnace to compare.
|
|
2997
|
+
* @param epsilon - Allowed floating-point difference (defaults to the value of `EPSILON` in `"saturon/math"`).
|
|
2523
2998
|
* @returns `true` if equal within tolerance.
|
|
2524
2999
|
*
|
|
2525
3000
|
* @remarks
|
|
@@ -2533,24 +3008,23 @@ var Saturon = (() => {
|
|
|
2533
3008
|
* - {@link deltaE94} (weighted improvements over LAB)
|
|
2534
3009
|
* - {@link deltaE2000} (most accurate, accounts for perceptual interactions)
|
|
2535
3010
|
*/
|
|
2536
|
-
equals(other, epsilon =
|
|
2537
|
-
const o = typeof other === "string" ? _Color.from(other) : other;
|
|
3011
|
+
equals(other, epsilon = EPSILON) {
|
|
2538
3012
|
const thisCoords = this.toArray({ fit: "none", precision: null });
|
|
2539
|
-
const otherCoords =
|
|
2540
|
-
if (
|
|
3013
|
+
const otherCoords = other.toArray({ fit: "none", precision: null });
|
|
3014
|
+
if (other.model === this.model)
|
|
2541
3015
|
return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
|
|
2542
3016
|
const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
2543
|
-
const b =
|
|
3017
|
+
const b = other.in("xyz-d65").toArray({ fit: "none", precision: null });
|
|
2544
3018
|
return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
|
|
2545
3019
|
}
|
|
2546
3020
|
/**
|
|
2547
3021
|
* Determines whether this color lies within a given gamut.
|
|
2548
3022
|
*
|
|
2549
3023
|
* @param gamut - Target color space.
|
|
2550
|
-
* @param epsilon - Floating-point tolerance (
|
|
3024
|
+
* @param epsilon - Floating-point tolerance (defaults to the value of `EPSILON` in `"saturon/math"`).
|
|
2551
3025
|
* @returns `true` if inside gamut, else `false`.
|
|
2552
3026
|
*/
|
|
2553
|
-
inGamut(gamut, epsilon =
|
|
3027
|
+
inGamut(gamut, epsilon = EPSILON) {
|
|
2554
3028
|
const g = gamut.trim().toLowerCase();
|
|
2555
3029
|
if (!(g in colorSpaces))
|
|
2556
3030
|
throw new Error(`Unsupported color gamut: '${g}'.`);
|
|
@@ -2560,8 +3034,8 @@ var Saturon = (() => {
|
|
|
2560
3034
|
const coords = this.in(g).toArray({ fit: "none", precision: null });
|
|
2561
3035
|
return Object.values(components).every(({ index, value }) => {
|
|
2562
3036
|
const v = coords[index];
|
|
2563
|
-
const [
|
|
2564
|
-
return v >=
|
|
3037
|
+
const [min2, max2] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
|
|
3038
|
+
return v >= min2 - epsilon && v <= max2 + epsilon;
|
|
2565
3039
|
});
|
|
2566
3040
|
}
|
|
2567
3041
|
};
|