chromonym 2.0.0 → 3.1.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/NOTICE.md CHANGED
@@ -51,6 +51,38 @@ color-library lookup that rounds it through sRGB loses information.
51
51
  If you need Pantone-certified color values, consult Pantone directly:
52
52
  <https://www.pantone.com/>.
53
53
 
54
+ ## Crayola®
55
+
56
+ **Crayola®** is a registered trademark of **Crayola LLC**. Chromonym is
57
+ **not affiliated with, endorsed by, licensed by, or certified by Crayola
58
+ LLC**.
59
+
60
+ The `crayola` palette in this library ships a **community-curated
61
+ subset** of Crayola crayon-color names with sRGB approximations of their
62
+ wrappers. The names are used as **nominative references** to publicly
63
+ identified Crayola colors — there is no trademark claim, and no
64
+ endorsement is implied. Hex values are derived from the widely-cited
65
+ Wikipedia list of Crayola crayon colors (CC BY-SA), which itself cites
66
+ Crayola's published approximations.
67
+
68
+ ### Scope
69
+
70
+ The shipped set is ~85 entries — the current 64-standard box, the
71
+ iconic fluorescent "Neon" range, and widely-recognized modern
72
+ additions. It is not exhaustive: historical, retired, specialty-box
73
+ (Gem Tones, Heads 'N Tails, Multicultural, etc.), and regional-variant
74
+ colors are not included. If you need a specific Crayola color that
75
+ isn't present, open an issue or define a BYO palette.
76
+
77
+ ### Accuracy
78
+
79
+ - Fluorescents and neons saturate outside sRGB by design; on-screen
80
+ sRGB approximations are necessarily softer than the physical crayons.
81
+ - Metallics (Silver, Gold, Copper) are flat sRGB approximations; the
82
+ physical crayons' reflective behavior is lost.
83
+ - Classic hues are close enough for comparative UI / identification
84
+ work but should not be used as a reference for print color matching.
85
+
54
86
  ## X11 / X.Org
55
87
 
56
88
  The `x11` palette is derived from the X.Org `rgb.txt` file, which is in
package/README.md CHANGED
@@ -13,14 +13,14 @@
13
13
  [![npm version](https://img.shields.io/npm/v/chromonym?color=cb3837&logo=npm)](https://www.npmjs.com/package/chromonym)
14
14
  [![Types: included](https://img.shields.io/npm/types/chromonym?color=3178c6&logo=typescript)](https://www.npmjs.com/package/chromonym)
15
15
  [![CI](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml)
16
-
17
- <sub>[bundle size](https://packagephobia.com/result?p=chromonym) • [coverage](https://codecov.io/github/simiancraft/chromonym) • [MIT license](https://opensource.org/licenses/MIT)</sub>
16
+ [![Coverage](https://img.shields.io/codecov/c/github/simiancraft/chromonym?logo=codecov)](https://codecov.io/github/simiancraft/chromonym)
17
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/chromonym?label=bundle%20size)](https://bundlephobia.com/package/chromonym)
18
18
 
19
19
  <p align="center">
20
20
  <code>identify</code> &nbsp;•&nbsp; <code>resolve</code> &nbsp;•&nbsp; <code>convert</code>
21
21
  </p>
22
22
 
23
- **The last color-naming library you'll ever need.** One API — `identify`, `resolve`, `convert` — works against any palette you throw at it. Ships CSS, X11, and Pantone out of the box; bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Perceptual accuracy via six distance metrics. Tree-shakes to the bone — you only pay for palettes you import.
23
+ **The last color-naming library you'll ever need.** One API — `identify`, `resolve`, `convert` — works against any palette you throw at it. Ships CSS, X11, Pantone, and Crayola out of the box; bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Six distance metrics — from fast Euclidean to industry-standard CIEDE2000 and OKLAB. Tree-shakes to the bone — you only pay for palettes you import.
24
24
 
25
25
  <p align="center">
26
26
  <a href="https://simiancraft.github.io/chromonym/">
@@ -33,7 +33,7 @@ Because "it's sort of magenta-ish, maybe?" doesn't copy-paste into code — and
33
33
  For color *manipulation* (mixing, scales, gamut mapping), reach for [`chroma-js`](https://gka.github.io/chroma.js/) or [`color.js`](https://colorjs.io/) — chromonym is the tool for *naming*.
34
34
 
35
35
  ```ts
36
- import { identify, resolve, convert, pantone, type Palette } from 'chromonym';
36
+ import { identify, resolve, convert, pantone, crayola, web, type Palette } from 'chromonym';
37
37
 
38
38
  // Your palette — defined inline, type-checked, no registration. Works the
39
39
  // same as the built-in ones because that's the whole point.
@@ -47,12 +47,16 @@ const brand = {
47
47
  identify('#ff2b3c', { palette: brand }) // 'acme red'
48
48
  resolve('Acme Ink', { palette: brand }) // '#0a0f2c'
49
49
 
50
- // Built-in palettes follow the identical shape — CSS, X11, Pantone out of the box.
50
+ // Built-in palettes follow the identical shape — CSS, X11, Pantone, Crayola out of the box.
51
51
  identify('#ff8080') // 'lightcoral' (web is the default)
52
52
  identify('#663399') // 'rebeccapurple'
53
53
  identify('#E20074', { palette: pantone }) // '213 C' — T-Mobile magenta
54
+ identify('#E20074', { palette: crayola }) // 'Razzmatazz' — the closest Crayon
54
55
  resolve('Pantone 185 C', { palette: pantone }) // '#e4002b'
55
56
 
57
+ // Cross-palette in one call: parse input as a name from `source`, return nearest in `palette`.
58
+ identify('rebeccapurple', { palette: pantone, source: web }) // '267 C'
59
+
56
60
  // Format conversion: HEX / RGB / RGBA / HSL / HSV (palette-free, tree-shakes cleanly)
57
61
  convert('#ff0000', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
58
62
  convert({ h: 120, s: 100, l: 50 }, { format: 'HEX' }) // '#00ff00'
@@ -76,67 +80,127 @@ yarn add chromonym
76
80
  npm install chromonym
77
81
  ```
78
82
 
83
+ ## Migrating from 1.x
84
+
85
+ v2 collapsed the five-verb draft API (`identify`, `identifyAll`, `translate`, `resolve`, `convert`) into **three verbs** by folding the duplicates into options on existing verbs. Mechanical rewrites:
86
+
87
+ ```ts
88
+ // v1 → v2
89
+ identifyAll(color, { palette, k: 3 }) // → identify(color, { palette, k: 3 })
90
+ translate(name, { from: web, to: pantone }) // → identify(name, { palette: pantone, source: web })
91
+ ```
92
+
93
+ `resolve` and `convert` are unchanged. If you were only using those two, no edits are needed. See `CHANGELOG.md` for the full list of breaking changes.
94
+
79
95
  ## API
80
96
 
81
- Three primary functions. All accept an optional `opts` object; defaults fire when omitted.
97
+ Three verbs. All accept an optional `opts` object; defaults fire when omitted.
98
+
99
+ ### Options × verbs, at a glance
100
+
101
+ chromonym's API is deliberately tight: **three verbs, five options, no new nouns per feature**. Options earn their seat on each verb only where the semantics fit. Same-named options mean the same thing everywhere they appear.
102
+
103
+ | | `identify` | `resolve` | `convert` |
104
+ |---|:---:|:---:|:---:|
105
+ | `palette` | ✓ target (default: `web`) | ✓ source (default: `web`) | ✓ optional — enables name I/O |
106
+ | `source` | ✓ when input is a name from another palette | — (the palette *is* the source) | — |
107
+ | `metric` | ✓ ΔE dispatch | — (no distance) | — (structural / exact-name only) |
108
+ | `format` | — (returns a name) | ✓ output format | ✓ output format |
109
+ | `k` | ✓ top-k (in the chosen metric's units) | ✓ fuzzy top-k by Levenshtein | ✗ — breaks strict-convert contract |
110
+
111
+ No aliases, no plugin hooks. Every verb defaults to single-result / strict behavior; supplying `k` opts into the ranked-array return and (for `resolve`) the fuzzy-matching path.
82
112
 
83
113
  ### `identify(input, opts?)`
84
114
 
85
- **Color → name.** Finds the nearest-match name in the chosen palette using the selected perceptual distance metric.
115
+ **Color (or palette name) → name.** Finds the nearest-match name in the chosen palette using the selected perceptual distance metric.
86
116
 
87
117
  ```ts
88
- function identify<C extends Palette = typeof web>(
89
- input: ColorInput,
90
- opts?: { palette?: C; metric?: DistanceMetric },
91
- ): Extract<keyof C['colors'], string> | null
118
+ // Simple case — single nearest name.
119
+ function identify<P extends Palette = typeof web>(
120
+ input: ColorInput | string,
121
+ opts?: { palette?: P; source?: Palette; metric?: DistanceMetric },
122
+ ): Extract<keyof P['colors'], string> | null;
123
+
124
+ // With `k` — ranked top-k matches with distances.
125
+ function identify<P extends Palette = typeof web>(
126
+ input: ColorInput | string,
127
+ opts: { palette?: P; source?: Palette; metric?: DistanceMetric; k: number },
128
+ ): Array<{ name: Extract<keyof P['colors'], string>; value: HexColor; distance: number }>;
92
129
  ```
93
130
 
94
- - **Default palette**: the built-in `web` palette (148 entries, small — included if you call `identify` without a palette).
95
- - **Default metric**: read from the palette's own `defaultMetric` — `'deltaE76'` for web and x11 (well-separated palettes), `'deltaE2000'` for pantone (dense, perceptually-packed). Override freely via `metric`.
96
- - Returns the matched name, or `null` if input is unrecognized (`detectFormat` returns `'UNKNOWN'`).
97
- - Return type is inferred from the palette's `colors` keys — so passing `{ palette: pantone }` narrows to `PantoneColorName | null`, and a BYO palette with a literal key union narrows to that union.
98
- - Ties go to whichever color was defined first — deterministic across runs, but not semantically meaningful.
131
+ - **`palette`** — target (default: `web`). The palette to search in.
132
+ - **`source`** — when `input` is a string that isn't structurally a color (hex / rgb / hsl / …), parse it as a name in this palette. Enables cross-palette lookups in one verb. Structural inputs always parse structurally even when `source` is set.
133
+ - **`metric`** — distance metric for the nearest-match step. Defaults to the target palette's own `defaultMetric`.
134
+ - **`k`** — when present, return the top-k matches sorted ascending by distance. Return type flips from `Name | null` to `Array<Match>`; each `Match` carries `{ name, value, distance }`. `value` is the palette's stored hex for rendering swatches; `distance` is in the metric's natural units (ΔE units for all `deltaE*` metrics, channel-unit Euclidean for the others).
135
+ - Returns `null` / `[]` when the input can't be parsed. Ties go to whichever color was defined first in the palette.
99
136
 
100
137
  ```ts
101
- import { identify, pantone, x11 } from 'chromonym';
102
-
103
- identify('#ff0000') // 'red' (web default)
104
- identify([255, 0, 0]) // 'red'
105
- identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
106
- identify('#ff0000', { palette: x11 }) // 'red' (x11 also has 'red 1' through 'red 4')
107
- identify('#e4002b', { palette: pantone }) // '185 C' (exact match — Target/Coca-Cola red)
108
- identify('#ff0000', { palette: pantone }) // '172 C' (nearest — pure red isn't a Pantone)
109
- identify('#ff0080', { metric: 'deltaE2000' }) // force perceptual-accurate match
110
- identify('#ff0000', { metric: 'euclidean-srgb' }) // force fastest (non-perceptual) match
138
+ import { identify, pantone, x11, web, crayola } from 'chromonym';
139
+
140
+ // Classic color → name
141
+ identify('#ff0000') // 'red' (web default)
142
+ identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
143
+ identify('#e4002b', { palette: pantone }) // '185 C' (exact match)
144
+ identify('#ff0000', { palette: pantone }) // '172 C' (nearest — pure red isn't a Pantone)
145
+
146
+ // Cross-palette: parse input as a name in `source`, match in `palette`
147
+ identify('rebeccapurple', { palette: pantone, source: web }) // '267 C'
148
+ identify('Razzmatazz', { palette: pantone, source: crayola }) // '213 C' (same family as T-Mobile magenta)
149
+ identify('dodgerblue', { palette: x11, source: web }) // 'dodger blue' (shared hex)
150
+
151
+ // Top-k matches with ΔE distances — powers "did you mean" UIs
152
+ identify('#ff4488', { palette: crayola, k: 3 })
153
+ // [
154
+ // { name: 'Violet Red', value: '#f75394', distance: 0.0251 },
155
+ // { name: 'Wild Watermelon', value: '#fc6c85', distance: 0.0682 },
156
+ // { name: 'Cerise', value: '#dd4492', distance: 0.0685 },
157
+ // ]
111
158
  ```
112
159
 
113
160
  ### `resolve(name, opts?)`
114
161
 
115
- **Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen palette, and returns the value in the chosen format.
162
+ **Name → color.** Normalizes the input, looks up the canonical key in the chosen palette, and returns the value in the chosen format. With `k`, switches to fuzzy matching via Levenshtein edit distance — perfect for user-typed input where typos, casing, and punctuation vary.
116
163
 
117
164
  ```ts
165
+ // Strict — exact match via normalizer.
118
166
  function resolve(
119
167
  name: string,
120
168
  opts?: { palette?: Palette; format?: ColorFormat },
121
- ): ColorValue | null
169
+ ): ColorValue | null;
170
+
171
+ // Fuzzy top-k via Levenshtein edit distance.
172
+ function resolve<P extends Palette = typeof web>(
173
+ name: string,
174
+ opts: { palette?: P; format?: ColorFormat; k: number },
175
+ ): Array<{ name: Extract<keyof P['colors'], string>; value: ColorValue; distance: number }>;
122
176
  ```
123
177
 
124
- - **Default palette**: the built-in `web` palette.
125
- - **Default format**: `'HEX'`.
126
- - Returns `null` if the normalized name isn't in the palette.
127
- - Normalization is the palette's own `normalize` function — web/x11 lowercase + strip non-alphanumeric, pantone also strips a `Pantone` / `PMS` prefix. So `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` all resolve identically in their respective palettes.
178
+ - **`palette`** — source (default: `web`). The palette to look the name up in.
179
+ - **`format`** — output format (default: `'HEX'`). Affects both the strict return and the `value` field in each fuzzy `Match`.
180
+ - **`k`** — when present, bypass strict lookup and do fuzzy top-k. `distance` is the Levenshtein edit count against the palette's normalized key. Strict exact matches have distance `0`.
181
+ - Normalization is always the palette's own `normalize` function, applied to both the input and the keys before comparison. So `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` all resolve identically in their respective palettes — strict mode uses normalization to reach exact match; fuzzy mode uses it as the input to the edit-distance ranking.
128
182
 
129
183
  ```ts
130
- import { resolve, pantone } from 'chromonym';
184
+ import { resolve, pantone, crayola } from 'chromonym';
131
185
 
186
+ // Strict (default) — exact match or null
132
187
  resolve('crimson') // '#dc143c'
133
188
  resolve('Alice Blue') // '#f0f8ff'
134
189
  resolve('alice-blue!') // '#f0f8ff'
135
- resolve('185 C', { palette: pantone }) // '#e4002b'
136
- resolve('Pantone 185 C', { palette: pantone }) // '#e4002b' (same, normalized)
190
+ resolve('185 C', { palette: pantone }) // '#e4002b'
191
+ resolve('Pantone 185 C', { palette: pantone }) // '#e4002b' (same, normalized)
137
192
  resolve('crimson', { format: 'RGB' }) // 'rgb(220, 20, 60)'
138
- resolve('crimson', { format: 'RGBA' }) // { r: 220, g: 20, b: 60, a: 1 }
139
193
  resolve('not-a-color') // null
194
+
195
+ // Fuzzy — typo-tolerant user input
196
+ resolve('rebecapurple', { palette: web, k: 3 })
197
+ // [
198
+ // { name: 'rebeccapurple', value: '#663399', distance: 1 },
199
+ // { name: 'mediumpurple', value: '#9370db', distance: 5 },
200
+ // { name: 'purple', value: '#800080', distance: 6 },
201
+ // ]
202
+ resolve('Razmataz', { palette: crayola, k: 1 })
203
+ // [{ name: 'Razzmatazz', value: '#e3256b', distance: 2 }]
140
204
  ```
141
205
 
142
206
  ### `convert(input, opts?)`
@@ -233,72 +297,74 @@ resolve('Nurgle Green', { palette: warhammer })
233
297
  // → '#748c3f' (your normalizer handles case + punctuation)
234
298
  ```
235
299
 
236
- ### The three we ship
300
+ ### The four we ship
237
301
 
238
302
  | Name | Entries | Source |
239
303
  |---|---|---|
240
304
  | `web` (default) | 148 | CSS Color Module Level 4 named colors |
241
305
  | `x11` | 658 | X.Org `rgb.txt` (public domain) |
242
306
  | `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
307
+ | `crayola` | ~85 | Crayola crayon colors — community approximations (not Crayola-licensed) |
243
308
 
244
309
  Importable directly, or via subpath exports for stricter tree-shaking:
245
310
 
246
311
  ```ts
247
- import { web, x11, pantone } from 'chromonym';
312
+ import { web, x11, pantone, crayola } from 'chromonym';
248
313
  // or: import { pantone } from 'chromonym/pantone';
249
314
 
250
315
  web.colors.crimson // '#dc143c'
251
- web.colors.aliceblue // '#f0f8ff' (CSS keyword form — paste-compatible)
316
+ web.colors.aliceblue // '#f0f8ff' (CSS keyword form — paste-compatible)
252
317
  pantone.colors['185 C'] // '#e4002b'
318
+ crayola.colors.Razzmatazz // '#e3256b'
319
+ crayola.colors['Granny Smith Apple'] // '#a8e4a0'
253
320
  ```
254
321
 
255
- Each palette's keys follow its own domain convention — so `identify`'s output pastes back into whatever ecosystem the name came from. `web` uses the CSS spec's single-word form (`rebeccapurple`), `x11` uses the X.Org docs' spaced form (`'antique white 1'`), `pantone` uses the official print notation (`'185 C'`).
322
+ Each palette's keys follow its own domain convention — so `identify`'s output pastes back into whatever ecosystem the name came from. `web` uses the CSS spec's single-word form (`rebeccapurple`), `x11` uses the X.Org docs' spaced form (`'antique white 1'`), `pantone` uses the official print notation (`'185 C'`), `crayola` uses the Title Case form printed on the crayon wrapper (`'Granny Smith Apple'`).
256
323
 
257
324
  *Trivia:* `web.colors.rebeccapurple` (`#663399`) entered CSS Color 4 in 2014 in memory of [Rebecca Meyer](https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/). The X11 set ships every gray name twice (`gray`, `grey`) plus numbered variants up to 99, so `x11.colors['gray 73']` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
258
325
 
259
326
  ### Cross-palette translation
260
327
 
261
- "What's this CSS color in X11?" / "What's our brand red in Pantone?" is a *nearest-match* question — palettes almost never share exact hex values, so there's no strict round-trip. That's `identify`'s job. Chain `convert` (strict name → hex) with `identify` (fuzzy hex → nearest name):
328
+ "What's this CSS color in X11?" / "What's our brand red in Pantone?" is a *nearest-match* question — palettes almost never share exact hex values, so there's no strict round-trip. `identify` handles it directly via `source`: parse the input as a name from one palette, return the nearest name from another.
262
329
 
263
330
  ```ts
264
- import { convert, identify, web, x11, pantone } from 'chromonym';
265
-
266
- // web → x11
267
- identify(convert('rebeccapurple', { palette: web }), { palette: x11 })
268
- // → 'dark orchid 4'
269
-
270
- identify(convert('dodgerblue', { palette: web }), { palette: x11 })
271
- // → 'dodger blue' (hex shared across both palettes — exact hit)
272
-
273
- // x11 → pantone
274
- identify(convert('dark slate gray 4', { palette: x11 }), { palette: pantone })
275
- // → '5483 C'
276
-
277
- identify(convert('debian red', { palette: x11 }), { palette: pantone })
278
- // → '1925 C'
279
-
280
- // web → pantone (the classic brand-matching case)
281
- identify(convert('hotpink', { palette: web }), { palette: pantone }) // → '812 C'
282
- identify(convert('dodgerblue', { palette: web }), { palette: pantone }) // → '279 C'
283
-
284
- // BYO → pantone (ship your brand palette, find the closest Pantone for print)
285
- identify(convert('acme red', { palette: brand }), { palette: pantone }) // → '1788 C'
286
-
287
- // Override the metric for the *nearest-match* leg if you want — input parsing
288
- // is always exact, so only the second call takes `metric`.
289
- identify(convert('rebeccapurple', { palette: web }), { palette: pantone, metric: 'deltaEok' })
290
- // → '526 C' (OKLAB disagrees with ΔE2000's '267 C' in the saturated-purple region)
331
+ import { identify, web, x11, pantone, crayola } from 'chromonym';
332
+
333
+ // Name in one palette → nearest name in another.
334
+ identify('rebeccapurple', { palette: x11, source: web }) // 'dark orchid 4'
335
+ identify('dodgerblue', { palette: x11, source: web }) // 'dodger blue' (shared hex)
336
+ identify('dark slate gray 4',{ palette: pantone, source: x11 }) // '5483 C'
337
+ identify('hotpink', { palette: pantone, source: web }) // '812 C'
338
+ identify('Razzmatazz', { palette: pantone, source: crayola }) // '213 C' (same family as T-Mobile magenta)
339
+ identify('Razzmatazz', { palette: web, source: crayola }) // 'deeppink'
340
+ identify('hotpink', { palette: crayola, source: web }) // 'Violet Red'
341
+ identify('forestgreen', { palette: crayola, source: web }) // 'Green'
342
+
343
+ // BYO works on either side — ship your brand palette, find the closest Pantone for print.
344
+ identify('acme red', { palette: pantone, source: brand }) // '1788 C'
345
+
346
+ // Override the metric for the nearest-match side.
347
+ identify('rebeccapurple', { palette: pantone, source: web, metric: 'deltaEok' })
348
+ // '526 C' (OKLAB disagrees with ΔE2000's '267 C' in the saturated-purple region)
349
+
350
+ // Top-k across palettes — what are the three closest Pantones to CSS "hotpink"?
351
+ identify('hotpink', { palette: pantone, source: web, k: 3 })
352
+ // [
353
+ // { name: '812 C', value: '#ff5fa2', distance: 3.56 },
354
+ // { name: '211 C', value: '#f57eb6', distance: 3.95 },
355
+ // { name: '218 C', value: '#e56db1', distance: 4.30 },
356
+ // ]
291
357
  ```
292
358
 
293
- Why two calls instead of one: `convert` is the strict verb (throws on a name miss), `identify` is the fuzzy one (always returns a nearest). Keeping them separate lets you decide — per call — whether you want a hard error on an unknown input or a best-effort neighbor.
294
-
295
- If this chain shows up often in your code, wrap it in your own helper:
359
+ If you want the underlying chain — keeping the intermediate hex, or using `convert`'s strict throw-on-miss behavior for unknown source names — `identify(convert(…))` still works:
296
360
 
297
361
  ```ts
298
- const translate = (name: string, from: Palette, to: Palette) =>
299
- identify(convert(name, { palette: from }), { palette: to });
362
+ const hex = convert('rebeccapurple', { palette: web }); // '#663399' (throws on miss)
363
+ const near = identify(hex, { palette: pantone }); // '267 C'
300
364
  ```
301
365
 
366
+ Rule of thumb: use `source` when you want one clean call and `null` on miss; use the chain when you want to inspect the intermediate hex or trigger a loud failure on unknown source names.
367
+
302
368
  ## Distance metrics
303
369
 
304
370
  *Six ways to ask "how different are two colors?"*
@@ -309,7 +375,7 @@ const translate = (name: string, from: Palette, to: Palette) =>
309
375
  |---|---|---|
310
376
  | `'euclidean-srgb'` | Raw `sqrt(Δr² + Δg² + Δb²)` in sRGB. Fastest. Not perceptually uniform — a unit of distance doesn't mean a unit of "visual difference." | Tight perf budgets; well-separated palettes where you just need the obvious answer. |
311
377
  | `'euclidean-linear'` | Same math, but on linearized (gamma-removed) RGB. Somewhat better in dark regions. Still not perceptual. | Physical-light mixing contexts; incremental upgrade from sRGB Euclidean. |
312
- | `'deltaE76'` | Euclidean in CIELAB (CIE 1976). Simple perceptual metric. 1 ΔE ≈ "just noticeable difference" for most of the gamut. | **Default for web and x11.** Meaningful perceptual accuracy at low cost. |
378
+ | `'deltaE76'` | Euclidean in CIELAB (CIE 1976). Simple perceptual metric. 1 ΔE is often cited as roughly a just-noticeable difference, though uniformity breaks down in saturated blues/purples — which is the reason ΔE2000 exists. | **Default for web and x11.** Meaningful perceptual accuracy at low cost. |
313
379
  | `'deltaE94'` | ΔE76 + chroma/hue weighting (CIE 1994). Fixes "saturated colors feel too far apart." | When ΔE76 is over-penalizing saturated matches in your use case. |
314
380
  | `'deltaE2000'` | Full CIEDE2000 formula with blue/purple rotation correction. Industry standard for print, design tools, Pantone workflows. | **Default for pantone.** Use whenever accuracy matters, especially in the blue region where ΔE76/94 break down. |
315
381
  | `'deltaEok'` | Euclidean distance in OKLAB (Björn Ottosson, 2020). OKLAB is perceptually uniform by construction — no weighting formula needed. Strictly more uniform than CIELAB; often a better nearest-match than ΔE2000 in saturated-blue regions, and cheaper to compute. | Use when you want perceptual accuracy without the CIEDE2000 rotation-term complexity. |
@@ -1 +1 @@
1
- {"version":3,"file":"hsl.d.ts","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAK7D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAqC/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAyB/C","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)` as HslString;\n}\n"]}
1
+ {"version":3,"file":"hsl.d.ts","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAK7D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAqC/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAyB/C","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)` as HslString;\n}\n"]}
1
+ {"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"hsv.d.ts","sourceRoot":"","sources":["../../src/conversions/hsv.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAK7D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CA+B/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAuB/C","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HsvInput, HsvString, Rgba } from '../types.js';\n\nconst HSV_RE =\n /^hsv\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSV input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and value are 0..100 percent.\n */\nexport function hsvToRgba(input: HsvInput): Rgba {\n let h: number;\n let s: number;\n let v: number;\n\n if (typeof input === 'string') {\n const match = HSV_RE.exec(input);\n if (!match) throw new Error(`Invalid hsv string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n v = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n v = requireFinite(input.v, 'v');\n }\n\n const saturation = s / 100;\n const value = v / 100;\n const chroma = value * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const valueOffset = value - chroma;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + valueOffset) * 255),\n g: Math.round((gPrime + valueOffset) * 255),\n b: Math.round((bPrime + valueOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsv(h, s%, v%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsv(rgba: Rgba): HsvString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const delta = max - min;\n const value = max;\n const saturation = max === 0 ? 0 : delta / max;\n\n let hue = 0;\n if (delta !== 0) {\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n }\n\n return `hsv(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(value * 100)}%)` as HsvString;\n}\n"]}
1
+ {"version":3,"file":"hsv.d.ts","sourceRoot":"","sources":["../../src/conversions/hsv.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAK7D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CA+B/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAuB/C","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HsvInput, HsvString, Rgba } from '../types.js';\n\nconst HSV_RE =\n /^hsv\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSV input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and value are 0..100 percent.\n */\nexport function hsvToRgba(input: HsvInput): Rgba {\n let h: number;\n let s: number;\n let v: number;\n\n if (typeof input === 'string') {\n const match = HSV_RE.exec(input);\n if (!match) throw new Error(`Invalid hsv string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n v = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n v = requireFinite(input.v, 'v');\n }\n\n const saturation = s / 100;\n const value = v / 100;\n const chroma = value * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const valueOffset = value - chroma;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + valueOffset) * 255),\n g: Math.round((gPrime + valueOffset) * 255),\n b: Math.round((bPrime + valueOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsv(h, s%, v%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsv(rgba: Rgba): HsvString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const delta = max - min;\n const value = max;\n const saturation = max === 0 ? 0 : delta / max;\n\n let hue = 0;\n if (delta !== 0) {\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n }\n\n return `hsv(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(value * 100)}%)`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"hsv.js","sourceRoot":"","sources":["../../src/conversions/hsv.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HsvInput, HsvString, Rgba } from '../types.js';\n\nconst HSV_RE =\n /^hsv\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSV input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and value are 0..100 percent.\n */\nexport function hsvToRgba(input: HsvInput): Rgba {\n let h: number;\n let s: number;\n let v: number;\n\n if (typeof input === 'string') {\n const match = HSV_RE.exec(input);\n if (!match) throw new Error(`Invalid hsv string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n v = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n v = requireFinite(input.v, 'v');\n }\n\n const saturation = s / 100;\n const value = v / 100;\n const chroma = value * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const valueOffset = value - chroma;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + valueOffset) * 255),\n g: Math.round((gPrime + valueOffset) * 255),\n b: Math.round((bPrime + valueOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsv(h, s%, v%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsv(rgba: Rgba): HsvString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const delta = max - min;\n const value = max;\n const saturation = max === 0 ? 0 : delta / max;\n\n let hue = 0;\n if (delta !== 0) {\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n }\n\n return `hsv(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(value * 100)}%)` as HsvString;\n}\n"]}
1
+ {"version":3,"file":"hsv.js","sourceRoot":"","sources":["../../src/conversions/hsv.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HsvInput, HsvString, Rgba } from '../types.js';\n\nconst HSV_RE =\n /^hsv\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSV input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and value are 0..100 percent.\n */\nexport function hsvToRgba(input: HsvInput): Rgba {\n let h: number;\n let s: number;\n let v: number;\n\n if (typeof input === 'string') {\n const match = HSV_RE.exec(input);\n if (!match) throw new Error(`Invalid hsv string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n v = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n v = requireFinite(input.v, 'v');\n }\n\n const saturation = s / 100;\n const value = v / 100;\n const chroma = value * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const valueOffset = value - chroma;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + valueOffset) * 255),\n g: Math.round((gPrime + valueOffset) * 255),\n b: Math.round((bPrime + valueOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsv(h, s%, v%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsv(rgba: Rgba): HsvString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const delta = max - min;\n const value = max;\n const saturation = max === 0 ? 0 : delta / max;\n\n let hue = 0;\n if (delta !== 0) {\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n }\n\n return `hsv(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(value * 100)}%)`;\n}\n"]}
@@ -1,25 +1,50 @@
1
1
  import { web } from './palettes/web.js';
2
- import type { ColorInput, DistanceMetric, Palette } from './types.js';
2
+ import type { ColorInput, DistanceMetric, HexColor, Palette } from './types.js';
3
3
  /** Extract string keys from a Palette's `colors` map. */
4
4
  type PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;
5
5
  /**
6
- * Identify the nearest-named color for any color input.
6
+ * One ranked nearest-match entry when `identify` is called with `k`.
7
+ * `value` is the palette's stored hex for the match (useful for rendering
8
+ * swatches). `distance` is in the selected metric's natural units — ΔE
9
+ * for `deltaE*` metrics, channel-unit Euclidean for the others.
10
+ */
11
+ export type IdentifyMatch<Name extends string> = {
12
+ readonly name: Name;
13
+ readonly value: HexColor;
14
+ readonly distance: number;
15
+ };
16
+ /**
17
+ * Identify the nearest-named color for any color input (or palette name
18
+ * from another palette via `source`).
19
+ *
20
+ * **Simple case — color in, name out:**
21
+ * identify('#ff0000') → 'red'
22
+ * identify('#ff0000', { palette: pantone }) → '172 C'
7
23
  *
8
- * The distance metric defaults to the palette's own `defaultMetric`
9
- * (web/x11: `deltaE76`, pantone: `deltaE2000`). Override with the `metric`
10
- * option — e.g. `{ metric: 'euclidean-srgb' }` for maximum speed, or
11
- * `{ metric: 'deltaE2000' }` for strictest perceptual accuracy.
24
+ * **Cross-palette — name from one palette, nearest in another:**
25
+ * identify('rebeccapurple', { palette: pantone, source: web }) → '267 C'
26
+ * identify('Razzmatazz', { palette: pantone, source: crayola }) → '213 C'
12
27
  *
13
- * Returns `null` if the input isn't a recognized color shape.
28
+ * **Top-k ranked matches with ΔE distances:**
29
+ * identify('#ff0000', { palette: pantone, k: 3 })
30
+ * // [{ name: '172 C', value: '#fa4616', distance: 3.2 }, ...]
14
31
  *
15
- * Defaults: palette = `web`.
32
+ * The distance metric defaults to the target palette's `defaultMetric`
33
+ * (web/x11: `deltaE76`, pantone: `deltaE2000`, crayola: `deltaEok`).
34
+ * Override with `metric`.
16
35
  *
17
- * BYO palettes: pass any object matching `Palette<Name>` and the
18
- * return type is inferred from its `colors` keys — no registration needed.
36
+ * Returns `null` (or `[]` when `k` is set) if the input can't be parsed.
19
37
  */
20
- export declare function identify<P extends Palette = typeof web>(input: ColorInput, opts?: {
38
+ export declare function identify<P extends Palette = typeof web>(input: ColorInput | string, opts?: {
21
39
  palette?: P;
40
+ source?: Palette;
22
41
  metric?: DistanceMetric;
23
42
  }): PaletteKey<P> | null;
43
+ export declare function identify<P extends Palette = typeof web>(input: ColorInput | string, opts: {
44
+ palette?: P;
45
+ source?: Palette;
46
+ metric?: DistanceMetric;
47
+ k: number;
48
+ }): Array<IdentifyMatch<PaletteKey<P>>>;
24
49
  export {};
25
50
  //# sourceMappingURL=identify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identify.d.ts","sourceRoot":"","sources":["../src/identify.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEtE,yDAAyD;AACzD,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAExE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,GAAG,EACrD,KAAK,EAAE,UAAU,EACjB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAO,GAClD,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAQtB","sourcesContent":["import { toRgba } from './convert.js';\nimport { detectFormat } from './detectFormat.js';\nimport { nearest } from './indexing.js';\nimport { web } from './palettes/web.js';\nimport type { ColorInput, DistanceMetric, Palette } from './types.js';\n\n/** Extract string keys from a Palette's `colors` map. */\ntype PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;\n\n/**\n * Identify the nearest-named color for any color input.\n *\n * The distance metric defaults to the palette's own `defaultMetric`\n * (web/x11: `deltaE76`, pantone: `deltaE2000`). Override with the `metric`\n * option — e.g. `{ metric: 'euclidean-srgb' }` for maximum speed, or\n * `{ metric: 'deltaE2000' }` for strictest perceptual accuracy.\n *\n * Returns `null` if the input isn't a recognized color shape.\n *\n * Defaults: palette = `web`.\n *\n * BYO palettes: pass any object matching `Palette<Name>` and the\n * return type is inferred from its `colors` keys — no registration needed.\n */\nexport function identify<P extends Palette = typeof web>(\n input: ColorInput,\n opts: { palette?: P; metric?: DistanceMetric } = {},\n): PaletteKey<P> | null {\n const format = detectFormat(input);\n if (format === 'UNKNOWN') return null;\n\n const palette = (opts.palette ?? web) as P;\n const rgba = toRgba(input, format);\n const name = nearest(rgba, palette, opts.metric ?? palette.defaultMetric);\n return (name as PaletteKey<P>) || null;\n}\n"]}
1
+ {"version":3,"file":"identify.d.ts","sourceRoot":"","sources":["../src/identify.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAQ,MAAM,YAAY,CAAC;AAEtF,yDAAyD;AACzD,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,IAAI,SAAS,MAAM,IAAI;IAC/C,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAmBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,GAAG,EACrD,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,GAChE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;AAGxB,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,GAAG,EACrD,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1E,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["import { hexToRgba } from './conversions/hex.js';\nimport { toRgba } from './convert.js';\nimport { detectFormat } from './detectFormat.js';\nimport { getNameIndex, nearest, nearestAll } from './indexing.js';\nimport { web } from './palettes/web.js';\nimport type { ColorInput, DistanceMetric, HexColor, Palette, Rgba } from './types.js';\n\n/** Extract string keys from a Palette's `colors` map. */\ntype PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;\n\n/**\n * One ranked nearest-match entry when `identify` is called with `k`.\n * `value` is the palette's stored hex for the match (useful for rendering\n * swatches). `distance` is in the selected metric's natural units — ΔE\n * for `deltaE*` metrics, channel-unit Euclidean for the others.\n */\nexport type IdentifyMatch<Name extends string> = {\n readonly name: Name;\n readonly value: HexColor;\n readonly distance: number;\n};\n\n/**\n * Parse a string input that may be a color literal or a palette name.\n * Returns the canonical Rgba, or `null` if nothing matches.\n */\nfunction parseInput(input: ColorInput | string, source?: Palette): Rgba | null {\n const detected = detectFormat(input as ColorInput);\n if (detected !== 'UNKNOWN') return toRgba(input as ColorInput, detected);\n if (source !== undefined && typeof input === 'string') {\n const canonical = getNameIndex(source).get(source.normalize(input));\n if (canonical === undefined) return null;\n const hex = (source.colors as Record<string, HexColor>)[canonical];\n if (hex === undefined) return null;\n return hexToRgba(hex);\n }\n return null;\n}\n\n/**\n * Identify the nearest-named color for any color input (or palette name\n * from another palette via `source`).\n *\n * **Simple case — color in, name out:**\n * identify('#ff0000') → 'red'\n * identify('#ff0000', { palette: pantone }) → '172 C'\n *\n * **Cross-palette — name from one palette, nearest in another:**\n * identify('rebeccapurple', { palette: pantone, source: web }) → '267 C'\n * identify('Razzmatazz', { palette: pantone, source: crayola }) → '213 C'\n *\n * **Top-k ranked matches with ΔE distances:**\n * identify('#ff0000', { palette: pantone, k: 3 })\n * // [{ name: '172 C', value: '#fa4616', distance: 3.2 }, ...]\n *\n * The distance metric defaults to the target palette's `defaultMetric`\n * (web/x11: `deltaE76`, pantone: `deltaE2000`, crayola: `deltaEok`).\n * Override with `metric`.\n *\n * Returns `null` (or `[]` when `k` is set) if the input can't be parsed.\n */\n\n// Overload 1: no `k` — single name (or null). Unchanged legacy shape.\nexport function identify<P extends Palette = typeof web>(\n input: ColorInput | string,\n opts?: { palette?: P; source?: Palette; metric?: DistanceMetric },\n): PaletteKey<P> | null;\n\n// Overload 2: `k` present — ranked Match[] with distances.\nexport function identify<P extends Palette = typeof web>(\n input: ColorInput | string,\n opts: { palette?: P; source?: Palette; metric?: DistanceMetric; k: number },\n): Array<IdentifyMatch<PaletteKey<P>>>;\n\n// Implementation.\nexport function identify(\n input: ColorInput | string,\n opts: { palette?: Palette; source?: Palette; metric?: DistanceMetric; k?: number } = {},\n): string | null | Array<IdentifyMatch<string>> {\n const palette = opts.palette ?? web;\n const metric = opts.metric ?? palette.defaultMetric;\n const rgba = parseInput(input, opts.source);\n\n if (opts.k !== undefined) {\n if (rgba === null) return [];\n const raw = nearestAll(rgba, palette, metric, opts.k);\n return raw.map((m) => {\n const value = (palette.colors as Record<string, HexColor>)[m.name];\n return { name: m.name, value: value as HexColor, distance: m.distance };\n });\n }\n\n if (rgba === null) return null;\n const name = nearest(rgba, palette, metric);\n return name || null;\n}\n"]}