smart-downscaler 0.4.5 → 0.6.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,264 +1,736 @@
1
1
  # Smart Pixel Art Downscaler
2
2
 
3
- A sophisticated Rust library for intelligent image downscaling with focus on pixel art quality.
3
+ A high-performance Rust library for intelligent image downscaling with pixel art quality preservation.
4
4
 
5
- **Available as both a native Rust library and a WebAssembly module for browser/Node.js usage.**
5
+ **Available as a native Rust library and WebAssembly module for browser/Node.js.**
6
6
 
7
+ [![Version](https://img.shields.io/badge/version-0.5.0-blue.svg)]()
8
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)]()
7
9
 
8
- ## What's New in v0.3.5
10
+ ---
9
11
 
10
- ### Performance: Direct LUT Optimization
12
+ ## Table of Contents
11
13
 
12
- The preprocessing pipeline has been completely rewritten for speed:
14
+ - [Features](#features)
15
+ - [Installation](#installation)
16
+ - [Quick Start](#quick-start)
17
+ - [Configuration Reference](#configuration-reference)
18
+ - [API Reference](#api-reference)
19
+ - [Presets](#presets)
20
+ - [Advanced Usage](#advanced-usage)
21
+ - [Performance Tips](#performance-tips)
22
+ - [Why Oklab?](#why-oklab)
23
+ - [CLI Reference](#cli-reference)
24
+ - [License](#license)
13
25
 
14
- | Old Method (v0.3) | New Method (v0.3.5) | Improvement |
15
- | ------------------------------ | --------------------- | --------------------- |
16
- | HashMap Pre-quantization | **Direct LUT (64MB)** | O(1) Access |
17
- | Oklab Conversion on All Pixels | **Cached Oklab** | \~100x Fewer Math Ops |
18
- | Iterative Resolution Cap | **One-pass NN** | Instant Resizing |
26
+ ---
19
27
 
20
- **Key Optimizations:**
28
+ ## Features
21
29
 
22
- 1. **Direct Lookup Table**: Uses a 64MB flat array to map 24-bit RGB colors to unique indices instantly. No hashing overhead.
30
+ | Feature | Description |
31
+ |---------|-------------|
32
+ | **Oklab Color Space** | Modern perceptual color space with superior hue linearity |
33
+ | **Multiple Palette Strategies** | 6 different extraction methods for various use cases |
34
+ | **Region Segmentation** | SLIC superpixels, hierarchical clustering, or fast union-find |
35
+ | **Edge-Aware Processing** | Sobel/Scharr detection preserves boundaries |
36
+ | **Spatial Coherence** | Neighbor and region voting for smooth results |
37
+ | **K-Centroid Tile Logic** | Advanced dominant color extraction per tile |
38
+ | **Performance Preprocessing** | Resolution capping and color pre-quantization |
39
+ | **WebAssembly Support** | Full browser compatibility with near-native speed |
23
40
 
24
- 2. **RGBA-Only Preprocessing**: Resolution capping and quantization now happen strictly in RGBA space before any expensive Oklab math.
41
+ ---
25
42
 
26
- 3. **Oklab Caching**: If color reduction is enabled (default), Oklab conversion is only performed once per unique color, not per pixel.
43
+ ## Installation
27
44
 
28
- <!---->
45
+ ### Rust (Native)
29
46
 
30
- const config = new WasmDownscaleConfig();
31
- config.max_resolution_mp = 1.5; // Fast nearest-neighbor cap
32
- config.max_color_preprocess = 16384; // Direct LUT quantization
47
+ ```toml
48
+ [dependencies]
49
+ smart-downscaler = "0.5.0"
50
+ ```
33
51
 
52
+ ### WebAssembly (npm)
34
53
 
35
- ### 🎯 K-Centroid Tile Logic
54
+ ```bash
55
+ npm install smart-downscaler
56
+ ```
36
57
 
37
- New `k_centroid` configuration allows finer control over how a source tile is reduced to a single representative color before matching:
58
+ ### WebAssembly (CDN)
38
59
 
39
- | Mode | Name | Description | Best For |
40
- | ---- | ------------ | ----------------------------------------------- | --------------------------------------------- |
41
- | `1` | **Average** | Simple average of all pixels (Default) | Smooth gradients, noise reduction |
42
- | `2` | **Dominant** | Average of the largest color cluster ($k=2$) | Sharp edges, separating foreground/background |
43
- | `3` | **Foremost** | Average of the "foremost" distinct part ($k=3$) | Complex textures, detailed sprites |
60
+ ```html
61
+ <script type="module">
62
+ import init, { downscale_rgba, WasmDownscaleConfig } from 'https://unpkg.com/smart-downscaler@0.5.0/smart_downscaler.js';
63
+ </script>
64
+ ```
44
65
 
45
- // Example: Use dominant part for sharper edges
46
- config.k_centroid = 2;
47
- config.k_centroid_iterations = 2;
66
+ ---
67
+
68
+ ## Quick Start
48
69
 
70
+ ### JavaScript/TypeScript (Browser)
71
+
72
+ ```javascript
73
+ import init, { downscale_rgba, WasmDownscaleConfig } from 'smart-downscaler';
74
+
75
+ // Initialize WASM module (required once)
76
+ await init();
77
+
78
+ // Get image data from canvas
79
+ const canvas = document.getElementById('myCanvas');
80
+ const ctx = canvas.getContext('2d');
81
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
82
+
83
+ // Create configuration
84
+ const config = new WasmDownscaleConfig();
85
+ config.palette_size = 16;
86
+ config.palette_strategy = 'oklab';
87
+
88
+ // Downscale to 64x64
89
+ const result = downscale_rgba(
90
+ imageData.data, // Uint8ClampedArray (RGBA)
91
+ imageData.width, // Source width
92
+ imageData.height, // Source height
93
+ 64, // Target width
94
+ 64, // Target height
95
+ config // Optional config
96
+ );
97
+
98
+ // Draw result
99
+ const outputData = new ImageData(result.data, result.width, result.height);
100
+ outputCtx.putImageData(outputData, 0, 0);
101
+
102
+ // Access palette and indices
103
+ console.log('Palette colors:', result.palette_size);
104
+ console.log('Palette RGB data:', result.palette); // Uint8Array
105
+ console.log('Pixel indices:', result.indices); // Uint8Array
106
+ ```
107
+
108
+ ### Rust (Native)
109
+
110
+ ```rust
111
+ use smart_downscaler::{smart_downscale, DownscaleConfig, Rgb};
112
+ use smart_downscaler::palette::PaletteStrategy;
113
+
114
+ // Create pixel data (from image crate or manually)
115
+ let pixels: Vec<Rgb> = image.pixels()
116
+ .map(|p| Rgb::new(p[0], p[1], p[2]))
117
+ .collect();
118
+
119
+ // Configure
120
+ let config = DownscaleConfig {
121
+ palette_size: 16,
122
+ palette_strategy: PaletteStrategy::OklabMedianCut,
123
+ ..Default::default()
124
+ };
125
+
126
+ // Downscale
127
+ let result = smart_downscale(
128
+ &pixels,
129
+ source_width,
130
+ source_height,
131
+ target_width,
132
+ target_height,
133
+ &config,
134
+ );
135
+
136
+ // Use result
137
+ println!("Output: {}x{}", result.width, result.height);
138
+ println!("Palette: {} colors", result.palette.len());
139
+ ```
140
+
141
+ ---
49
142
 
50
143
  ## Configuration Reference
51
144
 
52
- ### DownscaleConfig
145
+ ### Complete Parameter List
146
+
147
+ | Parameter | Type | Default | Range/Values | Description |
148
+ |-----------|------|---------|--------------|-------------|
149
+ | **Palette Settings** |||||
150
+ | `palette_size` | `usize` | `16` | `1-256` | Number of colors in output palette |
151
+ | `palette_strategy` | `string` | `"oklab"` | See [Palette Strategies](#palette-strategies) | Algorithm for palette extraction |
152
+ | `kmeans_iterations` | `usize` | `5` | `0-20` | K-Means refinement passes (0 = disabled) |
153
+ | **Spatial Coherence** |||||
154
+ | `neighbor_weight` | `f32` | `0.3` | `0.0-1.0` | Bias toward colors used by neighboring tiles |
155
+ | `region_weight` | `f32` | `0.2` | `0.0-1.0` | Bias toward colors used in same region |
156
+ | **Refinement** |||||
157
+ | `two_pass_refinement` | `bool` | `true` | `true/false` | Enable iterative smoothing pass |
158
+ | `refinement_iterations` | `usize` | `3` | `0-10` | Number of refinement passes |
159
+ | **Edge Detection** |||||
160
+ | `edge_weight` | `f32` | `0.5` | `0.0-1.0` | Balance between luminance and color edges |
161
+ | **Segmentation** |||||
162
+ | `segmentation_method` | `string` | `"hierarchy_fast"` | See [Segmentation Methods](#segmentation-methods) | Region detection algorithm |
163
+ | `slic_superpixels` | `usize` | `100` | `10-1000` | Number of superpixels (SLIC only) |
164
+ | `slic_compactness` | `f32` | `10.0` | `1.0-40.0` | Shape regularity (SLIC only) |
165
+ | `hierarchy_threshold` | `f32` | `15.0` | `5.0-50.0` | Color distance merge threshold |
166
+ | `hierarchy_min_size` | `usize` | `4` | `1-100` | Minimum region size in pixels |
167
+ | **Performance** |||||
168
+ | `max_resolution_mp` | `f32` | `1.6` | `0.0-10.0` | Resolution cap in megapixels (0 = disabled) |
169
+ | `max_color_preprocess` | `usize` | `16384` | `0-65536` | Pre-quantization limit (0 = disabled) |
170
+ | **Tile Processing** |||||
171
+ | `k_centroid` | `usize` | `1` | `1`, `2`, `3` | Tile color extraction mode |
172
+ | `k_centroid_iterations` | `usize` | `0` | `0-10` | K-Means iterations for tile color |
173
+
174
+ ---
175
+
176
+ ### Palette Strategies
177
+
178
+ | Strategy | String Value | Description | Best For |
179
+ |----------|--------------|-------------|----------|
180
+ | **Oklab Median Cut** | `"oklab"` | Perceptually uniform color space | General use, balanced results |
181
+ | **Saturation Weighted** | `"saturation"` | Preserves vibrant colors | Colorful artwork, game sprites |
182
+ | **Medoid** | `"medoid"` | Uses only exact source colors | Pixel-perfect reproduction |
183
+ | **K-Means++** | `"kmeans"` | Statistical clustering | Small palettes (4-8 colors) |
184
+ | **Legacy RGB** | `"legacy"` | Classic RGB median cut | Compatibility, comparison |
185
+ | **RGB Bitmask** | `"bitmask"` | Bit-masked clustering | Fast processing, high color counts |
186
+
187
+ ```javascript
188
+ // Examples
189
+ config.palette_strategy = 'oklab'; // Default, recommended
190
+ config.palette_strategy = 'saturation'; // Vibrant colors
191
+ config.palette_strategy = 'medoid'; // Exact source colors only
192
+ config.palette_strategy = 'kmeans'; // Good for tiny palettes
193
+ config.palette_strategy = 'legacy'; // RGB-space (not recommended)
194
+ config.palette_strategy = 'bitmask'; // Fast approximate
195
+ ```
196
+
197
+ ---
198
+
199
+ ### Segmentation Methods
200
+
201
+ | Method | String Value | Description | Performance | Quality |
202
+ |--------|--------------|-------------|-------------|---------|
203
+ | **None** | `"none"` | No region detection | ⚡⚡⚡ Fastest | Basic |
204
+ | **Hierarchy Fast** | `"hierarchy_fast"` | Union-find clustering | ⚡⚡ Fast | Good |
205
+ | **Hierarchy** | `"hierarchy"` | Full hierarchical merge | ⚡ Medium | Better |
206
+ | **SLIC** | `"slic"` | Superpixel segmentation | ⚡ Medium | Best edges |
207
+
208
+ ```javascript
209
+ // Examples
210
+ config.segmentation_method = 'none'; // Speed priority
211
+ config.segmentation_method = 'hierarchy_fast'; // Default, balanced
212
+ config.segmentation_method = 'hierarchy'; // Quality priority
213
+ config.segmentation_method = 'slic'; // Best for photos
214
+ ```
215
+
216
+ ---
217
+
218
+ ### K-Centroid Tile Modes
219
+
220
+ Controls how each source tile is reduced to a single representative color:
221
+
222
+ | Mode | Value | Description | Best For |
223
+ |------|-------|-------------|----------|
224
+ | **Average** | `1` | Simple weighted average of all pixels | Smooth gradients, noise reduction |
225
+ | **Dominant** | `2` | K-Means (k=2), uses largest cluster | Sharp edges, foreground/background separation |
226
+ | **Foremost** | `3` | K-Means (k=3), finer dominant detection | Complex textures, detailed sprites |
227
+
228
+ ```javascript
229
+ // Mode 1: Average (default) - smooth results
230
+ config.k_centroid = 1;
231
+ config.k_centroid_iterations = 0;
232
+
233
+ // Mode 2: Dominant - sharper edges
234
+ config.k_centroid = 2;
235
+ config.k_centroid_iterations = 2;
236
+
237
+ // Mode 3: Foremost - detailed preservation
238
+ config.k_centroid = 3;
239
+ config.k_centroid_iterations = 3;
240
+ ```
241
+
242
+ ---
53
243
 
54
- | Field | Type | Default | Description |
55
- | ----------------------- | ------ | --------- | ----------------------------- |
56
- | `k_centroid` | usize | 1 | 1=Avg, 2=Dominant, 3=Foremost |
57
- | `k_centroid_iterations` | usize | 0 | Refinement for tile color |
58
- | `max_resolution_mp` | f32 | 1.6 | Resolution cap (0=disabled) |
59
- | `max_color_preprocess` | usize | 16384 | LUT Quantization limit |
60
- | `segmentation` | Method | Hierarchy | Region detection method |
244
+ ## API Reference
61
245
 
246
+ ### Core Functions
247
+
248
+ #### `downscale(data, width, height, targetWidth, targetHeight, config?)`
249
+
250
+ Main downscale function accepting `Uint8Array` (RGBA).
251
+
252
+ ```javascript
253
+ const result = downscale(
254
+ rgbaData, // Uint8Array - RGBA pixel data
255
+ 800, // number - Source width
256
+ 600, // number - Source height
257
+ 64, // number - Target width
258
+ 48, // number - Target height
259
+ config // WasmDownscaleConfig? - Optional configuration
260
+ );
261
+ ```
62
262
 
63
- ## Command Line Interface
263
+ #### `downscale_rgba(data, width, height, targetWidth, targetHeight, config?)`
64
264
 
65
- # Enable Dominant Color mode (sharper details)
66
- smart-downscaler input.png output.png --k-centroid 2 --k-centroid-iterations 2
265
+ Same as `downscale` but accepts `Uint8ClampedArray` (from canvas `getImageData`).
67
266
 
68
- # Fast mode (reduce preprocessing limits)
69
- smart-downscaler input.png output.png --segmentation none --no-refinement
267
+ ```javascript
268
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
269
+ const result = downscale_rgba(
270
+ imageData.data, // Uint8ClampedArray
271
+ imageData.width,
272
+ imageData.height,
273
+ 64, 64,
274
+ config
275
+ );
276
+ ```
277
+
278
+ #### `downscale_simple(data, width, height, targetWidth, targetHeight, numColors)`
279
+
280
+ Simplified API with minimal parameters.
281
+
282
+ ```javascript
283
+ const result = downscale_simple(
284
+ rgbaData,
285
+ 800, 600,
286
+ 64, 48,
287
+ 16 // Number of palette colors
288
+ );
289
+ ```
290
+
291
+ #### `downscale_with_palette(data, width, height, targetWidth, targetHeight, palette, config?)`
292
+
293
+ Downscale using a pre-defined palette.
294
+
295
+ ```javascript
296
+ const palette = new Uint8Array([
297
+ 255, 0, 0, // Red
298
+ 0, 255, 0, // Green
299
+ 0, 0, 255, // Blue
300
+ 255, 255, 255, // White
301
+ ]);
302
+
303
+ const result = downscale_with_palette(
304
+ rgbaData,
305
+ 800, 600,
306
+ 64, 48,
307
+ palette, // Uint8Array - RGB, 3 bytes per color
308
+ config
309
+ );
310
+ ```
311
+
312
+ ---
313
+
314
+ ### Palette Functions
315
+
316
+ #### `extract_palette_from_image(data, width, height, numColors, iterations, strategy?)`
317
+
318
+ Extract palette without downscaling.
319
+
320
+ ```javascript
321
+ const palette = extract_palette_from_image(
322
+ rgbaData, // Uint8Array - RGBA pixel data
323
+ 800, // number - Image width (unused but required)
324
+ 600, // number - Image height (unused but required)
325
+ 16, // number - Number of colors to extract
326
+ 5, // number - K-Means iterations
327
+ 'saturation' // string? - Strategy (optional)
328
+ );
329
+ // Returns: Uint8Array - RGB palette (numColors * 3 bytes)
330
+ ```
70
331
 
332
+ #### `quantize_to_palette(data, width, height, palette)`
333
+
334
+ Quantize image to palette without resizing.
71
335
 
72
- ## Features
336
+ ```javascript
337
+ const result = quantize_to_palette(
338
+ rgbaData, // Uint8Array - RGBA pixel data
339
+ 800, // number - Image width
340
+ 600, // number - Image height
341
+ palette // Uint8Array - RGB palette
342
+ );
343
+ // Returns: WasmDownscaleResult (same size, quantized colors)
344
+ ```
73
345
 
74
- - **Oklab Color Space**: Modern perceptual color space with superior hue linearity
346
+ #### `get_palette_strategies()`
75
347
 
76
- - **Global Palette Extraction**: Median Cut + K-Means++ refinement
348
+ Get list of available palette strategies.
77
349
 
78
- - **Multiple Segmentation Methods**:
350
+ ```javascript
351
+ const strategies = get_palette_strategies();
352
+ // Returns: ['oklab', 'saturation', 'medoid', 'kmeans', 'legacy', 'bitmask']
353
+ ```
79
354
 
80
- - SLIC superpixels for fast, balanced regions
355
+ ---
81
356
 
82
- - VTracer-style hierarchical clustering for content-aware boundaries
357
+ ### Color Analysis Functions
83
358
 
84
- - Union-find based fast hierarchical clustering
359
+ #### `analyze_colors(data, maxColors, sortMethod)`
85
360
 
86
- - **Edge-Aware Processing**: Sobel/Scharr edge detection to preserve boundaries
361
+ Analyze unique colors in an image.
87
362
 
88
- - **Neighbor-Coherent Assignment**: Spatial coherence through neighbor and region voting
363
+ ```javascript
364
+ const analysis = analyze_colors(
365
+ rgbaData, // Uint8Array - RGBA pixel data
366
+ 1000, // number - Max colors to track
367
+ 'frequency' // string - Sort: 'frequency', 'morton', 'hilbert'
368
+ );
89
369
 
90
- - **Two-Pass Refinement**: Iterative optimization for smooth results
370
+ if (analysis.success) {
371
+ console.log('Unique colors:', analysis.color_count);
372
+ console.log('Total pixels:', analysis.total_pixels);
373
+
374
+ // Get individual color
375
+ const color = analysis.get_color(0);
376
+ console.log(`Most common: ${color.hex} (${color.percentage.toFixed(1)}%)`);
377
+
378
+ // Get as JSON array
379
+ const colors = analysis.to_json();
380
+ }
381
+ ```
91
382
 
92
- - **WebAssembly Support**: Run in browsers with full performance
383
+ **ColorEntry Properties:**
384
+ | Property | Type | Description |
385
+ |----------|------|-------------|
386
+ | `r` | `u8` | Red component (0-255) |
387
+ | `g` | `u8` | Green component (0-255) |
388
+ | `b` | `u8` | Blue component (0-255) |
389
+ | `count` | `u32` | Number of pixels |
390
+ | `percentage` | `f32` | Percentage of image |
391
+ | `hex` | `string` | Hex color code (`#rrggbb`) |
93
392
 
94
- - **Performance Preprocessing**: Resolution capping and color pre-quantization
393
+ ---
95
394
 
395
+ ### Utility Functions
96
396
 
97
- ## Installation
397
+ #### `rgb_to_oklab(r, g, b)`
98
398
 
99
- ### Native (Rust)
399
+ Convert RGB to Oklab color space.
100
400
 
101
- Add to your `Cargo.toml`:
401
+ ```javascript
402
+ const oklab = rgb_to_oklab(255, 128, 64);
403
+ // Returns: Float32Array [L, a, b]
404
+ // L: 0.0-1.0 (lightness)
405
+ // a: ~-0.4 to 0.4 (green-red)
406
+ // b: ~-0.4 to 0.4 (blue-yellow)
407
+ ```
102
408
 
103
- [dependencies]
104
- smart-downscaler = "0.3.5"
409
+ #### `oklab_to_rgb(l, a, b)`
105
410
 
411
+ Convert Oklab to RGB.
106
412
 
107
- ### WebAssembly (npm)
413
+ ```javascript
414
+ const rgb = oklab_to_rgb(0.7, 0.1, 0.05);
415
+ // Returns: Uint8Array [r, g, b]
416
+ ```
108
417
 
109
- npm install smart-downscaler
418
+ #### `get_chroma(r, g, b)`
110
419
 
420
+ Get color saturation/colorfulness.
111
421
 
112
- ## Quick Start
422
+ ```javascript
423
+ const chroma = get_chroma(255, 0, 0); // Pure red = high chroma
424
+ const gray_chroma = get_chroma(128, 128, 128); // Gray = 0 chroma
425
+ ```
113
426
 
114
- ### WebAssembly (Browser)
427
+ #### `get_lightness(r, g, b)`
115
428
 
116
- import init, { WasmDownscaleConfig, downscale_rgba } from 'smart-downscaler';
429
+ Get perceptual lightness (0.0-1.0).
117
430
 
118
- await init();
431
+ ```javascript
432
+ const lightness = get_lightness(255, 255, 255); // White = 1.0
433
+ const dark = get_lightness(0, 0, 0); // Black = 0.0
434
+ ```
119
435
 
120
- // Create config
121
- const config = new WasmDownscaleConfig();
436
+ #### `color_distance(r1, g1, b1, r2, g2, b2)`
122
437
 
123
- // PERFORMANCE: New Direct LUT settings
124
- config.max_resolution_mp = 1.5; // Nearest-neighbor cap (0 = disabled)
125
- config.max_color_preprocess = 16384; // Trigger LUT path if < 16k colors
438
+ Compute perceptual distance between two colors.
126
439
 
127
- // QUALITY: New K-Centroid settings
128
- config.k_centroid = 2; // 2 = Dominant Color Mode
129
- config.k_centroid_iterations = 2; // Refine the dominant color
440
+ ```javascript
441
+ const dist = color_distance(255, 0, 0, 0, 255, 0); // Red vs Green
442
+ // Returns: f32 - Euclidean distance in Oklab space
443
+ ```
130
444
 
131
- // Standard settings
132
- config.palette_size = 16;
133
- config.palette_strategy = 'oklab';
445
+ #### `version()`
134
446
 
135
- // Run
136
- const result = downscale_rgba(
137
- imageData.data,
138
- imageData.width, imageData.height,
139
- 64, 64, // Target size
140
- config
141
- );
447
+ Get library version.
142
448
 
143
- // Draw result
144
- const output = new ImageData(result.data, result.width, result.height);
145
- ctx.putImageData(output, 0, 0);
449
+ ```javascript
450
+ console.log(version()); // "0.5.0"
451
+ ```
146
452
 
453
+ ---
147
454
 
148
- ### Configuration Presets
455
+ ### Result Object
149
456
 
150
- // Speed optimized (max_resolution_mp: 1.0, max_color_preprocess: 8192)
151
- const fast = WasmDownscaleConfig.fast();
457
+ `WasmDownscaleResult` properties:
152
458
 
153
- // Best quality (max_resolution_mp: 2.0, max_color_preprocess: 32768)
154
- const quality = WasmDownscaleConfig.quality();
459
+ | Property | Type | Description |
460
+ |----------|------|-------------|
461
+ | `width` | `u32` | Output image width |
462
+ | `height` | `u32` | Output image height |
463
+ | `data` | `Uint8ClampedArray` | RGBA pixel data |
464
+ | `palette` | `Uint8Array` | RGB palette (3 bytes per color) |
465
+ | `indices` | `Uint8Array` | Palette index per pixel |
466
+ | `palette_size` | `usize` | Number of colors in palette |
155
467
 
156
- // Preserve vibrant colors (max_resolution_mp: 1.5, max_color_preprocess: 16384)
157
- const vibrant = WasmDownscaleConfig.vibrant();
468
+ **Methods:**
469
+ | Method | Returns | Description |
470
+ |--------|---------|-------------|
471
+ | `rgb_data()` | `Uint8Array` | Get RGB data (no alpha) |
158
472
 
159
- // Use only exact source colors
160
- const exact = WasmDownscaleConfig.exact_colors();
473
+ ---
161
474
 
475
+ ## Presets
162
476
 
163
- ## Why Oklab?
477
+ ### Built-in Configuration Presets
164
478
 
165
- Traditional RGB-based palette extraction has fundamental problems:
479
+ ```javascript
480
+ // Speed optimized
481
+ const fast = WasmDownscaleConfig.fast();
482
+ // palette_size: 16, kmeans_iterations: 3, no refinement, no segmentation
166
483
 
484
+ // Best quality
485
+ const quality = WasmDownscaleConfig.quality();
486
+ // palette_size: 32, kmeans_iterations: 10, hierarchy segmentation, k_centroid: 2
167
487
 
168
- ### The Problem: RGB Averaging Desaturates Colors
488
+ // Preserve vibrant colors
489
+ const vibrant = WasmDownscaleConfig.vibrant();
490
+ // palette_size: 24, saturation strategy, k_centroid: 2
169
491
 
170
- Red [255, 0, 0] + Cyan [0, 255, 255]
171
- RGB Average Gray [127, 127, 127] ❌
492
+ // Use only exact source colors
493
+ const exact = WasmDownscaleConfig.exact_colors();
494
+ // medoid strategy, no k-means refinement
495
+ ```
172
496
 
173
- When you average colors in RGB space, saturated colors get pulled toward gray. This is why downscaled images often look "washed out" or "tanned."
497
+ ### Preset Comparison
174
498
 
499
+ | Preset | Palette | K-Means | Segmentation | K-Centroid | Speed |
500
+ |--------|---------|---------|--------------|------------|-------|
501
+ | `fast()` | 16 | 3 | none | 1 (avg) | ⚡⚡⚡ |
502
+ | `default` | 16 | 5 | hierarchy_fast | 1 (avg) | ⚡⚡ |
503
+ | `vibrant()` | 24 | 8 | hierarchy_fast | 2 (dom) | ⚡ |
504
+ | `quality()` | 32 | 10 | hierarchy | 2 (dom) | 🐢 |
505
+ | `exact_colors()` | 16 | 0 | hierarchy_fast | 1 (avg) | ⚡⚡ |
175
506
 
176
- ### The Solution: Oklab Color Space
507
+ ---
177
508
 
178
- Oklab is a **perceptually uniform** color space where:
509
+ ## Advanced Usage
179
510
 
180
- - Euclidean distance = perceived color difference
511
+ ### Custom Palette Workflow
181
512
 
182
- - Averaging preserves hue and saturation
513
+ ```javascript
514
+ // 1. Extract palette from reference image
515
+ const referencePalette = extract_palette_from_image(
516
+ referenceImageData, w, h, 16, 10, 'saturation'
517
+ );
183
518
 
184
- - Interpolations look natural
519
+ // 2. Apply palette to multiple images
520
+ const results = images.map(img =>
521
+ downscale_with_palette(
522
+ img.data, img.width, img.height,
523
+ 64, 64,
524
+ referencePalette,
525
+ config
526
+ )
527
+ );
528
+ ```
529
+
530
+ ### Batch Processing with Progress
531
+
532
+ ```javascript
533
+ async function batchDownscale(images, config, onProgress) {
534
+ const results = [];
535
+
536
+ for (let i = 0; i < images.length; i++) {
537
+ const img = images[i];
538
+ const result = downscale_rgba(
539
+ img.data, img.width, img.height,
540
+ 64, 64, config
541
+ );
542
+ results.push(result);
543
+
544
+ onProgress((i + 1) / images.length * 100);
545
+
546
+ // Allow UI to update
547
+ await new Promise(r => setTimeout(r, 0));
548
+ }
549
+
550
+ return results;
551
+ }
552
+ ```
185
553
 
186
- <!---->
554
+ ### Analyzing Before Downscaling
187
555
 
188
- Red (Oklab) + Cyan (Oklab)
189
- Oklab Average Preserves colorfulness ✔
556
+ ```javascript
557
+ // Check image characteristics first
558
+ const analysis = analyze_colors(imageData, 10000, 'frequency');
190
559
 
560
+ if (!analysis.success) {
561
+ console.log('Image has more than 10,000 unique colors');
562
+ }
191
563
 
192
- ## API Reference
564
+ // Adjust config based on analysis
565
+ const config = new WasmDownscaleConfig();
566
+
567
+ if (analysis.color_count < 256) {
568
+ // Already low-color image - use medoid for exact colors
569
+ config.palette_strategy = 'medoid';
570
+ config.kmeans_iterations = 0;
571
+ } else {
572
+ // High-color image - use saturation weighting
573
+ config.palette_strategy = 'saturation';
574
+ config.kmeans_iterations = 8;
575
+ }
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Performance Tips
581
+
582
+ ### 1. Use Resolution Capping
583
+
584
+ For images larger than ~2MP, enable resolution capping:
585
+
586
+ ```javascript
587
+ config.max_resolution_mp = 1.5; // Cap at 1.5 megapixels
588
+ ```
589
+
590
+ ### 2. Enable Color Pre-quantization
193
591
 
194
- ### WasmDownscaleConfig
592
+ Reduces processing time significantly for high-color images:
195
593
 
196
- const config = new WasmDownscaleConfig();
594
+ ```javascript
595
+ config.max_color_preprocess = 16384; // Pre-quantize to 16K colors
596
+ ```
197
597
 
198
- // Palette settings
199
- config.palette_size = 16; // Number of output colors
200
- config.palette_strategy = 'oklab'; // 'oklab', 'saturation', 'medoid', 'kmeans', 'legacy'
201
- config.kmeans_iterations = 5; // Refinement iterations
598
+ ### 3. Choose Appropriate Segmentation
202
599
 
203
- // Spatial coherence
204
- config.neighbor_weight = 0.3; // [0-1] Prefer neighbor colors
205
- config.region_weight = 0.2; // [0-1] Prefer region colors
600
+ | Image Type | Recommended Segmentation |
601
+ |------------|-------------------------|
602
+ | Icons, sprites | `"none"` |
603
+ | Game art | `"hierarchy_fast"` |
604
+ | Photos | `"slic"` |
605
+ | Complex illustrations | `"hierarchy"` |
206
606
 
207
- // Refinement
208
- config.two_pass_refinement = true;
209
- config.refinement_iterations = 3;
607
+ ### 4. Reduce Iterations for Speed
210
608
 
211
- // Edge detection
212
- config.edge_weight = 0.5;
609
+ ```javascript
610
+ // Fast settings
611
+ config.kmeans_iterations = 3; // Instead of 5
612
+ config.refinement_iterations = 1; // Instead of 3
613
+ config.k_centroid_iterations = 1; // Instead of 2
614
+ ```
213
615
 
214
- // Segmentation
215
- config.segmentation_method = 'hierarchy_fast'; // 'none', 'slic', 'hierarchy', 'hierarchy_fast'
616
+ ### 5. Use Presets
216
617
 
217
- // Performance preprocessing
218
- config.max_resolution_mp = 1.5; // Cap resolution at 1.5 megapixels (0 = disabled)
219
- config.max_color_preprocess = 16384; // Pre-quantize to 16K colors max (0 = disabled)
618
+ ```javascript
619
+ // For real-time preview
620
+ const previewConfig = WasmDownscaleConfig.fast();
220
621
 
221
- // Tile Logic
222
- config.k_centroid = 1; // 1=Avg, 2=Dom, 3=Foremost
223
- config.k_centroid_iterations = 0; // Refine the dominant color
622
+ // For final export
623
+ const exportConfig = WasmDownscaleConfig.quality();
624
+ ```
224
625
 
626
+ ---
225
627
 
226
- ### Available Functions
628
+ ## Why Oklab?
629
+
630
+ ### The Problem: RGB Averaging
631
+
632
+ When averaging colors in RGB space, saturated colors become desaturated:
227
633
 
228
- | Function | Description |
229
- | ------------------------------------------------------------------ | --------------------------- |
230
- | `downscale(data, w, h, tw, th, config?)` | Main downscale function |
231
- | `downscale_rgba(data, w, h, tw, th, config?)` | For Uint8ClampedArray input |
232
- | `downscale_simple(data, w, h, tw, th, colors)` | Simple API |
233
- | `downscale_with_palette(...)` | Use custom palette |
234
- | `extract_palette_from_image(data, w, h, colors, iters, strategy?)` | Extract palette only |
235
- | `quantize_to_palette(data, w, h, palette)` | Quantize without resizing |
236
- | `get_palette_strategies()` | List available strategies |
634
+ ```
635
+ Red [255, 0, 0]
636
+ Cyan [ 0, 255, 255]
637
+ ─────────────────────
638
+ RGB Average Gray [127, 127, 127]
639
+ ```
237
640
 
641
+ This is why traditional downscalers produce "washed out" results.
642
+
643
+ ### The Solution: Oklab Color Space
238
644
 
239
- ### WasmDownscaleResult
645
+ Oklab is a **perceptually uniform** color space where:
240
646
 
241
- result.width // Output width
242
- result.height // Output height
243
- result.data // Uint8ClampedArray (RGBA)
244
- result.rgb_data() // Uint8Array (RGB only)
245
- result.palette // Uint8Array (RGB, 3 bytes per color)
246
- result.indices // Uint8Array (palette index per pixel)
247
- result.palette_size // Number of colors
647
+ - Euclidean distance = perceived color difference
648
+ - Averaging preserves hue and saturation
649
+ - Interpolations look natural
248
650
 
651
+ ```
652
+ Red (Oklab) L=0.63, a=0.22, b=0.13
653
+ Cyan (Oklab) L=0.91, a=-0.15, b=-0.09
654
+ ─────────────────────────────────────
655
+ Oklab Average → Preserves colorfulness ✓
656
+ ```
657
+
658
+ ### Visual Comparison
659
+
660
+ | Method | Result | Issue |
661
+ |--------|--------|-------|
662
+ | RGB Average | Muddy grays | Desaturation |
663
+ | Lab Average | Better, some hue shift | Non-uniform |
664
+ | **Oklab Average** | Vibrant, natural | ✓ Best |
665
+
666
+ ---
667
+
668
+ ## CLI Reference
669
+
670
+ ```bash
671
+ # Basic usage
672
+ smart-downscaler input.png output.png -w 64 -h 64
673
+
674
+ # With palette size
675
+ smart-downscaler input.png output.png -w 64 -h 64 -c 16
676
+
677
+ # Quality preset
678
+ smart-downscaler input.png output.png -w 64 -h 64 --preset quality
679
+
680
+ # Custom configuration
681
+ smart-downscaler input.png output.png \
682
+ --width 64 \
683
+ --height 64 \
684
+ --colors 24 \
685
+ --strategy saturation \
686
+ --segmentation hierarchy_fast \
687
+ --k-centroid 2 \
688
+ --k-centroid-iterations 2
689
+
690
+ # Fast mode (no refinement)
691
+ smart-downscaler input.png output.png -w 64 -h 64 \
692
+ --segmentation none \
693
+ --no-refinement
694
+
695
+ # Extract palette only
696
+ smart-downscaler input.png --extract-palette palette.hex -c 16
697
+ ```
698
+
699
+ ### CLI Options
700
+
701
+ | Option | Short | Default | Description |
702
+ |--------|-------|---------|-------------|
703
+ | `--width` | `-w` | *required* | Target width |
704
+ | `--height` | `-h` | *required* | Target height |
705
+ | `--colors` | `-c` | `16` | Palette size |
706
+ | `--strategy` | `-s` | `oklab` | Palette strategy |
707
+ | `--segmentation` | | `hierarchy_fast` | Segmentation method |
708
+ | `--k-centroid` | | `1` | Tile color mode |
709
+ | `--k-centroid-iterations` | | `0` | Tile refinement |
710
+ | `--no-refinement` | | false | Disable two-pass |
711
+ | `--preset` | `-p` | | Use preset (fast/quality/vibrant) |
712
+ | `--extract-palette` | | | Output palette only |
713
+
714
+ ---
249
715
 
250
716
  ## License
251
717
 
252
- MIT
718
+ MIT License
253
719
 
720
+ ---
254
721
 
255
722
  ## Credits
256
723
 
257
- - Oklab color space by Björn Ottosson
258
-
259
- - SLIC superpixel algorithm
724
+ - **Oklab color space** by Björn Ottosson
725
+ - **SLIC superpixels** algorithm
726
+ - **K-Means++** initialization
727
+ - **VTracer** hierarchical clustering approach
260
728
 
261
- - K-Means++ initialization
729
+ ---
262
730
 
263
- - VTracer hierarchical clustering approach
731
+ ## Links
264
732
 
733
+ - [GitHub Repository](https://github.com/user/smart-downscaler)
734
+ - [npm Package](https://www.npmjs.com/package/smart-downscaler)
735
+ - [Crates.io](https://crates.io/crates/smart-downscaler)
736
+ - [API Documentation](https://docs.rs/smart-downscaler)
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "Pixagram"
6
6
  ],
7
7
  "description": "Intelligent pixel art downscaler with region-aware color quantization",
8
- "version": "0.4.5",
8
+ "version": "0.6.0",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
@@ -1,34 +1,18 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
- /**
5
- * Result of color analysis
6
- */
7
4
  export class ColorAnalysisResult {
8
5
  private constructor();
9
6
  free(): void;
10
7
  [Symbol.dispose](): void;
11
- /**
12
- * Get color at index
13
- */
14
8
  get_color(index: number): ColorEntry | undefined;
15
- /**
16
- * Get all colors as a flat array: [r, g, b, count(4 bytes), percentage(4 bytes), ...]
17
- * Each color is 11 bytes
18
- */
19
9
  get_colors_flat(): Uint8Array;
20
- /**
21
- * Get colors as JSON-compatible array
22
- */
23
10
  to_json(): any;
24
11
  readonly color_count: number;
25
12
  readonly success: boolean;
26
13
  readonly total_pixels: number;
27
14
  }
28
15
 
29
- /**
30
- * A single color entry with statistics
31
- */
32
16
  export class ColorEntry {
33
17
  private constructor();
34
18
  free(): void;
@@ -52,13 +36,7 @@ export class WasmDownscaleConfig {
52
36
  edge_weight: number;
53
37
  hierarchy_min_size: number;
54
38
  hierarchy_threshold: number;
55
- /**
56
- * Iterations for tile centroid
57
- */
58
39
  k_centroid_iterations: number;
59
- /**
60
- * K-Means centroid mode (1=Avg, 2=Dom, 3=Foremost)
61
- */
62
40
  k_centroid: number;
63
41
  kmeans_iterations: number;
64
42
  max_color_preprocess: number;
@@ -88,78 +66,39 @@ export class WasmDownscaleResult {
88
66
  }
89
67
 
90
68
  /**
91
- * Analyze colors in an image
92
- *
93
- * # Arguments
94
- * * `image_data` - RGBA pixel data
95
- * * `max_colors` - Maximum number of unique colors to track (stops if exceeded)
96
- * * `sort_method` - Sorting method: "frequency", "morton", or "hilbert"
97
- *
98
- * # Returns
99
- * ColorAnalysisResult with array of colors (r, g, b, count, percentage, hex)
100
- * If unique colors exceed max_colors, returns early with success=false
69
+ * Analyze colors FIX: uses HashMap<u32, usize> for O(1) lookup (was O(n) linear scan)
101
70
  */
102
71
  export function analyze_colors(image_data: Uint8Array, max_colors: number, sort_method: string): ColorAnalysisResult;
103
72
 
104
- /**
105
- * Compute perceptual color distance between two RGB colors
106
- */
107
73
  export function color_distance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number;
108
74
 
109
75
  export function downscale(image_data: Uint8Array, width: number, height: number, target_width: number, target_height: number, config?: WasmDownscaleConfig | null): WasmDownscaleResult;
110
76
 
111
77
  export function downscale_rgba(image_data: Uint8ClampedArray, width: number, height: number, target_width: number, target_height: number, config?: WasmDownscaleConfig | null): WasmDownscaleResult;
112
78
 
113
- /**
114
- * Simple downscale function with minimal parameters
115
- */
116
79
  export function downscale_simple(image_data: Uint8Array, width: number, height: number, target_width: number, target_height: number, num_colors: number): WasmDownscaleResult;
117
80
 
118
81
  export function downscale_with_palette(image_data: Uint8Array, width: number, height: number, target_width: number, target_height: number, palette_data: Uint8Array, config?: WasmDownscaleConfig | null): WasmDownscaleResult;
119
82
 
120
- /**
121
- * Extract a color palette from an image without downscaling
122
- */
123
83
  export function extract_palette_from_image(image_data: Uint8Array, _width: number, _height: number, num_colors: number, kmeans_iterations: number, strategy?: string | null): Uint8Array;
124
84
 
125
- /**
126
- * Get chroma (saturation) of an RGB color
127
- */
128
85
  export function get_chroma(r: number, g: number, b: number): number;
129
86
 
130
- /**
131
- * Get lightness of an RGB color in Oklab space
132
- */
133
87
  export function get_lightness(r: number, g: number, b: number): number;
134
88
 
135
- /**
136
- * Get available palette strategies
137
- */
138
89
  export function get_palette_strategies(): Array<any>;
139
90
 
140
91
  export function init(): void;
141
92
 
142
- /**
143
- * Log a message to the browser console (for debugging)
144
- */
145
93
  export function log(message: string): void;
146
94
 
147
- /**
148
- * Convert Oklab to RGB (utility function for JS)
149
- */
150
95
  export function oklab_to_rgb(l: number, a: number, b: number): Uint8Array;
151
96
 
152
97
  /**
153
- * Quantize an image to a specific palette without resizing
98
+ * Quantize to palette FIX: compute Oklab once per pixel (was twice)
154
99
  */
155
100
  export function quantize_to_palette(image_data: Uint8Array, width: number, height: number, palette_data: Uint8Array): WasmDownscaleResult;
156
101
 
157
- /**
158
- * Convert RGB to Oklab (utility function for JS)
159
- */
160
102
  export function rgb_to_oklab(r: number, g: number, b: number): Float32Array;
161
103
 
162
- /**
163
- * Get library version
164
- */
165
104
  export function version(): string;
@@ -1,6 +1,3 @@
1
- /**
2
- * Result of color analysis
3
- */
4
1
  export class ColorAnalysisResult {
5
2
  static __wrap(ptr) {
6
3
  ptr = ptr >>> 0;
@@ -27,7 +24,6 @@ export class ColorAnalysisResult {
27
24
  return ret >>> 0;
28
25
  }
29
26
  /**
30
- * Get color at index
31
27
  * @param {number} index
32
28
  * @returns {ColorEntry | undefined}
33
29
  */
@@ -36,8 +32,6 @@ export class ColorAnalysisResult {
36
32
  return ret === 0 ? undefined : ColorEntry.__wrap(ret);
37
33
  }
38
34
  /**
39
- * Get all colors as a flat array: [r, g, b, count(4 bytes), percentage(4 bytes), ...]
40
- * Each color is 11 bytes
41
35
  * @returns {Uint8Array}
42
36
  */
43
37
  get_colors_flat() {
@@ -52,7 +46,6 @@ export class ColorAnalysisResult {
52
46
  return ret !== 0;
53
47
  }
54
48
  /**
55
- * Get colors as JSON-compatible array
56
49
  * @returns {any}
57
50
  */
58
51
  to_json() {
@@ -72,9 +65,6 @@ export class ColorAnalysisResult {
72
65
  }
73
66
  if (Symbol.dispose) ColorAnalysisResult.prototype[Symbol.dispose] = ColorAnalysisResult.prototype.free;
74
67
 
75
- /**
76
- * A single color entry with statistics
77
- */
78
68
  export class ColorEntry {
79
69
  static __wrap(ptr) {
80
70
  ptr = ptr >>> 0;
@@ -186,7 +176,6 @@ export class WasmDownscaleConfig {
186
176
  return ret;
187
177
  }
188
178
  /**
189
- * Iterations for tile centroid
190
179
  * @returns {number}
191
180
  */
192
181
  get k_centroid_iterations() {
@@ -194,7 +183,6 @@ export class WasmDownscaleConfig {
194
183
  return ret >>> 0;
195
184
  }
196
185
  /**
197
- * K-Means centroid mode (1=Avg, 2=Dom, 3=Foremost)
198
186
  * @returns {number}
199
187
  */
200
188
  get k_centroid() {
@@ -290,14 +278,12 @@ export class WasmDownscaleConfig {
290
278
  wasm.__wbg_set_wasmdownscaleconfig_hierarchy_threshold(this.__wbg_ptr, arg0);
291
279
  }
292
280
  /**
293
- * Iterations for tile centroid
294
281
  * @param {number} arg0
295
282
  */
296
283
  set k_centroid_iterations(arg0) {
297
284
  wasm.__wbg_set_wasmdownscaleconfig_k_centroid_iterations(this.__wbg_ptr, arg0);
298
285
  }
299
286
  /**
300
- * K-Means centroid mode (1=Avg, 2=Dom, 3=Foremost)
301
287
  * @param {number} arg0
302
288
  */
303
289
  set k_centroid(arg0) {
@@ -517,16 +503,7 @@ export class WasmDownscaleResult {
517
503
  if (Symbol.dispose) WasmDownscaleResult.prototype[Symbol.dispose] = WasmDownscaleResult.prototype.free;
518
504
 
519
505
  /**
520
- * Analyze colors in an image
521
- *
522
- * # Arguments
523
- * * `image_data` - RGBA pixel data
524
- * * `max_colors` - Maximum number of unique colors to track (stops if exceeded)
525
- * * `sort_method` - Sorting method: "frequency", "morton", or "hilbert"
526
- *
527
- * # Returns
528
- * ColorAnalysisResult with array of colors (r, g, b, count, percentage, hex)
529
- * If unique colors exceed max_colors, returns early with success=false
506
+ * Analyze colors FIX: uses HashMap<u32, usize> for O(1) lookup (was O(n) linear scan)
530
507
  * @param {Uint8Array} image_data
531
508
  * @param {number} max_colors
532
509
  * @param {string} sort_method
@@ -543,7 +520,6 @@ export function analyze_colors(image_data, max_colors, sort_method) {
543
520
  }
544
521
 
545
522
  /**
546
- * Compute perceptual color distance between two RGB colors
547
523
  * @param {number} r1
548
524
  * @param {number} g1
549
525
  * @param {number} b1
@@ -602,7 +578,6 @@ export function downscale_rgba(image_data, width, height, target_width, target_h
602
578
  }
603
579
 
604
580
  /**
605
- * Simple downscale function with minimal parameters
606
581
  * @param {Uint8Array} image_data
607
582
  * @param {number} width
608
583
  * @param {number} height
@@ -643,7 +618,6 @@ export function downscale_with_palette(image_data, width, height, target_width,
643
618
  }
644
619
 
645
620
  /**
646
- * Extract a color palette from an image without downscaling
647
621
  * @param {Uint8Array} image_data
648
622
  * @param {number} _width
649
623
  * @param {number} _height
@@ -663,7 +637,6 @@ export function extract_palette_from_image(image_data, _width, _height, num_colo
663
637
  }
664
638
 
665
639
  /**
666
- * Get chroma (saturation) of an RGB color
667
640
  * @param {number} r
668
641
  * @param {number} g
669
642
  * @param {number} b
@@ -675,7 +648,6 @@ export function get_chroma(r, g, b) {
675
648
  }
676
649
 
677
650
  /**
678
- * Get lightness of an RGB color in Oklab space
679
651
  * @param {number} r
680
652
  * @param {number} g
681
653
  * @param {number} b
@@ -687,7 +659,6 @@ export function get_lightness(r, g, b) {
687
659
  }
688
660
 
689
661
  /**
690
- * Get available palette strategies
691
662
  * @returns {Array<any>}
692
663
  */
693
664
  export function get_palette_strategies() {
@@ -700,7 +671,6 @@ export function init() {
700
671
  }
701
672
 
702
673
  /**
703
- * Log a message to the browser console (for debugging)
704
674
  * @param {string} message
705
675
  */
706
676
  export function log(message) {
@@ -710,7 +680,6 @@ export function log(message) {
710
680
  }
711
681
 
712
682
  /**
713
- * Convert Oklab to RGB (utility function for JS)
714
683
  * @param {number} l
715
684
  * @param {number} a
716
685
  * @param {number} b
@@ -722,7 +691,7 @@ export function oklab_to_rgb(l, a, b) {
722
691
  }
723
692
 
724
693
  /**
725
- * Quantize an image to a specific palette without resizing
694
+ * Quantize to palette FIX: compute Oklab once per pixel (was twice)
726
695
  * @param {Uint8Array} image_data
727
696
  * @param {number} width
728
697
  * @param {number} height
@@ -738,7 +707,6 @@ export function quantize_to_palette(image_data, width, height, palette_data) {
738
707
  }
739
708
 
740
709
  /**
741
- * Convert RGB to Oklab (utility function for JS)
742
710
  * @param {number} r
743
711
  * @param {number} g
744
712
  * @param {number} b
@@ -750,7 +718,6 @@ export function rgb_to_oklab(r, g, b) {
750
718
  }
751
719
 
752
720
  /**
753
- * Get library version
754
721
  * @returns {string}
755
722
  */
756
723
  export function version() {
Binary file