@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.
@@ -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
+ );