perf(importer): O(1) palette lookup via 5-bit LUT, auto-fit on load

Resolves browser freeze when importing large images.

- image-to-palette.js: add lazy 32^3 Uint8Array LUT (32 KB) for
  nearest-color lookup. Replaces 256-iteration linear scan per pixel
  in quantizeNearest, error-diffusion, and ordered-dither paths.
- ImageImporter.svelte: cap initial resizeW/resizeH to canvas bounds
  so a full-res photo does not run the pipeline at source size before
  the user can touch Fit.
This commit is contained in:
tiennm99 committed 2026-04-18 14:30:47 +07:00
1 parent cfbac2a586
commit d6503676c6
2 files changed
+33 -5

No files matched your search

+8 -2
View File
@@ -191,8 +191,14 @@
srcRgba = data; srcRgba = data;
srcWidth = w; srcWidth = w;
srcHeight = h; srcHeight = h;
resizeW = w; // Cap initial output to canvas bounds. A full-res photo (e.g. 6000×4000)
resizeH = h; // running through the palette pipeline on load would otherwise freeze
// the main thread. User can still scale up via the W/H inputs.
const maxW = Math.max(1, CANVAS_WIDTH - originX);
const maxH = Math.max(1, CANVAS_HEIGHT - originY);
const ratio = Math.min(maxW / w, maxH / h, 1);
resizeW = Math.max(1, Math.floor(w * ratio));
resizeH = Math.max(1, Math.floor(h * ratio));
// paletteIndices recomputed reactively via $effect below. // paletteIndices recomputed reactively via $effect below.
} catch (err) { } catch (err) {
errorText = `Failed to load image: ${err.message || err}`; errorText = `Failed to load image: ${err.message || err}`;
+25 -3
View File
@@ -25,6 +25,28 @@ export function nearestColorIndex(r, g, b) {
return best; return best;
} }
// 5-bit-per-channel RGB→palette LUT. 32³ = 32,768 entries (32 KB).
// Build cost ≈ 32³ × 256 ≈ 8M ops, amortized across millions of pixel lookups.
let _lut = null;
function buildLut() {
const lut = new Uint8Array(32 * 32 * 32);
for (let r5 = 0; r5 < 32; r5++) {
const r = (r5 << 3) | 4;
for (let g5 = 0; g5 < 32; g5++) {
const g = (g5 << 3) | 4;
for (let b5 = 0; b5 < 32; b5++) {
const b = (b5 << 3) | 4;
lut[(r5 << 10) | (g5 << 5) | b5] = nearestColorIndex(r, g, b);
}
}
}
return lut;
}
function lutNearest(r, g, b) {
if (!_lut) _lut = buildLut();
return _lut[((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)];
}
/** /**
* Convert an RGBA pixel buffer into a flat array of palette indices. * Convert an RGBA pixel buffer into a flat array of palette indices.
* Pixels with alpha < threshold become -1 (caller should skip on upload). * Pixels with alpha < threshold become -1 (caller should skip on upload).
@@ -99,7 +121,7 @@ function quantizeNearest(rgba, width, height, alphaThreshold) {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const off = i * 4; const off = i * 4;
if (rgba[off + 3] < alphaThreshold) { out[i] = -1; continue; } if (rgba[off + 3] < alphaThreshold) { out[i] = -1; continue; }
out[i] = nearestColorIndex(rgba[off], rgba[off + 1], rgba[off + 2]); out[i] = lutNearest(rgba[off], rgba[off + 1], rgba[off + 2]);
} }
return out; return out;
} }
@@ -122,7 +144,7 @@ function runErrorDiffusion(rgba, width, height, alphaThreshold, kernel) {
if (rgba[i * 4 + 3] < alphaThreshold) { out[i] = -1; continue; } if (rgba[i * 4 + 3] < alphaThreshold) { out[i] = -1; continue; }
const r = buf[i * 3], g = buf[i * 3 + 1], b = buf[i * 3 + 2]; const r = buf[i * 3], g = buf[i * 3 + 1], b = buf[i * 3 + 2];
const idx = nearestColorIndex( const idx = lutNearest(
r < 0 ? 0 : r > 255 ? 255 : r, r < 0 ? 0 : r > 255 ? 255 : r,
g < 0 ? 0 : g > 255 ? 255 : g, g < 0 ? 0 : g > 255 ? 255 : g,
b < 0 ? 0 : b > 255 ? 255 : b, b < 0 ? 0 : b > 255 ? 255 : b,
@@ -165,7 +187,7 @@ function runOrderedDither(rgba, width, height, alphaThreshold, matrix) {
const r = rgba[i * 4] + bias; const r = rgba[i * 4] + bias;
const g = rgba[i * 4 + 1] + bias; const g = rgba[i * 4 + 1] + bias;
const b = rgba[i * 4 + 2] + bias; const b = rgba[i * 4 + 2] + bias;
out[i] = nearestColorIndex( out[i] = lutNearest(
r < 0 ? 0 : r > 255 ? 255 : r, r < 0 ? 0 : r > 255 ? 255 : r,
g < 0 ? 0 : g > 255 ? 255 : g, g < 0 ? 0 : g > 255 ? 255 : g,
b < 0 ? 0 : b > 255 ? 255 : b, b < 0 ? 0 : b > 255 ? 255 : b,