@rafters/color-utils 0.2.3
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 +169 -0
- package/package.json +44 -0
- package/src/accessibility.ts +277 -0
- package/src/analysis.ts +166 -0
- package/src/apca-w3.d.ts +28 -0
- package/src/builder.ts +164 -0
- package/src/color-wheel.ts +235 -0
- package/src/colorjs.d.ts +36 -0
- package/src/conversion.ts +92 -0
- package/src/gamut.ts +117 -0
- package/src/harmony.ts +194 -0
- package/src/index.ts +110 -0
- package/src/internal/math.ts +35 -0
- package/src/manipulation.ts +16 -0
- package/src/naming/generator.ts +161 -0
- package/src/naming/hue-hubs.ts +2452 -0
- package/src/naming/index.ts +50 -0
- package/src/naming/quantize.ts +85 -0
- package/src/naming/word-banks.ts +154 -0
- package/src/scale-positions.ts +31 -0
- package/src/semantic.ts +397 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Color Naming Module
|
|
3
|
+
*
|
|
4
|
+
* Generates unique, semantically meaningful color names from OKLCH values
|
|
5
|
+
* using temperature and perceptual weight for word selection.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { generateColorName } from '@rafters/color-utils';
|
|
10
|
+
*
|
|
11
|
+
* const name = generateColorName({ l: 0.65, c: 0.12, h: 230, alpha: 1 });
|
|
12
|
+
* // Returns: "luminous-true-cobalt"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { generateColorName, generateColorNameWithMetadata } from './generator.js';
|
|
17
|
+
export {
|
|
18
|
+
BLUE_HUB,
|
|
19
|
+
type ChromaBand,
|
|
20
|
+
GREEN_HUB,
|
|
21
|
+
getChromaBand,
|
|
22
|
+
getExpandedMaterialWord,
|
|
23
|
+
getLightnessBand,
|
|
24
|
+
getSubIndex,
|
|
25
|
+
HUE_HUBS,
|
|
26
|
+
type HueCell,
|
|
27
|
+
type HueHub,
|
|
28
|
+
type HueMatrix,
|
|
29
|
+
hasExpandedHub,
|
|
30
|
+
type LightnessBand,
|
|
31
|
+
RED_HUB,
|
|
32
|
+
} from './hue-hubs.js';
|
|
33
|
+
export {
|
|
34
|
+
C_BUCKET_COUNT,
|
|
35
|
+
getAllBuckets,
|
|
36
|
+
getCBucket,
|
|
37
|
+
getHBucket,
|
|
38
|
+
getLBucket,
|
|
39
|
+
H_BUCKET_COUNT,
|
|
40
|
+
L_BUCKET_COUNT,
|
|
41
|
+
TOTAL_COMBINATIONS,
|
|
42
|
+
} from './quantize.js';
|
|
43
|
+
export {
|
|
44
|
+
INTENSITY_WORDS,
|
|
45
|
+
type IntensityWord,
|
|
46
|
+
LUMINOSITY_WORDS,
|
|
47
|
+
type LuminosityWord,
|
|
48
|
+
MATERIAL_WORDS,
|
|
49
|
+
type MaterialWord,
|
|
50
|
+
} from './word-banks.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quantization functions for OKLCH to bucket mapping
|
|
3
|
+
*
|
|
4
|
+
* These functions map continuous OKLCH values to discrete bucket indices
|
|
5
|
+
* for deterministic word selection from the word banks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { clamp, clamp01, MAX_CHROMA, normalizeHue } from '../internal/math.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get luminosity bucket index (0-9) from lightness value (0-1)
|
|
12
|
+
* 10 buckets of 0.1 each
|
|
13
|
+
*/
|
|
14
|
+
export function getLBucket(l: number): number {
|
|
15
|
+
// Clamp to valid range
|
|
16
|
+
const clamped = clamp01(l);
|
|
17
|
+
// Map to 0-9 bucket index
|
|
18
|
+
const bucket = Math.floor(clamped * 10);
|
|
19
|
+
// Handle edge case where l === 1.0
|
|
20
|
+
return Math.min(bucket, 9);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get chroma bucket index (0-7) from chroma value (0-0.37+)
|
|
25
|
+
* 8 buckets with non-linear spacing for perceptual uniformity
|
|
26
|
+
*/
|
|
27
|
+
export function getCBucket(c: number): number {
|
|
28
|
+
// Clamp to valid range (OKLCH chroma rarely exceeds 0.37)
|
|
29
|
+
const clamped = clamp(c, 0, MAX_CHROMA);
|
|
30
|
+
|
|
31
|
+
// Non-linear bucket boundaries optimized for perceptual differences
|
|
32
|
+
// Lower chroma values get finer granularity
|
|
33
|
+
if (clamped < 0.03) return 0; // achromatic
|
|
34
|
+
if (clamped < 0.06) return 1; // very muted
|
|
35
|
+
if (clamped < 0.1) return 2; // muted
|
|
36
|
+
if (clamped < 0.15) return 3; // moderate
|
|
37
|
+
if (clamped < 0.2) return 4; // saturated
|
|
38
|
+
if (clamped < 0.25) return 5; // vivid
|
|
39
|
+
if (clamped < 0.3) return 6; // very vivid
|
|
40
|
+
return 7; // maximum saturation
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get hue bucket index (0-17) from hue angle (0-360)
|
|
45
|
+
* 18 buckets of 20 degrees each
|
|
46
|
+
*/
|
|
47
|
+
export function getHBucket(h: number): number {
|
|
48
|
+
// Normalize hue to 0-360 range (handle negative and > 360)
|
|
49
|
+
const normalized = normalizeHue(h);
|
|
50
|
+
// Map to 0-17 bucket index
|
|
51
|
+
const bucket = Math.floor(normalized / 20);
|
|
52
|
+
// Handle edge case where h === 360
|
|
53
|
+
return Math.min(bucket, 17);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all bucket indices for an OKLCH color
|
|
58
|
+
* Useful for debugging and testing
|
|
59
|
+
*/
|
|
60
|
+
export function getAllBuckets(
|
|
61
|
+
l: number,
|
|
62
|
+
c: number,
|
|
63
|
+
h: number,
|
|
64
|
+
): {
|
|
65
|
+
lBucket: number;
|
|
66
|
+
cBucket: number;
|
|
67
|
+
hBucket: number;
|
|
68
|
+
} {
|
|
69
|
+
return {
|
|
70
|
+
lBucket: getLBucket(l),
|
|
71
|
+
cBucket: getCBucket(c),
|
|
72
|
+
hBucket: getHBucket(h),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Constants for bucket counts
|
|
77
|
+
export const L_BUCKET_COUNT = 10;
|
|
78
|
+
export const C_BUCKET_COUNT = 8;
|
|
79
|
+
export const H_BUCKET_COUNT = 18;
|
|
80
|
+
|
|
81
|
+
// Total possible unique combinations
|
|
82
|
+
export const TOTAL_COMBINATIONS = L_BUCKET_COUNT * C_BUCKET_COUNT * H_BUCKET_COUNT;
|
|
83
|
+
// = 10 * 8 * 18 = 1,440 unique chromatic names
|
|
84
|
+
// + 10 * 8 = 80 achromatic names (no hue word)
|
|
85
|
+
// = 1,520 total unique names minimum (without temperature/density variants)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word Banks for Deterministic Color Naming
|
|
3
|
+
*
|
|
4
|
+
* Three independent word banks map to OKLCH dimensions:
|
|
5
|
+
* - Luminosity (L): brightness-based words describing light levels
|
|
6
|
+
* - Intensity (C): chroma-descriptive words describing saturation level
|
|
7
|
+
* - Material (H): objects/places/nature describing hue
|
|
8
|
+
*
|
|
9
|
+
* Each bank is organized by bucket index. Temperature and perceptual
|
|
10
|
+
* density select which variant of a word to use for semantic meaning.
|
|
11
|
+
*
|
|
12
|
+
* RESERVED SEGMENTS (#1637): no bank word may be 'to', 'via', or 'from',
|
|
13
|
+
* nor start/end with one as a hyphen segment. Generated names join bank
|
|
14
|
+
* words with single hyphens, and the fill signature parser splits on
|
|
15
|
+
* '-to-' -- a violating word would make a family name unparseable as a
|
|
16
|
+
* fill. Enforced by the reservation test in test/naming.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Luminosity words by lightness bucket (0-1 in 10 steps)
|
|
21
|
+
* Index 0 = darkest (0.0-0.1), Index 9 = lightest (0.9-1.0)
|
|
22
|
+
*
|
|
23
|
+
* Uses brightness-based words that describe luminance without implying
|
|
24
|
+
* grayscale materials, allowing natural composition with chromatic hues.
|
|
25
|
+
*/
|
|
26
|
+
export const LUMINOSITY_WORDS: readonly string[] = [
|
|
27
|
+
'shadow', // 0.0-0.1: darkest
|
|
28
|
+
'dark', // 0.1-0.2
|
|
29
|
+
'deep', // 0.2-0.3
|
|
30
|
+
'dim', // 0.3-0.4
|
|
31
|
+
'mid', // 0.4-0.5
|
|
32
|
+
'balanced', // 0.5-0.6
|
|
33
|
+
'luminous', // 0.6-0.7
|
|
34
|
+
'pale', // 0.7-0.8
|
|
35
|
+
'faint', // 0.8-0.9
|
|
36
|
+
'brilliant', // 0.9-1.0: lightest
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Intensity words by chroma bucket, split by perceptual density
|
|
41
|
+
* Each density level has words for 8 chroma buckets (0-0.37+)
|
|
42
|
+
*
|
|
43
|
+
* Words describe saturation/chroma level, not personality.
|
|
44
|
+
*/
|
|
45
|
+
export const INTENSITY_WORDS: Record<'light' | 'medium' | 'heavy', readonly string[]> = {
|
|
46
|
+
light: [
|
|
47
|
+
'dust', // 0.00-0.03: near-achromatic (achromatic threshold is 0.02, not 0.03)
|
|
48
|
+
'wash', // 0.03-0.06
|
|
49
|
+
'soft', // 0.06-0.10
|
|
50
|
+
'clean', // 0.10-0.15
|
|
51
|
+
'clear', // 0.15-0.20
|
|
52
|
+
'pure', // 0.20-0.25
|
|
53
|
+
'vivid', // 0.25-0.30
|
|
54
|
+
'bright', // 0.30+
|
|
55
|
+
],
|
|
56
|
+
medium: [
|
|
57
|
+
'ash', // 0.00-0.03
|
|
58
|
+
'faded', // 0.03-0.06
|
|
59
|
+
'quiet', // 0.06-0.10
|
|
60
|
+
'true', // 0.10-0.15
|
|
61
|
+
'firm', // 0.15-0.20
|
|
62
|
+
'bold', // 0.20-0.25
|
|
63
|
+
'sharp', // 0.25-0.30
|
|
64
|
+
'neon', // 0.30+
|
|
65
|
+
],
|
|
66
|
+
heavy: [
|
|
67
|
+
'smoke', // 0.00-0.03
|
|
68
|
+
'muted', // 0.03-0.06
|
|
69
|
+
'worn', // 0.06-0.10
|
|
70
|
+
'rich', // 0.10-0.15
|
|
71
|
+
'strong', // 0.15-0.20
|
|
72
|
+
'dense', // 0.20-0.25 ("deep" excluded: already in LUMINOSITY_WORDS)
|
|
73
|
+
'full', // 0.25-0.30
|
|
74
|
+
'hot', // 0.30+
|
|
75
|
+
],
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Material words by hue bucket (18 buckets at 20 each), split by temperature
|
|
80
|
+
* Temperature influences word choice for more semantic meaning
|
|
81
|
+
*/
|
|
82
|
+
export const MATERIAL_WORDS: Record<'warm' | 'cool' | 'neutral', readonly string[]> = {
|
|
83
|
+
warm: [
|
|
84
|
+
'ember', // 0-20: red
|
|
85
|
+
'copper', // 20-40: orange-red
|
|
86
|
+
'amber', // 40-60: orange
|
|
87
|
+
'honey', // 60-80: yellow-orange
|
|
88
|
+
'citrine', // 80-100: yellow
|
|
89
|
+
'chartreuse', // 100-120: yellow-green
|
|
90
|
+
'fern', // 120-140: green
|
|
91
|
+
'sage', // 140-160: green-cyan
|
|
92
|
+
'lagoon', // 160-180: cyan
|
|
93
|
+
'azure', // 180-200: cyan-blue
|
|
94
|
+
'cerulean', // 200-220: light blue
|
|
95
|
+
'cobalt', // 220-240: blue
|
|
96
|
+
'indigo', // 240-260: blue-violet
|
|
97
|
+
'iris', // 260-280: violet
|
|
98
|
+
'orchid', // 280-300: purple
|
|
99
|
+
'fuchsia', // 300-320: magenta
|
|
100
|
+
'rose', // 320-340: pink
|
|
101
|
+
'crimson', // 340-360: red-pink
|
|
102
|
+
],
|
|
103
|
+
cool: [
|
|
104
|
+
'rust', // 0-20
|
|
105
|
+
'terracotta', // 20-40
|
|
106
|
+
'sienna', // 40-60
|
|
107
|
+
'flax', // 60-80
|
|
108
|
+
'canary', // 80-100
|
|
109
|
+
'lime', // 100-120
|
|
110
|
+
'moss', // 120-140
|
|
111
|
+
'mint', // 140-160
|
|
112
|
+
'teal', // 160-180
|
|
113
|
+
'glacier', // 180-200
|
|
114
|
+
'sky', // 200-220
|
|
115
|
+
'sapphire', // 220-240
|
|
116
|
+
'navy', // 240-260
|
|
117
|
+
'violet', // 260-280
|
|
118
|
+
'plum', // 280-300
|
|
119
|
+
'magenta', // 300-320
|
|
120
|
+
'raspberry', // 320-340
|
|
121
|
+
'scarlet', // 340-360
|
|
122
|
+
],
|
|
123
|
+
neutral: [
|
|
124
|
+
'clay', // 0-20
|
|
125
|
+
'cinnamon', // 20-40
|
|
126
|
+
'marigold', // 40-60
|
|
127
|
+
'buttercup', // 60-80
|
|
128
|
+
'pear', // 80-100
|
|
129
|
+
'olive', // 100-120
|
|
130
|
+
'spruce', // 120-140
|
|
131
|
+
'seafoam', // 140-160
|
|
132
|
+
'aqua', // 160-180
|
|
133
|
+
'arctic', // 180-200
|
|
134
|
+
'pacific', // 200-220
|
|
135
|
+
'denim', // 220-240
|
|
136
|
+
'midnight', // 240-260
|
|
137
|
+
'lavender', // 260-280
|
|
138
|
+
'amethyst', // 280-300
|
|
139
|
+
'peony', // 300-320
|
|
140
|
+
'cherry', // 320-340
|
|
141
|
+
'poppy', // 340-360
|
|
142
|
+
],
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
145
|
+
// Type exports for external use
|
|
146
|
+
export type LuminosityWord = (typeof LUMINOSITY_WORDS)[number];
|
|
147
|
+
export type IntensityWord =
|
|
148
|
+
| (typeof INTENSITY_WORDS.light)[number]
|
|
149
|
+
| (typeof INTENSITY_WORDS.medium)[number]
|
|
150
|
+
| (typeof INTENSITY_WORDS.heavy)[number];
|
|
151
|
+
export type MaterialWord =
|
|
152
|
+
| (typeof MATERIAL_WORDS.warm)[number]
|
|
153
|
+
| (typeof MATERIAL_WORDS.cool)[number]
|
|
154
|
+
| (typeof MATERIAL_WORDS.neutral)[number];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard 11-position color scale conventions.
|
|
3
|
+
*
|
|
4
|
+
* SCALE_POSITIONS maps array indices 0-10 to the canonical Tailwind-style
|
|
5
|
+
* position labels (50, 100, 200, ..., 950). POSITION_TO_INDEX is the inverse.
|
|
6
|
+
*
|
|
7
|
+
* Selection logic (foreground pairs, state ladders, dark counterparts) lives
|
|
8
|
+
* in semantic.ts — this module is scale vocabulary only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SCALE_POSITIONS = [
|
|
12
|
+
'50',
|
|
13
|
+
'100',
|
|
14
|
+
'200',
|
|
15
|
+
'300',
|
|
16
|
+
'400',
|
|
17
|
+
'500',
|
|
18
|
+
'600',
|
|
19
|
+
'700',
|
|
20
|
+
'800',
|
|
21
|
+
'900',
|
|
22
|
+
'950',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export const POSITION_TO_INDEX: Record<string, number> = SCALE_POSITIONS.reduce(
|
|
26
|
+
(acc, position, index) => {
|
|
27
|
+
acc[position] = index;
|
|
28
|
+
return acc;
|
|
29
|
+
},
|
|
30
|
+
{} as Record<string, number>,
|
|
31
|
+
);
|