@luntta/swatch 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/CONTRIBUTING.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +189 -0
- package/README.md +463 -0
- package/package.json +57 -0
- package/scripts/pack-check.mjs +18 -0
- package/src/bootstrap.js +159 -0
- package/src/core/registry.js +81 -0
- package/src/core/state.js +36 -0
- package/src/core/swatch-class.js +524 -0
- package/src/data/cvd-matrices.js +179 -0
- package/src/data/named-colors.js +157 -0
- package/src/format/css.js +256 -0
- package/src/operations/accessibility.js +103 -0
- package/src/operations/apca.js +80 -0
- package/src/operations/blend.js +72 -0
- package/src/operations/channels.js +123 -0
- package/src/operations/cvd.js +119 -0
- package/src/operations/deltaE.js +207 -0
- package/src/operations/gamut.js +206 -0
- package/src/operations/image.js +192 -0
- package/src/operations/manipulation.js +100 -0
- package/src/operations/mix.js +129 -0
- package/src/operations/naming.js +158 -0
- package/src/operations/palette.js +133 -0
- package/src/operations/random.js +75 -0
- package/src/operations/temperature.js +126 -0
- package/src/operations/tint-shade.js +42 -0
- package/src/palettes/colorbrewer.js +232 -0
- package/src/palettes/index.js +58 -0
- package/src/palettes/viridis.js +59 -0
- package/src/parse/css.js +241 -0
- package/src/parse/hex.js +38 -0
- package/src/parse/index.js +43 -0
- package/src/parse/legacy.js +88 -0
- package/src/parse/named.js +11 -0
- package/src/parse/objects.js +125 -0
- package/src/scale/index.js +382 -0
- package/src/scale/interpolators.js +83 -0
- package/src/spaces/a98.js +55 -0
- package/src/spaces/cmyk.js +75 -0
- package/src/spaces/display-p3.js +50 -0
- package/src/spaces/hsl.js +93 -0
- package/src/spaces/hsluv.js +211 -0
- package/src/spaces/hsv.js +78 -0
- package/src/spaces/hwb.js +48 -0
- package/src/spaces/lab.js +70 -0
- package/src/spaces/lch.js +65 -0
- package/src/spaces/oklab.js +79 -0
- package/src/spaces/oklch.js +53 -0
- package/src/spaces/prophoto.js +72 -0
- package/src/spaces/rec2020.js +65 -0
- package/src/spaces/srgb.js +85 -0
- package/src/spaces/xyz.js +71 -0
- package/src/swatch.js +57 -0
- package/src/util/math.js +53 -0
- package/src/util/matrix.js +92 -0
- package/src/util/suggest.js +66 -0
- package/types/swatch.d.ts +664 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// ColorBrewer 2.0 — Cynthia A. Brewer, Pennsylvania State University.
|
|
2
|
+
//
|
|
3
|
+
// Each palette is stored as a fixed-length array of hex strings
|
|
4
|
+
// matching the largest size published in the Brewer tables. The
|
|
5
|
+
// scale builder interpolates between adjacent stops; for discrete
|
|
6
|
+
// class use, call .classes(n) on the resulting scale.
|
|
7
|
+
//
|
|
8
|
+
// Data is used under the Apache-style ColorBrewer license:
|
|
9
|
+
//
|
|
10
|
+
// Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and
|
|
11
|
+
// The Pennsylvania State University.
|
|
12
|
+
//
|
|
13
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
14
|
+
// you may not use this file except in compliance with the License.
|
|
15
|
+
// You may obtain a copy of the License at
|
|
16
|
+
//
|
|
17
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
//
|
|
19
|
+
// Unless required by applicable law or agreed to in writing,
|
|
20
|
+
// software distributed under the License is distributed on an
|
|
21
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
|
22
|
+
// either express or implied. See the License for the specific
|
|
23
|
+
// language governing permissions and limitations under the License.
|
|
24
|
+
|
|
25
|
+
// ─── Sequential ─────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export const Blues = [
|
|
28
|
+
"#f7fbff",
|
|
29
|
+
"#deebf7",
|
|
30
|
+
"#c6dbef",
|
|
31
|
+
"#9ecae1",
|
|
32
|
+
"#6baed6",
|
|
33
|
+
"#4292c6",
|
|
34
|
+
"#2171b5",
|
|
35
|
+
"#08519c",
|
|
36
|
+
"#08306b"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const Greens = [
|
|
40
|
+
"#f7fcf5",
|
|
41
|
+
"#e5f5e0",
|
|
42
|
+
"#c7e9c0",
|
|
43
|
+
"#a1d99b",
|
|
44
|
+
"#74c476",
|
|
45
|
+
"#41ab5d",
|
|
46
|
+
"#238b45",
|
|
47
|
+
"#006d2c",
|
|
48
|
+
"#00441b"
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const Reds = [
|
|
52
|
+
"#fff5f0",
|
|
53
|
+
"#fee0d2",
|
|
54
|
+
"#fcbba1",
|
|
55
|
+
"#fc9272",
|
|
56
|
+
"#fb6a4a",
|
|
57
|
+
"#ef3b2c",
|
|
58
|
+
"#cb181d",
|
|
59
|
+
"#a50f15",
|
|
60
|
+
"#67000d"
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export const Oranges = [
|
|
64
|
+
"#fff5eb",
|
|
65
|
+
"#fee6ce",
|
|
66
|
+
"#fdd0a2",
|
|
67
|
+
"#fdae6b",
|
|
68
|
+
"#fd8d3c",
|
|
69
|
+
"#f16913",
|
|
70
|
+
"#d94801",
|
|
71
|
+
"#a63603",
|
|
72
|
+
"#7f2704"
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const Purples = [
|
|
76
|
+
"#fcfbfd",
|
|
77
|
+
"#efedf5",
|
|
78
|
+
"#dadaeb",
|
|
79
|
+
"#bcbddc",
|
|
80
|
+
"#9e9ac8",
|
|
81
|
+
"#807dba",
|
|
82
|
+
"#6a51a3",
|
|
83
|
+
"#54278f",
|
|
84
|
+
"#3f007d"
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export const Greys = [
|
|
88
|
+
"#ffffff",
|
|
89
|
+
"#f0f0f0",
|
|
90
|
+
"#d9d9d9",
|
|
91
|
+
"#bdbdbd",
|
|
92
|
+
"#969696",
|
|
93
|
+
"#737373",
|
|
94
|
+
"#525252",
|
|
95
|
+
"#252525",
|
|
96
|
+
"#000000"
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// ─── Diverging ──────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export const RdBu = [
|
|
102
|
+
"#67001f",
|
|
103
|
+
"#b2182b",
|
|
104
|
+
"#d6604d",
|
|
105
|
+
"#f4a582",
|
|
106
|
+
"#fddbc7",
|
|
107
|
+
"#f7f7f7",
|
|
108
|
+
"#d1e5f0",
|
|
109
|
+
"#92c5de",
|
|
110
|
+
"#4393c3",
|
|
111
|
+
"#2166ac",
|
|
112
|
+
"#053061"
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const RdYlBu = [
|
|
116
|
+
"#a50026",
|
|
117
|
+
"#d73027",
|
|
118
|
+
"#f46d43",
|
|
119
|
+
"#fdae61",
|
|
120
|
+
"#fee090",
|
|
121
|
+
"#ffffbf",
|
|
122
|
+
"#e0f3f8",
|
|
123
|
+
"#abd9e9",
|
|
124
|
+
"#74add1",
|
|
125
|
+
"#4575b4",
|
|
126
|
+
"#313695"
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export const PiYG = [
|
|
130
|
+
"#8e0152",
|
|
131
|
+
"#c51b7d",
|
|
132
|
+
"#de77ae",
|
|
133
|
+
"#f1b6da",
|
|
134
|
+
"#fde0ef",
|
|
135
|
+
"#f7f7f7",
|
|
136
|
+
"#e6f5d0",
|
|
137
|
+
"#b8e186",
|
|
138
|
+
"#7fbc41",
|
|
139
|
+
"#4d9221",
|
|
140
|
+
"#276419"
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export const BrBG = [
|
|
144
|
+
"#543005",
|
|
145
|
+
"#8c510a",
|
|
146
|
+
"#bf812d",
|
|
147
|
+
"#dfc27d",
|
|
148
|
+
"#f6e8c3",
|
|
149
|
+
"#f5f5f5",
|
|
150
|
+
"#c7eae5",
|
|
151
|
+
"#80cdc1",
|
|
152
|
+
"#35978f",
|
|
153
|
+
"#01665e",
|
|
154
|
+
"#003c30"
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
export const Spectral = [
|
|
158
|
+
"#9e0142",
|
|
159
|
+
"#d53e4f",
|
|
160
|
+
"#f46d43",
|
|
161
|
+
"#fdae61",
|
|
162
|
+
"#fee08b",
|
|
163
|
+
"#ffffbf",
|
|
164
|
+
"#e6f598",
|
|
165
|
+
"#abdda4",
|
|
166
|
+
"#66c2a5",
|
|
167
|
+
"#3288bd",
|
|
168
|
+
"#5e4fa2"
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// ─── Qualitative ────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
export const Set1 = [
|
|
174
|
+
"#e41a1c",
|
|
175
|
+
"#377eb8",
|
|
176
|
+
"#4daf4a",
|
|
177
|
+
"#984ea3",
|
|
178
|
+
"#ff7f00",
|
|
179
|
+
"#ffff33",
|
|
180
|
+
"#a65628",
|
|
181
|
+
"#f781bf",
|
|
182
|
+
"#999999"
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
export const Set2 = [
|
|
186
|
+
"#66c2a5",
|
|
187
|
+
"#fc8d62",
|
|
188
|
+
"#8da0cb",
|
|
189
|
+
"#e78ac3",
|
|
190
|
+
"#a6d854",
|
|
191
|
+
"#ffd92f",
|
|
192
|
+
"#e5c494",
|
|
193
|
+
"#b3b3b3"
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
export const Set3 = [
|
|
197
|
+
"#8dd3c7",
|
|
198
|
+
"#ffffb3",
|
|
199
|
+
"#bebada",
|
|
200
|
+
"#fb8072",
|
|
201
|
+
"#80b1d3",
|
|
202
|
+
"#fdb462",
|
|
203
|
+
"#b3de69",
|
|
204
|
+
"#fccde5",
|
|
205
|
+
"#d9d9d9",
|
|
206
|
+
"#bc80bd",
|
|
207
|
+
"#ccebc5",
|
|
208
|
+
"#ffed6f"
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
export const Pastel1 = [
|
|
212
|
+
"#fbb4ae",
|
|
213
|
+
"#b3cde3",
|
|
214
|
+
"#ccebc5",
|
|
215
|
+
"#decbe4",
|
|
216
|
+
"#fed9a6",
|
|
217
|
+
"#ffffcc",
|
|
218
|
+
"#e5d8bd",
|
|
219
|
+
"#fddaec",
|
|
220
|
+
"#f2f2f2"
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
export const Dark2 = [
|
|
224
|
+
"#1b9e77",
|
|
225
|
+
"#d95f02",
|
|
226
|
+
"#7570b3",
|
|
227
|
+
"#e7298a",
|
|
228
|
+
"#66a61e",
|
|
229
|
+
"#e6ab02",
|
|
230
|
+
"#a6761d",
|
|
231
|
+
"#666666"
|
|
232
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Built-in palette registry.
|
|
2
|
+
//
|
|
3
|
+
// Registers the matplotlib perceptually-uniform maps
|
|
4
|
+
// (viridis/magma/plasma/inferno/cividis) and the ColorBrewer 2.0
|
|
5
|
+
// sequential/diverging/qualitative sets. Users can look up any of
|
|
6
|
+
// them via swatch.scale('<name>').
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
viridis,
|
|
10
|
+
magma,
|
|
11
|
+
plasma,
|
|
12
|
+
inferno,
|
|
13
|
+
cividis
|
|
14
|
+
} from "./viridis.js";
|
|
15
|
+
import * as brewer from "./colorbrewer.js";
|
|
16
|
+
|
|
17
|
+
const REGISTRY = new Map();
|
|
18
|
+
|
|
19
|
+
export function registerPalette(name, stops) {
|
|
20
|
+
REGISTRY.set(name, stops);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPalette(name) {
|
|
24
|
+
return REGISTRY.get(name) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function listPalettes() {
|
|
28
|
+
return Array.from(REGISTRY.keys());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Matplotlib perceptually-uniform.
|
|
32
|
+
registerPalette("viridis", viridis);
|
|
33
|
+
registerPalette("magma", magma);
|
|
34
|
+
registerPalette("plasma", plasma);
|
|
35
|
+
registerPalette("inferno", inferno);
|
|
36
|
+
registerPalette("cividis", cividis);
|
|
37
|
+
|
|
38
|
+
// ColorBrewer sequential.
|
|
39
|
+
registerPalette("Blues", brewer.Blues);
|
|
40
|
+
registerPalette("Greens", brewer.Greens);
|
|
41
|
+
registerPalette("Reds", brewer.Reds);
|
|
42
|
+
registerPalette("Oranges", brewer.Oranges);
|
|
43
|
+
registerPalette("Purples", brewer.Purples);
|
|
44
|
+
registerPalette("Greys", brewer.Greys);
|
|
45
|
+
|
|
46
|
+
// ColorBrewer diverging.
|
|
47
|
+
registerPalette("RdBu", brewer.RdBu);
|
|
48
|
+
registerPalette("RdYlBu", brewer.RdYlBu);
|
|
49
|
+
registerPalette("PiYG", brewer.PiYG);
|
|
50
|
+
registerPalette("BrBG", brewer.BrBG);
|
|
51
|
+
registerPalette("Spectral", brewer.Spectral);
|
|
52
|
+
|
|
53
|
+
// ColorBrewer qualitative.
|
|
54
|
+
registerPalette("Set1", brewer.Set1);
|
|
55
|
+
registerPalette("Set2", brewer.Set2);
|
|
56
|
+
registerPalette("Set3", brewer.Set3);
|
|
57
|
+
registerPalette("Pastel1", brewer.Pastel1);
|
|
58
|
+
registerPalette("Dark2", brewer.Dark2);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Matplotlib perceptually-uniform colormaps — viridis family.
|
|
2
|
+
//
|
|
3
|
+
// Stored as key-stop hex arrays (not matplotlib's full 256-entry
|
|
4
|
+
// look-up tables). The scale builder interpolates between these key
|
|
5
|
+
// points in oklab, which reproduces the smooth perceptual ramp the
|
|
6
|
+
// originals were designed around. Endpoints are exact; intermediate
|
|
7
|
+
// samples match the 5-stop summaries published in the matplotlib
|
|
8
|
+
// docs and the Smith & van der Walt paper on viridis.
|
|
9
|
+
//
|
|
10
|
+
// Original data © Stéfan van der Walt and Nathaniel Smith, released
|
|
11
|
+
// under CC0 (public domain). See https://bids.github.io/colormap/
|
|
12
|
+
|
|
13
|
+
export const viridis = [
|
|
14
|
+
"#440154",
|
|
15
|
+
"#3b528b",
|
|
16
|
+
"#21918c",
|
|
17
|
+
"#5ec962",
|
|
18
|
+
"#fde725"
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const magma = [
|
|
22
|
+
"#000004",
|
|
23
|
+
"#3b0f70",
|
|
24
|
+
"#8c2981",
|
|
25
|
+
"#de4968",
|
|
26
|
+
"#fe9f6d",
|
|
27
|
+
"#fcfdbf"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const plasma = [
|
|
31
|
+
"#0d0887",
|
|
32
|
+
"#6a00a8",
|
|
33
|
+
"#b12a90",
|
|
34
|
+
"#e16462",
|
|
35
|
+
"#fca636",
|
|
36
|
+
"#f0f921"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const inferno = [
|
|
40
|
+
"#000004",
|
|
41
|
+
"#420a68",
|
|
42
|
+
"#932667",
|
|
43
|
+
"#dd513a",
|
|
44
|
+
"#fca50a",
|
|
45
|
+
"#fcffa4"
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const cividis = [
|
|
49
|
+
"#00224e",
|
|
50
|
+
"#123570",
|
|
51
|
+
"#3b496c",
|
|
52
|
+
"#575c6d",
|
|
53
|
+
"#707173",
|
|
54
|
+
"#8a8678",
|
|
55
|
+
"#a59c74",
|
|
56
|
+
"#c3b369",
|
|
57
|
+
"#e1cc55",
|
|
58
|
+
"#fee838"
|
|
59
|
+
];
|
package/src/parse/css.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// CSS Color 4 string parsers.
|
|
2
|
+
//
|
|
3
|
+
// Handles:
|
|
4
|
+
//
|
|
5
|
+
// rgb(R G B [/ A]) — modern syntax only (legacy
|
|
6
|
+
// comma form is parse/legacy.js)
|
|
7
|
+
// hsl(H S L [/ A]) — ditto
|
|
8
|
+
// hwb(H W B [/ A]) — note: HWB space registers in
|
|
9
|
+
// Phase 8; until then the
|
|
10
|
+
// returned state is { space: 'hwb' }
|
|
11
|
+
// and will fail conversion
|
|
12
|
+
// lab(L a b [/ A]) — CIE Lab D50 (CSS spec)
|
|
13
|
+
// lch(L C H [/ A]) — CIE LCh D50
|
|
14
|
+
// oklab(L a b [/ A]) — OKLab (L in 0..1 internally
|
|
15
|
+
// but CSS uses 0..100% percent)
|
|
16
|
+
// oklch(L C H [/ A]) — OKLCh (same L convention)
|
|
17
|
+
// color(<space> r g b [/ A]) — srgb, srgb-linear, display-p3,
|
|
18
|
+
// rec2020, a98-rgb, prophoto-rgb,
|
|
19
|
+
// xyz / xyz-d65 / xyz-d50
|
|
20
|
+
//
|
|
21
|
+
// Also accepts the `none` keyword (treated as 0 — the CSS spec treats
|
|
22
|
+
// it as "missing" for the purposes of interpolation, but for parsing
|
|
23
|
+
// into our canonical state zero is the right default) and percentages
|
|
24
|
+
// where the spec allows them.
|
|
25
|
+
|
|
26
|
+
import { registerCssParser } from "./index.js";
|
|
27
|
+
|
|
28
|
+
const WS = /\s+/;
|
|
29
|
+
|
|
30
|
+
function tokenize(body) {
|
|
31
|
+
// Split on slashes (alpha separator), then on commas (legacy inside
|
|
32
|
+
// the modern form, e.g. oklch() doesn't accept commas but rgb() modern
|
|
33
|
+
// syntax does via the legacy parser). For Phase 4 we only split on
|
|
34
|
+
// whitespace within each slash-part.
|
|
35
|
+
const slashIdx = body.indexOf("/");
|
|
36
|
+
let main = body;
|
|
37
|
+
let alpha = null;
|
|
38
|
+
if (slashIdx >= 0) {
|
|
39
|
+
main = body.slice(0, slashIdx).trim();
|
|
40
|
+
alpha = body.slice(slashIdx + 1).trim();
|
|
41
|
+
}
|
|
42
|
+
const parts = main.split(WS).filter(Boolean);
|
|
43
|
+
if (alpha != null) parts.push(alpha);
|
|
44
|
+
return parts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseNumberOrNone(token) {
|
|
48
|
+
if (token === "none") return 0;
|
|
49
|
+
return parseFloat(token);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parsePercentOrNumber(token, percentBase) {
|
|
53
|
+
if (token === "none") return 0;
|
|
54
|
+
if (token.endsWith("%")) {
|
|
55
|
+
return (parseFloat(token.slice(0, -1)) / 100) * percentBase;
|
|
56
|
+
}
|
|
57
|
+
return parseFloat(token);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseAlpha(token) {
|
|
61
|
+
if (token == null) return 1;
|
|
62
|
+
if (token === "none") return 0;
|
|
63
|
+
if (token.endsWith("%")) return parseFloat(token.slice(0, -1)) / 100;
|
|
64
|
+
return parseFloat(token);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseHue(token) {
|
|
68
|
+
if (token === "none") return 0;
|
|
69
|
+
if (/turn$/i.test(token)) return parseFloat(token.slice(0, -4)) * 360;
|
|
70
|
+
if (/rad$/i.test(token)) return (parseFloat(token.slice(0, -3)) * 180) / Math.PI;
|
|
71
|
+
if (/deg$/i.test(token)) return parseFloat(token.slice(0, -3));
|
|
72
|
+
return parseFloat(token);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseFnBody(input, name) {
|
|
76
|
+
const lower = input.toLowerCase();
|
|
77
|
+
const prefix = name + "(";
|
|
78
|
+
if (!lower.startsWith(prefix) || !lower.endsWith(")")) return null;
|
|
79
|
+
return input.slice(prefix.length, -1).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseRgbModern(input) {
|
|
83
|
+
// Modern `rgb()` only (slash alpha). Legacy comma form is handled
|
|
84
|
+
// in parse/legacy.js. A modern form has no comma.
|
|
85
|
+
const body = parseFnBody(input, "rgb");
|
|
86
|
+
if (body == null) return null;
|
|
87
|
+
if (body.indexOf(",") >= 0) return null;
|
|
88
|
+
const parts = tokenize(body);
|
|
89
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
90
|
+
const parse = (token) => {
|
|
91
|
+
if (token === "none") return 0;
|
|
92
|
+
if (token.endsWith("%")) return parseFloat(token.slice(0, -1)) / 100;
|
|
93
|
+
return parseFloat(token) / 255;
|
|
94
|
+
};
|
|
95
|
+
const r = parse(parts[0]);
|
|
96
|
+
const g = parse(parts[1]);
|
|
97
|
+
const b = parse(parts[2]);
|
|
98
|
+
const a = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
99
|
+
if ([r, g, b, a].some(Number.isNaN)) return null;
|
|
100
|
+
return { space: "srgb", coords: [r, g, b], alpha: a };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseHslModern(input) {
|
|
104
|
+
const body = parseFnBody(input, "hsl");
|
|
105
|
+
if (body == null) return null;
|
|
106
|
+
if (body.indexOf(",") >= 0) return null;
|
|
107
|
+
const parts = tokenize(body);
|
|
108
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
109
|
+
const h = parseHue(parts[0]);
|
|
110
|
+
const s = parsePercentOrNumber(parts[1], 100);
|
|
111
|
+
const l = parsePercentOrNumber(parts[2], 100);
|
|
112
|
+
const a = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
113
|
+
if ([h, s, l, a].some(Number.isNaN)) return null;
|
|
114
|
+
return { space: "hsl", coords: [h, s, l], alpha: a };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseHwb(input) {
|
|
118
|
+
const body = parseFnBody(input, "hwb");
|
|
119
|
+
if (body == null) return null;
|
|
120
|
+
const parts = tokenize(body);
|
|
121
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
122
|
+
const h = parseHue(parts[0]);
|
|
123
|
+
const w = parsePercentOrNumber(parts[1], 100);
|
|
124
|
+
const bl = parsePercentOrNumber(parts[2], 100);
|
|
125
|
+
const a = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
126
|
+
if ([h, w, bl, a].some(Number.isNaN)) return null;
|
|
127
|
+
return { space: "hwb", coords: [h, w, bl], alpha: a };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseLab(input) {
|
|
131
|
+
const body = parseFnBody(input, "lab");
|
|
132
|
+
if (body == null) return null;
|
|
133
|
+
const parts = tokenize(body);
|
|
134
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
135
|
+
// CSS lab() L is 0..100 (number or percent of 100).
|
|
136
|
+
const L = parsePercentOrNumber(parts[0], 100);
|
|
137
|
+
// a, b are signed, ±125 (number or percent of 125).
|
|
138
|
+
const a = parsePercentOrNumber(parts[1], 125);
|
|
139
|
+
const b = parsePercentOrNumber(parts[2], 125);
|
|
140
|
+
const alpha = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
141
|
+
if ([L, a, b, alpha].some(Number.isNaN)) return null;
|
|
142
|
+
// CSS lab() is D50.
|
|
143
|
+
return { space: "lab-d50", coords: [L, a, b], alpha };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseLch(input) {
|
|
147
|
+
const body = parseFnBody(input, "lch");
|
|
148
|
+
if (body == null) return null;
|
|
149
|
+
const parts = tokenize(body);
|
|
150
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
151
|
+
const L = parsePercentOrNumber(parts[0], 100);
|
|
152
|
+
const C = parsePercentOrNumber(parts[1], 150);
|
|
153
|
+
const H = parseHue(parts[2]);
|
|
154
|
+
const alpha = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
155
|
+
if ([L, C, H, alpha].some(Number.isNaN)) return null;
|
|
156
|
+
return { space: "lch-d50", coords: [L, C, H], alpha };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseOklab(input) {
|
|
160
|
+
const body = parseFnBody(input, "oklab");
|
|
161
|
+
if (body == null) return null;
|
|
162
|
+
const parts = tokenize(body);
|
|
163
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
164
|
+
// CSS oklab() L is 0..1 (percent 0..100% of 1).
|
|
165
|
+
const L = parsePercentOrNumber(parts[0], 1);
|
|
166
|
+
const a = parsePercentOrNumber(parts[1], 0.4);
|
|
167
|
+
const b = parsePercentOrNumber(parts[2], 0.4);
|
|
168
|
+
const alpha = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
169
|
+
if ([L, a, b, alpha].some(Number.isNaN)) return null;
|
|
170
|
+
return { space: "oklab", coords: [L, a, b], alpha };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseOklch(input) {
|
|
174
|
+
const body = parseFnBody(input, "oklch");
|
|
175
|
+
if (body == null) return null;
|
|
176
|
+
const parts = tokenize(body);
|
|
177
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
178
|
+
const L = parsePercentOrNumber(parts[0], 1);
|
|
179
|
+
const C = parsePercentOrNumber(parts[1], 0.4);
|
|
180
|
+
const H = parseHue(parts[2]);
|
|
181
|
+
const alpha = parts.length === 4 ? parseAlpha(parts[3]) : 1;
|
|
182
|
+
if ([L, C, H, alpha].some(Number.isNaN)) return null;
|
|
183
|
+
return { space: "oklch", coords: [L, C, H], alpha };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Map CSS color() space IDs to v3 registry ids.
|
|
187
|
+
const COLOR_FN_SPACES = {
|
|
188
|
+
srgb: "srgb",
|
|
189
|
+
"srgb-linear": "srgb-linear",
|
|
190
|
+
"display-p3": "display-p3",
|
|
191
|
+
rec2020: "rec2020",
|
|
192
|
+
"a98-rgb": "a98",
|
|
193
|
+
"prophoto-rgb": "prophoto",
|
|
194
|
+
xyz: "xyz",
|
|
195
|
+
"xyz-d65": "xyz",
|
|
196
|
+
"xyz-d50": "xyz-d50"
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function parseColorFn(input) {
|
|
200
|
+
const body = parseFnBody(input, "color");
|
|
201
|
+
if (body == null) return null;
|
|
202
|
+
const parts = tokenize(body);
|
|
203
|
+
if (parts.length < 4 || parts.length > 5) return null;
|
|
204
|
+
const spaceToken = parts[0].toLowerCase();
|
|
205
|
+
const spaceId = COLOR_FN_SPACES[spaceToken];
|
|
206
|
+
if (!spaceId) return null;
|
|
207
|
+
// Channels are numbers or percentages; percentages normalize to
|
|
208
|
+
// [0, 1] in the space's natural domain. For XYZ the percent base is 1
|
|
209
|
+
// per the CSS spec.
|
|
210
|
+
const parseChannel = (token) => {
|
|
211
|
+
if (token === "none") return 0;
|
|
212
|
+
if (token.endsWith("%")) return parseFloat(token.slice(0, -1)) / 100;
|
|
213
|
+
return parseFloat(token);
|
|
214
|
+
};
|
|
215
|
+
const c1 = parseChannel(parts[1]);
|
|
216
|
+
const c2 = parseChannel(parts[2]);
|
|
217
|
+
const c3 = parseChannel(parts[3]);
|
|
218
|
+
const a = parts.length === 5 ? parseAlpha(parts[4]) : 1;
|
|
219
|
+
if ([c1, c2, c3, a].some(Number.isNaN)) return null;
|
|
220
|
+
return { space: spaceId, coords: [c1, c2, c3], alpha: a };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function parseCss(input) {
|
|
224
|
+
if (typeof input !== "string") return null;
|
|
225
|
+
const trimmed = input.trim();
|
|
226
|
+
const lower = trimmed.toLowerCase();
|
|
227
|
+
// Quick prefix switch.
|
|
228
|
+
if (lower.startsWith("rgb(")) return parseRgbModern(trimmed);
|
|
229
|
+
if (lower.startsWith("hsl(")) return parseHslModern(trimmed);
|
|
230
|
+
if (lower.startsWith("hwb(")) return parseHwb(trimmed);
|
|
231
|
+
if (lower.startsWith("lab(")) return parseLab(trimmed);
|
|
232
|
+
if (lower.startsWith("lch(")) return parseLch(trimmed);
|
|
233
|
+
if (lower.startsWith("oklab(")) return parseOklab(trimmed);
|
|
234
|
+
if (lower.startsWith("oklch(")) return parseOklch(trimmed);
|
|
235
|
+
if (lower.startsWith("color(")) return parseColorFn(trimmed);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Register as the primary CSS parser so parse/index.js dispatches here
|
|
240
|
+
// before falling through to the legacy rgb/hsl matchers.
|
|
241
|
+
registerCssParser(parseCss);
|
package/src/parse/hex.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Hex color parsing.
|
|
2
|
+
//
|
|
3
|
+
// Accepts 3, 4, 6, and 8 hex digit forms with optional leading `#`:
|
|
4
|
+
//
|
|
5
|
+
// #rgb → { space: 'srgb', coords: [r,g,b], alpha: 1 }
|
|
6
|
+
// #rgba → { space: 'srgb', coords: [r,g,b], alpha: a }
|
|
7
|
+
// #rrggbb → { space: 'srgb', coords: [r,g,b], alpha: 1 }
|
|
8
|
+
// #rrggbbaa → { space: 'srgb', coords: [r,g,b], alpha: a }
|
|
9
|
+
//
|
|
10
|
+
// Returns null if the input is not a hex string so callers can fall
|
|
11
|
+
// through to other parsers.
|
|
12
|
+
|
|
13
|
+
const HEX = /^#?([\da-f]{3,8})$/i;
|
|
14
|
+
|
|
15
|
+
export function parseHex(input) {
|
|
16
|
+
if (typeof input !== "string") return null;
|
|
17
|
+
const match = HEX.exec(input.trim());
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const digits = match[1];
|
|
20
|
+
const len = digits.length;
|
|
21
|
+
if (len === 3 || len === 4) {
|
|
22
|
+
const r = parseInt(digits[0] + digits[0], 16) / 255;
|
|
23
|
+
const g = parseInt(digits[1] + digits[1], 16) / 255;
|
|
24
|
+
const b = parseInt(digits[2] + digits[2], 16) / 255;
|
|
25
|
+
const a =
|
|
26
|
+
len === 4 ? parseInt(digits[3] + digits[3], 16) / 255 : 1;
|
|
27
|
+
return { space: "srgb", coords: [r, g, b], alpha: a };
|
|
28
|
+
}
|
|
29
|
+
if (len === 6 || len === 8) {
|
|
30
|
+
const r = parseInt(digits.slice(0, 2), 16) / 255;
|
|
31
|
+
const g = parseInt(digits.slice(2, 4), 16) / 255;
|
|
32
|
+
const b = parseInt(digits.slice(4, 6), 16) / 255;
|
|
33
|
+
const a =
|
|
34
|
+
len === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;
|
|
35
|
+
return { space: "srgb", coords: [r, g, b], alpha: a };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Main input dispatcher. Returns a v3 state object, or throws on
|
|
2
|
+
// unrecognized input. Order is significant: v3 instance → object → string
|
|
3
|
+
// forms (CSS Color 4 → legacy → hex → named).
|
|
4
|
+
//
|
|
5
|
+
// parse/css.js lands in Phase 4; until then the modern CSS Color 4 forms
|
|
6
|
+
// fall through to legacy (for rgb/hsl) or fail.
|
|
7
|
+
|
|
8
|
+
import { Swatch } from "../core/swatch-class.js";
|
|
9
|
+
import { parseObject } from "./objects.js";
|
|
10
|
+
import { parseLegacy } from "./legacy.js";
|
|
11
|
+
import { parseHex } from "./hex.js";
|
|
12
|
+
import { parseNamed } from "./named.js";
|
|
13
|
+
|
|
14
|
+
let cssParser = null; // wired by parse/css.js later
|
|
15
|
+
|
|
16
|
+
export function registerCssParser(fn) {
|
|
17
|
+
cssParser = fn;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseInput(input) {
|
|
21
|
+
if (input instanceof Swatch) return input._state;
|
|
22
|
+
|
|
23
|
+
if (input && typeof input === "object") {
|
|
24
|
+
const obj = parseObject(input);
|
|
25
|
+
if (obj) return obj;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof input === "string") {
|
|
29
|
+
const trimmed = input.trim();
|
|
30
|
+
if (cssParser) {
|
|
31
|
+
const css = cssParser(trimmed);
|
|
32
|
+
if (css) return css;
|
|
33
|
+
}
|
|
34
|
+
const legacy = parseLegacy(trimmed);
|
|
35
|
+
if (legacy) return legacy;
|
|
36
|
+
const hex = parseHex(trimmed);
|
|
37
|
+
if (hex) return hex;
|
|
38
|
+
const named = parseNamed(trimmed);
|
|
39
|
+
if (named) return named;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|