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.
- package/NOTICE.md +64 -4
- package/README.md +290 -83
- package/dist/conversions/hex.d.ts +1 -1
- package/dist/conversions/hex.d.ts.map +1 -1
- package/dist/conversions/hex.js +2 -2
- package/dist/conversions/hex.js.map +1 -1
- package/dist/conversions/hsl.d.ts +1 -1
- package/dist/conversions/hsl.d.ts.map +1 -1
- package/dist/conversions/hsl.js +2 -2
- package/dist/conversions/hsl.js.map +1 -1
- package/dist/conversions/hsv.d.ts +1 -1
- package/dist/conversions/hsv.d.ts.map +1 -1
- package/dist/conversions/hsv.js +2 -2
- package/dist/conversions/hsv.js.map +1 -1
- package/dist/conversions/index.d.ts +5 -5
- package/dist/conversions/index.d.ts.map +1 -1
- package/dist/conversions/index.js +5 -5
- package/dist/conversions/index.js.map +1 -1
- package/dist/conversions/pantone.d.ts +9 -7
- package/dist/conversions/pantone.d.ts.map +1 -1
- package/dist/conversions/pantone.js +11 -10
- package/dist/conversions/pantone.js.map +1 -1
- package/dist/conversions/rgb.d.ts +1 -1
- package/dist/conversions/rgb.d.ts.map +1 -1
- package/dist/conversions/rgb.js +1 -1
- package/dist/conversions/rgb.js.map +1 -1
- package/dist/convert.d.ts +45 -5
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +68 -17
- package/dist/convert.js.map +1 -1
- package/dist/detectFormat.d.ts +1 -1
- package/dist/detectFormat.d.ts.map +1 -1
- package/dist/detectFormat.js +2 -6
- package/dist/detectFormat.js.map +1 -1
- package/dist/identify.d.ts +41 -19
- package/dist/identify.d.ts.map +1 -1
- package/dist/identify.js +37 -22
- package/dist/identify.js.map +1 -1
- package/dist/index.d.ts +15 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/indexing.d.ts +48 -24
- package/dist/indexing.d.ts.map +1 -1
- package/dist/indexing.js +111 -22
- package/dist/indexing.js.map +1 -1
- package/dist/math/clamp.d.ts.map +1 -1
- package/dist/math/clamp.js.map +1 -1
- package/dist/math/colorSpace.d.ts +1 -1
- package/dist/math/colorSpace.d.ts.map +1 -1
- package/dist/math/colorSpace.js.map +1 -1
- package/dist/math/deltaE.d.ts.map +1 -1
- package/dist/math/deltaE.js.map +1 -1
- package/dist/math/editDistance.d.ts +17 -0
- package/dist/math/editDistance.d.ts.map +1 -0
- package/dist/math/editDistance.js +48 -0
- package/dist/math/editDistance.js.map +1 -0
- package/dist/math/euclideanDistance.d.ts +1 -1
- package/dist/math/euclideanDistance.d.ts.map +1 -1
- package/dist/math/euclideanDistance.js.map +1 -1
- package/dist/math/hueSector.d.ts.map +1 -1
- package/dist/math/hueSector.js.map +1 -1
- package/dist/palettes/crayola.d.ts +200 -0
- package/dist/palettes/crayola.d.ts.map +1 -0
- package/dist/palettes/crayola.js +121 -0
- package/dist/palettes/crayola.js.map +1 -0
- package/dist/palettes/index.d.ts +5 -0
- package/dist/palettes/index.d.ts.map +1 -0
- package/dist/palettes/index.js +5 -0
- package/dist/palettes/index.js.map +1 -0
- package/dist/palettes/normalize.d.ts +17 -0
- package/dist/palettes/normalize.d.ts.map +1 -0
- package/dist/palettes/normalize.js +19 -0
- package/dist/palettes/normalize.js.map +1 -0
- package/dist/palettes/pantone.d.ts +1834 -0
- package/dist/palettes/pantone.d.ts.map +1 -0
- package/dist/palettes/pantone.js +925 -0
- package/dist/palettes/pantone.js.map +1 -0
- package/dist/palettes/web.d.ts +320 -0
- package/dist/palettes/web.d.ts.map +1 -0
- package/dist/{colorspaces → palettes}/web.js +18 -3
- package/dist/palettes/web.js.map +1 -0
- package/dist/palettes/x11.d.ts +1346 -0
- package/dist/palettes/x11.d.ts.map +1 -0
- package/dist/palettes/x11.js +686 -0
- package/dist/palettes/x11.js.map +1 -0
- package/dist/resolve.d.ts +42 -7
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +48 -20
- package/dist/resolve.js.map +1 -1
- package/dist/types.d.ts +10 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +38 -18
- package/dist/colorspaces/index.d.ts +0 -4
- package/dist/colorspaces/index.d.ts.map +0 -1
- package/dist/colorspaces/index.js +0 -4
- package/dist/colorspaces/index.js.map +0 -1
- package/dist/colorspaces/pantone.d.ts +0 -919
- package/dist/colorspaces/pantone.d.ts.map +0 -1
- package/dist/colorspaces/pantone.js +0 -918
- package/dist/colorspaces/pantone.js.map +0 -1
- package/dist/colorspaces/registry.d.ts +0 -26
- package/dist/colorspaces/registry.d.ts.map +0 -1
- package/dist/colorspaces/registry.js +0 -36
- package/dist/colorspaces/registry.js.map +0 -1
- package/dist/colorspaces/web.d.ts +0 -156
- package/dist/colorspaces/web.d.ts.map +0 -1
- package/dist/colorspaces/web.js.map +0 -1
- package/dist/colorspaces/x11.d.ts +0 -667
- package/dist/colorspaces/x11.d.ts.map +0 -1
- package/dist/colorspaces/x11.js +0 -666
- package/dist/colorspaces/x11.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,18 +11,16 @@
|
|
|
11
11
|
# chromonym
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/chromonym)
|
|
14
|
-
[](https://packagephobia.com/result?p=chromonym)
|
|
15
|
-
[](https://packagephobia.com/result?p=chromonym)
|
|
16
14
|
[](https://www.npmjs.com/package/chromonym)
|
|
17
15
|
[](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml)
|
|
18
|
-
|
|
19
|
-
[
|
|
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> • <code>resolve</code> • <code>convert</code>
|
|
23
21
|
</p>
|
|
24
22
|
|
|
25
|
-
**
|
|
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
|
|
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
|
-
//
|
|
43
|
-
|
|
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
|
-
//
|
|
46
|
-
|
|
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,
|
|
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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
- Return type
|
|
86
|
-
- Ties go to whichever color was defined first
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
identify('#ff0000'
|
|
93
|
-
identify(
|
|
94
|
-
identify('#
|
|
95
|
-
identify('#ff0000', {
|
|
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
|
|
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?: {
|
|
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
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- Normalization
|
|
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', {
|
|
119
|
-
resolve('Pantone 185 C', {
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
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
|
|
138
|
-
- Alpha
|
|
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
|
-
|
|
142
|
-
convert('#ff0000'
|
|
143
|
-
convert('#ff0000', { format: '
|
|
144
|
-
convert([255, 0, 0], { format: 'HEX' })
|
|
145
|
-
convert(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
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
|
-
|
|
309
|
+
Importable directly, or via subpath exports for stricter tree-shaking:
|
|
160
310
|
|
|
161
311
|
```ts
|
|
162
|
-
import { web, x11, pantone } from 'chromonym';
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
388
|
+
// Defaults: read from the palette's own `defaultMetric`.
|
|
187
389
|
identify('#ff8080') // deltaE76 (web default)
|
|
188
|
-
identify('#ff8080', {
|
|
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', {
|
|
395
|
+
identify('#ff8080', { palette: pantone, metric: 'euclidean-srgb' }) // force fast for Pantone
|
|
194
396
|
```
|
|
195
397
|
|
|
196
|
-
The low-level `rgbaToPantone`
|
|
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
|
-
-
|
|
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
|
|
250
|
-
| Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput
|
|
251
|
-
| Palette container | `
|
|
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
|
|
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`
|
|
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 +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,
|
|
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"]}
|
package/dist/conversions/hex.js
CHANGED
|
@@ -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,
|
|
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 +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,
|
|
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"]}
|
package/dist/conversions/hsl.js
CHANGED
|
@@ -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.
|