chromonym 1.4.1 → 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.
Files changed (114) hide show
  1. package/NOTICE.md +64 -4
  2. package/README.md +290 -83
  3. package/dist/conversions/hex.d.ts +1 -1
  4. package/dist/conversions/hex.d.ts.map +1 -1
  5. package/dist/conversions/hex.js +2 -2
  6. package/dist/conversions/hex.js.map +1 -1
  7. package/dist/conversions/hsl.d.ts +1 -1
  8. package/dist/conversions/hsl.d.ts.map +1 -1
  9. package/dist/conversions/hsl.js +2 -2
  10. package/dist/conversions/hsl.js.map +1 -1
  11. package/dist/conversions/hsv.d.ts +1 -1
  12. package/dist/conversions/hsv.d.ts.map +1 -1
  13. package/dist/conversions/hsv.js +2 -2
  14. package/dist/conversions/hsv.js.map +1 -1
  15. package/dist/conversions/index.d.ts +5 -5
  16. package/dist/conversions/index.d.ts.map +1 -1
  17. package/dist/conversions/index.js +5 -5
  18. package/dist/conversions/index.js.map +1 -1
  19. package/dist/conversions/pantone.d.ts +9 -7
  20. package/dist/conversions/pantone.d.ts.map +1 -1
  21. package/dist/conversions/pantone.js +11 -10
  22. package/dist/conversions/pantone.js.map +1 -1
  23. package/dist/conversions/rgb.d.ts +1 -1
  24. package/dist/conversions/rgb.d.ts.map +1 -1
  25. package/dist/conversions/rgb.js +1 -1
  26. package/dist/conversions/rgb.js.map +1 -1
  27. package/dist/convert.d.ts +45 -5
  28. package/dist/convert.d.ts.map +1 -1
  29. package/dist/convert.js +68 -17
  30. package/dist/convert.js.map +1 -1
  31. package/dist/detectFormat.d.ts +1 -1
  32. package/dist/detectFormat.d.ts.map +1 -1
  33. package/dist/detectFormat.js +2 -6
  34. package/dist/detectFormat.js.map +1 -1
  35. package/dist/identify.d.ts +41 -19
  36. package/dist/identify.d.ts.map +1 -1
  37. package/dist/identify.js +37 -22
  38. package/dist/identify.js.map +1 -1
  39. package/dist/index.d.ts +15 -14
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +14 -13
  42. package/dist/index.js.map +1 -1
  43. package/dist/indexing.d.ts +48 -24
  44. package/dist/indexing.d.ts.map +1 -1
  45. package/dist/indexing.js +111 -22
  46. package/dist/indexing.js.map +1 -1
  47. package/dist/math/clamp.d.ts.map +1 -1
  48. package/dist/math/clamp.js.map +1 -1
  49. package/dist/math/colorSpace.d.ts +1 -1
  50. package/dist/math/colorSpace.d.ts.map +1 -1
  51. package/dist/math/colorSpace.js.map +1 -1
  52. package/dist/math/deltaE.d.ts.map +1 -1
  53. package/dist/math/deltaE.js.map +1 -1
  54. package/dist/math/editDistance.d.ts +17 -0
  55. package/dist/math/editDistance.d.ts.map +1 -0
  56. package/dist/math/editDistance.js +48 -0
  57. package/dist/math/editDistance.js.map +1 -0
  58. package/dist/math/euclideanDistance.d.ts +1 -1
  59. package/dist/math/euclideanDistance.d.ts.map +1 -1
  60. package/dist/math/euclideanDistance.js.map +1 -1
  61. package/dist/math/hueSector.d.ts.map +1 -1
  62. package/dist/math/hueSector.js.map +1 -1
  63. package/dist/palettes/crayola.d.ts +200 -0
  64. package/dist/palettes/crayola.d.ts.map +1 -0
  65. package/dist/palettes/crayola.js +121 -0
  66. package/dist/palettes/crayola.js.map +1 -0
  67. package/dist/palettes/index.d.ts +5 -0
  68. package/dist/palettes/index.d.ts.map +1 -0
  69. package/dist/palettes/index.js +5 -0
  70. package/dist/palettes/index.js.map +1 -0
  71. package/dist/palettes/normalize.d.ts +17 -0
  72. package/dist/palettes/normalize.d.ts.map +1 -0
  73. package/dist/palettes/normalize.js +19 -0
  74. package/dist/palettes/normalize.js.map +1 -0
  75. package/dist/palettes/pantone.d.ts +1834 -0
  76. package/dist/palettes/pantone.d.ts.map +1 -0
  77. package/dist/palettes/pantone.js +925 -0
  78. package/dist/palettes/pantone.js.map +1 -0
  79. package/dist/palettes/web.d.ts +320 -0
  80. package/dist/palettes/web.d.ts.map +1 -0
  81. package/dist/{colorspaces → palettes}/web.js +18 -3
  82. package/dist/palettes/web.js.map +1 -0
  83. package/dist/palettes/x11.d.ts +1346 -0
  84. package/dist/palettes/x11.d.ts.map +1 -0
  85. package/dist/palettes/x11.js +686 -0
  86. package/dist/palettes/x11.js.map +1 -0
  87. package/dist/resolve.d.ts +42 -7
  88. package/dist/resolve.d.ts.map +1 -1
  89. package/dist/resolve.js +48 -20
  90. package/dist/resolve.js.map +1 -1
  91. package/dist/types.d.ts +10 -6
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js +2 -1
  94. package/dist/types.js.map +1 -1
  95. package/package.json +38 -18
  96. package/dist/colorspaces/index.d.ts +0 -4
  97. package/dist/colorspaces/index.d.ts.map +0 -1
  98. package/dist/colorspaces/index.js +0 -4
  99. package/dist/colorspaces/index.js.map +0 -1
  100. package/dist/colorspaces/pantone.d.ts +0 -919
  101. package/dist/colorspaces/pantone.d.ts.map +0 -1
  102. package/dist/colorspaces/pantone.js +0 -918
  103. package/dist/colorspaces/pantone.js.map +0 -1
  104. package/dist/colorspaces/registry.d.ts +0 -26
  105. package/dist/colorspaces/registry.d.ts.map +0 -1
  106. package/dist/colorspaces/registry.js +0 -36
  107. package/dist/colorspaces/registry.js.map +0 -1
  108. package/dist/colorspaces/web.d.ts +0 -156
  109. package/dist/colorspaces/web.d.ts.map +0 -1
  110. package/dist/colorspaces/web.js.map +0 -1
  111. package/dist/colorspaces/x11.d.ts +0 -667
  112. package/dist/colorspaces/x11.d.ts.map +0 -1
  113. package/dist/colorspaces/x11.js +0 -666
  114. package/dist/colorspaces/x11.js.map +0 -1
package/README.md CHANGED
@@ -11,18 +11,16 @@
11
11
  # chromonym
12
12
 
13
13
  [![npm version](https://img.shields.io/npm/v/chromonym?color=cb3837&logo=npm)](https://www.npmjs.com/package/chromonym)
14
- [![publish size](https://badgen.net/packagephobia/publish/chromonym?color=green)](https://packagephobia.com/result?p=chromonym)
15
- [![install size](https://badgen.net/packagephobia/install/chromonym?color=green)](https://packagephobia.com/result?p=chromonym)
16
14
  [![Types: included](https://img.shields.io/npm/types/chromonym?color=3178c6&logo=typescript)](https://www.npmjs.com/package/chromonym)
17
15
  [![CI](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml)
18
- [![codecov](https://codecov.io/gh/simiancraft/chromonym/branch/main/graph/badge.svg)](https://codecov.io/gh/simiancraft/chromonym)
19
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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>
20
18
 
21
19
  <p align="center">
22
20
  <code>identify</code> &nbsp;•&nbsp; <code>resolve</code> &nbsp;•&nbsp; <code>convert</code>
23
21
  </p>
24
22
 
25
- **Tree-shakeable color naming for TypeScript** — across CSS, X11, and Pantone, with six distance metrics (four perceptual).
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.
26
24
 
27
25
  <p align="center">
28
26
  <a href="https://simiancraft.github.io/chromonym/">
@@ -30,31 +28,49 @@
30
28
  </a>
31
29
  </p>
32
30
 
33
- Because "it's sort of magenta-ish, maybe?" doesn't copy-paste into code, and the nearest Pantone to your brand hex is something you should be able to answer without opening a swatch book.
34
-
35
- The identification and resolution mechanism is the same across every colorspace, so future additions (RAL, HKS, NCS, or similar) are one file each. There's no public `registerColorspace` API yet — open an issue if you need a specific set.
31
+ Because "it's sort of magenta-ish, maybe?" doesn't copy-paste into code — and naming a color is the same problem whether your palette is CSS, Pantone, your company's brand kit, or a homebrew set of faction colors. Chromonym treats every palette as a first-class `Palette<Name>` object and runs the same nearest-neighbor machinery over all of them.
36
32
 
37
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*.
38
34
 
39
35
  ```ts
40
- import { identify, resolve, convert } from 'chromonym';
41
-
42
- // What Pantone is this brand color closest to? (perceptually, via CIEDE2000)
43
- identify('#E20074', { colorspace: 'pantone' }) // '213C' — T-Mobile magenta
36
+ import { identify, resolve, convert, pantone, crayola, web, type Palette } from 'chromonym';
37
+
38
+ // Your palette — defined inline, type-checked, no registration. Works the
39
+ // same as the built-in ones because that's the whole point.
40
+ const brand = {
41
+ name: 'acme-brand',
42
+ colors: { 'acme red': '#ff2a3b', 'acme ink': '#0a0f2c', 'acme mist': '#e6ecf5' },
43
+ normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
44
+ defaultMetric: 'deltaEok',
45
+ } as const satisfies Palette;
46
+
47
+ identify('#ff2b3c', { palette: brand }) // 'acme red'
48
+ resolve('Acme Ink', { palette: brand }) // '#0a0f2c'
49
+
50
+ // Built-in palettes follow the identical shape — CSS, X11, Pantone, Crayola out of the box.
51
+ identify('#ff8080') // 'lightcoral' (web is the default)
52
+ identify('#663399') // 'rebeccapurple'
53
+ identify('#E20074', { palette: pantone }) // '213 C' — T-Mobile magenta
54
+ identify('#E20074', { palette: crayola }) // 'Razzmatazz' — the closest Crayon
55
+ resolve('Pantone 185 C', { palette: pantone }) // '#e4002b'
44
56
 
45
- // Resolve any Pantone code back to RGB — prefix, spacing, case all fine
46
- resolve('Pantone 185 C', { colorspace: 'pantone' })// '#e4002b'
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'
47
59
 
48
- // Format conversion, format-detecting input
60
+ // Format conversion: HEX / RGB / RGBA / HSL / HSV (palette-free, tree-shakes cleanly)
49
61
  convert('#ff0000', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
50
62
  convert({ h: 120, s: 100, l: 50 }, { format: 'HEX' }) // '#00ff00'
51
63
 
52
- // The classic: nearest CSS name
53
- identify('#ff8080') // 'lightcoral'
54
- identify('#bada55') // 'yellowgreen' — also the best-spelled hex
55
- identify('#663399') // 'rebeccapurple'
64
+ // Same convert API understands palette names both ways when you pass one:
65
+ // name in → any structural format out; color in → exact palette key out.
66
+ // `'NAME'` is an exact reverse lookup (throws on miss; use `identify` for fuzzy).
67
+ convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
68
+ convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C' (exact match)
69
+ convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
56
70
  ```
57
71
 
72
+ The identification machinery is palette-agnostic — RAL, HKS, NCS, Munsell, or a bespoke in-house set are the same amount of work (a small file each, or a single object literal). PRs welcome; meanwhile, BYO lets you ship today.
73
+
58
74
  ## Install
59
75
 
60
76
  ```sh
@@ -64,136 +80,322 @@ yarn add chromonym
64
80
  npm install chromonym
65
81
  ```
66
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
+
67
95
  ## API
68
96
 
69
- 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.
70
112
 
71
113
  ### `identify(input, opts?)`
72
114
 
73
- **Color → name.** Finds the nearest-match name in the chosen colorspace 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.
74
116
 
75
117
  ```ts
76
- function identify<C extends ColorspaceName = 'web'>(
77
- input: ColorInput,
78
- opts?: { colorspace?: C; metric?: DistanceMetric },
79
- ): WebColorName | X11ColorName | PantoneColorName | 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 }>;
80
129
  ```
81
130
 
82
- - **Default colorspace**: `'web'`.
83
- - **Default metric**: picked per colorspace — `'deltaE76'` for web and x11 (well-separated palettes), `'deltaE2000'` for pantone (dense, perceptually-packed). Override freely via `metric`.
84
- - Returns the matched name, or `null` if input is unrecognized (`detectFormat` returns `'UNKNOWN'`).
85
- - Return type narrows based on the `colorspace` option — e.g. `identify(hex, { colorspace: 'pantone' })` returns `PantoneColorName | null`, and the no-args call returns `WebColorName | null`.
86
- - 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.
87
136
 
88
137
  ```ts
89
- identify('#ff0000') // 'red'
90
- identify([255, 0, 0]) // 'red'
91
- identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
92
- identify('#ff0000', { colorspace: 'x11' }) // 'red' (or 'red1', X11 has numbered variants)
93
- identify('#ff0000', { colorspace: 'pantone' }) // nearest Pantone C code, e.g. '185C'
94
- identify('#ff0080', { metric: 'deltaE2000' }) // force perceptual-accurate match
95
- 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
+ // ]
96
158
  ```
97
159
 
98
160
  ### `resolve(name, opts?)`
99
161
 
100
- **Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen colorspace, 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.
101
163
 
102
164
  ```ts
165
+ // Strict — exact match via normalizer.
103
166
  function resolve(
104
167
  name: string,
105
- opts?: { colorspace?: ColorspaceName; format?: ColorFormat },
106
- ): ColorValue | null
168
+ opts?: { palette?: Palette; format?: ColorFormat },
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 }>;
107
176
  ```
108
177
 
109
- - **Default colorspace**: `'web'`.
110
- - **Default format**: `'HEX'`.
111
- - Returns `null` if the normalized name isn't in the colorspace.
112
- - Normalization lets users pass `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` etc. — all resolved identically.
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.
113
182
 
114
183
  ```ts
184
+ import { resolve, pantone, crayola } from 'chromonym';
185
+
186
+ // Strict (default) — exact match or null
115
187
  resolve('crimson') // '#dc143c'
116
188
  resolve('Alice Blue') // '#f0f8ff'
117
189
  resolve('alice-blue!') // '#f0f8ff'
118
- resolve('185 C', { colorspace: 'pantone' }) // '#e4002b'
119
- resolve('Pantone 185 C', { colorspace: 'pantone' })// '#e4002b' (same, normalized)
190
+ resolve('185 C', { palette: pantone }) // '#e4002b'
191
+ resolve('Pantone 185 C', { palette: pantone }) // '#e4002b' (same, normalized)
120
192
  resolve('crimson', { format: 'RGB' }) // 'rgb(220, 20, 60)'
121
- resolve('crimson', { format: 'RGBA' }) // { r: 220, g: 20, b: 60, a: 1 }
122
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 }]
123
204
  ```
124
205
 
125
206
  ### `convert(input, opts?)`
126
207
 
127
- **Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba` representation, and returns the value in the target format. Colorspace-independent.
208
+ **Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba`, and returns the value in the target format.
209
+
210
+ Structural on its own (HEX ↔ RGB ↔ RGBA ↔ HSL ↔ HSV, no palette data bundled). Pass an optional `palette` and the same one API understands palette names both ways — input as a name, or `format: 'NAME'` output for exact reverse lookup.
128
211
 
129
212
  ```ts
130
- function convert(
131
- input: ColorInput,
132
- opts?: { format?: ColorFormat },
133
- ): ColorValue
213
+ // Structural (palette-free, tree-shakes to ~5 KB min)
214
+ function convert(input: ColorInput, opts?: { format?: ColorFormat }): ColorValue;
215
+
216
+ // With a palette — input can be a palette name, output can be 'NAME' (exact key)
217
+ function convert<P extends Palette>(
218
+ input: ColorInput | string,
219
+ opts: { palette: P; format: 'NAME' },
220
+ ): PaletteKey<P>;
221
+ function convert<P extends Palette>(
222
+ input: ColorInput | string,
223
+ opts: { palette: P; format?: ColorFormat },
224
+ ): ColorValue;
134
225
  ```
135
226
 
136
227
  - **Default format**: `'HEX'`.
137
- - Throws if input format is unrecognized.
138
- - Alpha information is preserved when converting into RGBA/RGB(A) string forms, and discarded for HEX (6-digit) / HSL / HSV / PANTONE output.
228
+ - Throws on unrecognized input (parser-flavored — `identify` / `resolve` return `null` instead).
229
+ - Alpha is preserved only in `'RGBA'` output (canonical `{ r, g, b, a }` object); dropped by `'HEX'` (6-digit), `'RGB'`, `'HSL'`, `'HSV'`, and `'NAME'`.
230
+ - **Palette is caller-supplied**: no palette is pulled unless you import one and pass it. Tree-shake stays intact — `convert`-only bundles include zero palette data.
231
+ - `format: 'NAME'` is **exact-match only**; it throws if the input isn't a pixel-exact palette entry. Use `identify` for nearest-match.
232
+ - Priority: if the input is structurally valid (hex, rgb, hsl, …), that wins over palette-name lookup. So `convert('#ff0000', { palette: brand })` always means "parse as hex."
139
233
 
140
234
  ```ts
141
- convert('#ff0000') // '#ff0000' (identity)
142
- convert('#ff0000', { format: 'RGB' }) // 'rgb(255, 0, 0)'
143
- convert('#ff0000', { format: 'RGBA' }) // { r: 255, g: 0, b: 0, a: 1 }
144
- convert([255, 0, 0], { format: 'HEX' }) // '#ff0000'
145
- convert('rgb(255, 0, 0)', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
146
- convert({ h: 0, s: 100, l: 50 }, { format: 'HEX' })// '#ff0000'
147
- convert('185 C', { format: 'HEX' }) // '#e4002b'
148
- convert('#e4002b', { format: 'PANTONE' }) // '185C' (nearest Pantone C)
235
+ // Structural — no palette imported, nothing bundled
236
+ convert('#ff0000') // '#ff0000' (identity)
237
+ convert('#ff0000', { format: 'RGB' }) // 'rgb(255, 0, 0)'
238
+ convert([255, 0, 0], { format: 'HEX' }) // '#ff0000'
239
+ convert({ h: 0, s: 100, l: 50 }, { format: 'HEX' }) // '#ff0000'
240
+
241
+ // With a palette — palette-name in, any format out
242
+ import { pantone } from 'chromonym';
243
+ convert('185 C', { palette: pantone }) // '#e4002b'
244
+ convert('Pantone 185 C', { palette: pantone }) // '#e4002b' (normalizer strips prefix)
245
+ convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
246
+
247
+ // With a palette — color in, exact palette key out
248
+ convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C'
249
+ convert('#663399', { palette: web, format: 'NAME' }) // 'rebeccapurple'
250
+ convert('#ff0000', { palette: pantone, format: 'NAME' }) // throws — use identify for nearest
251
+
252
+ // BYO palettes work the same way
253
+ convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
254
+ convert('#ff2a3b', { palette: brand, format: 'NAME' }) // 'acme red'
149
255
  ```
150
256
 
151
- ## Colorspaces
257
+ The low-level `pantoneToRgba` / `rgbaToPantone` functions (from `chromonym` or `chromonym/conversions/pantone`) remain available for single-purpose Pantone work when you don't need the full `convert` ceremony.
258
+
259
+ ## Palettes
260
+
261
+ Every palette — yours or ours — is a `Palette<Name>` object. Define it once, pass it anywhere the three verbs accept one.
262
+
263
+ ```ts
264
+ type Palette<Name extends string = string> = {
265
+ readonly name: string; // human-readable label
266
+ readonly colors: Record<Name, HexColor>; // the lookup data
267
+ readonly normalize: (s: string) => string; // user-input → canonical key
268
+ readonly defaultMetric: DistanceMetric; // used by identify when no metric override
269
+ };
270
+ ```
271
+
272
+ ### Bring your own
273
+
274
+ The primary shape. Any object matching `Palette<Name>` works — no registry, no plugin, no side effects. Bundlers only include the palettes you actually import.
275
+
276
+ ```ts
277
+ import { identify, resolve, type Palette } from 'chromonym';
278
+
279
+ const warhammer = {
280
+ name: 'warhammer40k',
281
+ colors: {
282
+ 'world eaters red': '#8b1a1a',
283
+ 'adeptus red': '#652022',
284
+ 'sons of malice white': '#e8e4d8',
285
+ 'the flawless host purple': '#6b2d7d',
286
+ 'nurgle green': '#748c3f',
287
+ 'alpha legion teal': '#2a6d7a',
288
+ },
289
+ normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
290
+ defaultMetric: 'deltaE2000',
291
+ } as const satisfies Palette;
292
+
293
+ identify('#750c0c', { palette: warhammer })
294
+ // → 'world eaters red' (return type narrows to the palette's key union)
295
+
296
+ resolve('Nurgle Green', { palette: warhammer })
297
+ // → '#748c3f' (your normalizer handles case + punctuation)
298
+ ```
299
+
300
+ ### The four we ship
152
301
 
153
302
  | Name | Entries | Source |
154
303
  |---|---|---|
155
- | `'web'` (default) | 148 | CSS Color Module Level 4 named colors |
156
- | `'x11'` | 658 | X.Org `rgb.txt` (public domain) |
157
- | `'pantone'` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
304
+ | `web` (default) | 148 | CSS Color Module Level 4 named colors |
305
+ | `x11` | 658 | X.Org `rgb.txt` (public domain) |
306
+ | `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
307
+ | `crayola` | ~85 | Crayola crayon colors — community approximations (not Crayola-licensed) |
158
308
 
159
- Each colorspace is a pure data object: `Record<string, HexColor>`. Importable directly:
309
+ Importable directly, or via subpath exports for stricter tree-shaking:
160
310
 
161
311
  ```ts
162
- import { web, x11, pantone } from 'chromonym';
163
- web.crimson // '#dc143c'
312
+ import { web, x11, pantone, crayola } from 'chromonym';
313
+ // or: import { pantone } from 'chromonym/pantone';
314
+
315
+ web.colors.crimson // '#dc143c'
316
+ web.colors.aliceblue // '#f0f8ff' (CSS keyword form — paste-compatible)
317
+ pantone.colors['185 C'] // '#e4002b'
318
+ crayola.colors.Razzmatazz // '#e3256b'
319
+ crayola.colors['Granny Smith Apple'] // '#a8e4a0'
164
320
  ```
165
321
 
166
- *Trivia:* `web.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.gray73` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
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'`).
323
+
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.
325
+
326
+ ### Cross-palette translation
327
+
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.
329
+
330
+ ```ts
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
+ // ]
357
+ ```
358
+
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:
360
+
361
+ ```ts
362
+ const hex = convert('rebeccapurple', { palette: web }); // '#663399' (throws on miss)
363
+ const near = identify(hex, { palette: pantone }); // '267 C'
364
+ ```
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.
167
367
 
168
368
  ## Distance metrics
169
369
 
170
370
  *Six ways to ask "how different are two colors?"*
171
371
 
172
- `identify` picks the nearest-named color by comparing your input to every entry in the chosen colorspace. *How* it measures "nearest" is controlled by `metric`. Six options, roughly ordered from fast-and-approximate to slow-and-correct:
372
+ `identify` picks the nearest-named color by comparing your input to every entry in the chosen palette. *How* it measures "nearest" is controlled by `metric`. Six options, roughly ordered from fast-and-approximate to slow-and-correct:
173
373
 
174
374
  | Metric | What it does | When to use |
175
375
  |---|---|---|
176
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. |
177
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. |
178
- | `'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. |
179
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. |
180
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. |
181
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. |
182
382
 
183
383
  Each metric trades cost for accuracy — the full scan over 907 Pantone entries is well under 1 ms even with ΔE2000. For interactive UIs, cost is usually irrelevant; drop to a cheaper metric when batch-processing millions of colors or when you need specific tie-breaking behavior.
184
384
 
385
+ > **Note on x11's default (ΔE76):** x11 has 658 entries, many of them saturated blue/purple ramps where ΔE76 is a known weak spot. The default optimizes for UI scrubbing speed (~5 µs vs ~90 µs for ΔE2000). If perceptual accuracy matters more than latency, override per call with `{ metric: 'deltaEok' }` — same cost as ΔE76 on cached Lab/OKLAB indexes, better separation in blues.
386
+
185
387
  ```ts
186
- // Defaults: deltaE76 for web/x11, deltaE2000 for pantone.
388
+ // Defaults: read from the palette's own `defaultMetric`.
187
389
  identify('#ff8080') // deltaE76 (web default)
188
- identify('#ff8080', { colorspace: 'pantone' }) // deltaE2000 (pantone default)
390
+ identify('#ff8080', { palette: pantone }) // deltaE2000 (pantone default)
189
391
 
190
392
  // Override per-call:
191
393
  identify('#ff8080', { metric: 'euclidean-srgb' }) // fastest
192
394
  identify('#ff8080', { metric: 'deltaE2000' }) // most accurate
193
- identify('#ff8080', { colorspace: 'pantone', metric: 'euclidean-srgb' }) // force fast for Pantone
395
+ identify('#ff8080', { palette: pantone, metric: 'euclidean-srgb' }) // force fast for Pantone
194
396
  ```
195
397
 
196
- The low-level `rgbaToPantone` always uses `'deltaE2000'`.
398
+ The low-level `rgbaToPantone` reads `pantone.defaultMetric` (currently `'deltaE2000'`).
197
399
 
198
400
  ## Formats
199
401
 
@@ -204,7 +406,6 @@ The low-level `rgbaToPantone` always uses `'deltaE2000'`.
204
406
  | `'RGBA'` | `'rgba(r, g, b, a)'`, `[r, g, b, a]`, `{ r, g, b, a }` | `{ r, g, b, a }` object |
205
407
  | `'HSL'` | `'hsl(h, s%, l%)'`, `{ h, s, l }` | `'hsl(h, s%, l%)'` string |
206
408
  | `'HSV'` | `'hsv(h, s%, v%)'`, `{ h, s, v }` | `'hsv(h, s%, v%)'` string |
207
- | `'PANTONE'` | `'185 C'`, `'185C'`, `'Pantone 185 C'`, etc. | `'185C'`-style string |
208
409
 
209
410
  ## Error handling
210
411
 
@@ -229,6 +430,7 @@ const tryConvert = (input: ColorInput, opts = {}) => {
229
430
  ## Utilities
230
431
 
231
432
  - **`detectFormat(input): DetectedFormat`** — runtime format detection. Returns one of the format keys or `'UNKNOWN'`.
433
+ - **`isColor(input): boolean`** — type-guard wrapper around `detectFormat`. Returns `true` if the input is a recognized structural color shape.
232
434
  - **`COLOR_FORMATS: ReadonlySet<ColorFormat>`** — the Set of valid format keys.
233
435
  - **Per-format converters** (tree-shakeable) — `hexToRgba`, `rgbaToHex`, `rgbToRgba`, `rgbaToRgb`, `hslToRgba`, `rgbaToHsl`, `hsvToRgba`, `rgbaToHsv`, `pantoneToRgba`, `rgbaToPantone`. Use these directly if you only need one conversion and want maximum tree-shaking.
234
436
 
@@ -237,7 +439,9 @@ const tryConvert = (input: ColorInput, opts = {}) => {
237
439
  - `sideEffects: false` in `package.json`.
238
440
  - All exports are named; no default export.
239
441
  - Barrel uses explicit re-exports.
240
- - Importing `{ resolve }` or `{ identify }` pulls in all three colorspaces (necessary for the string-keyed `colorspace` option). For strict tree-shaking of colorspace data, use the low-level converters and colorspace objects directly.
442
+ - `identify` / `resolve` take the palette **as an object**, not a string key. You only pay for palettes you actually import: calling `identify(hex)` with no override pulls in `web` (148 entries) and nothing else; passing `{ palette: pantone }` adds pantone. X11 and pantone are *not* bundled unless you reference them.
443
+ - BYO palettes have zero library cost beyond your own data.
444
+ - Subpath exports (`chromonym/web`, `chromonym/x11`, `chromonym/pantone`, `chromonym/conversions/hex`, `chromonym/math/deltaE`, etc.) let you import a single palette or converter without going through the root barrel.
241
445
 
242
446
  ## Types
243
447
 
@@ -246,9 +450,10 @@ Re-exported from the root barrel — `import type { ... } from 'chromonym'`:
246
450
  | Category | Types |
247
451
  |---|---|
248
452
  | Input / output unions | `ColorInput`, `ColorValue` |
249
- | Format & colorspace keys | `ColorFormat`, `ColorspaceName` |
250
- | Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput`, `PantoneCode` |
251
- | Palette container | `Colorspace` |
453
+ | Format keys | `ColorFormat` |
454
+ | Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput` |
455
+ | Palette container | `Palette<Name>`, `NormalizeFn` |
456
+ | Distance selector | `DistanceMetric` |
252
457
  | Color-name unions | `WebColorName`, `X11ColorName`, `PantoneColorName` |
253
458
 
254
459
  ## Development
@@ -261,7 +466,7 @@ bun test
261
466
  bun run build
262
467
  ```
263
468
 
264
- Regenerating colorspace data:
469
+ Regenerating palette data:
265
470
 
266
471
  ```sh
267
472
  bun run scripts/generate-x11.ts # reads /usr/share/X11/rgb.txt
@@ -284,8 +489,10 @@ chromonym deliberately limits its scope to color *naming* (resolving and identif
284
489
 
285
490
  MIT © [the-simian](https://github.com/the-simian)
286
491
 
492
+ <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
493
+
287
494
  ---
288
495
 
289
496
  > ### Pantone® trademark notice
290
497
  >
291
- > **Pantone®** is a registered trademark of **Pantone LLC**. Chromonym is **not affiliated with, endorsed by, or certified by Pantone LLC**. The `pantone` colorspace ships **community-derived sRGB approximations** of the Pantone Coated (C) set. Values will **not** match a licensed Pantone reference exactly and are unsuitable for print color specification. See [`NOTICE.md`](./NOTICE.md) for full text.
498
+ > **Pantone®** is a registered trademark of **Pantone LLC**. Chromonym is **not affiliated with, endorsed by, or certified by Pantone LLC**. The `pantone` palette ships **community-derived sRGB approximations** of the Pantone Coated (C) set. Values will **not** match a licensed Pantone reference exactly and are unsuitable for print color specification. See [`NOTICE.md`](./NOTICE.md) for full text.
@@ -1,4 +1,4 @@
1
- import type { HexColor, Rgba } from '../types';
1
+ import type { HexColor, Rgba } from '../types.js';
2
2
  /**
3
3
  * Parse a hex color string into the canonical Rgba representation.
4
4
  * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).
@@ -1 +1 @@
1
- {"version":3,"file":"hex.d.ts","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAsB7C;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAE9C"}
1
+ {"version":3,"file":"hex.d.ts","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAsB7C;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAE9C","sourcesContent":["import { HEX_RE } from '../detectFormat.js';\nimport { clamp } from '../math/clamp.js';\nimport type { HexColor, Rgba } from '../types.js';\n\n/**\n * Parse a hex color string into the canonical Rgba representation.\n * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).\n * Throws on any other input shape.\n */\nexport function hexToRgba(hex: HexColor): Rgba {\n const match = HEX_RE.exec(hex);\n const body = match?.[1];\n if (!body) throw new Error(`Invalid hex color: ${hex}`);\n\n if (body.length === 3) {\n const r = body.charAt(0);\n const g = body.charAt(1);\n const b = body.charAt(2);\n return {\n r: parseInt(r + r, 16),\n g: parseInt(g + g, 16),\n b: parseInt(b + b, 16),\n a: 1,\n };\n }\n\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n const a = body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n}\n\nfunction channelToHex(n: number): string {\n return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');\n}\n\n/**\n * Emit a 6-digit lowercase hex string. Alpha is dropped; out-of-range\n * channel values are clamped to 0..255; fractional values are rounded.\n */\nexport function rgbaToHex(rgba: Rgba): HexColor {\n return `#${channelToHex(rgba.r)}${channelToHex(rgba.g)}${channelToHex(rgba.b)}` as HexColor;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { HEX_RE } from '../detectFormat';
2
- import { clamp } from '../math/clamp';
1
+ import { HEX_RE } from '../detectFormat.js';
2
+ import { clamp } from '../math/clamp.js';
3
3
  /**
4
4
  * Parse a hex color string into the canonical Rgba representation.
5
5
  * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).
@@ -1 +1 @@
1
- {"version":3,"file":"hex.js","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAExD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO;YACL,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,CAAC;SACL,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAc,CAAC;AAC9F,CAAC"}
1
+ {"version":3,"file":"hex.js","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAExD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO;YACL,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,CAAC;SACL,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAc,CAAC;AAC9F,CAAC","sourcesContent":["import { HEX_RE } from '../detectFormat.js';\nimport { clamp } from '../math/clamp.js';\nimport type { HexColor, Rgba } from '../types.js';\n\n/**\n * Parse a hex color string into the canonical Rgba representation.\n * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).\n * Throws on any other input shape.\n */\nexport function hexToRgba(hex: HexColor): Rgba {\n const match = HEX_RE.exec(hex);\n const body = match?.[1];\n if (!body) throw new Error(`Invalid hex color: ${hex}`);\n\n if (body.length === 3) {\n const r = body.charAt(0);\n const g = body.charAt(1);\n const b = body.charAt(2);\n return {\n r: parseInt(r + r, 16),\n g: parseInt(g + g, 16),\n b: parseInt(b + b, 16),\n a: 1,\n };\n }\n\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n const a = body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n}\n\nfunction channelToHex(n: number): string {\n return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');\n}\n\n/**\n * Emit a 6-digit lowercase hex string. Alpha is dropped; out-of-range\n * channel values are clamped to 0..255; fractional values are rounded.\n */\nexport function rgbaToHex(rgba: Rgba): HexColor {\n return `#${channelToHex(rgba.r)}${channelToHex(rgba.g)}${channelToHex(rgba.b)}` as HexColor;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { HslInput, HslString, Rgba } from '../types';
1
+ import type { HslInput, HslString, Rgba } from '../types.js';
2
2
  /**
3
3
  * Parse HSL input (string or object) into canonical Rgba.
4
4
  * Hue wraps modulo 360; saturation and lightness are 0..100 percent.
@@ -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,UAAU,CAAC;AAK1D;;;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"}
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,5 +1,5 @@
1
- import { requireFinite } from '../math/clamp';
2
- import { hueSectorToPrime } from '../math/hueSector';
1
+ import { requireFinite } from '../math/clamp.js';
2
+ import { hueSectorToPrime } from '../math/hueSector.js';
3
3
  const HSL_RE = /^hsl\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)%?\s*,\s*(-?\d+(?:\.\d+)?)%?\s*\)$/i;
4
4
  /**
5
5
  * Parse HSL input (string or object) into canonical Rgba.