color-value-tools 1.1.4 → 1.1.6
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/LICENSE +21 -0
- package/README.md +505 -95
- package/dist/cjs/index.js +17 -24
- package/dist/cli/src/index.js +17 -24
- package/dist/esm/index.js +17 -24
- package/package.json +15 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danil Lisin Vladimirovich (macrulez)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,12 +4,58 @@
|
|
|
4
4
|
</h1>
|
|
5
5
|
<img
|
|
6
6
|
src="https://s3.twcstorage.ru/c9a2cc89-780f97fd-311d-4a1a-b86f-c25665c9dc46/images/npm/color-value-tools.webp"
|
|
7
|
-
alt="
|
|
7
|
+
alt="color-value-tools"
|
|
8
8
|
style="max-width:100%;width:auto;height:300px;border-radius:12px"
|
|
9
9
|
/>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
A comprehensive utility library for parsing, converting, manipulating, and analyzing color values across all major color models — hex, RGB, HSL, HSV, HWB, Lab, LCH,
|
|
12
|
+
A comprehensive utility library for parsing, converting, manipulating, and analyzing color values across all major color models — hex, RGB, HSL, HSV, HWB, Lab, LCH, Oklab, Oklch, CMYK, Display P3 — plus CSS variables and named colors. Zero dependencies.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [Detection](#detection)
|
|
22
|
+
- [Parsing & Normalization](#parsing--normalization)
|
|
23
|
+
- [Conversions](#conversions)
|
|
24
|
+
- [Manipulation](#manipulation)
|
|
25
|
+
- [Interpolation & Scales](#interpolation--scales)
|
|
26
|
+
- [Color Harmonies](#color-harmonies)
|
|
27
|
+
- [Palette Generation](#palette-generation)
|
|
28
|
+
- [Accessibility (WCAG)](#accessibility-wcag)
|
|
29
|
+
- [Color Blindness Simulation](#color-blindness-simulation)
|
|
30
|
+
- [Formatting](#formatting)
|
|
31
|
+
- [Cache Management](#cache-management)
|
|
32
|
+
- [Generator Functions](#generator-functions)
|
|
33
|
+
- [CLI](#cli)
|
|
34
|
+
- [Cookbook](#cookbook)
|
|
35
|
+
- [TypeScript types](#typescript-types)
|
|
36
|
+
- [Architecture](#architecture)
|
|
37
|
+
- [Bundle size & dependencies](#bundle-size--dependencies)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Universal parser** — `normalizeColor()` accepts hex (3/4/6/8-digit), `rgb()` / `rgba()`, `hsl()` / `hsla()`, `hwb()`, `oklch()` / `oklcha()`, `color(display-p3 ...)`, CSS named colors (all 148), and plain `{r,g,b}` / `{h,s,l}` objects
|
|
44
|
+
- **Conversions** — every path between hex, RGB, HSL, HSV, HWB, Lab, LCH, Oklab, Oklch, CMYK, Display P3; all round-trip accurate
|
|
45
|
+
- **Manipulation** — `lighten`, `darken`, `saturate`, `desaturate`, `invertColor`, `grayscale`, `setAlpha`, `rotateHue`, `adjustHexBrightness`
|
|
46
|
+
- **Mixing & Interpolation** — `mixColors` and `interpolateColors` in 6 color spaces with 4 hue interpolation modes; `createColorScale` across multiple anchor colors with custom positions; `midpointColor` for a perceptual midpoint
|
|
47
|
+
- **Color Harmonies** — `complement`, `triadic`, `analogous`, `splitComplementary`, `tetradic`
|
|
48
|
+
- **Palette Generation** — `colorShades`, `monochromatic`, `tints`, `shades`, `tones` — all Oklab-accurate where applicable
|
|
49
|
+
- **WCAG Accessibility** — `relativeLuminance`, `contrastRatio`, `wcagLevel`, `bestTextColor`, `bestContrastColor`, `bestContrastPalette`, `isReadableOnBackground` (solid, semi-transparent, and gradient backgrounds)
|
|
50
|
+
- **Color Blindness Simulation** — protanopia, deuteranopia, tritanopia using Vienot 1999 matrices on linear RGB
|
|
51
|
+
- **Perceptual color distance** — `colorDeltaE` implementing the full CIEDE2000 formula
|
|
52
|
+
- **CSS Formatting** — `toHslString`, `toHwbString`, `toOklchString`, `toColorP3String`
|
|
53
|
+
- **Cache** — LRU-style `normalizeColorCached`; `getCacheStats`, `clearColorCache`, `enableCache` / `disableCache`
|
|
54
|
+
- **Generator functions** — `generateGradientColors*`, `generateTints*`, `generateShades*` for lazy iteration without pre-allocating arrays
|
|
55
|
+
- **CLI (`cvt`)** — inspect any color from the terminal: full info, all conversions, WCAG contrast, shades, harmonies, nearest named color
|
|
56
|
+
- **Zero dependencies** — no runtime deps; ships as tree-shakeable ESM + CommonJS
|
|
57
|
+
|
|
58
|
+
---
|
|
13
59
|
|
|
14
60
|
## Installation
|
|
15
61
|
|
|
@@ -17,7 +63,11 @@ A comprehensive utility library for parsing, converting, manipulating, and analy
|
|
|
17
63
|
npm install color-value-tools
|
|
18
64
|
```
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
No peer dependencies required.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
21
71
|
|
|
22
72
|
```ts
|
|
23
73
|
import {
|
|
@@ -26,70 +76,100 @@ import {
|
|
|
26
76
|
complement, triadic,
|
|
27
77
|
wcagLevel, bestTextColor,
|
|
28
78
|
colorDeltaE, randomColor,
|
|
29
|
-
} from 'color-value-tools'
|
|
79
|
+
} from 'color-value-tools'
|
|
30
80
|
|
|
31
81
|
// Parse any color format
|
|
32
|
-
normalizeColor('#3498db')
|
|
33
|
-
// { type: 'hex', hex: '#3498db', r: 52, g: 152, b: 219, h: 204, s: 70, l: 53,
|
|
82
|
+
normalizeColor('#3498db')
|
|
83
|
+
// { type: 'hex', hex: '#3498db', r: 52, g: 152, b: 219, h: 204, s: 70, l: 53, v: 86, a: 1 }
|
|
34
84
|
|
|
35
85
|
// Manipulate
|
|
36
|
-
lighten('#3498db', 15)
|
|
37
|
-
darken('#3498db', 15)
|
|
38
|
-
saturate('#3498db', 20)
|
|
86
|
+
lighten('#3498db', 15) // '#6ab4e8'
|
|
87
|
+
darken('#3498db', 15) // '#1a6da3'
|
|
88
|
+
saturate('#3498db', 20) // '#1a8fe8'
|
|
39
89
|
|
|
40
90
|
// Harmonies
|
|
41
|
-
complement('#3498db')
|
|
42
|
-
triadic('#3498db')
|
|
91
|
+
complement('#3498db') // '#db6034'
|
|
92
|
+
triadic('#3498db') // ['#3498db', '#db3498', '#98db34']
|
|
43
93
|
|
|
44
94
|
// WCAG accessibility
|
|
45
|
-
wcagLevel('#ffffff', '#3498db')
|
|
46
|
-
bestTextColor('#3498db')
|
|
95
|
+
wcagLevel('#ffffff', '#3498db') // 'AA'
|
|
96
|
+
bestTextColor('#3498db') // '#ffffff'
|
|
47
97
|
|
|
48
98
|
// Perceptual color distance (CIEDE2000)
|
|
49
|
-
colorDeltaE('#ff0000', '#fe0000')
|
|
99
|
+
colorDeltaE('#ff0000', '#fe0000') // ~0.9
|
|
50
100
|
|
|
51
101
|
// Random color
|
|
52
|
-
randomColor({ hRange: [200, 260], sRange: [60, 80] })
|
|
102
|
+
randomColor({ hRange: [200, 260], sRange: [60, 80] })
|
|
53
103
|
```
|
|
54
104
|
|
|
55
105
|
CommonJS:
|
|
106
|
+
|
|
56
107
|
```js
|
|
57
|
-
const { normalizeColor, mixColors } = require('color-value-tools')
|
|
108
|
+
const { normalizeColor, mixColors } = require('color-value-tools')
|
|
58
109
|
```
|
|
59
110
|
|
|
60
111
|
---
|
|
61
112
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
### Detection
|
|
113
|
+
## Detection
|
|
65
114
|
|
|
66
115
|
| Function | Description |
|
|
67
116
|
|---|---|
|
|
68
117
|
| `getColorType(value)` | Returns `'hex'` \| `'css-var'` \| `'rgb'` \| `'hsl'` \| `'named'` \| `'oklch'` \| `'color'` \| `'unknown'` |
|
|
69
|
-
| `isHexColor(value)` | Detects 3-, 4-,
|
|
118
|
+
| `isHexColor(value)` | Detects 3-, 4-, or 6-digit hex strings (with or without `#`) |
|
|
70
119
|
| `isRgbColor(value)` | Detects `rgb()` / `rgba()` strings |
|
|
71
120
|
| `isHslColor(value)` | Detects `hsl()` / `hsla()` strings |
|
|
72
121
|
| `isOklchColor(value)` | Detects `oklch()` / `oklcha()` strings |
|
|
73
|
-
| `isColorFunction(value)` | Detects `color(display-p3 ...)`
|
|
122
|
+
| `isColorFunction(value)` | Detects `color(display-p3 ...)`, `color(srgb ...)`, `color(srgb-linear ...)` strings |
|
|
74
123
|
| `isCssVariable(value)` | Checks for `var(--name)` pattern |
|
|
75
124
|
| `extractCssVariableName(value)` | Extracts `--name` from `var(--name, fallback)` |
|
|
76
125
|
|
|
77
|
-
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Parsing & Normalization
|
|
78
129
|
|
|
79
130
|
| Function | Description |
|
|
80
131
|
|---|---|
|
|
81
|
-
| `normalizeColor(input)` | Universal parser. Accepts hex, `rgb()`, `hsl()`, `oklch()`, `color()`, named color, `{r,g,b}`
|
|
82
|
-
| `normalizeColorCached(input)` | Same as `normalizeColor` but uses an internal LRU-style cache |
|
|
132
|
+
| `normalizeColor(input)` | Universal parser. Accepts hex (3/4/6/8-digit), `rgb()`, `hsl()`, `hwb()`, `oklch()`, `color()`, named color, `{r,g,b}` object, `{h,s,l}` object. Returns `{ type, hex, r, g, b, a, h, s, l, v }` |
|
|
133
|
+
| `normalizeColorCached(input)` | Same as `normalizeColor` but uses an internal LRU-style cache. String input only |
|
|
83
134
|
| `normalizeHex(hex)` | Normalizes 3- or 6-digit hex to lowercase 6-digit with `#` |
|
|
84
|
-
| `rgbaStringToRgba(str)` | Parses `rgb()` / `rgba()` string to `{r, g, b, a}
|
|
85
|
-
| `hex8ToRgba(hex)` | Parses 8-digit hex (`#rrggbbaa`) to `{r, g, b, a}` |
|
|
135
|
+
| `rgbaStringToRgba(str)` | Parses `rgb()` / `rgba()` string to `{r, g, b, a}`; supports percentage channels |
|
|
136
|
+
| `hex8ToRgba(hex)` | Parses 8-digit hex (`#rrggbbaa`) or 4-digit hex (`#rgba`) to `{r, g, b, a}` |
|
|
86
137
|
| `shortHexToRgba(hex)` | Parses 4-digit hex (`#rgba`) to `{r, g, b, a}` — e.g. `#f0f0` → `{r:255,g:0,b:255,a:0}` |
|
|
87
138
|
| `parseHwbString(str)` | Parses `hwb()` string to `{H, W, B, alpha}` |
|
|
88
|
-
| `parseOklchString(str)` | Parses `oklch(L C H / alpha)` to `{L, C, H, alpha}
|
|
139
|
+
| `parseOklchString(str)` | Parses `oklch(L C H / alpha)` to `{L, C, H, alpha}`; accepts percentage L |
|
|
89
140
|
| `parseColorFn(str)` | Parses `color(display-p3 r g b / alpha)` to `{space, r, g, b, alpha}` |
|
|
90
141
|
| `parseCssVar(value)` | Parses `var(--name, fallback)` to `{variableName, fallback?}` |
|
|
91
142
|
|
|
92
|
-
###
|
|
143
|
+
### `normalizeColor` return value
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
{
|
|
147
|
+
type: 'hex' | 'rgb' | 'hsl' | 'oklch' | 'color' | 'named' | 'css-var' | 'unknown'
|
|
148
|
+
hex?: string // '#rrggbb'
|
|
149
|
+
r?: number // 0–255
|
|
150
|
+
g?: number // 0–255
|
|
151
|
+
b?: number // 0–255
|
|
152
|
+
a?: number // 0–1
|
|
153
|
+
h?: number // hue 0–360
|
|
154
|
+
s?: number // HSL saturation 0–100
|
|
155
|
+
l?: number // HSL lightness 0–100
|
|
156
|
+
v?: number // HSV value 0–100
|
|
157
|
+
raw?: string // set for css-var type
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Object input
|
|
163
|
+
normalizeColor({ r: 52, g: 152, b: 219 }) // → full result with hex, h, s, l, v
|
|
164
|
+
normalizeColor({ h: 204, s: 70, l: 53 }) // → full result with hex, r, g, b, v
|
|
165
|
+
|
|
166
|
+
// CSS variable — returns raw, no color channels
|
|
167
|
+
normalizeColor('var(--brand-color)') // → { type: 'css-var', raw: 'var(--brand-color)' }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Conversions
|
|
93
173
|
|
|
94
174
|
| Function | Description |
|
|
95
175
|
|---|---|
|
|
@@ -103,6 +183,7 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
103
183
|
| `hsvToRgb(h, s, v)` | `→ {r, g, b}` |
|
|
104
184
|
| `rgbToHex({r,g,b})` | `→ #rrggbb` |
|
|
105
185
|
| `rgbaToHex({r,g,b,a})` | `→ #rrggbbaa` |
|
|
186
|
+
| `rgbaToHex8({r,g,b,a})` | Alias for `rgbaToHex` |
|
|
106
187
|
| `rgbToRgbaString({r,g,b}, a)` | `→ rgba(...)` string |
|
|
107
188
|
| `rgbToHsl({r,g,b})` | `→ [h, s, l]` |
|
|
108
189
|
| `rgbToHsv({r,g,b})` | `→ [h, s, v]` |
|
|
@@ -121,9 +202,10 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
121
202
|
| `rgbToDisplayP3({r,g,b})` | `→ {r, g, b}` in Display P3 space (0–1 per channel) |
|
|
122
203
|
| `displayP3ToRgb({r,g,b})` | `→ {r, g, b}` sRGB (0–255) |
|
|
123
204
|
| `toDisplayP3Hex(color)` | Converts any color → Display P3 → hex |
|
|
124
|
-
| `rgbaToHex8({r,g,b,a})` | Alias for `rgbaToHex` |
|
|
125
205
|
|
|
126
|
-
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Manipulation
|
|
127
209
|
|
|
128
210
|
| Function | Description |
|
|
129
211
|
|---|---|
|
|
@@ -133,21 +215,64 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
133
215
|
| `desaturate(color, amount)` | Decrease HSL saturation by `amount` (0–100) |
|
|
134
216
|
| `setAlpha(color, alpha)` | Returns `rgba(...)` with the given alpha (0–1) |
|
|
135
217
|
| `getAlpha(color)` | Returns the alpha channel value (0–1) |
|
|
136
|
-
| `invertColor(color)` | Inverts RGB channels |
|
|
137
|
-
| `grayscale(color)` | Converts to grayscale using ITU-R BT.709 weights |
|
|
138
|
-
| `rotateHue(hex, degrees)` | Rotates hue by degrees
|
|
218
|
+
| `invertColor(color)` | Inverts all three RGB channels |
|
|
219
|
+
| `grayscale(color)` | Converts to grayscale using ITU-R BT.709 perceptual weights |
|
|
220
|
+
| `rotateHue(hex, degrees)` | Rotates hue by degrees; supports negative values |
|
|
139
221
|
| `adjustHexBrightness(hex, offsetPercent)` | Lightens (positive) or darkens (negative) by percentage |
|
|
140
|
-
| `mixColors(c1, c2, t, opts?)` | Interpolates between two colors. `t` = 0–1. `mode`: `'rgb'
|
|
222
|
+
| `mixColors(c1, c2, t, opts?)` | Interpolates between two colors. `t` = 0–1. `mode`: `'rgb'` \| `'hsl'` \| `'lab'` \| `'lch'` \| `'oklab'` \| `'oklch'`. `hueInterpolation`: `'shorter'` \| `'longer'` \| `'increasing'` \| `'decreasing'` |
|
|
223
|
+
|
|
224
|
+
### `mixColors` example
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { mixColors } from 'color-value-tools'
|
|
228
|
+
|
|
229
|
+
// RGB mix at midpoint
|
|
230
|
+
mixColors('#e74c3c', '#3498db', 0.5)
|
|
231
|
+
// → '#8dc6bc'
|
|
141
232
|
|
|
142
|
-
|
|
233
|
+
// Perceptually even mix in Oklab
|
|
234
|
+
mixColors('#e74c3c', '#3498db', 0.5, { mode: 'oklab', format: 'hex' })
|
|
235
|
+
|
|
236
|
+
// HSL with "longer" hue path
|
|
237
|
+
mixColors('#e74c3c', '#3498db', 0.5, { mode: 'hsl', hueInterpolation: 'longer' })
|
|
238
|
+
|
|
239
|
+
// Get rgba string output
|
|
240
|
+
mixColors('#ff0000', '#0000ff', 0.25, { mode: 'lab', format: 'rgba' })
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Interpolation & Scales
|
|
143
246
|
|
|
144
247
|
| Function | Description |
|
|
145
248
|
|---|---|
|
|
146
|
-
| `interpolateColors(c1, c2, steps, opts?)` | Returns array of `steps` colors from `c1` to `c2`. Same `space
|
|
249
|
+
| `interpolateColors(c1, c2, steps, opts?)` | Returns array of `steps` colors from `c1` to `c2`. Same `space` / `format` / `hueInterpolation` options as `mixColors` |
|
|
147
250
|
| `createColorScale(anchors, steps, opts?)` | Generates a `steps`-color scale across multiple anchor colors with optional positions (0–1) |
|
|
148
251
|
| `midpointColor(c1, c2, opts?)` | Perceptual midpoint between two colors (default space: `'oklab'`) |
|
|
149
252
|
|
|
150
|
-
###
|
|
253
|
+
### `createColorScale` — multi-stop gradient
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { createColorScale } from 'color-value-tools'
|
|
257
|
+
|
|
258
|
+
// Evenly spaced anchors
|
|
259
|
+
createColorScale(['#ff0000', '#ffff00', '#00ff00'], 9)
|
|
260
|
+
|
|
261
|
+
// With explicit positions
|
|
262
|
+
createColorScale(
|
|
263
|
+
[
|
|
264
|
+
{ color: '#1a1a2e', position: 0 },
|
|
265
|
+
{ color: '#e94560', position: 0.4 },
|
|
266
|
+
{ color: '#f5a623', position: 1 },
|
|
267
|
+
],
|
|
268
|
+
12,
|
|
269
|
+
{ space: 'oklch' }
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Color Harmonies
|
|
151
276
|
|
|
152
277
|
| Function | Description |
|
|
153
278
|
|---|---|
|
|
@@ -157,7 +282,17 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
157
282
|
| `splitComplementary(color)` | Base + two colors at 150° and 210° |
|
|
158
283
|
| `tetradic(color)` | 4 colors evenly spaced 90° apart |
|
|
159
284
|
|
|
160
|
-
|
|
285
|
+
```ts
|
|
286
|
+
import { triadic, analogous, tetradic } from 'color-value-tools'
|
|
287
|
+
|
|
288
|
+
triadic('#6c3483') // ['#6c3483', '#34836c', '#83346c']
|
|
289
|
+
analogous('#6c3483', 45) // ['#833469', '#6c3483', '#3469…']
|
|
290
|
+
tetradic('#6c3483') // 4 colors
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Palette Generation
|
|
161
296
|
|
|
162
297
|
| Function | Description |
|
|
163
298
|
|---|---|
|
|
@@ -167,7 +302,9 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
167
302
|
| `shades(color, steps?)` | Mix toward black in Oklab (default 5 steps) |
|
|
168
303
|
| `tones(color, steps?, gray?)` | Mix toward gray in Oklab (default 5 steps, gray `#808080`) |
|
|
169
304
|
|
|
170
|
-
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Accessibility (WCAG)
|
|
171
308
|
|
|
172
309
|
| Function | Description |
|
|
173
310
|
|---|---|
|
|
@@ -176,14 +313,56 @@ const { normalizeColor, mixColors } = require('color-value-tools');
|
|
|
176
313
|
| `wcagLevel(fg, bg)` | Returns `'AAA'` \| `'AA'` \| `'AA-large'` \| `'fail'` |
|
|
177
314
|
| `bestTextColor(bg)` | Returns `'#000000'` or `'#ffffff'` for best contrast on `bg` |
|
|
178
315
|
| `bestContrastColor(bg, candidates)` | Picks the most readable color from an array of candidates |
|
|
179
|
-
| `bestContrastPalette(bg, palettes, opts?)` | Picks the palette with best overall contrast. Returns `{paletteIndex, palette, minContrastRatio, avgContrastRatio}` |
|
|
180
|
-
| `isReadableOnBackground(text, bg, opts?)` | Checks readability on solid, semi-transparent, or gradient backgrounds. Returns `{readable, minContrastRatio, wcagLevel}` |
|
|
316
|
+
| `bestContrastPalette(bg, palettes, opts?)` | Picks the palette with best overall contrast. Returns `{ paletteIndex, palette, minContrastRatio, avgContrastRatio }` |
|
|
317
|
+
| `isReadableOnBackground(text, bg, opts?)` | Checks readability on solid, semi-transparent, or gradient backgrounds. Returns `{ readable, minContrastRatio, wcagLevel }` |
|
|
181
318
|
| `isDark(color, threshold?)` | `true` if luminance is below threshold (default 0.5) |
|
|
182
319
|
| `isLight(color, threshold?)` | Inverse of `isDark` |
|
|
183
320
|
|
|
184
|
-
###
|
|
321
|
+
### `isReadableOnBackground` — complex backgrounds
|
|
322
|
+
|
|
323
|
+
`background` can be a plain color string, a semi-transparent spec, or a gradient with multiple stops:
|
|
185
324
|
|
|
186
|
-
|
|
325
|
+
```ts
|
|
326
|
+
import { isReadableOnBackground } from 'color-value-tools'
|
|
327
|
+
|
|
328
|
+
// Solid background
|
|
329
|
+
isReadableOnBackground('#ffffff', '#3498db')
|
|
330
|
+
// → { readable: true, minContrastRatio: 3.05, wcagLevel: 'AA-large' }
|
|
331
|
+
|
|
332
|
+
// Semi-transparent overlay composited over white
|
|
333
|
+
isReadableOnBackground('#ffffff', {
|
|
334
|
+
type: 'semi-transparent',
|
|
335
|
+
color: 'rgba(52, 152, 219, 0.5)',
|
|
336
|
+
underlay: '#f0f0f0',
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// Gradient — worst stop is used for the result
|
|
340
|
+
isReadableOnBackground('#ffffff', {
|
|
341
|
+
type: 'gradient',
|
|
342
|
+
stops: ['#1a1a2e', '#e94560', '#f5a623'],
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// AAA + large text
|
|
346
|
+
isReadableOnBackground('#000000', '#f0f0f0', { level: 'AAA', largeText: true })
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### `bestContrastPalette` — pick accessible palette
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { bestContrastPalette } from 'color-value-tools'
|
|
353
|
+
|
|
354
|
+
const result = bestContrastPalette('#1a1a2e', [
|
|
355
|
+
['#ffffff', '#f0f0f0', '#cccccc'],
|
|
356
|
+
['#ffff00', '#ffd700', '#ff8c00'],
|
|
357
|
+
])
|
|
358
|
+
// → { paletteIndex: 0, palette: [...], minContrastRatio: 12.1, avgContrastRatio: 14.3 }
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Color Blindness Simulation
|
|
364
|
+
|
|
365
|
+
Simulates perception using Vienot 1999 matrices applied to linearized RGB.
|
|
187
366
|
|
|
188
367
|
| Function | Description |
|
|
189
368
|
|---|---|
|
|
@@ -192,7 +371,16 @@ Simulates perception using Vienot 1999 matrices applied to linear RGB.
|
|
|
192
371
|
| `simulateTritanopia(color)` | No S-cones (blue-blind) |
|
|
193
372
|
| `simulateColorBlindness(color, type)` | Generic — `type`: `'protanopia'` \| `'deuteranopia'` \| `'tritanopia'` |
|
|
194
373
|
|
|
195
|
-
|
|
374
|
+
```ts
|
|
375
|
+
import { simulateColorBlindness } from 'color-value-tools'
|
|
376
|
+
|
|
377
|
+
simulateColorBlindness('#e74c3c', 'deuteranopia') // '#9a7b00'
|
|
378
|
+
simulateColorBlindness('#3498db', 'protanopia') // '#5282db'
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Formatting
|
|
196
384
|
|
|
197
385
|
| Function | Description |
|
|
198
386
|
|---|---|
|
|
@@ -201,48 +389,81 @@ Simulates perception using Vienot 1999 matrices applied to linear RGB.
|
|
|
201
389
|
| `toOklchString(color, alpha?)` | Converts any color to `oklch(L C H)` CSS string |
|
|
202
390
|
| `toColorP3String(color, alpha?)` | Converts any color to `color(display-p3 r g b)` CSS string |
|
|
203
391
|
|
|
204
|
-
|
|
392
|
+
```ts
|
|
393
|
+
import { toOklchString, toColorP3String, toHwbString } from 'color-value-tools'
|
|
205
394
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
395
|
+
toOklchString('#3498db') // 'oklch(0.6 0.1175 234.77)'
|
|
396
|
+
toColorP3String('#3498db') // 'color(display-p3 0.2493 0.5875 0.8479)'
|
|
397
|
+
toHwbString(204, 20, 14) // 'hwb(204 20% 14%)'
|
|
398
|
+
toHwbString(204, 20, 14, 0.8) // 'hwb(204 20% 14% / 0.8)'
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
212
402
|
|
|
213
|
-
|
|
403
|
+
## Cache Management
|
|
214
404
|
|
|
215
|
-
Useful
|
|
405
|
+
Useful in rendering loops, color pickers, or any context with repeated `normalizeColor` calls on the same values.
|
|
216
406
|
|
|
217
407
|
| Function | Description |
|
|
218
408
|
|---|---|
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
409
|
+
| `normalizeColorCached(input)` | Cached version of `normalizeColor` for repeated string calls |
|
|
410
|
+
| `clearColorCache()` | Clears the normalization cache and resets hit counter |
|
|
411
|
+
| `getCacheStats()` | Returns `{ size: number, hits: number }` |
|
|
412
|
+
| `enableCache()` | Re-enables the cache (on by default) |
|
|
413
|
+
| `disableCache()` | Disables caching (useful in tests) |
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { normalizeColorCached, getCacheStats, clearColorCache } from 'color-value-tools'
|
|
222
417
|
|
|
223
|
-
|
|
418
|
+
// First call — parsed and cached
|
|
419
|
+
normalizeColorCached('#3498db')
|
|
420
|
+
|
|
421
|
+
// Second call — instant cache hit
|
|
422
|
+
normalizeColorCached('#3498db')
|
|
423
|
+
|
|
424
|
+
getCacheStats() // { size: 1, hits: 1 }
|
|
425
|
+
clearColorCache() // { size: 0, hits: 0 } reset
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Generator Functions
|
|
431
|
+
|
|
432
|
+
Useful for large palettes and frame-by-frame animations without allocating full arrays upfront.
|
|
224
433
|
|
|
225
434
|
| Function | Description |
|
|
226
435
|
|---|---|
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
436
|
+
| `generateGradientColors*(start, end, steps, opts?)` | Yields `steps` colors from `start` to `end`. Same `mode` / `format` options as `mixColors` |
|
|
437
|
+
| `generateTints*(color, steps, opts?)` | Yields tints toward white in Oklab |
|
|
438
|
+
| `generateShades*(color, steps, opts?)` | Yields shades toward black in Oklab |
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { generateGradientColors, generateTints } from 'color-value-tools'
|
|
442
|
+
|
|
443
|
+
// Process one color at a time — no intermediate array
|
|
444
|
+
for (const color of generateGradientColors('#ff0000', '#0000ff', 1000, { mode: 'oklch' })) {
|
|
445
|
+
renderPixel(color)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Collect lazily
|
|
449
|
+
const first5 = [...generateTints('#e74c3c', 100)].slice(0, 5)
|
|
450
|
+
```
|
|
230
451
|
|
|
231
452
|
---
|
|
232
453
|
|
|
233
454
|
## CLI
|
|
234
455
|
|
|
235
|
-
|
|
456
|
+
The `cvt` CLI is included in the package. Install globally or use via `npx`:
|
|
236
457
|
|
|
237
458
|
```bash
|
|
238
459
|
npm install -g color-value-tools
|
|
239
460
|
```
|
|
240
461
|
|
|
241
462
|
```bash
|
|
242
|
-
# Full info (default)
|
|
463
|
+
# Full info (default command)
|
|
243
464
|
cvt "#3498db"
|
|
244
465
|
|
|
245
|
-
# All format conversions
|
|
466
|
+
# All format conversions — hex, rgb, hsl, hsv, hwb, lab, lch, oklab, oklch, cmyk
|
|
246
467
|
cvt "#3498db" convert
|
|
247
468
|
|
|
248
469
|
# Contrast ratio and WCAG level
|
|
@@ -251,13 +472,27 @@ cvt "#3498db" contrast "#ffffff"
|
|
|
251
472
|
# Generate 7 shades
|
|
252
473
|
cvt "#3498db" shades 7
|
|
253
474
|
|
|
254
|
-
# All harmonies
|
|
475
|
+
# All harmonies — complement, triadic, analogous, split-comp, tetradic
|
|
255
476
|
cvt "#3498db" harmonies
|
|
256
477
|
|
|
257
478
|
# Nearest CSS named color
|
|
258
479
|
cvt "cornflowerblue" nearest
|
|
259
480
|
```
|
|
260
481
|
|
|
482
|
+
Accepts any valid color format: hex, `rgb()`, `hsl()`, `oklch()`, named colors.
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
cvt "#3498db"
|
|
486
|
+
────────────────────────────────────────
|
|
487
|
+
Type: hex
|
|
488
|
+
Hex: #3498db
|
|
489
|
+
RGB: rgb(52, 152, 219)
|
|
490
|
+
HSL: hsl(204, 70%, 53%)
|
|
491
|
+
HSV: hsv(204, 76%, 86%)
|
|
492
|
+
Nearest named: cornflowerblue
|
|
493
|
+
Best text on it: #ffffff
|
|
494
|
+
```
|
|
495
|
+
|
|
261
496
|
---
|
|
262
497
|
|
|
263
498
|
## Cookbook
|
|
@@ -265,78 +500,253 @@ cvt "cornflowerblue" nearest
|
|
|
265
500
|
### Generate an accessible button palette
|
|
266
501
|
|
|
267
502
|
```ts
|
|
268
|
-
import { colorShades, bestTextColor, wcagLevel } from 'color-value-tools'
|
|
503
|
+
import { colorShades, bestTextColor, wcagLevel } from 'color-value-tools'
|
|
269
504
|
|
|
270
505
|
function buttonPalette(base: string) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
bg: shade,
|
|
506
|
+
return colorShades(base, 9).map(shade => ({
|
|
507
|
+
bg: shade,
|
|
274
508
|
text: bestTextColor(shade),
|
|
275
509
|
wcag: wcagLevel(bestTextColor(shade), shade),
|
|
276
|
-
}))
|
|
510
|
+
}))
|
|
277
511
|
}
|
|
278
512
|
|
|
279
|
-
buttonPalette('#3498db')
|
|
513
|
+
buttonPalette('#3498db')
|
|
280
514
|
// [{ bg: '#ffffff', text: '#000000', wcag: 'AAA' }, ...]
|
|
281
515
|
```
|
|
282
516
|
|
|
283
|
-
### Theme-aware
|
|
517
|
+
### Theme-aware color adaptation
|
|
284
518
|
|
|
285
519
|
```ts
|
|
286
|
-
import { isDark, lighten, darken } from 'color-value-tools'
|
|
520
|
+
import { isDark, lighten, darken } from 'color-value-tools'
|
|
287
521
|
|
|
288
522
|
function adaptToTheme(color: string, isDarkTheme: boolean): string {
|
|
289
|
-
return isDarkTheme ? lighten(color, 20) : darken(color, 10)
|
|
523
|
+
return isDarkTheme ? lighten(color, 20) : darken(color, 10)
|
|
290
524
|
}
|
|
291
525
|
```
|
|
292
526
|
|
|
293
|
-
### Mix two brand colors at a midpoint
|
|
527
|
+
### Mix two brand colors at a perceptual midpoint
|
|
294
528
|
|
|
295
529
|
```ts
|
|
296
|
-
import {
|
|
530
|
+
import { midpointColor } from 'color-value-tools'
|
|
297
531
|
|
|
298
|
-
const mid =
|
|
299
|
-
|
|
532
|
+
const mid = midpointColor('#e74c3c', '#3498db') // Oklab midpoint
|
|
533
|
+
const midLch = midpointColor('#e74c3c', '#3498db', { space: 'oklch' })
|
|
300
534
|
```
|
|
301
535
|
|
|
302
|
-
### Build a triadic
|
|
536
|
+
### Build a triadic scheme and check contrast
|
|
303
537
|
|
|
304
538
|
```ts
|
|
305
|
-
import { triadic, contrastRatio } from 'color-value-tools'
|
|
539
|
+
import { triadic, contrastRatio } from 'color-value-tools'
|
|
306
540
|
|
|
307
|
-
const [base, second, third] = triadic('#6c3483')
|
|
308
|
-
console.log(contrastRatio(base, '#ffffff'))
|
|
309
|
-
console.log(contrastRatio(second, '#ffffff'))
|
|
541
|
+
const [base, second, third] = triadic('#6c3483')
|
|
542
|
+
console.log(contrastRatio(base, '#ffffff')) // e.g. 8.4
|
|
543
|
+
console.log(contrastRatio(second, '#ffffff'))
|
|
310
544
|
```
|
|
311
545
|
|
|
312
|
-
### Convert
|
|
546
|
+
### Convert any color string to all formats at once
|
|
313
547
|
|
|
314
548
|
```ts
|
|
315
|
-
import { normalizeColor, rgbToOklch, rgbToCmyk } from 'color-value-tools'
|
|
549
|
+
import { normalizeColor, rgbToOklch, rgbToCmyk, toColorP3String } from 'color-value-tools'
|
|
316
550
|
|
|
317
|
-
const n = normalizeColor('hsl(204, 70%, 53%)')
|
|
318
|
-
const oklch = rgbToOklch({ r: n.r!, g: n.g!, b: n.b! })
|
|
319
|
-
const cmyk = rgbToCmyk({ r: n.r!, g: n.g!, b: n.b! })
|
|
320
|
-
|
|
551
|
+
const n = normalizeColor('hsl(204, 70%, 53%)')
|
|
552
|
+
const oklch = rgbToOklch({ r: n.r!, g: n.g!, b: n.b! })
|
|
553
|
+
const cmyk = rgbToCmyk({ r: n.r!, g: n.g!, b: n.b! })
|
|
554
|
+
const p3 = toColorP3String(n.hex!)
|
|
555
|
+
console.log(n.hex, oklch, cmyk, p3)
|
|
321
556
|
```
|
|
322
557
|
|
|
323
|
-
### Find the
|
|
558
|
+
### Find the nearest CSS named color
|
|
324
559
|
|
|
325
560
|
```ts
|
|
326
|
-
import { toNearestNamedColor } from 'color-value-tools'
|
|
561
|
+
import { toNearestNamedColor } from 'color-value-tools'
|
|
327
562
|
|
|
328
|
-
toNearestNamedColor('#1a8ccc')
|
|
563
|
+
toNearestNamedColor('#1a8ccc') // 'steelblue'
|
|
564
|
+
toNearestNamedColor('#e74c3c') // 'tomato'
|
|
329
565
|
```
|
|
330
566
|
|
|
331
567
|
### Random palette within a hue range
|
|
332
568
|
|
|
333
569
|
```ts
|
|
334
|
-
import { randomColor, colorShades } from 'color-value-tools'
|
|
570
|
+
import { randomColor, colorShades } from 'color-value-tools'
|
|
571
|
+
|
|
572
|
+
const accent = randomColor({ hRange: [200, 260], sRange: [60, 80], lRange: [40, 60] })
|
|
573
|
+
const palette = colorShades(accent, 5)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Simulate color blindness for a palette
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { triadic, simulateColorBlindness } from 'color-value-tools'
|
|
580
|
+
|
|
581
|
+
const palette = triadic('#e74c3c')
|
|
582
|
+
const deuteranopia = palette.map(c => simulateColorBlindness(c, 'deuteranopia'))
|
|
583
|
+
// Compare palette vs deuteranopia to verify distinguishability
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Lazy gradient — process 10 000 colors one at a time
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { generateGradientColors } from 'color-value-tools'
|
|
590
|
+
|
|
591
|
+
for (const color of generateGradientColors('#1a1a2e', '#f5a623', 10_000, { mode: 'oklch' })) {
|
|
592
|
+
ctx.fillStyle = color
|
|
593
|
+
ctx.fillRect(x++, 0, 1, height)
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## TypeScript types
|
|
600
|
+
|
|
601
|
+
All public types are exported from the package root:
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
import type {
|
|
605
|
+
ColorType, // 'hex' | 'css-var' | 'rgb' | 'hsl' | 'named' | 'oklch' | 'color' | 'unknown'
|
|
606
|
+
WcagLevel, // 'AAA' | 'AA' | 'AA-large' | 'fail'
|
|
607
|
+
ColorBlindnessType, // 'protanopia' | 'deuteranopia' | 'tritanopia'
|
|
608
|
+
BackgroundSpec, // string | { type: 'semi-transparent'; ... } | { type: 'gradient'; ... }
|
|
609
|
+
PaletteScore, // { palette, minContrastRatio, avgContrastRatio }
|
|
610
|
+
} from 'color-value-tools'
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### `BackgroundSpec` — discriminated union
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
import type { BackgroundSpec } from 'color-value-tools'
|
|
617
|
+
|
|
618
|
+
const solid: BackgroundSpec = '#3498db'
|
|
335
619
|
|
|
336
|
-
const
|
|
337
|
-
|
|
620
|
+
const semiTransparent: BackgroundSpec = {
|
|
621
|
+
type: 'semi-transparent',
|
|
622
|
+
color: 'rgba(0,0,0,0.4)',
|
|
623
|
+
underlay: '#ffffff', // optional, defaults to white
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const gradient: BackgroundSpec = {
|
|
627
|
+
type: 'gradient',
|
|
628
|
+
stops: ['#1a1a2e', '#e94560', '#f5a623'],
|
|
629
|
+
}
|
|
338
630
|
```
|
|
339
631
|
|
|
632
|
+
### `PaletteScore` — from `bestContrastPalette`
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import type { PaletteScore } from 'color-value-tools'
|
|
636
|
+
|
|
637
|
+
const result: PaletteScore & { paletteIndex: number } = bestContrastPalette(bg, palettes)
|
|
638
|
+
result.paletteIndex // number
|
|
639
|
+
result.palette // string[]
|
|
640
|
+
result.minContrastRatio // number
|
|
641
|
+
result.avgContrastRatio // number
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Inference from `normalizeColor`
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import { normalizeColor } from 'color-value-tools'
|
|
648
|
+
|
|
649
|
+
const n = normalizeColor('#3498db')
|
|
650
|
+
// TypeScript knows: n.hex, n.r, n.g, n.b, n.h, n.s, n.l, n.v, n.a, n.type
|
|
651
|
+
|
|
652
|
+
if (n.type !== 'unknown' && n.type !== 'css-var') {
|
|
653
|
+
const r: number = n.r! // available for all resolved color types
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Architecture
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
color-value-tools
|
|
663
|
+
│
|
|
664
|
+
├── Detection
|
|
665
|
+
│ getColorType, isHexColor, isRgbColor, isHslColor,
|
|
666
|
+
│ isOklchColor, isColorFunction, isCssVariable
|
|
667
|
+
│
|
|
668
|
+
├── Parsing
|
|
669
|
+
│ normalizeColor → universal entry point; handles all formats + objects
|
|
670
|
+
│ rgbaStringToRgba → rgb()/rgba() parser
|
|
671
|
+
│ hex8ToRgba → 8-digit and 4-digit hex with alpha
|
|
672
|
+
│ parseOklchString → oklch(L C H / alpha) with % L support
|
|
673
|
+
│ parseColorFn → color(display-p3 ...) / color(srgb ...)
|
|
674
|
+
│ parseHwbString → hwb() string
|
|
675
|
+
│ parseCssVar → var(--name, fallback)
|
|
676
|
+
│
|
|
677
|
+
├── Color Math — every space via RGB as the hub
|
|
678
|
+
│ sRGB ↔ Linear (srgbChanToLinear / linearChanToSrgb)
|
|
679
|
+
│ XYZ D65 intermediate for Lab/LCH
|
|
680
|
+
│ Oklab/Oklch — Björn Ottosson matrices
|
|
681
|
+
│ Display P3 — ICC sRGB→P3 and P3→sRGB matrices
|
|
682
|
+
│
|
|
683
|
+
├── Manipulation
|
|
684
|
+
│ lighten / darken / saturate / desaturate — via HSL
|
|
685
|
+
│ invertColor — RGB invert
|
|
686
|
+
│ grayscale — BT.709 perceptual weights
|
|
687
|
+
│ rotateHue — via HSL hue shift
|
|
688
|
+
│ adjustHexBrightness — linear channel blend toward 0/255
|
|
689
|
+
│ setAlpha / getAlpha — rgba string manipulation
|
|
690
|
+
│
|
|
691
|
+
├── mixColors (core of interpolation system)
|
|
692
|
+
│ 6 color space modes: rgb | hsl | lab | lch | oklab | oklch
|
|
693
|
+
│ 4 hue interpolation modes: shorter | longer | increasing | decreasing
|
|
694
|
+
│ 4 output formats: hex | rgb | rgba | hsl
|
|
695
|
+
│ Used internally by: interpolateColors, createColorScale,
|
|
696
|
+
│ midpointColor, tints, shades, tones,
|
|
697
|
+
│ generateGradientColors*, generateTints*, generateShades*
|
|
698
|
+
│
|
|
699
|
+
├── Color Harmonies
|
|
700
|
+
│ All implemented as hue rotations on normalized hex
|
|
701
|
+
│
|
|
702
|
+
├── Palette Generation
|
|
703
|
+
│ colorShades — HSL lightness sweep
|
|
704
|
+
│ monochromatic — HSL saturation sweep
|
|
705
|
+
│ tints / shades / tones — Oklab interpolation via mixColors
|
|
706
|
+
│
|
|
707
|
+
├── Accessibility
|
|
708
|
+
│ relativeLuminance — WCAG linearization formula
|
|
709
|
+
│ contrastRatio — (L1+0.05)/(L2+0.05)
|
|
710
|
+
│ wcagLevel — ratio thresholds 3 / 4.5 / 7
|
|
711
|
+
│ isReadableOnBackground — handles 3 background types;
|
|
712
|
+
│ semi-transparent: alpha-composites onto underlay before ratio check
|
|
713
|
+
│ gradient: evaluates each stop, uses minimum ratio
|
|
714
|
+
│ bestContrastPalette — score = avg×0.4 + min×0.6
|
|
715
|
+
│
|
|
716
|
+
├── Color Blindness
|
|
717
|
+
│ Vienot 1999 matrices on linear RGB
|
|
718
|
+
│ 3 types: protanopia / deuteranopia / tritanopia
|
|
719
|
+
│
|
|
720
|
+
├── Cache (Section 5.1)
|
|
721
|
+
│ Simple Map<string, NormalizeResult>
|
|
722
|
+
│ normalizeColorCached — Map lookup before calling normalizeColor
|
|
723
|
+
│ getCacheStats / clearColorCache / enableCache / disableCache
|
|
724
|
+
│
|
|
725
|
+
├── Generator functions (Section 5.2)
|
|
726
|
+
│ ES2015 generator syntax (*) — yield one color at a time
|
|
727
|
+
│ generateGradientColors* → mixColors inside the generator
|
|
728
|
+
│ generateTints* / generateShades* → Oklab mix toward white/black
|
|
729
|
+
│
|
|
730
|
+
└── CLI (dist/cli/bin/cli.js → cvt)
|
|
731
|
+
Commands: info | convert | contrast | shades | harmonies | nearest
|
|
732
|
+
Accepts any color format normalizeColor understands
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Bundle size & dependencies
|
|
738
|
+
|
|
739
|
+
| | |
|
|
740
|
+
|---|---|
|
|
741
|
+
| **Runtime dependencies** | None |
|
|
742
|
+
| **Peer dependencies** | None |
|
|
743
|
+
| **Dev dependencies** | TypeScript, Vitest |
|
|
744
|
+
| **ESM entry** | `dist/esm/index.js` (tree-shakeable) |
|
|
745
|
+
| **CJS entry** | `dist/cjs/index.js` |
|
|
746
|
+
| **CLI entry** | `dist/cli/bin/cli.js` → `cvt` |
|
|
747
|
+
|
|
748
|
+
The package ships both ESM (`type: module`) and CommonJS modules. Every function is a named export — unused functions are eliminated by bundlers that support tree-shaking.
|
|
749
|
+
|
|
340
750
|
---
|
|
341
751
|
|
|
342
752
|
## License
|
|
@@ -363,4 +773,4 @@ Open source takes time and effort. If my work saves you time or brings value, co
|
|
|
363
773
|
<img src="https://img.shields.io/badge/Donate-CryptoCloud-8A2BE2?style=for-the-badge&logo=cryptocurrency&logoColor=white" alt="Donate via CryptoCloud">
|
|
364
774
|
</a>
|
|
365
775
|
|
|
366
|
-
Thank you for being part of this journey. ❤️
|
|
776
|
+
Thank you for being part of this journey. ❤️
|
package/dist/cjs/index.js
CHANGED
|
@@ -236,11 +236,11 @@ function displayP3ToRgb(p3) {
|
|
|
236
236
|
const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
|
|
237
237
|
const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
|
|
238
238
|
const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
|
|
239
|
-
// Apply sRGB gamma
|
|
239
|
+
// Apply sRGB gamma and scale to 0-255
|
|
240
240
|
return {
|
|
241
|
-
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
|
|
242
|
-
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
|
|
243
|
-
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
|
|
241
|
+
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
|
|
242
|
+
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
|
|
243
|
+
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
// Helpers for Display P3 (operate on 0-1 values)
|
|
@@ -254,8 +254,11 @@ function linearChanToSrgb(v) {
|
|
|
254
254
|
function toDisplayP3Hex(color) {
|
|
255
255
|
const n = normalizeColorRaw(color);
|
|
256
256
|
const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
return rgbToHex({
|
|
258
|
+
r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
|
|
259
|
+
g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
|
|
260
|
+
b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
|
|
261
|
+
});
|
|
259
262
|
}
|
|
260
263
|
// Internal raw normalizer (no cache, no circular dep issues)
|
|
261
264
|
function normalizeColorRaw(color) {
|
|
@@ -660,13 +663,7 @@ function normalizeColor(input) {
|
|
|
660
663
|
const str = input.trim();
|
|
661
664
|
if (isCssVariable(str))
|
|
662
665
|
return { type: 'css-var', raw: str };
|
|
663
|
-
|
|
664
|
-
const hex = normalizeHex(str);
|
|
665
|
-
const [r, g, b] = hexToRgb(hex);
|
|
666
|
-
const [h, s, l] = hexToHsl(hex);
|
|
667
|
-
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
668
|
-
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
669
|
-
}
|
|
666
|
+
// 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
|
|
670
667
|
const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
|
|
671
668
|
if (hex8) {
|
|
672
669
|
const rgba = hex8ToRgba(str);
|
|
@@ -678,6 +675,13 @@ function normalizeColor(input) {
|
|
|
678
675
|
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
679
676
|
}
|
|
680
677
|
}
|
|
678
|
+
if (isHexColor(str)) {
|
|
679
|
+
const hex = normalizeHex(str);
|
|
680
|
+
const [r, g, b] = hexToRgb(hex);
|
|
681
|
+
const [h, s, l] = hexToHsl(hex);
|
|
682
|
+
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
683
|
+
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
684
|
+
}
|
|
681
685
|
if (isRgbColor(str)) {
|
|
682
686
|
const rgba = rgbaStringToRgba(str);
|
|
683
687
|
if (rgba) {
|
|
@@ -704,17 +708,6 @@ function normalizeColor(input) {
|
|
|
704
708
|
}
|
|
705
709
|
if (str === 'transparent')
|
|
706
710
|
return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
|
|
707
|
-
// 4-digit #RGBA hex
|
|
708
|
-
if (/^#[0-9a-f]{4}$/i.test(str)) {
|
|
709
|
-
const rgba = shortHexToRgba(str);
|
|
710
|
-
if (rgba) {
|
|
711
|
-
const { r, g, b, a } = rgba;
|
|
712
|
-
const hex = rgbToHex({ r, g, b });
|
|
713
|
-
const [h, s, l] = rgbToHsl({ r, g, b });
|
|
714
|
-
const [, , vv] = rgbToHsv({ r, g, b });
|
|
715
|
-
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
711
|
// oklch / oklcha
|
|
719
712
|
if (isOklchColor(str)) {
|
|
720
713
|
const parsed = parseOklchString(str);
|
package/dist/cli/src/index.js
CHANGED
|
@@ -236,11 +236,11 @@ function displayP3ToRgb(p3) {
|
|
|
236
236
|
const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
|
|
237
237
|
const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
|
|
238
238
|
const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
|
|
239
|
-
// Apply sRGB gamma
|
|
239
|
+
// Apply sRGB gamma and scale to 0-255
|
|
240
240
|
return {
|
|
241
|
-
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
|
|
242
|
-
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
|
|
243
|
-
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
|
|
241
|
+
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
|
|
242
|
+
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
|
|
243
|
+
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
// Helpers for Display P3 (operate on 0-1 values)
|
|
@@ -254,8 +254,11 @@ function linearChanToSrgb(v) {
|
|
|
254
254
|
function toDisplayP3Hex(color) {
|
|
255
255
|
const n = normalizeColorRaw(color);
|
|
256
256
|
const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
return rgbToHex({
|
|
258
|
+
r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
|
|
259
|
+
g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
|
|
260
|
+
b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
|
|
261
|
+
});
|
|
259
262
|
}
|
|
260
263
|
// Internal raw normalizer (no cache, no circular dep issues)
|
|
261
264
|
function normalizeColorRaw(color) {
|
|
@@ -660,13 +663,7 @@ function normalizeColor(input) {
|
|
|
660
663
|
const str = input.trim();
|
|
661
664
|
if (isCssVariable(str))
|
|
662
665
|
return { type: 'css-var', raw: str };
|
|
663
|
-
|
|
664
|
-
const hex = normalizeHex(str);
|
|
665
|
-
const [r, g, b] = hexToRgb(hex);
|
|
666
|
-
const [h, s, l] = hexToHsl(hex);
|
|
667
|
-
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
668
|
-
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
669
|
-
}
|
|
666
|
+
// 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
|
|
670
667
|
const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
|
|
671
668
|
if (hex8) {
|
|
672
669
|
const rgba = hex8ToRgba(str);
|
|
@@ -678,6 +675,13 @@ function normalizeColor(input) {
|
|
|
678
675
|
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
679
676
|
}
|
|
680
677
|
}
|
|
678
|
+
if (isHexColor(str)) {
|
|
679
|
+
const hex = normalizeHex(str);
|
|
680
|
+
const [r, g, b] = hexToRgb(hex);
|
|
681
|
+
const [h, s, l] = hexToHsl(hex);
|
|
682
|
+
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
683
|
+
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
684
|
+
}
|
|
681
685
|
if (isRgbColor(str)) {
|
|
682
686
|
const rgba = rgbaStringToRgba(str);
|
|
683
687
|
if (rgba) {
|
|
@@ -704,17 +708,6 @@ function normalizeColor(input) {
|
|
|
704
708
|
}
|
|
705
709
|
if (str === 'transparent')
|
|
706
710
|
return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
|
|
707
|
-
// 4-digit #RGBA hex
|
|
708
|
-
if (/^#[0-9a-f]{4}$/i.test(str)) {
|
|
709
|
-
const rgba = shortHexToRgba(str);
|
|
710
|
-
if (rgba) {
|
|
711
|
-
const { r, g, b, a } = rgba;
|
|
712
|
-
const hex = rgbToHex({ r, g, b });
|
|
713
|
-
const [h, s, l] = rgbToHsl({ r, g, b });
|
|
714
|
-
const [, , vv] = rgbToHsv({ r, g, b });
|
|
715
|
-
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
711
|
// oklch / oklcha
|
|
719
712
|
if (isOklchColor(str)) {
|
|
720
713
|
const parsed = parseOklchString(str);
|
package/dist/esm/index.js
CHANGED
|
@@ -136,11 +136,11 @@ export function displayP3ToRgb(p3) {
|
|
|
136
136
|
const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
|
|
137
137
|
const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
|
|
138
138
|
const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
|
|
139
|
-
// Apply sRGB gamma
|
|
139
|
+
// Apply sRGB gamma and scale to 0-255
|
|
140
140
|
return {
|
|
141
|
-
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
|
|
142
|
-
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
|
|
143
|
-
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
|
|
141
|
+
r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
|
|
142
|
+
g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
|
|
143
|
+
b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
// Helpers for Display P3 (operate on 0-1 values)
|
|
@@ -154,8 +154,11 @@ function linearChanToSrgb(v) {
|
|
|
154
154
|
export function toDisplayP3Hex(color) {
|
|
155
155
|
const n = normalizeColorRaw(color);
|
|
156
156
|
const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
return rgbToHex({
|
|
158
|
+
r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
|
|
159
|
+
g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
|
|
160
|
+
b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
|
|
161
|
+
});
|
|
159
162
|
}
|
|
160
163
|
// Internal raw normalizer (no cache, no circular dep issues)
|
|
161
164
|
function normalizeColorRaw(color) {
|
|
@@ -560,13 +563,7 @@ export function normalizeColor(input) {
|
|
|
560
563
|
const str = input.trim();
|
|
561
564
|
if (isCssVariable(str))
|
|
562
565
|
return { type: 'css-var', raw: str };
|
|
563
|
-
|
|
564
|
-
const hex = normalizeHex(str);
|
|
565
|
-
const [r, g, b] = hexToRgb(hex);
|
|
566
|
-
const [h, s, l] = hexToHsl(hex);
|
|
567
|
-
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
568
|
-
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
569
|
-
}
|
|
566
|
+
// 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
|
|
570
567
|
const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
|
|
571
568
|
if (hex8) {
|
|
572
569
|
const rgba = hex8ToRgba(str);
|
|
@@ -578,6 +575,13 @@ export function normalizeColor(input) {
|
|
|
578
575
|
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
579
576
|
}
|
|
580
577
|
}
|
|
578
|
+
if (isHexColor(str)) {
|
|
579
|
+
const hex = normalizeHex(str);
|
|
580
|
+
const [r, g, b] = hexToRgb(hex);
|
|
581
|
+
const [h, s, l] = hexToHsl(hex);
|
|
582
|
+
const [hh, ss, vv] = rgbToHsv({ r, g, b });
|
|
583
|
+
return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
|
|
584
|
+
}
|
|
581
585
|
if (isRgbColor(str)) {
|
|
582
586
|
const rgba = rgbaStringToRgba(str);
|
|
583
587
|
if (rgba) {
|
|
@@ -604,17 +608,6 @@ export function normalizeColor(input) {
|
|
|
604
608
|
}
|
|
605
609
|
if (str === 'transparent')
|
|
606
610
|
return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
|
|
607
|
-
// 4-digit #RGBA hex
|
|
608
|
-
if (/^#[0-9a-f]{4}$/i.test(str)) {
|
|
609
|
-
const rgba = shortHexToRgba(str);
|
|
610
|
-
if (rgba) {
|
|
611
|
-
const { r, g, b, a } = rgba;
|
|
612
|
-
const hex = rgbToHex({ r, g, b });
|
|
613
|
-
const [h, s, l] = rgbToHsl({ r, g, b });
|
|
614
|
-
const [, , vv] = rgbToHsv({ r, g, b });
|
|
615
|
-
return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
611
|
// oklch / oklcha
|
|
619
612
|
if (isOklchColor(str)) {
|
|
620
613
|
const parsed = parseOklchString(str);
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "color-value-tools",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"description": "Parse, convert and manipulate colors across hex, RGB, HSL, Lab, LCH, OKLCH and CMYK, with WCAG contrast checks and CSS variables. Zero dependencies.",
|
|
5
|
+
"author": "macrulez <macrulezru@gmail.com> (https://macrulez.ru/en)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/macrulezru/color-value-tools#readme",
|
|
5
8
|
"repository": {
|
|
6
9
|
"type": "git",
|
|
7
10
|
"url": "git+https://github.com/macrulezru/color-value-tools.git"
|
|
8
11
|
},
|
|
9
|
-
"homepage": "https://macrulez.ru/#/en",
|
|
10
12
|
"bugs": {
|
|
11
13
|
"url": "https://github.com/macrulezru/color-value-tools/issues"
|
|
12
14
|
},
|
|
@@ -15,18 +17,24 @@
|
|
|
15
17
|
"types": "dist/cjs/index.d.ts",
|
|
16
18
|
"exports": {
|
|
17
19
|
".": {
|
|
20
|
+
"types": "./dist/cjs/index.d.ts",
|
|
18
21
|
"import": "./dist/esm/index.js",
|
|
19
|
-
"require": "./dist/cjs/index.js"
|
|
20
|
-
"types": "./dist/cjs/index.d.ts"
|
|
22
|
+
"require": "./dist/cjs/index.js"
|
|
21
23
|
}
|
|
22
24
|
},
|
|
23
25
|
"bin": {
|
|
24
26
|
"cvt": "./dist/cli/bin/cli.js"
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
27
|
-
"dist"
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
28
32
|
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
29
36
|
"scripts": {
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
30
38
|
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.cli.json && node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({type:'module'}))\"",
|
|
31
39
|
"test": "vitest run"
|
|
32
40
|
},
|
|
@@ -36,19 +44,13 @@
|
|
|
36
44
|
"hex",
|
|
37
45
|
"rgb",
|
|
38
46
|
"hsl",
|
|
39
|
-
"hsv",
|
|
40
|
-
"hwb",
|
|
41
|
-
"oklab",
|
|
42
47
|
"oklch",
|
|
43
48
|
"cmyk",
|
|
44
|
-
"
|
|
45
|
-
"lch",
|
|
49
|
+
"color-conversion",
|
|
46
50
|
"wcag",
|
|
47
51
|
"accessibility",
|
|
48
52
|
"typescript"
|
|
49
53
|
],
|
|
50
|
-
"author": "",
|
|
51
|
-
"license": "MIT",
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@types/node": "^25.5.0",
|
|
54
56
|
"typescript": "^5.0.0",
|