colorlip 0.2.0 → 1.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/README.md CHANGED
@@ -1,154 +1,394 @@
1
1
  # colorlip
2
2
 
3
- Fast, general-purpose dominant color extraction library for Node.js and Browser. Especially strong with illustrations and artwork. Zero-dependency core.
3
+ Perceptually tuned dominant color and palette extraction for Node.js and the browser.
4
+
5
+ `colorlip` is a lightweight, fast, TypeScript-first library for extracting dominant colors and compact palettes from images. It is tuned to pick colors that feel more visually representative, especially for illustrations, artwork, and product images where impression matters more than raw pixel counts. It is designed with visually driven use cases in mind, including illustration communities, social platforms, and commerce experiences.
6
+
7
+ [日本語版 README](./README.ja.md)
8
+
9
+ ![colorlip preview](./docs/public/hero.jpg)
4
10
 
5
11
  ## Features
6
12
 
7
- - Adaptive color extraction using CIELAB Delta E perceptual distance
8
- - Rich output: hex, HSL, Lab, LCH, OKLab, OKLCH, CSS color strings, and hue category per color
9
- - Platform-agnostic core (`colorlip`) works anywhere
10
- - Built-in adapters for **Node.js (sharp)** and **Browser (Canvas API)**
11
- - TypeScript-first with full type definitions
12
- - Zero runtime dependencies in core
13
+ - Perceptually tuned color extraction for visually representative results
14
+ - Composition-aware weighting with center/border and edge-aware heuristics
15
+ - Adaptive palette extraction based on image statistics
16
+ - Natural merging of nearby colors using CIELAB Delta E
17
+ - Practical palette API with role-based `dominant`, `accent`, and `swatches`
18
+ - Rich color output: `hex`, `HSL`, `Lab`, `LCH`, `OKLab`, `OKLCH`, CSS color strings, and hue category
19
+ - Platform-agnostic core for raw pixel data
20
+ - Built-in adapters for `sharp` and Canvas
21
+ - Zero runtime dependencies in the core package
22
+
23
+ ## Why colorlip
24
+
25
+ `colorlip` is not just a color quantizer.
26
+
27
+ Many palette extractors mainly summarize image-wide color distribution. `colorlip` is tuned to choose colors that feel important in the image, using lightweight composition-aware heuristics and perceptual color spaces.
28
+
29
+ - It adapts thresholds from the image itself instead of relying only on fixed cutoffs
30
+ - It uses center/border and edge-aware weighting so large background areas do not always win
31
+ - It merges nearby colors perceptually in Lab, then picks `dominant` and `accent` for different roles
32
+ - It is especially tuned for illustrations, artwork, thumbnails, and visually curated image sets
13
33
 
14
34
  ## Install
15
35
 
36
+ Core only:
37
+
16
38
  ```bash
17
39
  npm install colorlip
18
40
  ```
19
41
 
20
- For Node.js usage with sharp adapter:
42
+ Node.js with the `sharp` adapter:
21
43
 
22
44
  ```bash
23
45
  npm install colorlip sharp
24
46
  ```
25
47
 
48
+ `sharp` is an optional peer dependency and is only required when you use `colorlip/sharp`.
49
+
26
50
  ## Quick Start
27
51
 
28
- ### Node.js (sharp)
52
+ ### Node.js
29
53
 
30
54
  ```ts
31
- import { colorlipFromFile } from "colorlip/sharp";
32
-
33
- const colors = await colorlipFromFile("photo.jpg");
34
-
35
- console.log(colors[0]);
36
- // {
37
- // r: 42, g: 98, b: 168, hex: '#2A62A8', percentage: 0.34,
38
- // hue: 213, saturation: 75, lightness: 41, hueCategory: 'blue',
39
- // lab: { L: 41.2, a: -2.3, b: -40.1 },
40
- // lch: { L: 41.2, C: 40.2, H: 266.7 },
41
- // oklab: { L: 0.49, a: -0.03, b: -0.12 },
42
- // oklch: { L: 0.49, C: 0.12, H: 256.7 },
43
- // css: { rgb: 'rgb(42 98 168)', hsl: 'hsl(213 60% 41%)', ... }
44
- // }
55
+ import { getColors, getPalette } from "colorlip/sharp";
56
+
57
+ const colors = await getColors("photo.jpg");
58
+ const palette = await getPalette("photo.jpg");
59
+
60
+ console.log(colors[0]?.hex);
61
+ console.log(palette.dominant?.hex);
62
+ console.log(palette.accent?.hex);
63
+ console.log(palette.swatches);
45
64
  ```
46
65
 
47
- ### Browser (Canvas API)
66
+ ### Browser
48
67
 
49
68
  ```ts
50
- import { colorlipFromImage } from "colorlip/canvas";
69
+ import { getColors, getPalette } from "colorlip/canvas";
51
70
 
52
- const colors = await colorlipFromImage(imgElement);
71
+ const colors = await getColors(imgElement);
72
+ const palette = await getPalette(imgElement);
53
73
  ```
54
74
 
55
- ### Raw pixels (any environment)
75
+ ### Raw Pixels
56
76
 
57
77
  ```ts
58
- import { colorlip } from "colorlip";
78
+ import { getColors, getPalette } from "colorlip";
59
79
 
60
- const colors = colorlip(pixelData, width, height, channels);
80
+ const colors = getColors(pixelData, width, height, channels);
81
+ const palette = getPalette(pixelData, width, height, channels);
61
82
  ```
62
83
 
84
+ ## Package Entry Points
85
+
86
+ ### `colorlip`
87
+
88
+ Core API for raw pixel buffers.
89
+
90
+ ### `colorlip/sharp`
91
+
92
+ Node.js adapter backed by `sharp`. It loads the image, downsizes it, and passes raw pixels to the core.
93
+
94
+ ### `colorlip/canvas`
95
+
96
+ Browser adapter backed by the Canvas API. It accepts browser image sources, draws them to a canvas, and passes `ImageData` to the core.
97
+
63
98
  ## API
64
99
 
65
- ### `colorlipFromFile(filePath, options?)` — Node.js / sharp
100
+ ### Core
66
101
 
67
- Reads an image file and returns dominant colors.
102
+ ```ts
103
+ import {
104
+ aggregateColors,
105
+ colorlip,
106
+ createDominantColor,
107
+ extractFallbackPalette,
108
+ getColors,
109
+ getHueCategory,
110
+ getPalette,
111
+ rgbToHex,
112
+ rgbToHsl,
113
+ } from "colorlip";
114
+ ```
68
115
 
69
- ### `colorlipFromBuffer(buffer, options?)` — Node.js / sharp
116
+ #### `getColors(data, width, height, channels, options?)`
70
117
 
71
- Extracts from an in-memory image buffer.
118
+ Extract dominant colors from raw pixel data.
72
119
 
73
- ### `colorlipFromImage(source, options?)` Browser / Canvas
120
+ - `data`: `Uint8Array | Uint8ClampedArray`
121
+ - `width`: image width
122
+ - `height`: image height
123
+ - `channels`: typically `3` or `4`
74
124
 
75
- Accepts `HTMLImageElement`, `ImageBitmap`, `Blob`, or image URL string.
125
+ Returns `ColorlipColor[]`.
76
126
 
77
- ### `colorlipFromImageData(imageData, options?)` — Browser / Canvas
127
+ #### `getPalette(data, width, height, channels, options?)`
78
128
 
79
- Extracts from a Canvas `ImageData` object directly.
129
+ Extract a structured palette from raw pixel data.
80
130
 
81
- ### `colorlip(data, width, height, channels, options?)` — Core
131
+ Returns:
82
132
 
83
- Low-level function that works with raw pixel data (`Uint8Array` / `Uint8ClampedArray`). Platform-agnostic.
133
+ ```ts
134
+ interface ColorlipPalette {
135
+ dominant: ColorlipColor | null;
136
+ accent: ColorlipColor | null;
137
+ swatches: ColorlipColor[];
138
+ }
139
+ ```
140
+
141
+ #### `colorlip(...)`
142
+
143
+ Compatibility alias of `getColors(...)`.
84
144
 
85
- ### Options
145
+ ### Node.js Adapter
146
+
147
+ ```ts
148
+ import {
149
+ colorlip,
150
+ colorlipFromBuffer,
151
+ colorlipFromFile,
152
+ getColors,
153
+ getColorsFromPixels,
154
+ getPalette,
155
+ getPaletteFromPixels,
156
+ } from "colorlip/sharp";
157
+ ```
158
+
159
+ #### `getColors(source, options?)`
160
+
161
+ - `source`: `string | Buffer | Uint8Array`
162
+
163
+ Loads an image through `sharp` and returns `Promise<ColorlipColor[]>`.
164
+
165
+ #### `getPalette(source, options?)`
166
+
167
+ - `source`: `string | Buffer | Uint8Array`
168
+
169
+ Loads an image through `sharp` and returns `Promise<ColorlipPalette>`.
170
+
171
+ #### `getColorsFromPixels(...)`
172
+
173
+ Re-export of the raw-pixel core API.
174
+
175
+ #### `getPaletteFromPixels(...)`
176
+
177
+ Re-export of the raw-pixel core palette API.
178
+
179
+ #### Legacy aliases
180
+
181
+ - `colorlipFromFile(...)`
182
+ - `colorlipFromBuffer(...)`
183
+
184
+ ### Browser Adapter
185
+
186
+ ```ts
187
+ import {
188
+ colorlip,
189
+ colorlipFromImage,
190
+ colorlipFromImageData,
191
+ getColors,
192
+ getColorsFromImageData,
193
+ getColorsFromPixels,
194
+ getPalette,
195
+ getPaletteFromImageData,
196
+ getPaletteFromPixels,
197
+ } from "colorlip/canvas";
198
+ ```
199
+
200
+ #### `getColors(source, options?)`
201
+
202
+ - `source`: `HTMLImageElement | ImageBitmap | Blob | string`
203
+
204
+ Returns `Promise<ColorlipColor[]>`.
205
+
206
+ #### `getPalette(source, options?)`
207
+
208
+ - `source`: `HTMLImageElement | ImageBitmap | Blob | string`
209
+
210
+ Returns `Promise<ColorlipPalette>`.
211
+
212
+ #### `getColorsFromImageData(imageData, options?)`
213
+
214
+ Extract colors directly from `ImageData`.
215
+
216
+ #### `getPaletteFromImageData(imageData, options?)`
217
+
218
+ Extract a palette directly from `ImageData`.
219
+
220
+ #### `getColorsFromPixels(...)`
221
+
222
+ Re-export of the raw-pixel core API.
223
+
224
+ #### `getPaletteFromPixels(...)`
225
+
226
+ Re-export of the raw-pixel core palette API.
227
+
228
+ #### Legacy aliases
229
+
230
+ - `colorlipFromImage(...)`
231
+ - `colorlipFromImageData(...)`
232
+
233
+ ## Options
86
234
 
87
235
  ```ts
88
236
  interface ExtractOptions {
89
- numColors?: number; // Number of colors to extract (default: 3)
90
- saturationThreshold?: number; // Saturation filter threshold (default: 0.15)
91
- brightnessMin?: number; // Min brightness filter (default: 20)
92
- brightnessMax?: number; // Max brightness filter (default: 235)
93
- quantizationStep?: number; // Quantization step size (default: 12)
237
+ numColors?: number; // default: 3
238
+ saturationThreshold?: number; // default: 0.15
239
+ brightnessMin?: number; // default: 20
240
+ brightnessMax?: number; // default: 235
241
+ quantizationStep?: number; // default: 12
242
+ }
243
+ ```
244
+
245
+ Default values:
246
+
247
+ ```ts
248
+ {
249
+ numColors: 3,
250
+ saturationThreshold: 0.15,
251
+ brightnessMin: 20,
252
+ brightnessMax: 235,
253
+ quantizationStep: 12,
94
254
  }
95
255
  ```
96
256
 
97
- ### Output
257
+ ## Output
98
258
 
99
- Each color in the result array is a `DominantColor`:
259
+ Each extracted color is returned as a `ColorlipColor`:
100
260
 
101
261
  ```ts
102
- interface DominantColor {
103
- r: number; // 0-255
104
- g: number; // 0-255
105
- b: number; // 0-255
106
- hex: string; // e.g. "#2A62A8"
107
- percentage: number; // Relative weight (0-1)
108
- hue: number; // 0-360
109
- saturation: number; // 0-100
110
- lightness: number; // 0-100
111
- hueCategory: HueCategory; // "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "violet" | "gray"
112
-
113
- lab: { L: number; a: number; b: number } // CIE L*a*b*
114
- lch: { L: number; C: number; H: number } // CIE LCH
115
- oklab: { L: number; a: number; b: number } // OKLab
116
- oklch: { L: number; C: number; H: number } // OKLCH
262
+ interface ColorlipColor {
263
+ r: number;
264
+ g: number;
265
+ b: number;
266
+ hex: string;
267
+ percentage: number;
268
+ hue: number;
269
+ saturation: number;
270
+ lightness: number;
271
+ hueCategory: HueCategory;
272
+ lab: { L: number; a: number; b: number };
273
+ lch: { L: number; C: number; H: number };
274
+ oklab: { L: number; a: number; b: number };
275
+ oklch: { L: number; C: number; H: number };
276
+ css: {
277
+ rgb: string;
278
+ hsl: string;
279
+ lab: string;
280
+ lch: string;
281
+ oklab: string;
282
+ oklch: string;
283
+ };
284
+ }
285
+ ```
286
+
287
+ Example:
117
288
 
289
+ ```ts
290
+ {
291
+ r: 42,
292
+ g: 98,
293
+ b: 168,
294
+ hex: "#2A62A8",
295
+ percentage: 0.34,
296
+ hue: 213,
297
+ saturation: 60,
298
+ lightness: 41,
299
+ hueCategory: "blue",
300
+ lab: { L: 41.2, a: -2.3, b: -40.1 },
301
+ lch: { L: 41.2, C: 40.2, H: 266.7 },
302
+ oklab: { L: 0.49, a: -0.03, b: -0.12 },
303
+ oklch: { L: 0.49, C: 0.12, H: 256.7 },
118
304
  css: {
119
- rgb: string // "rgb(42 98 168)"
120
- hsl: string // "hsl(213 60% 41%)"
121
- lab: string // "lab(43.1 -2.3 -40.1)"
122
- lch: string // "lch(43.1 40.2 266.7)"
123
- oklab: string // "oklab(0.49 -0.03 -0.12)"
124
- oklch: string // "oklch(0.49 0.12 256.7)"
125
- }
305
+ rgb: "rgb(42 98 168)",
306
+ hsl: "hsl(213 60% 41%)",
307
+ lab: "lab(41.2 -2.3 -40.1)",
308
+ lch: "lch(41.2 40.2 266.7)",
309
+ oklab: "oklab(0.49 -0.03 -0.12)",
310
+ oklch: "oklch(0.49 0.12 256.7)",
311
+ },
126
312
  }
127
313
  ```
128
314
 
129
- ### Utility functions
315
+ ## Utility Functions
130
316
 
131
317
  ```ts
132
- import { rgbToHex, rgbToHsl, getHueCategory, createDominantColor, aggregateColors } from "colorlip";
318
+ import {
319
+ aggregateColors,
320
+ createDominantColor,
321
+ getHueCategory,
322
+ rgbToHex,
323
+ rgbToHsl,
324
+ } from "colorlip";
133
325
  ```
134
326
 
135
327
  | Function | Description |
136
- |----------|-------------|
137
- | `rgbToHex(r, g, b)` | RGB hex string (e.g. `"#FF00AA"`) |
138
- | `rgbToHsl(r, g, b)` | RGB `{ h, s, l }` |
139
- | `getHueCategory(hue)` | Hue (0–360) `"red"` \| `"orange"` \| \| `"gray"` |
140
- | `createDominantColor(r, g, b, percentage)` | Build a full `DominantColor` object from RGB + weight |
141
- | `aggregateColors(colorSets, numColors?)` | Merge multiple extraction results into top-N colors |
328
+ | --- | --- |
329
+ | `rgbToHex(r, g, b)` | Convert RGB to `#RRGGBB` |
330
+ | `rgbToHsl(r, g, b)` | Convert RGB to `{ h, s, l }` |
331
+ | `getHueCategory(hue)` | Convert hue to `"red" \| "orange" \| ... \| "gray"` |
332
+ | `createDominantColor(r, g, b, percentage)` | Build a full `ColorlipColor` |
333
+ | `aggregateColors(colorSets, numColors?)` | Merge multiple extraction results into top colors |
334
+ | `extractFallbackPalette(...)` | Run the simplified fallback extractor directly |
142
335
 
143
336
  ## How It Works
144
337
 
145
- 1. **Resize** Image is downscaled to 150x150 max via adapter (sharp / canvas)
146
- 2. **Analyze** — Sampling pass estimates image characteristics (median saturation, edge centrality)
147
- 3. **Extract** Single-pass pixel scan with adaptive saturation threshold, center weighting, and edge weighting
148
- 4. **Quantize** Colors are bucketed by quantization step into a Map
149
- 5. **Score** Each color bucket is scored by weight, saturation, and spatial variance
150
- 6. **Merge** Similar colors are merged using CIE76 Delta E (threshold: 15)
151
- 7. **Fallback** If no colors pass the filter, a simpler histogram-based extraction is used
338
+ The extraction pipeline is:
339
+
340
+ 1. Adapter input is resized to a maximum of `150x150` pixels. Raw-pixel core calls skip this step.
341
+ 2. A sampling pass estimates median saturation, saturation spread, and how centrally edges are concentrated.
342
+ 3. Pixels with `alpha < 0.5` are ignored. Remaining pixels are included with alpha-based weighting.
343
+ 4. Pixels are filtered by adaptive saturation floor and configured brightness range.
344
+ 5. Surviving pixels are quantized into bins and weighted by center bias, edge strength, saturation, and alpha.
345
+ 6. Nearby bins are pre-merged in Lab space using an adaptive Delta E threshold.
346
+ 7. Clusters are scored from weighted presence, then adjusted by spatial distribution, center/border bias, centroid position, spread, and accent preference in OKLCH space.
347
+ 8. Final swatches are selected with another adaptive Delta E merge pass.
348
+ 9. `dominant` is chosen from the leading swatches, not only by raw rank but also by a dominant-preference pass in OKLCH space. This helps promote colors that feel more representative and visually salient, instead of always keeping the darkest or heaviest cluster at the top.
349
+ 10. `accent` is chosen from perceptually distant candidates or swatches.
350
+ 11. If the main path produces no candidates, a simpler histogram-based fallback extractor is used.
351
+
352
+ ## Dominant Selection
353
+
354
+ `palette.dominant` is selected in two stages:
355
+
356
+ 1. `colorlip` first builds and scores cluster candidates from weighted image regions.
357
+ 2. After final swatches are decided, the leading swatches are re-ranked for `dominant` selection using:
358
+ - the original extraction score
359
+ - weighted presence
360
+ - a perceptual preference in OKLCH space
361
+
362
+ This extra pass keeps `dominant` closer to the color that feels like the image's representative color, especially for photos where a large dark area might otherwise win by area alone.
363
+
364
+ ## Alpha Handling
365
+
366
+ When the input has an alpha channel:
367
+
368
+ - Pixels with `alpha < 128` are ignored
369
+ - Pixels with `alpha >= 128` are kept
370
+ - Their contribution is weighted by `(alpha / 255) ** 2`
371
+
372
+ This reduces semi-transparent edge noise while still allowing partially visible colors to contribute.
373
+
374
+ ## Notes
375
+
376
+ - The core accepts both `Uint8Array` and `Uint8ClampedArray`
377
+ - `channels` is expected to be `3` or `4`
378
+ - Browser string sources are fetched with `fetch(...)`
379
+ - Browser adapter resizing happens through `OffscreenCanvas`
380
+ - Grayscale or heavily filtered inputs may fall back to the simpler histogram path
381
+
382
+ ## Compatibility Aliases
383
+
384
+ These names remain available for compatibility:
385
+
386
+ - `DominantColor` as a type alias of `ColorlipColor`
387
+ - `colorlip(...)`
388
+ - `colorlipFromFile(...)`
389
+ - `colorlipFromBuffer(...)`
390
+ - `colorlipFromImage(...)`
391
+ - `colorlipFromImageData(...)`
152
392
 
153
393
  ## License
154
394