chromonym 1.4.1 → 2.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 +32 -4
- package/README.md +216 -75
- 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 +13 -16
- package/dist/identify.d.ts.map +1 -1
- package/dist/identify.js +12 -12
- package/dist/identify.js.map +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/indexing.d.ts +30 -24
- package/dist/indexing.d.ts.map +1 -1
- package/dist/indexing.js +49 -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/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/index.d.ts +4 -0
- package/dist/palettes/index.d.ts.map +1 -0
- package/dist/palettes/index.js +4 -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 +9 -7
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +18 -17
- 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 +32 -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/NOTICE.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
**not affiliated with, endorsed by, licensed by, or certified by Pantone
|
|
7
7
|
LLC**.
|
|
8
8
|
|
|
9
|
-
The `pantone`
|
|
9
|
+
The `pantone` palette in this library ships **community-derived sRGB
|
|
10
10
|
approximations** of the Pantone Coated (C) color set, sourced from
|
|
11
11
|
[`color_library`](https://github.com/draganradu/color_library) (MIT © Radu
|
|
12
12
|
Dragan). These values:
|
|
@@ -16,21 +16,49 @@ Dragan). These values:
|
|
|
16
16
|
exactly.
|
|
17
17
|
- Are intended for on-screen reference, nearest-name identification, and
|
|
18
18
|
general tooling — **not for print color specification**.
|
|
19
|
-
- Use standard color codes (e.g. `
|
|
19
|
+
- Use standard color codes (e.g. `185 C`) as **nominative references** to
|
|
20
20
|
publicly identified Pantone colors. No endorsement or claim of accuracy
|
|
21
21
|
is implied.
|
|
22
22
|
|
|
23
|
+
### Accuracy envelope
|
|
24
|
+
|
|
25
|
+
The approximations are closest for mid-chroma colors under a D65 / 2°
|
|
26
|
+
observer. As a rough guide:
|
|
27
|
+
|
|
28
|
+
- **Mid-chroma Pantones**: ΔE*00 ≲ 5 versus Pantone's own sRGB previews.
|
|
29
|
+
Usable for nearest-name identification and comparative swatch UI.
|
|
30
|
+
- **Saturated edges**: ΔE*00 commonly 5–10, higher in the deep-blue and
|
|
31
|
+
blue-violet region (~2736 C through ~2746 C). `identify` will still
|
|
32
|
+
converge on a plausible neighbor, but the *which* can depend on metric
|
|
33
|
+
choice — see the "Distance metrics" section of the README.
|
|
34
|
+
- **Fluorescents (801 C – 814 C)** and the **metallic series (8xxx C)**
|
|
35
|
+
are defined outside the sRGB gamut. Their sRGB approximations are
|
|
36
|
+
perceptually wrong by construction — the physical inks can't be
|
|
37
|
+
reproduced on a standard monitor. Do not use these values as a
|
|
38
|
+
reference for brand-critical matching; identify carefully or skip.
|
|
39
|
+
- **Neons / warm reds** clipping against the sRGB gamut boundary (Pantone
|
|
40
|
+
Red 032 / Warm Red / Red 021 area) are approximated via hard clip.
|
|
41
|
+
|
|
42
|
+
### What `identify` does *not* guarantee
|
|
43
|
+
|
|
44
|
+
`identify(sampledRgba, { palette: pantone })` returns the nearest
|
|
45
|
+
entry **in the shipped sRGB approximation set**. It does **not** imply
|
|
46
|
+
that the physical ink bearing that Pantone code would match your sampled
|
|
47
|
+
color in a print workflow, under a different light source, or on
|
|
48
|
+
different paper stock. Pantone is a spectral specification; any
|
|
49
|
+
color-library lookup that rounds it through sRGB loses information.
|
|
50
|
+
|
|
23
51
|
If you need Pantone-certified color values, consult Pantone directly:
|
|
24
52
|
<https://www.pantone.com/>.
|
|
25
53
|
|
|
26
54
|
## X11 / X.Org
|
|
27
55
|
|
|
28
|
-
The `x11`
|
|
56
|
+
The `x11` palette is derived from the X.Org `rgb.txt` file, which is in
|
|
29
57
|
the public domain.
|
|
30
58
|
|
|
31
59
|
## CSS / SVG named colors
|
|
32
60
|
|
|
33
|
-
The `web`
|
|
61
|
+
The `web` palette mirrors the CSS Color Module Level 4 named-color
|
|
34
62
|
set, which is a public specification.
|
|
35
63
|
|
|
36
64
|
## Upstream dependencies
|
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, and Pantone out of the box; bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Perceptual accuracy via six distance metrics. Tree-shakes to the bone — you only pay for palettes you import.
|
|
26
24
|
|
|
27
25
|
<p align="center">
|
|
28
26
|
<a href="https://simiancraft.github.io/chromonym/">
|
|
@@ -30,31 +28,45 @@
|
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
import { identify, resolve, convert, pantone, 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 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
|
+
resolve('Pantone 185 C', { palette: pantone }) // '#e4002b'
|
|
47
55
|
|
|
48
|
-
// Format conversion,
|
|
56
|
+
// Format conversion: HEX / RGB / RGBA / HSL / HSV (palette-free, tree-shakes cleanly)
|
|
49
57
|
convert('#ff0000', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
|
|
50
58
|
convert({ h: 120, s: 100, l: 50 }, { format: 'HEX' }) // '#00ff00'
|
|
51
59
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
// Same convert API understands palette names both ways when you pass one:
|
|
61
|
+
// name in → any structural format out; color in → exact palette key out.
|
|
62
|
+
// `'NAME'` is an exact reverse lookup (throws on miss; use `identify` for fuzzy).
|
|
63
|
+
convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
|
|
64
|
+
convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C' (exact match)
|
|
65
|
+
convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
|
|
56
66
|
```
|
|
57
67
|
|
|
68
|
+
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.
|
|
69
|
+
|
|
58
70
|
## Install
|
|
59
71
|
|
|
60
72
|
```sh
|
|
@@ -70,53 +82,58 @@ Three primary functions. All accept an optional `opts` object; defaults fire whe
|
|
|
70
82
|
|
|
71
83
|
### `identify(input, opts?)`
|
|
72
84
|
|
|
73
|
-
**Color → name.** Finds the nearest-match name in the chosen
|
|
85
|
+
**Color → name.** Finds the nearest-match name in the chosen palette using the selected perceptual distance metric.
|
|
74
86
|
|
|
75
87
|
```ts
|
|
76
|
-
function identify<C extends
|
|
88
|
+
function identify<C extends Palette = typeof web>(
|
|
77
89
|
input: ColorInput,
|
|
78
|
-
opts?: {
|
|
79
|
-
):
|
|
90
|
+
opts?: { palette?: C; metric?: DistanceMetric },
|
|
91
|
+
): Extract<keyof C['colors'], string> | null
|
|
80
92
|
```
|
|
81
93
|
|
|
82
|
-
- **Default
|
|
83
|
-
- **Default metric**:
|
|
94
|
+
- **Default palette**: the built-in `web` palette (148 entries, small — included if you call `identify` without a palette).
|
|
95
|
+
- **Default metric**: read from the palette's own `defaultMetric` — `'deltaE76'` for web and x11 (well-separated palettes), `'deltaE2000'` for pantone (dense, perceptually-packed). Override freely via `metric`.
|
|
84
96
|
- Returns the matched name, or `null` if input is unrecognized (`detectFormat` returns `'UNKNOWN'`).
|
|
85
|
-
- Return type
|
|
97
|
+
- Return type is inferred from the palette's `colors` keys — so passing `{ palette: pantone }` narrows to `PantoneColorName | null`, and a BYO palette with a literal key union narrows to that union.
|
|
86
98
|
- Ties go to whichever color was defined first — deterministic across runs, but not semantically meaningful.
|
|
87
99
|
|
|
88
100
|
```ts
|
|
89
|
-
identify
|
|
90
|
-
|
|
91
|
-
identify(
|
|
92
|
-
identify(
|
|
93
|
-
identify(
|
|
94
|
-
identify('#
|
|
95
|
-
identify('#
|
|
101
|
+
import { identify, pantone, x11 } from 'chromonym';
|
|
102
|
+
|
|
103
|
+
identify('#ff0000') // 'red' (web default)
|
|
104
|
+
identify([255, 0, 0]) // 'red'
|
|
105
|
+
identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
|
|
106
|
+
identify('#ff0000', { palette: x11 }) // 'red' (x11 also has 'red 1' through 'red 4')
|
|
107
|
+
identify('#e4002b', { palette: pantone }) // '185 C' (exact match — Target/Coca-Cola red)
|
|
108
|
+
identify('#ff0000', { palette: pantone }) // '172 C' (nearest — pure red isn't a Pantone)
|
|
109
|
+
identify('#ff0080', { metric: 'deltaE2000' }) // force perceptual-accurate match
|
|
110
|
+
identify('#ff0000', { metric: 'euclidean-srgb' }) // force fastest (non-perceptual) match
|
|
96
111
|
```
|
|
97
112
|
|
|
98
113
|
### `resolve(name, opts?)`
|
|
99
114
|
|
|
100
|
-
**Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen
|
|
115
|
+
**Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen palette, and returns the value in the chosen format.
|
|
101
116
|
|
|
102
117
|
```ts
|
|
103
118
|
function resolve(
|
|
104
119
|
name: string,
|
|
105
|
-
opts?: {
|
|
120
|
+
opts?: { palette?: Palette; format?: ColorFormat },
|
|
106
121
|
): ColorValue | null
|
|
107
122
|
```
|
|
108
123
|
|
|
109
|
-
- **Default
|
|
124
|
+
- **Default palette**: the built-in `web` palette.
|
|
110
125
|
- **Default format**: `'HEX'`.
|
|
111
|
-
- Returns `null` if the normalized name isn't in the
|
|
112
|
-
- Normalization
|
|
126
|
+
- Returns `null` if the normalized name isn't in the palette.
|
|
127
|
+
- Normalization is the palette's own `normalize` function — web/x11 lowercase + strip non-alphanumeric, pantone also strips a `Pantone` / `PMS` prefix. So `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` all resolve identically in their respective palettes.
|
|
113
128
|
|
|
114
129
|
```ts
|
|
130
|
+
import { resolve, pantone } from 'chromonym';
|
|
131
|
+
|
|
115
132
|
resolve('crimson') // '#dc143c'
|
|
116
133
|
resolve('Alice Blue') // '#f0f8ff'
|
|
117
134
|
resolve('alice-blue!') // '#f0f8ff'
|
|
118
|
-
resolve('185 C', {
|
|
119
|
-
resolve('Pantone 185 C', {
|
|
135
|
+
resolve('185 C', { palette: pantone }) // '#e4002b'
|
|
136
|
+
resolve('Pantone 185 C', { palette: pantone }) // '#e4002b' (same, normalized)
|
|
120
137
|
resolve('crimson', { format: 'RGB' }) // 'rgb(220, 20, 60)'
|
|
121
138
|
resolve('crimson', { format: 'RGBA' }) // { r: 220, g: 20, b: 60, a: 1 }
|
|
122
139
|
resolve('not-a-color') // null
|
|
@@ -124,52 +141,169 @@ resolve('not-a-color') // null
|
|
|
124
141
|
|
|
125
142
|
### `convert(input, opts?)`
|
|
126
143
|
|
|
127
|
-
**Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba
|
|
144
|
+
**Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba`, and returns the value in the target format.
|
|
145
|
+
|
|
146
|
+
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
147
|
|
|
129
148
|
```ts
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
149
|
+
// Structural (palette-free, tree-shakes to ~5 KB min)
|
|
150
|
+
function convert(input: ColorInput, opts?: { format?: ColorFormat }): ColorValue;
|
|
151
|
+
|
|
152
|
+
// With a palette — input can be a palette name, output can be 'NAME' (exact key)
|
|
153
|
+
function convert<P extends Palette>(
|
|
154
|
+
input: ColorInput | string,
|
|
155
|
+
opts: { palette: P; format: 'NAME' },
|
|
156
|
+
): PaletteKey<P>;
|
|
157
|
+
function convert<P extends Palette>(
|
|
158
|
+
input: ColorInput | string,
|
|
159
|
+
opts: { palette: P; format?: ColorFormat },
|
|
160
|
+
): ColorValue;
|
|
134
161
|
```
|
|
135
162
|
|
|
136
163
|
- **Default format**: `'HEX'`.
|
|
137
|
-
- Throws
|
|
138
|
-
- Alpha
|
|
164
|
+
- Throws on unrecognized input (parser-flavored — `identify` / `resolve` return `null` instead).
|
|
165
|
+
- Alpha is preserved only in `'RGBA'` output (canonical `{ r, g, b, a }` object); dropped by `'HEX'` (6-digit), `'RGB'`, `'HSL'`, `'HSV'`, and `'NAME'`.
|
|
166
|
+
- **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.
|
|
167
|
+
- `format: 'NAME'` is **exact-match only**; it throws if the input isn't a pixel-exact palette entry. Use `identify` for nearest-match.
|
|
168
|
+
- 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."
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Structural — no palette imported, nothing bundled
|
|
172
|
+
convert('#ff0000') // '#ff0000' (identity)
|
|
173
|
+
convert('#ff0000', { format: 'RGB' }) // 'rgb(255, 0, 0)'
|
|
174
|
+
convert([255, 0, 0], { format: 'HEX' }) // '#ff0000'
|
|
175
|
+
convert({ h: 0, s: 100, l: 50 }, { format: 'HEX' }) // '#ff0000'
|
|
176
|
+
|
|
177
|
+
// With a palette — palette-name in, any format out
|
|
178
|
+
import { pantone } from 'chromonym';
|
|
179
|
+
convert('185 C', { palette: pantone }) // '#e4002b'
|
|
180
|
+
convert('Pantone 185 C', { palette: pantone }) // '#e4002b' (normalizer strips prefix)
|
|
181
|
+
convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
|
|
182
|
+
|
|
183
|
+
// With a palette — color in, exact palette key out
|
|
184
|
+
convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C'
|
|
185
|
+
convert('#663399', { palette: web, format: 'NAME' }) // 'rebeccapurple'
|
|
186
|
+
convert('#ff0000', { palette: pantone, format: 'NAME' }) // throws — use identify for nearest
|
|
187
|
+
|
|
188
|
+
// BYO palettes work the same way
|
|
189
|
+
convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
|
|
190
|
+
convert('#ff2a3b', { palette: brand, format: 'NAME' }) // 'acme red'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
## Palettes
|
|
196
|
+
|
|
197
|
+
Every palette — yours or ours — is a `Palette<Name>` object. Define it once, pass it anywhere the three verbs accept one.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
type Palette<Name extends string = string> = {
|
|
201
|
+
readonly name: string; // human-readable label
|
|
202
|
+
readonly colors: Record<Name, HexColor>; // the lookup data
|
|
203
|
+
readonly normalize: (s: string) => string; // user-input → canonical key
|
|
204
|
+
readonly defaultMetric: DistanceMetric; // used by identify when no metric override
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Bring your own
|
|
209
|
+
|
|
210
|
+
The primary shape. Any object matching `Palette<Name>` works — no registry, no plugin, no side effects. Bundlers only include the palettes you actually import.
|
|
139
211
|
|
|
140
212
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
import { identify, resolve, type Palette } from 'chromonym';
|
|
214
|
+
|
|
215
|
+
const warhammer = {
|
|
216
|
+
name: 'warhammer40k',
|
|
217
|
+
colors: {
|
|
218
|
+
'world eaters red': '#8b1a1a',
|
|
219
|
+
'adeptus red': '#652022',
|
|
220
|
+
'sons of malice white': '#e8e4d8',
|
|
221
|
+
'the flawless host purple': '#6b2d7d',
|
|
222
|
+
'nurgle green': '#748c3f',
|
|
223
|
+
'alpha legion teal': '#2a6d7a',
|
|
224
|
+
},
|
|
225
|
+
normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
|
|
226
|
+
defaultMetric: 'deltaE2000',
|
|
227
|
+
} as const satisfies Palette;
|
|
228
|
+
|
|
229
|
+
identify('#750c0c', { palette: warhammer })
|
|
230
|
+
// → 'world eaters red' (return type narrows to the palette's key union)
|
|
231
|
+
|
|
232
|
+
resolve('Nurgle Green', { palette: warhammer })
|
|
233
|
+
// → '#748c3f' (your normalizer handles case + punctuation)
|
|
149
234
|
```
|
|
150
235
|
|
|
151
|
-
|
|
236
|
+
### The three we ship
|
|
152
237
|
|
|
153
238
|
| Name | Entries | Source |
|
|
154
239
|
|---|---|---|
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
240
|
+
| `web` (default) | 148 | CSS Color Module Level 4 named colors |
|
|
241
|
+
| `x11` | 658 | X.Org `rgb.txt` (public domain) |
|
|
242
|
+
| `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
|
|
158
243
|
|
|
159
|
-
|
|
244
|
+
Importable directly, or via subpath exports for stricter tree-shaking:
|
|
160
245
|
|
|
161
246
|
```ts
|
|
162
247
|
import { web, x11, pantone } from 'chromonym';
|
|
163
|
-
|
|
248
|
+
// or: import { pantone } from 'chromonym/pantone';
|
|
249
|
+
|
|
250
|
+
web.colors.crimson // '#dc143c'
|
|
251
|
+
web.colors.aliceblue // '#f0f8ff' (CSS keyword form — paste-compatible)
|
|
252
|
+
pantone.colors['185 C'] // '#e4002b'
|
|
164
253
|
```
|
|
165
254
|
|
|
166
|
-
|
|
255
|
+
Each palette's keys follow its own domain convention — so `identify`'s output pastes back into whatever ecosystem the name came from. `web` uses the CSS spec's single-word form (`rebeccapurple`), `x11` uses the X.Org docs' spaced form (`'antique white 1'`), `pantone` uses the official print notation (`'185 C'`).
|
|
256
|
+
|
|
257
|
+
*Trivia:* `web.colors.rebeccapurple` (`#663399`) entered CSS Color 4 in 2014 in memory of [Rebecca Meyer](https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/). The X11 set ships every gray name twice (`gray`, `grey`) plus numbered variants up to 99, so `x11.colors['gray 73']` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
|
|
258
|
+
|
|
259
|
+
### Cross-palette translation
|
|
260
|
+
|
|
261
|
+
"What's this CSS color in X11?" / "What's our brand red in Pantone?" is a *nearest-match* question — palettes almost never share exact hex values, so there's no strict round-trip. That's `identify`'s job. Chain `convert` (strict name → hex) with `identify` (fuzzy hex → nearest name):
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { convert, identify, web, x11, pantone } from 'chromonym';
|
|
265
|
+
|
|
266
|
+
// web → x11
|
|
267
|
+
identify(convert('rebeccapurple', { palette: web }), { palette: x11 })
|
|
268
|
+
// → 'dark orchid 4'
|
|
269
|
+
|
|
270
|
+
identify(convert('dodgerblue', { palette: web }), { palette: x11 })
|
|
271
|
+
// → 'dodger blue' (hex shared across both palettes — exact hit)
|
|
272
|
+
|
|
273
|
+
// x11 → pantone
|
|
274
|
+
identify(convert('dark slate gray 4', { palette: x11 }), { palette: pantone })
|
|
275
|
+
// → '5483 C'
|
|
276
|
+
|
|
277
|
+
identify(convert('debian red', { palette: x11 }), { palette: pantone })
|
|
278
|
+
// → '1925 C'
|
|
279
|
+
|
|
280
|
+
// web → pantone (the classic brand-matching case)
|
|
281
|
+
identify(convert('hotpink', { palette: web }), { palette: pantone }) // → '812 C'
|
|
282
|
+
identify(convert('dodgerblue', { palette: web }), { palette: pantone }) // → '279 C'
|
|
283
|
+
|
|
284
|
+
// BYO → pantone (ship your brand palette, find the closest Pantone for print)
|
|
285
|
+
identify(convert('acme red', { palette: brand }), { palette: pantone }) // → '1788 C'
|
|
286
|
+
|
|
287
|
+
// Override the metric for the *nearest-match* leg if you want — input parsing
|
|
288
|
+
// is always exact, so only the second call takes `metric`.
|
|
289
|
+
identify(convert('rebeccapurple', { palette: web }), { palette: pantone, metric: 'deltaEok' })
|
|
290
|
+
// → '526 C' (OKLAB disagrees with ΔE2000's '267 C' in the saturated-purple region)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Why two calls instead of one: `convert` is the strict verb (throws on a name miss), `identify` is the fuzzy one (always returns a nearest). Keeping them separate lets you decide — per call — whether you want a hard error on an unknown input or a best-effort neighbor.
|
|
294
|
+
|
|
295
|
+
If this chain shows up often in your code, wrap it in your own helper:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const translate = (name: string, from: Palette, to: Palette) =>
|
|
299
|
+
identify(convert(name, { palette: from }), { palette: to });
|
|
300
|
+
```
|
|
167
301
|
|
|
168
302
|
## Distance metrics
|
|
169
303
|
|
|
170
304
|
*Six ways to ask "how different are two colors?"*
|
|
171
305
|
|
|
172
|
-
`identify` picks the nearest-named color by comparing your input to every entry in the chosen
|
|
306
|
+
`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
307
|
|
|
174
308
|
| Metric | What it does | When to use |
|
|
175
309
|
|---|---|---|
|
|
@@ -182,18 +316,20 @@ web.crimson // '#dc143c'
|
|
|
182
316
|
|
|
183
317
|
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
318
|
|
|
319
|
+
> **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.
|
|
320
|
+
|
|
185
321
|
```ts
|
|
186
|
-
// Defaults:
|
|
322
|
+
// Defaults: read from the palette's own `defaultMetric`.
|
|
187
323
|
identify('#ff8080') // deltaE76 (web default)
|
|
188
|
-
identify('#ff8080', {
|
|
324
|
+
identify('#ff8080', { palette: pantone }) // deltaE2000 (pantone default)
|
|
189
325
|
|
|
190
326
|
// Override per-call:
|
|
191
327
|
identify('#ff8080', { metric: 'euclidean-srgb' }) // fastest
|
|
192
328
|
identify('#ff8080', { metric: 'deltaE2000' }) // most accurate
|
|
193
|
-
identify('#ff8080', {
|
|
329
|
+
identify('#ff8080', { palette: pantone, metric: 'euclidean-srgb' }) // force fast for Pantone
|
|
194
330
|
```
|
|
195
331
|
|
|
196
|
-
The low-level `rgbaToPantone`
|
|
332
|
+
The low-level `rgbaToPantone` reads `pantone.defaultMetric` (currently `'deltaE2000'`).
|
|
197
333
|
|
|
198
334
|
## Formats
|
|
199
335
|
|
|
@@ -204,7 +340,6 @@ The low-level `rgbaToPantone` always uses `'deltaE2000'`.
|
|
|
204
340
|
| `'RGBA'` | `'rgba(r, g, b, a)'`, `[r, g, b, a]`, `{ r, g, b, a }` | `{ r, g, b, a }` object |
|
|
205
341
|
| `'HSL'` | `'hsl(h, s%, l%)'`, `{ h, s, l }` | `'hsl(h, s%, l%)'` string |
|
|
206
342
|
| `'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
343
|
|
|
209
344
|
## Error handling
|
|
210
345
|
|
|
@@ -229,6 +364,7 @@ const tryConvert = (input: ColorInput, opts = {}) => {
|
|
|
229
364
|
## Utilities
|
|
230
365
|
|
|
231
366
|
- **`detectFormat(input): DetectedFormat`** — runtime format detection. Returns one of the format keys or `'UNKNOWN'`.
|
|
367
|
+
- **`isColor(input): boolean`** — type-guard wrapper around `detectFormat`. Returns `true` if the input is a recognized structural color shape.
|
|
232
368
|
- **`COLOR_FORMATS: ReadonlySet<ColorFormat>`** — the Set of valid format keys.
|
|
233
369
|
- **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
370
|
|
|
@@ -237,7 +373,9 @@ const tryConvert = (input: ColorInput, opts = {}) => {
|
|
|
237
373
|
- `sideEffects: false` in `package.json`.
|
|
238
374
|
- All exports are named; no default export.
|
|
239
375
|
- Barrel uses explicit re-exports.
|
|
240
|
-
-
|
|
376
|
+
- `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.
|
|
377
|
+
- BYO palettes have zero library cost beyond your own data.
|
|
378
|
+
- 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
379
|
|
|
242
380
|
## Types
|
|
243
381
|
|
|
@@ -246,9 +384,10 @@ Re-exported from the root barrel — `import type { ... } from 'chromonym'`:
|
|
|
246
384
|
| Category | Types |
|
|
247
385
|
|---|---|
|
|
248
386
|
| Input / output unions | `ColorInput`, `ColorValue` |
|
|
249
|
-
| Format
|
|
250
|
-
| Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput
|
|
251
|
-
| Palette container | `
|
|
387
|
+
| Format keys | `ColorFormat` |
|
|
388
|
+
| Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput` |
|
|
389
|
+
| Palette container | `Palette<Name>`, `NormalizeFn` |
|
|
390
|
+
| Distance selector | `DistanceMetric` |
|
|
252
391
|
| Color-name unions | `WebColorName`, `X11ColorName`, `PantoneColorName` |
|
|
253
392
|
|
|
254
393
|
## Development
|
|
@@ -261,7 +400,7 @@ bun test
|
|
|
261
400
|
bun run build
|
|
262
401
|
```
|
|
263
402
|
|
|
264
|
-
Regenerating
|
|
403
|
+
Regenerating palette data:
|
|
265
404
|
|
|
266
405
|
```sh
|
|
267
406
|
bun run scripts/generate-x11.ts # reads /usr/share/X11/rgb.txt
|
|
@@ -284,8 +423,10 @@ chromonym deliberately limits its scope to color *naming* (resolving and identif
|
|
|
284
423
|
|
|
285
424
|
MIT © [the-simian](https://github.com/the-simian)
|
|
286
425
|
|
|
426
|
+
<p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
|
|
427
|
+
|
|
287
428
|
---
|
|
288
429
|
|
|
289
430
|
> ### Pantone® trademark notice
|
|
290
431
|
>
|
|
291
|
-
> **Pantone®** is a registered trademark of **Pantone LLC**. Chromonym is **not affiliated with, endorsed by, or certified by Pantone LLC**. The `pantone`
|
|
432
|
+
> **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.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)` as HslString;\n}\n"]}
|