dembrandt 0.25.0 → 0.26.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/README.md +2 -2
- package/dist/lib/assertions.d.ts +36 -0
- package/dist/lib/assertions.js +46 -0
- package/dist/lib/assertions.js.map +1 -0
- package/dist/lib/color-parse.d.ts +39 -0
- package/dist/lib/color-parse.js +508 -0
- package/dist/lib/color-parse.js.map +1 -0
- package/dist/lib/colors.d.ts +1 -0
- package/dist/lib/colors.js +32 -58
- package/dist/lib/colors.js.map +1 -1
- package/dist/lib/extractors/colors.js +73 -13
- package/dist/lib/extractors/colors.js.map +1 -1
- package/dist/lib/extractors/index.js +28 -127
- package/dist/lib/extractors/index.js.map +1 -1
- package/dist/lib/normalize.js.map +1 -1
- package/dist/package.json +23 -13
- package/dist/test/assertions.test.d.ts +1 -0
- package/dist/test/assertions.test.js +66 -0
- package/dist/test/assertions.test.js.map +1 -0
- package/dist/test/color-parse.test.d.ts +1 -0
- package/dist/test/color-parse.test.js +241 -0
- package/dist/test/color-parse.test.js.map +1 -0
- package/dist/test/colors.test.js +19 -0
- package/dist/test/colors.test.js.map +1 -1
- package/dist/test/drift.test.js.map +1 -1
- package/dist/test/dtcg-validate.test.js.map +1 -1
- package/dist/test/exit-codes.test.js.map +1 -1
- package/dist/test/findings.test.js.map +1 -1
- package/dist/test/html.test.js.map +1 -1
- package/dist/test/merger.test.js.map +1 -1
- package/package.json +23 -13
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ dembrandt dembrandt.com --brand-guide
|
|
|
249
249
|
The official action wraps extract → compare → gate into one step: it installs a matching Chromium, runs a pinned CLI version, fails the job on drift, and renders the drifted tokens as inline annotations on the PR.
|
|
250
250
|
|
|
251
251
|
```yaml
|
|
252
|
-
- uses: dembrandt/dembrandt@v0.
|
|
252
|
+
- uses: dembrandt/dembrandt@v0.26.0
|
|
253
253
|
with:
|
|
254
254
|
url: https://preview.example.com
|
|
255
255
|
baseline: .dembrandt/baseline.json
|
|
@@ -267,7 +267,7 @@ jobs:
|
|
|
267
267
|
runs-on: ubuntu-latest
|
|
268
268
|
steps:
|
|
269
269
|
- uses: actions/checkout@v4
|
|
270
|
-
- uses: dembrandt/dembrandt@v0.
|
|
270
|
+
- uses: dembrandt/dembrandt@v0.26.0
|
|
271
271
|
with:
|
|
272
272
|
url: ${{ github.event.deployment_status.environment_url }}
|
|
273
273
|
baseline: .dembrandt/baseline.json
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DriftReport, DriftCategory } from './drift.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declarative drift assertions (DEM-81), lighthouserc model: .dembrandtrc
|
|
4
|
+
* declares per-metric limits with error/warn severity, and the gate fails only
|
|
5
|
+
* on error-level breaches. Evaluation is pure — the report in, verdicts out —
|
|
6
|
+
* so the whole layer is unit-testable without a browser or a baseline file.
|
|
7
|
+
*
|
|
8
|
+
* "assertions": {
|
|
9
|
+
* "score": ["error", { "max": 10 }],
|
|
10
|
+
* "color": ["error", { "max": 0.2 }],
|
|
11
|
+
* "typography": ["warn", { "max": 0.3 }],
|
|
12
|
+
* "added": ["warn", { "max": 5 }],
|
|
13
|
+
* "removed": ["error", { "max": 0 }]
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
export type AssertionSeverity = 'error' | 'warn' | 'off';
|
|
17
|
+
/** score = overall 0..100; categories = per-category 0..1; counts = summary. */
|
|
18
|
+
export type AssertionMetric = 'score' | 'added' | 'changed' | 'removed' | DriftCategory;
|
|
19
|
+
export type AssertionSpec = [AssertionSeverity, {
|
|
20
|
+
max: number;
|
|
21
|
+
}];
|
|
22
|
+
export type Assertions = Partial<Record<AssertionMetric, AssertionSpec>>;
|
|
23
|
+
export interface AssertionResult {
|
|
24
|
+
metric: AssertionMetric;
|
|
25
|
+
severity: 'error' | 'warn';
|
|
26
|
+
actual: number;
|
|
27
|
+
max: number;
|
|
28
|
+
}
|
|
29
|
+
/** Malformed specs are collected, not thrown: a typo in .dembrandtrc must
|
|
30
|
+
* surface as a config warning, never crash a CI run mid-gate. */
|
|
31
|
+
export interface AssertionEvaluation {
|
|
32
|
+
failures: AssertionResult[];
|
|
33
|
+
warnings: AssertionResult[];
|
|
34
|
+
invalid: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare function evaluateAssertions(report: DriftReport, assertions: Assertions): AssertionEvaluation;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const CATEGORIES = new Set(['color', 'typography', 'spacing', 'radius', 'shadow']);
|
|
2
|
+
function metricValue(report, metric) {
|
|
3
|
+
if (metric === 'score')
|
|
4
|
+
return report.score;
|
|
5
|
+
if (metric === 'added' || metric === 'changed' || metric === 'removed')
|
|
6
|
+
return report.summary[metric];
|
|
7
|
+
if (CATEGORIES.has(metric)) {
|
|
8
|
+
const cat = report.categories.find((c) => c.category === metric);
|
|
9
|
+
return cat ? cat.score : null; // category degraded out of the report — nothing to assert
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function evaluateAssertions(report, assertions) {
|
|
14
|
+
const failures = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
const invalid = [];
|
|
17
|
+
for (const [metric, spec] of Object.entries(assertions)) {
|
|
18
|
+
if (!Array.isArray(spec) || spec.length !== 2) {
|
|
19
|
+
invalid.push(String(metric));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const [severity, limits] = spec;
|
|
23
|
+
if (severity !== 'error' && severity !== 'warn' && severity !== 'off') {
|
|
24
|
+
invalid.push(String(metric));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (severity === 'off')
|
|
28
|
+
continue;
|
|
29
|
+
const max = limits?.max;
|
|
30
|
+
if (typeof max !== 'number' || Number.isNaN(max)) {
|
|
31
|
+
invalid.push(String(metric));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const actual = metricValue(report, metric);
|
|
35
|
+
if (actual === null) {
|
|
36
|
+
if (!CATEGORIES.has(metric))
|
|
37
|
+
invalid.push(String(metric));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (actual > max) {
|
|
41
|
+
(severity === 'error' ? failures : warnings).push({ metric, severity, actual, max });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { failures, warnings, invalid };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=assertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.js","sourceRoot":"","sources":["../../lib/assertions.ts"],"names":[],"mappings":"AAgCA,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAExG,SAAS,WAAW,CAAC,MAAmB,EAAE,MAAuB;IAC/D,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC;IAC5C,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;QACjE,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,0DAA0D;IAC3F,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAUD,MAAM,UAAU,kBAAkB,CAAC,MAAmB,EAAE,UAAsB;IAC5E,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAiC,EAAE,CAAC;QACxF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAC1F,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,IAA0B,CAAC;QACtD,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAClH,IAAI,QAAQ,KAAK,KAAK;YAAE,SAAS;QACjC,MAAM,GAAG,GAAI,MAA4B,EAAE,GAAG,CAAC;QAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAE7F,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAC1D,SAAS;QACX,CAAC;QACD,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;YACjB,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-complete CSS color parser (CSS Color Level 4).
|
|
3
|
+
*
|
|
4
|
+
* One entry point, parseCssColor(), accepts every color notation a stylesheet
|
|
5
|
+
* or computed style can contain — named colors, hex (3/4/6/8), legacy
|
|
6
|
+
* comma-separated rgb()/rgba()/hsl()/hsla(), modern space-separated syntax
|
|
7
|
+
* with slash alpha, hwb(), lab()/lch() (D50), oklab()/oklch(), and color()
|
|
8
|
+
* with the common predefined spaces — and resolves it to sRGB 8-bit channels
|
|
9
|
+
* plus float alpha. Out-of-gamut values are mapped by the CSS Color 4
|
|
10
|
+
* chroma-reduction algorithm, not channel clipping.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free and strict-clean; the browser-context mirror in
|
|
13
|
+
* lib/extractors/colors.ts must stay in sync with this module.
|
|
14
|
+
*/
|
|
15
|
+
export interface RgbaColor {
|
|
16
|
+
r: number;
|
|
17
|
+
g: number;
|
|
18
|
+
b: number;
|
|
19
|
+
a: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse any CSS color string to sRGB 8-bit channels + float alpha.
|
|
23
|
+
* Returns null for anything that is not an absolute color (currentcolor,
|
|
24
|
+
* inherit, var() references, malformed input).
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseCssColor(input: string): RgbaColor | null;
|
|
27
|
+
/** #rrggbb for opaque colors, #rrggbbaa when alpha < 1. */
|
|
28
|
+
export declare function serializeHex(c: RgbaColor): string;
|
|
29
|
+
/** Legacy rgb()/rgba() serialisation, the canonical interchange form in the extractor. */
|
|
30
|
+
export declare function serializeRgb(c: RgbaColor): string;
|
|
31
|
+
/**
|
|
32
|
+
* Convenience for extractor injection paths: any CSS color string to its
|
|
33
|
+
* opaque 6-hex identity plus a legacy serialisation that preserves alpha.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeCssColor(input: string): {
|
|
36
|
+
hex: string;
|
|
37
|
+
legacy: string;
|
|
38
|
+
alpha: number;
|
|
39
|
+
} | null;
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-complete CSS color parser (CSS Color Level 4).
|
|
3
|
+
*
|
|
4
|
+
* One entry point, parseCssColor(), accepts every color notation a stylesheet
|
|
5
|
+
* or computed style can contain — named colors, hex (3/4/6/8), legacy
|
|
6
|
+
* comma-separated rgb()/rgba()/hsl()/hsla(), modern space-separated syntax
|
|
7
|
+
* with slash alpha, hwb(), lab()/lch() (D50), oklab()/oklch(), and color()
|
|
8
|
+
* with the common predefined spaces — and resolves it to sRGB 8-bit channels
|
|
9
|
+
* plus float alpha. Out-of-gamut values are mapped by the CSS Color 4
|
|
10
|
+
* chroma-reduction algorithm, not channel clipping.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free and strict-clean; the browser-context mirror in
|
|
13
|
+
* lib/extractors/colors.ts must stay in sync with this module.
|
|
14
|
+
*/
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Matrices and transfer functions
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
19
|
+
[3.2404542, -1.5371385, -0.4985314],
|
|
20
|
+
[-0.9692660, 1.8760108, 0.0415560],
|
|
21
|
+
[0.0556434, -0.2040259, 1.0572252],
|
|
22
|
+
];
|
|
23
|
+
const LINEAR_P3_TO_XYZ_D65 = [
|
|
24
|
+
[0.4865709486482162, 0.26566769316909306, 0.19821728523436247],
|
|
25
|
+
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
26
|
+
[0.0, 0.04511338185890264, 1.043944368900976],
|
|
27
|
+
];
|
|
28
|
+
// Bradford chromatic adaptation. CSS lab()/lch() are specified at D50 while
|
|
29
|
+
// sRGB lives at D65, so parsing adapts D50 → D65.
|
|
30
|
+
const XYZ_D50_TO_D65 = [
|
|
31
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
32
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
33
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753],
|
|
34
|
+
];
|
|
35
|
+
const D50_WHITE = { x: 0.96422, y: 1.0, z: 0.82521 };
|
|
36
|
+
function applyMatrix(m, v) {
|
|
37
|
+
return {
|
|
38
|
+
x: m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z,
|
|
39
|
+
y: m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z,
|
|
40
|
+
z: m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function srgbToLinear(c) {
|
|
44
|
+
const abs = Math.abs(c);
|
|
45
|
+
const sign = c < 0 ? -1 : 1;
|
|
46
|
+
return abs <= 0.04045 ? c / 12.92 : sign * Math.pow((abs + 0.055) / 1.055, 2.4);
|
|
47
|
+
}
|
|
48
|
+
function linearToSrgb(c) {
|
|
49
|
+
const abs = Math.abs(c);
|
|
50
|
+
const sign = c < 0 ? -1 : 1;
|
|
51
|
+
return abs <= 0.0031308 ? c * 12.92 : sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
|
|
52
|
+
}
|
|
53
|
+
function linearSrgbToOklab(r, g, b) {
|
|
54
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
55
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
56
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
57
|
+
return {
|
|
58
|
+
l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
59
|
+
a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
60
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
64
|
+
const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
|
|
65
|
+
const m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
|
|
66
|
+
const s = Math.pow(L - 0.0894841775 * a - 1.2914855480 * b, 3);
|
|
67
|
+
return {
|
|
68
|
+
r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
69
|
+
g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
70
|
+
b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Gamut mapping (CSS Color 4 §13.2: chroma reduction in OKLCH with local
|
|
75
|
+
// clipping when the clipped candidate is within a just-noticeable difference)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
const GAMUT_EPS = 0.000075;
|
|
78
|
+
const JND = 0.02;
|
|
79
|
+
function inGamut(rgb) {
|
|
80
|
+
return (rgb.r >= -GAMUT_EPS && rgb.r <= 1 + GAMUT_EPS &&
|
|
81
|
+
rgb.g >= -GAMUT_EPS && rgb.g <= 1 + GAMUT_EPS &&
|
|
82
|
+
rgb.b >= -GAMUT_EPS && rgb.b <= 1 + GAMUT_EPS);
|
|
83
|
+
}
|
|
84
|
+
function clamp01(v) {
|
|
85
|
+
return Math.min(1, Math.max(0, v));
|
|
86
|
+
}
|
|
87
|
+
function deltaEok(c1, c2) {
|
|
88
|
+
return Math.sqrt((c1.l - c2.l) ** 2 + (c1.a - c2.a) ** 2 + (c1.b - c2.b) ** 2);
|
|
89
|
+
}
|
|
90
|
+
/** Map a (possibly out-of-gamut) linear sRGB triple into gamut, return 8-bit channels. */
|
|
91
|
+
function gamutMapLinear(lin) {
|
|
92
|
+
const encode = (rgb) => ({
|
|
93
|
+
r: Math.round(clamp01(linearToSrgb(rgb.r)) * 255),
|
|
94
|
+
g: Math.round(clamp01(linearToSrgb(rgb.g)) * 255),
|
|
95
|
+
b: Math.round(clamp01(linearToSrgb(rgb.b)) * 255),
|
|
96
|
+
});
|
|
97
|
+
if (inGamut(lin))
|
|
98
|
+
return encode(lin);
|
|
99
|
+
const ok = linearSrgbToOklab(lin.r, lin.g, lin.b);
|
|
100
|
+
if (ok.l >= 1)
|
|
101
|
+
return { r: 255, g: 255, b: 255 };
|
|
102
|
+
if (ok.l <= 0)
|
|
103
|
+
return { r: 0, g: 0, b: 0 };
|
|
104
|
+
const chroma = Math.sqrt(ok.a * ok.a + ok.b * ok.b);
|
|
105
|
+
const hueA = chroma === 0 ? 0 : ok.a / chroma;
|
|
106
|
+
const hueB = chroma === 0 ? 0 : ok.b / chroma;
|
|
107
|
+
const EPSILON = 0.0001;
|
|
108
|
+
let min = 0;
|
|
109
|
+
let max = chroma;
|
|
110
|
+
let minInGamut = true;
|
|
111
|
+
let current = lin;
|
|
112
|
+
while (max - min > EPSILON) {
|
|
113
|
+
const mid = (min + max) / 2;
|
|
114
|
+
current = oklabToLinearSrgb(ok.l, hueA * mid, hueB * mid);
|
|
115
|
+
if (minInGamut && inGamut(current)) {
|
|
116
|
+
min = mid;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const clipped = { r: clamp01(current.r), g: clamp01(current.g), b: clamp01(current.b) };
|
|
120
|
+
const clippedOk = linearSrgbToOklab(clipped.r, clipped.g, clipped.b);
|
|
121
|
+
const dE = deltaEok(clippedOk, { l: ok.l, a: hueA * mid, b: hueB * mid });
|
|
122
|
+
if (dE < JND) {
|
|
123
|
+
if (JND - dE < EPSILON)
|
|
124
|
+
return encode(clipped);
|
|
125
|
+
minInGamut = false;
|
|
126
|
+
min = mid;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
max = mid;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return encode({ r: clamp01(current.r), g: clamp01(current.g), b: clamp01(current.b) });
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Component tokenization
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
const NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
139
|
+
/** Parse a number or percentage token; percentRef is what 100% maps to. 'none' is 0. */
|
|
140
|
+
function parseNumeric(token, percentRef) {
|
|
141
|
+
if (token === 'none')
|
|
142
|
+
return 0;
|
|
143
|
+
if (token.endsWith('%')) {
|
|
144
|
+
const v = token.slice(0, -1);
|
|
145
|
+
if (!NUMBER_RE.test(v))
|
|
146
|
+
return null;
|
|
147
|
+
return (parseFloat(v) / 100) * percentRef;
|
|
148
|
+
}
|
|
149
|
+
if (!NUMBER_RE.test(token))
|
|
150
|
+
return null;
|
|
151
|
+
return parseFloat(token);
|
|
152
|
+
}
|
|
153
|
+
/** Parse a hue token with optional angle unit, returned in degrees. 'none' is 0. */
|
|
154
|
+
function parseHue(token) {
|
|
155
|
+
if (token === 'none')
|
|
156
|
+
return 0;
|
|
157
|
+
const m = token.match(/^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(deg|rad|grad|turn)?$/i);
|
|
158
|
+
if (!m)
|
|
159
|
+
return null;
|
|
160
|
+
const v = parseFloat(m[1]);
|
|
161
|
+
switch ((m[2] || 'deg').toLowerCase()) {
|
|
162
|
+
case 'rad': return (v * 180) / Math.PI;
|
|
163
|
+
case 'grad': return v * 0.9;
|
|
164
|
+
case 'turn': return v * 360;
|
|
165
|
+
default: return v;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function parseAlpha(token) {
|
|
169
|
+
if (token === undefined)
|
|
170
|
+
return 1;
|
|
171
|
+
const v = parseNumeric(token, 1);
|
|
172
|
+
if (v === null)
|
|
173
|
+
return null;
|
|
174
|
+
return clamp01(v);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Split a color function body into channel tokens + optional alpha, accepting
|
|
178
|
+
* both the legacy comma grammar (alpha as 4th comma argument) and the modern
|
|
179
|
+
* space grammar (alpha after a slash).
|
|
180
|
+
*/
|
|
181
|
+
function splitArgs(body) {
|
|
182
|
+
const trimmed = body.trim();
|
|
183
|
+
if (trimmed.includes(',')) {
|
|
184
|
+
const parts = trimmed.split(',').map((p) => p.trim());
|
|
185
|
+
if (parts.some((p) => p === ''))
|
|
186
|
+
return null;
|
|
187
|
+
if (parts.length === 4)
|
|
188
|
+
return { channels: parts.slice(0, 3), alpha: parts[3] };
|
|
189
|
+
if (parts.length === 3)
|
|
190
|
+
return { channels: parts, alpha: undefined };
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const slash = trimmed.split('/');
|
|
194
|
+
if (slash.length > 2)
|
|
195
|
+
return null;
|
|
196
|
+
const channels = slash[0].trim().split(/\s+/);
|
|
197
|
+
const alpha = slash.length === 2 ? slash[1].trim() : undefined;
|
|
198
|
+
if (alpha === '')
|
|
199
|
+
return null;
|
|
200
|
+
return { channels, alpha };
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Per-notation parsers
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
function fromHex(input) {
|
|
206
|
+
const m = input.match(/^#([0-9a-f]{3,8})$/i);
|
|
207
|
+
if (!m)
|
|
208
|
+
return null;
|
|
209
|
+
const h = m[1];
|
|
210
|
+
const dup = (c) => parseInt(c + c, 16);
|
|
211
|
+
if (h.length === 3)
|
|
212
|
+
return { r: dup(h[0]), g: dup(h[1]), b: dup(h[2]), a: 1 };
|
|
213
|
+
if (h.length === 4)
|
|
214
|
+
return { r: dup(h[0]), g: dup(h[1]), b: dup(h[2]), a: dup(h[3]) / 255 };
|
|
215
|
+
const pair = (i) => parseInt(h.slice(i, i + 2), 16);
|
|
216
|
+
if (h.length === 6)
|
|
217
|
+
return { r: pair(0), g: pair(2), b: pair(4), a: 1 };
|
|
218
|
+
if (h.length === 8)
|
|
219
|
+
return { r: pair(0), g: pair(2), b: pair(4), a: pair(6) / 255 };
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function fromRgb(args) {
|
|
223
|
+
const channel = (t) => parseNumeric(t, 255);
|
|
224
|
+
const r = channel(args.channels[0]);
|
|
225
|
+
const g = channel(args.channels[1]);
|
|
226
|
+
const b = channel(args.channels[2]);
|
|
227
|
+
const a = parseAlpha(args.alpha);
|
|
228
|
+
if (r === null || g === null || b === null || a === null)
|
|
229
|
+
return null;
|
|
230
|
+
const clamp255 = (v) => Math.min(255, Math.max(0, Math.round(v)));
|
|
231
|
+
return { r: clamp255(r), g: clamp255(g), b: clamp255(b), a };
|
|
232
|
+
}
|
|
233
|
+
function hslChannels(h, s, l) {
|
|
234
|
+
const sat = clamp01(s);
|
|
235
|
+
const light = clamp01(l);
|
|
236
|
+
const hue = ((h % 360) + 360) % 360;
|
|
237
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat;
|
|
238
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
239
|
+
const m = light - c / 2;
|
|
240
|
+
let rgb;
|
|
241
|
+
if (hue < 60)
|
|
242
|
+
rgb = [c, x, 0];
|
|
243
|
+
else if (hue < 120)
|
|
244
|
+
rgb = [x, c, 0];
|
|
245
|
+
else if (hue < 180)
|
|
246
|
+
rgb = [0, c, x];
|
|
247
|
+
else if (hue < 240)
|
|
248
|
+
rgb = [0, x, c];
|
|
249
|
+
else if (hue < 300)
|
|
250
|
+
rgb = [x, 0, c];
|
|
251
|
+
else
|
|
252
|
+
rgb = [c, 0, x];
|
|
253
|
+
return {
|
|
254
|
+
r: Math.round((rgb[0] + m) * 255),
|
|
255
|
+
g: Math.round((rgb[1] + m) * 255),
|
|
256
|
+
b: Math.round((rgb[2] + m) * 255),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function fromHsl(args) {
|
|
260
|
+
const h = parseHue(args.channels[0]);
|
|
261
|
+
const s = parseNumeric(args.channels[1], 1);
|
|
262
|
+
const l = parseNumeric(args.channels[2], 1);
|
|
263
|
+
const a = parseAlpha(args.alpha);
|
|
264
|
+
if (h === null || s === null || l === null || a === null)
|
|
265
|
+
return null;
|
|
266
|
+
// Bare numbers for s/l are percentages in disguise (hsl(120 100 50) is valid
|
|
267
|
+
// modern syntax): a token without '%' still means 0-100.
|
|
268
|
+
const norm = (raw, v) => (raw.endsWith('%') || raw === 'none' ? v : v / 100);
|
|
269
|
+
return { ...hslChannels(h, norm(args.channels[1], s), norm(args.channels[2], l)), a };
|
|
270
|
+
}
|
|
271
|
+
function fromHwb(args) {
|
|
272
|
+
const h = parseHue(args.channels[0]);
|
|
273
|
+
const wRaw = parseNumeric(args.channels[1], 1);
|
|
274
|
+
const bRaw = parseNumeric(args.channels[2], 1);
|
|
275
|
+
const a = parseAlpha(args.alpha);
|
|
276
|
+
if (h === null || wRaw === null || bRaw === null || a === null)
|
|
277
|
+
return null;
|
|
278
|
+
const norm = (raw, v) => (raw.endsWith('%') || raw === 'none' ? v : v / 100);
|
|
279
|
+
const w = clamp01(norm(args.channels[1], wRaw));
|
|
280
|
+
const blk = clamp01(norm(args.channels[2], bRaw));
|
|
281
|
+
if (w + blk >= 1) {
|
|
282
|
+
const gray = Math.round((w / (w + blk)) * 255);
|
|
283
|
+
return { r: gray, g: gray, b: gray, a };
|
|
284
|
+
}
|
|
285
|
+
const base = hslChannels(h, 1, 0.5);
|
|
286
|
+
const mix = (c) => Math.round(((c / 255) * (1 - w - blk) + w) * 255);
|
|
287
|
+
return { r: mix(base.r), g: mix(base.g), b: mix(base.b), a };
|
|
288
|
+
}
|
|
289
|
+
function fromOklab(args) {
|
|
290
|
+
const L = parseNumeric(args.channels[0], 1);
|
|
291
|
+
const aCh = parseNumeric(args.channels[1], 0.4);
|
|
292
|
+
const bCh = parseNumeric(args.channels[2], 0.4);
|
|
293
|
+
const alpha = parseAlpha(args.alpha);
|
|
294
|
+
if (L === null || aCh === null || bCh === null || alpha === null)
|
|
295
|
+
return null;
|
|
296
|
+
const lin = oklabToLinearSrgb(clamp01(L), aCh, bCh);
|
|
297
|
+
return { ...gamutMapLinear(lin), a: alpha };
|
|
298
|
+
}
|
|
299
|
+
function fromOklch(args) {
|
|
300
|
+
const L = parseNumeric(args.channels[0], 1);
|
|
301
|
+
const C = parseNumeric(args.channels[1], 0.4);
|
|
302
|
+
const H = parseHue(args.channels[2]);
|
|
303
|
+
const alpha = parseAlpha(args.alpha);
|
|
304
|
+
if (L === null || C === null || H === null || alpha === null)
|
|
305
|
+
return null;
|
|
306
|
+
const hRad = (H * Math.PI) / 180;
|
|
307
|
+
const lin = oklabToLinearSrgb(clamp01(L), Math.max(0, C) * Math.cos(hRad), Math.max(0, C) * Math.sin(hRad));
|
|
308
|
+
return { ...gamutMapLinear(lin), a: alpha };
|
|
309
|
+
}
|
|
310
|
+
function labD50ToLinearSrgb(l, a, b) {
|
|
311
|
+
const fy = (l + 16) / 116;
|
|
312
|
+
const fx = fy + a / 500;
|
|
313
|
+
const fz = fy - b / 200;
|
|
314
|
+
const finv = (t) => {
|
|
315
|
+
const t3 = t * t * t;
|
|
316
|
+
return t3 > 0.008856 ? t3 : (116 * t - 16) / 903.3;
|
|
317
|
+
};
|
|
318
|
+
const xyzD50 = {
|
|
319
|
+
x: finv(fx) * D50_WHITE.x,
|
|
320
|
+
y: finv(fy) * D50_WHITE.y,
|
|
321
|
+
z: finv(fz) * D50_WHITE.z,
|
|
322
|
+
};
|
|
323
|
+
const xyzD65 = applyMatrix(XYZ_D50_TO_D65, xyzD50);
|
|
324
|
+
const lin = applyMatrix(XYZ_D65_TO_LINEAR_SRGB, xyzD65);
|
|
325
|
+
return { r: lin.x, g: lin.y, b: lin.z };
|
|
326
|
+
}
|
|
327
|
+
function fromLab(args) {
|
|
328
|
+
const L = parseNumeric(args.channels[0], 100);
|
|
329
|
+
const aCh = parseNumeric(args.channels[1], 125);
|
|
330
|
+
const bCh = parseNumeric(args.channels[2], 125);
|
|
331
|
+
const alpha = parseAlpha(args.alpha);
|
|
332
|
+
if (L === null || aCh === null || bCh === null || alpha === null)
|
|
333
|
+
return null;
|
|
334
|
+
const lin = labD50ToLinearSrgb(Math.min(100, Math.max(0, L)), aCh, bCh);
|
|
335
|
+
return { ...gamutMapLinear(lin), a: alpha };
|
|
336
|
+
}
|
|
337
|
+
function fromLch(args) {
|
|
338
|
+
const L = parseNumeric(args.channels[0], 100);
|
|
339
|
+
const C = parseNumeric(args.channels[1], 150);
|
|
340
|
+
const H = parseHue(args.channels[2]);
|
|
341
|
+
const alpha = parseAlpha(args.alpha);
|
|
342
|
+
if (L === null || C === null || H === null || alpha === null)
|
|
343
|
+
return null;
|
|
344
|
+
const hRad = (H * Math.PI) / 180;
|
|
345
|
+
const lin = labD50ToLinearSrgb(Math.min(100, Math.max(0, L)), Math.max(0, C) * Math.cos(hRad), Math.max(0, C) * Math.sin(hRad));
|
|
346
|
+
return { ...gamutMapLinear(lin), a: alpha };
|
|
347
|
+
}
|
|
348
|
+
function fromColorFunction(args) {
|
|
349
|
+
const [space, ...rest] = args.channels;
|
|
350
|
+
if (!space || rest.length !== 3)
|
|
351
|
+
return null;
|
|
352
|
+
const c1 = parseNumeric(rest[0], 1);
|
|
353
|
+
const c2 = parseNumeric(rest[1], 1);
|
|
354
|
+
const c3 = parseNumeric(rest[2], 1);
|
|
355
|
+
const alpha = parseAlpha(args.alpha);
|
|
356
|
+
if (c1 === null || c2 === null || c3 === null || alpha === null)
|
|
357
|
+
return null;
|
|
358
|
+
let lin;
|
|
359
|
+
switch (space) {
|
|
360
|
+
case 'srgb':
|
|
361
|
+
lin = { r: srgbToLinear(c1), g: srgbToLinear(c2), b: srgbToLinear(c3) };
|
|
362
|
+
break;
|
|
363
|
+
case 'srgb-linear':
|
|
364
|
+
lin = { r: c1, g: c2, b: c3 };
|
|
365
|
+
break;
|
|
366
|
+
case 'display-p3': {
|
|
367
|
+
const p3 = { x: srgbToLinear(c1), y: srgbToLinear(c2), z: srgbToLinear(c3) };
|
|
368
|
+
const xyz = applyMatrix(LINEAR_P3_TO_XYZ_D65, p3);
|
|
369
|
+
const s = applyMatrix(XYZ_D65_TO_LINEAR_SRGB, xyz);
|
|
370
|
+
lin = { r: s.x, g: s.y, b: s.z };
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'xyz':
|
|
374
|
+
case 'xyz-d65': {
|
|
375
|
+
const s = applyMatrix(XYZ_D65_TO_LINEAR_SRGB, { x: c1, y: c2, z: c3 });
|
|
376
|
+
lin = { r: s.x, g: s.y, b: s.z };
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case 'xyz-d50': {
|
|
380
|
+
const d65 = applyMatrix(XYZ_D50_TO_D65, { x: c1, y: c2, z: c3 });
|
|
381
|
+
const s = applyMatrix(XYZ_D65_TO_LINEAR_SRGB, d65);
|
|
382
|
+
lin = { r: s.x, g: s.y, b: s.z };
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
default:
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return { ...gamutMapLinear(lin), a: alpha };
|
|
389
|
+
}
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Named colors (CSS Color 4 §6.1, full table + transparent)
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
const NAMED_COLORS = {
|
|
394
|
+
aliceblue: 'f0f8ff', antiquewhite: 'faebd7', aqua: '00ffff', aquamarine: '7fffd4',
|
|
395
|
+
azure: 'f0ffff', beige: 'f5f5dc', bisque: 'ffe4c4', black: '000000',
|
|
396
|
+
blanchedalmond: 'ffebcd', blue: '0000ff', blueviolet: '8a2be2', brown: 'a52a2a',
|
|
397
|
+
burlywood: 'deb887', cadetblue: '5f9ea0', chartreuse: '7fff00', chocolate: 'd2691e',
|
|
398
|
+
coral: 'ff7f50', cornflowerblue: '6495ed', cornsilk: 'fff8dc', crimson: 'dc143c',
|
|
399
|
+
cyan: '00ffff', darkblue: '00008b', darkcyan: '008b8b', darkgoldenrod: 'b8860b',
|
|
400
|
+
darkgray: 'a9a9a9', darkgreen: '006400', darkgrey: 'a9a9a9', darkkhaki: 'bdb76b',
|
|
401
|
+
darkmagenta: '8b008b', darkolivegreen: '556b2f', darkorange: 'ff8c00', darkorchid: '9932cc',
|
|
402
|
+
darkred: '8b0000', darksalmon: 'e9967a', darkseagreen: '8fbc8f', darkslateblue: '483d8b',
|
|
403
|
+
darkslategray: '2f4f4f', darkslategrey: '2f4f4f', darkturquoise: '00ced1', darkviolet: '9400d3',
|
|
404
|
+
deeppink: 'ff1493', deepskyblue: '00bfff', dimgray: '696969', dimgrey: '696969',
|
|
405
|
+
dodgerblue: '1e90ff', firebrick: 'b22222', floralwhite: 'fffaf0', forestgreen: '228b22',
|
|
406
|
+
fuchsia: 'ff00ff', gainsboro: 'dcdcdc', ghostwhite: 'f8f8ff', gold: 'ffd700',
|
|
407
|
+
goldenrod: 'daa520', gray: '808080', green: '008000', greenyellow: 'adff2f',
|
|
408
|
+
grey: '808080', honeydew: 'f0fff0', hotpink: 'ff69b4', indianred: 'cd5c5c',
|
|
409
|
+
indigo: '4b0082', ivory: 'fffff0', khaki: 'f0e68c', lavender: 'e6e6fa',
|
|
410
|
+
lavenderblush: 'fff0f5', lawngreen: '7cfc00', lemonchiffon: 'fffacd', lightblue: 'add8e6',
|
|
411
|
+
lightcoral: 'f08080', lightcyan: 'e0ffff', lightgoldenrodyellow: 'fafad2', lightgray: 'd3d3d3',
|
|
412
|
+
lightgreen: '90ee90', lightgrey: 'd3d3d3', lightpink: 'ffb6c1', lightsalmon: 'ffa07a',
|
|
413
|
+
lightseagreen: '20b2aa', lightskyblue: '87cefa', lightslategray: '778899', lightslategrey: '778899',
|
|
414
|
+
lightsteelblue: 'b0c4de', lightyellow: 'ffffe0', lime: '00ff00', limegreen: '32cd32',
|
|
415
|
+
linen: 'faf0e6', magenta: 'ff00ff', maroon: '800000', mediumaquamarine: '66cdaa',
|
|
416
|
+
mediumblue: '0000cd', mediumorchid: 'ba55d3', mediumpurple: '9370db', mediumseagreen: '3cb371',
|
|
417
|
+
mediumslateblue: '7b68ee', mediumspringgreen: '00fa9a', mediumturquoise: '48d1cc', mediumvioletred: 'c71585',
|
|
418
|
+
midnightblue: '191970', mintcream: 'f5fffa', mistyrose: 'ffe4e1', moccasin: 'ffe4b5',
|
|
419
|
+
navajowhite: 'ffdead', navy: '000080', oldlace: 'fdf5e6', olive: '808000',
|
|
420
|
+
olivedrab: '6b8e23', orange: 'ffa500', orangered: 'ff4500', orchid: 'da70d6',
|
|
421
|
+
palegoldenrod: 'eee8aa', palegreen: '98fb98', paleturquoise: 'afeeee', palevioletred: 'db7093',
|
|
422
|
+
papayawhip: 'ffefd5', peachpuff: 'ffdab9', peru: 'cd853f', pink: 'ffc0cb',
|
|
423
|
+
plum: 'dda0dd', powderblue: 'b0e0e6', purple: '800080', rebeccapurple: '663399',
|
|
424
|
+
red: 'ff0000', rosybrown: 'bc8f8f', royalblue: '4169e1', saddlebrown: '8b4513',
|
|
425
|
+
salmon: 'fa8072', sandybrown: 'f4a460', seagreen: '2e8b57', seashell: 'fff5ee',
|
|
426
|
+
sienna: 'a0522d', silver: 'c0c0c0', skyblue: '87ceeb', slateblue: '6a5acd',
|
|
427
|
+
slategray: '708090', slategrey: '708090', snow: 'fffafa', springgreen: '00ff7f',
|
|
428
|
+
steelblue: '4682b4', tan: 'd2b48c', teal: '008080', thistle: 'd8bfd8',
|
|
429
|
+
tomato: 'ff6347', turquoise: '40e0d0', violet: 'ee82ee', wheat: 'f5deb3',
|
|
430
|
+
white: 'ffffff', whitesmoke: 'f5f5f5', yellow: 'ffff00', yellowgreen: '9acd32',
|
|
431
|
+
};
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// Public API
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
/**
|
|
436
|
+
* Parse any CSS color string to sRGB 8-bit channels + float alpha.
|
|
437
|
+
* Returns null for anything that is not an absolute color (currentcolor,
|
|
438
|
+
* inherit, var() references, malformed input).
|
|
439
|
+
*/
|
|
440
|
+
export function parseCssColor(input) {
|
|
441
|
+
if (typeof input !== 'string')
|
|
442
|
+
return null;
|
|
443
|
+
const str = input.trim();
|
|
444
|
+
if (str === '')
|
|
445
|
+
return null;
|
|
446
|
+
if (str.startsWith('#'))
|
|
447
|
+
return fromHex(str);
|
|
448
|
+
const lower = str.toLowerCase();
|
|
449
|
+
if (lower === 'transparent')
|
|
450
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
451
|
+
const named = NAMED_COLORS[lower];
|
|
452
|
+
if (named)
|
|
453
|
+
return fromHex(`#${named}`);
|
|
454
|
+
const fn = lower.match(/^([a-z-]+)\(\s*([^)]*)\s*\)$/);
|
|
455
|
+
if (!fn)
|
|
456
|
+
return null;
|
|
457
|
+
const args = splitArgs(fn[2]);
|
|
458
|
+
if (!args || args.channels.length < 3)
|
|
459
|
+
return null;
|
|
460
|
+
switch (fn[1]) {
|
|
461
|
+
case 'rgb':
|
|
462
|
+
case 'rgba':
|
|
463
|
+
return args.channels.length === 3 ? fromRgb(args) : null;
|
|
464
|
+
case 'hsl':
|
|
465
|
+
case 'hsla':
|
|
466
|
+
return args.channels.length === 3 ? fromHsl(args) : null;
|
|
467
|
+
case 'hwb':
|
|
468
|
+
return args.channels.length === 3 ? fromHwb(args) : null;
|
|
469
|
+
case 'oklab':
|
|
470
|
+
return args.channels.length === 3 ? fromOklab(args) : null;
|
|
471
|
+
case 'oklch':
|
|
472
|
+
return args.channels.length === 3 ? fromOklch(args) : null;
|
|
473
|
+
case 'lab':
|
|
474
|
+
return args.channels.length === 3 ? fromLab(args) : null;
|
|
475
|
+
case 'lch':
|
|
476
|
+
return args.channels.length === 3 ? fromLch(args) : null;
|
|
477
|
+
case 'color':
|
|
478
|
+
return fromColorFunction(args);
|
|
479
|
+
default:
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** #rrggbb for opaque colors, #rrggbbaa when alpha < 1. */
|
|
484
|
+
export function serializeHex(c) {
|
|
485
|
+
const pair = (v) => Math.round(v).toString(16).padStart(2, '0');
|
|
486
|
+
const base = `#${pair(c.r)}${pair(c.g)}${pair(c.b)}`;
|
|
487
|
+
if (c.a >= 1)
|
|
488
|
+
return base;
|
|
489
|
+
return `${base}${pair(clamp01(c.a) * 255)}`;
|
|
490
|
+
}
|
|
491
|
+
/** Legacy rgb()/rgba() serialisation, the canonical interchange form in the extractor. */
|
|
492
|
+
export function serializeRgb(c) {
|
|
493
|
+
if (c.a >= 1)
|
|
494
|
+
return `rgb(${c.r}, ${c.g}, ${c.b})`;
|
|
495
|
+
const alpha = Math.round(clamp01(c.a) * 10000) / 10000;
|
|
496
|
+
return `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Convenience for extractor injection paths: any CSS color string to its
|
|
500
|
+
* opaque 6-hex identity plus a legacy serialisation that preserves alpha.
|
|
501
|
+
*/
|
|
502
|
+
export function normalizeCssColor(input) {
|
|
503
|
+
const c = parseCssColor(input);
|
|
504
|
+
if (!c)
|
|
505
|
+
return null;
|
|
506
|
+
return { hex: serializeHex({ ...c, a: 1 }), legacy: serializeRgb(c), alpha: c.a };
|
|
507
|
+
}
|
|
508
|
+
//# sourceMappingURL=color-parse.js.map
|