image-to-pixel 3.0.0 → 3.0.1

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.
Files changed (68) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +69 -69
  3. package/dist/editor.css.map +1 -1
  4. package/dist/editor.esm.js +1 -1
  5. package/dist/editor.esm.js.map +1 -1
  6. package/dist/fonts/README.md +6 -6
  7. package/dist/fonts/geist-LICENSE.txt +93 -93
  8. package/dist/image-to-pixel.bundle.esm.js +13 -13
  9. package/dist/image-to-pixel.bundle.esm.js.map +2 -2
  10. package/dist/image-to-pixel.cjs +2 -2
  11. package/dist/image-to-pixel.cjs.map +2 -2
  12. package/dist/image-to-pixel.esm.js +2 -2
  13. package/dist/image-to-pixel.esm.js.map +2 -2
  14. package/dist/image-to-pixel.js +2 -2
  15. package/dist/image-to-pixel.js.map +2 -2
  16. package/dist/image-to-pixel.min.js +11 -11
  17. package/dist/image-to-pixel.min.js.map +2 -2
  18. package/dist/video.esm.js +1 -1
  19. package/dist/video.esm.js.map +1 -1
  20. package/lib/core/color.js +69 -69
  21. package/lib/core/cpu/pipeline.js +21 -21
  22. package/lib/core/cpu/stages.js +227 -227
  23. package/lib/core/dither/bluenoise-data.js +3 -3
  24. package/lib/core/dither/kernels.js +27 -27
  25. package/lib/core/dither/matrices.js +95 -95
  26. package/lib/core/engine.js +158 -158
  27. package/lib/core/export/canvas.js +22 -22
  28. package/lib/core/export/crc32.js +15 -15
  29. package/lib/core/export/gif.js +153 -153
  30. package/lib/core/export/png.js +94 -94
  31. package/lib/core/export/scale.js +103 -103
  32. package/lib/core/export/sheet.js +95 -95
  33. package/lib/core/export/zip.js +55 -55
  34. package/lib/core/gpu/backend.js +159 -159
  35. package/lib/core/gpu/shaders.js +258 -258
  36. package/lib/core/palette/builtin.js +36 -36
  37. package/lib/core/palette/extract.js +69 -69
  38. package/lib/core/palette/lospec.js +24 -24
  39. package/lib/core/palette/parse.js +112 -112
  40. package/lib/core/plan.js +58 -58
  41. package/lib/core/presets.js +42 -42
  42. package/lib/core/settings.js +274 -274
  43. package/lib/core/source.js +89 -89
  44. package/lib/core/video/frames.js +144 -144
  45. package/lib/core/video/mux-mp4.js +131 -131
  46. package/lib/core/video/mux-webm.js +127 -127
  47. package/lib/editor/controls.js +139 -139
  48. package/lib/editor/dom.js +65 -65
  49. package/lib/editor/exporter.js +185 -185
  50. package/lib/editor/icons.js +61 -61
  51. package/lib/editor/index.js +793 -793
  52. package/lib/editor/inspector.js +328 -328
  53. package/lib/editor/library.js +182 -182
  54. package/lib/editor/persist.js +18 -18
  55. package/lib/editor/samples.js +90 -90
  56. package/lib/editor/stage.js +239 -239
  57. package/lib/editor/store.js +80 -80
  58. package/lib/editor/styles/editor.css +385 -385
  59. package/lib/editor/styles/fonts/README.md +6 -6
  60. package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
  61. package/lib/editor/styles/tokens.css +89 -89
  62. package/lib/editor/ui.js +97 -97
  63. package/lib/index.js +3 -1
  64. package/lib/video.js +92 -92
  65. package/package.json +92 -92
  66. package/types/editor.d.ts +34 -34
  67. package/types/index.d.ts +196 -196
  68. package/types/video.d.ts +43 -43
@@ -1,227 +1,227 @@
1
- // CPU reference implementation of every pipeline stage.
2
- // The GLSL in gpu/shaders.js mirrors these functions line for line; tests compare the two.
3
- // Working buffers are Float32Array RGBA, rows top-down, values 0..1.
4
-
5
- import { oklab, LUMA } from '../color.js';
6
-
7
- /**
8
- * Resample to w × h. 'box' averages up to 16×16 evenly spaced source texels per output pixel
9
- * (alpha-weighted); 'nearest' takes the centre texel. Integer maths, identical on the GPU.
10
- */
11
- export function resample(src, w, h, mode = 'box', crop = null) {
12
- const ox = crop ? crop.x : 0, oy = crop ? crop.y : 0;
13
- const sw = crop ? crop.w : src.width, sh = crop ? crop.h : src.height, d = src.data, stride = src.width;
14
- const out = new Float32Array(w * h * 4);
15
- const nx = mode === 'nearest' ? 1 : Math.min(16, Math.max(1, Math.ceil(sw / w)));
16
- const ny = mode === 'nearest' ? 1 : Math.min(16, Math.max(1, Math.ceil(sh / h)));
17
- const xs = new Int32Array(w * nx), ys = new Int32Array(h * ny);
18
- for (let x = 0; x < w; x++) for (let i = 0; i < nx; i++) xs[x * nx + i] = ox + Math.min(sw - 1, Math.floor(((2 * (x * nx + i) + 1) * sw) / (2 * w * nx)));
19
- for (let y = 0; y < h; y++) for (let j = 0; j < ny; j++) ys[y * ny + j] = oy + Math.min(sh - 1, Math.floor(((2 * (y * ny + j) + 1) * sh) / (2 * h * ny)));
20
- for (let y = 0; y < h; y++) {
21
- for (let x = 0; x < w; x++) {
22
- let r = 0, g = 0, b = 0, a = 0;
23
- for (let j = 0; j < ny; j++) {
24
- const row = ys[y * ny + j] * stride;
25
- for (let i = 0; i < nx; i++) {
26
- const o = (row + xs[x * nx + i]) * 4;
27
- const al = d[o + 3] / 255;
28
- r += d[o] / 255 * al; g += d[o + 1] / 255 * al; b += d[o + 2] / 255 * al; a += al;
29
- }
30
- }
31
- const o = (y * w + x) * 4;
32
- if (a > 0) { out[o] = r / a; out[o + 1] = g / a; out[o + 2] = b / a; }
33
- out[o + 3] = a / (nx * ny);
34
- }
35
- }
36
- return out;
37
- }
38
-
39
- /** Brightness, contrast, saturation, hue, gamma — in that order, in place. */
40
- export function adjust(buf, { brightness = 0, contrast = 0, saturation = 0, hue = 0, gamma = 1 }) {
41
- if (!brightness && !contrast && !saturation && !hue && gamma === 1) return buf;
42
- const f = contrast >= 0 ? 1 / (1 - 0.99 * contrast) : 1 + contrast;
43
- const ig = 1 / gamma;
44
- // luminance-preserving hue rotation: the CSS hue-rotate() matrix (rows sum to 1, so greys stay grey)
45
- const hk = Math.cos(hue * Math.PI / 180), hq = Math.sin(hue * Math.PI / 180);
46
- const M = [
47
- 0.213 + 0.787 * hk - 0.213 * hq, 0.715 - 0.715 * hk - 0.715 * hq, 0.072 - 0.072 * hk + 0.928 * hq,
48
- 0.213 - 0.213 * hk + 0.143 * hq, 0.715 + 0.285 * hk + 0.140 * hq, 0.072 - 0.072 * hk - 0.283 * hq,
49
- 0.213 - 0.213 * hk - 0.787 * hq, 0.715 - 0.715 * hk + 0.715 * hq, 0.072 + 0.928 * hk + 0.072 * hq,
50
- ];
51
- for (let o = 0; o < buf.length; o += 4) {
52
- let r = buf[o] + brightness, g = buf[o + 1] + brightness, b = buf[o + 2] + brightness;
53
- r = (r - 0.5) * f + 0.5; g = (g - 0.5) * f + 0.5; b = (b - 0.5) * f + 0.5;
54
- const l = r * LUMA[0] + g * LUMA[1] + b * LUMA[2];
55
- const k = 1 + saturation;
56
- r = l + (r - l) * k; g = l + (g - l) * k; b = l + (b - l) * k;
57
- if (hue) {
58
- const r2 = M[0] * r + M[1] * g + M[2] * b, g2 = M[3] * r + M[4] * g + M[5] * b, b2 = M[6] * r + M[7] * g + M[8] * b;
59
- r = r2; g = g2; b = b2;
60
- }
61
- buf[o] = Math.pow(clamp01(r), ig); buf[o + 1] = Math.pow(clamp01(g), ig); buf[o + 2] = Math.pow(clamp01(b), ig);
62
- }
63
- return buf;
64
- }
65
-
66
- /** Binary alpha: below the threshold becomes fully transparent, the rest fully opaque. */
67
- export function alphaCut(buf, threshold) {
68
- for (let o = 3; o < buf.length; o += 4) buf[o] = buf[o] >= threshold && buf[o] > 0 ? 1 : 0;
69
- return buf;
70
- }
71
-
72
- export const clamp01 = v => (v < 0 ? 0 : v > 1 ? 1 : v);
73
-
74
- /** Precompute what nearest() needs for a palette. */
75
- export function paletteSpace(palette, distance) {
76
- const n = palette.length;
77
- const coords = new Float64Array(n * 3);
78
- const rgb = new Float64Array(n * 3);
79
- palette.forEach((c, i) => {
80
- rgb.set([c[0] / 255, c[1] / 255, c[2] / 255], i * 3);
81
- coords.set(distance === 1 ? [c[0] / 255, c[1] / 255, c[2] / 255] : oklab(c[0] / 255, c[1] / 255, c[2] / 255), i * 3);
82
- });
83
- return { n, coords, rgb, distance };
84
- }
85
-
86
- const W_RGB = [0.299, 0.587, 0.114];
87
-
88
- /** Index of the nearest palette colour (ties: lowest index). */
89
- export function nearest(sp, r, g, b) {
90
- let p0, p1, p2, w0 = 1, w1 = 1, w2 = 1;
91
- if (sp.distance === 1) { p0 = r; p1 = g; p2 = b; w0 = W_RGB[0]; w1 = W_RGB[1]; w2 = W_RGB[2]; }
92
- else [p0, p1, p2] = oklab(r, g, b);
93
- let best = 0, bd = Infinity;
94
- const c = sp.coords;
95
- for (let i = 0; i < sp.n; i++) {
96
- const d0 = p0 - c[i * 3], d1 = p1 - c[i * 3 + 1], d2 = p2 - c[i * 3 + 2];
97
- const dist = w0 * d0 * d0 + w1 * d1 * d1 + w2 * d2 * d2;
98
- if (dist < bd) { bd = dist; best = i; }
99
- }
100
- return best;
101
- }
102
-
103
- /** No dithering / ordered dithering → RGBA bytes. */
104
- export function quantizeOrdered(buf, w, h, plan) {
105
- const out = new Uint8ClampedArray(w * h * 4);
106
- const sp = plan.quantize ? paletteSpace(plan.palette, plan.distance) : null;
107
- const map = plan.kind === 'ordered' ? plan.map : null;
108
- const amt = map ? plan.spread * plan.settings.dither.strength : 0;
109
- for (let y = 0; y < h; y++) {
110
- for (let x = 0; x < w; x++) {
111
- const o = (y * w + x) * 4;
112
- if (buf[o + 3] < 0.5) continue; // transparent stays 0,0,0,0
113
- let r = buf[o], g = buf[o + 1], b = buf[o + 2];
114
- if (sp) {
115
- if (map) {
116
- const t = (map.values[(y % map.h) * map.w + (x % map.w)] + 0.5) / map.levels - 0.5;
117
- r = clamp01(r + t * amt); g = clamp01(g + t * amt); b = clamp01(b + t * amt);
118
- }
119
- const i = nearest(sp, r, g, b);
120
- out[o] = plan.palette[i][0]; out[o + 1] = plan.palette[i][1]; out[o + 2] = plan.palette[i][2];
121
- } else {
122
- out[o] = Math.round(r * 255); out[o + 1] = Math.round(g * 255); out[o + 2] = Math.round(b * 255);
123
- }
124
- out[o + 3] = 255;
125
- }
126
- }
127
- return out;
128
- }
129
-
130
- /** Error diffusion (left → right, top → bottom) → RGBA bytes. */
131
- export function quantizeDiffusion(buf, w, h, plan) {
132
- const out = new Uint8ClampedArray(w * h * 4);
133
- const sp = paletteSpace(plan.palette, plan.distance);
134
- const acc = new Float32Array(w * h * 3);
135
- const strength = plan.settings.dither.strength;
136
- const kernel = plan.kernel;
137
- for (let y = 0; y < h; y++) {
138
- for (let x = 0; x < w; x++) {
139
- const p = y * w + x, o = p * 4;
140
- if (buf[o + 3] < 0.5) continue;
141
- const r = clamp01(buf[o] + acc[p * 3] * strength);
142
- const g = clamp01(buf[o + 1] + acc[p * 3 + 1] * strength);
143
- const b = clamp01(buf[o + 2] + acc[p * 3 + 2] * strength);
144
- const i = nearest(sp, r, g, b);
145
- const er = r - sp.rgb[i * 3], eg = g - sp.rgb[i * 3 + 1], eb = b - sp.rgb[i * 3 + 2];
146
- for (const [dx, dy, wt] of kernel) {
147
- const xx = x + dx, yy = y + dy;
148
- if (xx < 0 || xx >= w || yy >= h) continue;
149
- const q = (yy * w + xx) * 3;
150
- acc[q] += er * wt; acc[q + 1] += eg * wt; acc[q + 2] += eb * wt;
151
- }
152
- out[o] = plan.palette[i][0]; out[o + 1] = plan.palette[i][1]; out[o + 2] = plan.palette[i][2]; out[o + 3] = 255;
153
- }
154
- }
155
- return out;
156
- }
157
-
158
- const key = (d, o) => (d[o + 3] === 0 ? -1 : (d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
159
- const N4 = [[0, -1], [1, 0], [0, 1], [-1, 0]];
160
-
161
- /** Replace pixels that share no colour with any 4-neighbour by their most common neighbour. */
162
- export function removeOrphans(px, w, h) {
163
- const src = px.slice();
164
- for (let y = 0; y < h; y++) {
165
- for (let x = 0; x < w; x++) {
166
- const o = (y * w + x) * 4, c = key(src, o);
167
- const ks = [], os = [];
168
- for (const [dx, dy] of N4) {
169
- const xx = x + dx, yy = y + dy;
170
- if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
171
- const oo = (yy * w + xx) * 4;
172
- ks.push(key(src, oo)); os.push(oo);
173
- }
174
- if (ks.length < 2 || ks.includes(c)) continue;
175
- let best = 0, bc = 0;
176
- for (let i = 0; i < ks.length; i++) {
177
- let n = 0; for (let j = 0; j < ks.length; j++) if (ks[j] === ks[i]) n++;
178
- if (n > bc) { bc = n; best = i; }
179
- }
180
- px.set(src.subarray(os[best], os[best] + 4), o);
181
- }
182
- }
183
- return px;
184
- }
185
-
186
- /** 3×3 majority: a colour that fills at least 5 of the 9 cells wins. */
187
- export function smoothMajority(px, w, h) {
188
- const src = px.slice();
189
- for (let y = 0; y < h; y++) {
190
- for (let x = 0; x < w; x++) {
191
- const o = (y * w + x) * 4, c = key(src, o);
192
- const ks = [], os = [];
193
- for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
194
- const xx = Math.min(w - 1, Math.max(0, x + dx)), yy = Math.min(h - 1, Math.max(0, y + dy));
195
- const oo = (yy * w + xx) * 4; ks.push(key(src, oo)); os.push(oo);
196
- }
197
- for (let i = 0; i < 9; i++) {
198
- if (ks[i] === c) continue;
199
- let n = 0; for (let j = 0; j < 9; j++) if (ks[j] === ks[i]) n++;
200
- if (n >= 5) { px.set(src.subarray(os[i], os[i] + 4), o); break; }
201
- }
202
- }
203
- }
204
- return px;
205
- }
206
-
207
- /** Outline around (outer) or inside (inner) the opaque shape. */
208
- export function outline(px, w, h, { mode, color, corners }) {
209
- if (mode === 'none') return px;
210
- const src = px.slice();
211
- const opaque = (x, y) => x >= 0 && y >= 0 && x < w && y < h && src[(y * w + x) * 4 + 3] > 0;
212
- const inside = (x, y) => x >= 0 && y >= 0 && x < w && y < h;
213
- const nb = corners ? [[0, -1], [1, 0], [0, 1], [-1, 0], [-1, -1], [1, -1], [1, 1], [-1, 1]] : N4;
214
- for (let y = 0; y < h; y++) {
215
- for (let x = 0; x < w; x++) {
216
- const me = opaque(x, y);
217
- let hit = false;
218
- for (const [dx, dy] of nb) {
219
- const xx = x + dx, yy = y + dy;
220
- if (!inside(xx, yy)) continue;
221
- if (mode === 'outer' ? (!me && opaque(xx, yy)) : (me && !opaque(xx, yy))) { hit = true; break; }
222
- }
223
- if (hit) { const o = (y * w + x) * 4; px[o] = color[0]; px[o + 1] = color[1]; px[o + 2] = color[2]; px[o + 3] = 255; }
224
- }
225
- }
226
- return px;
227
- }
1
+ // CPU reference implementation of every pipeline stage.
2
+ // The GLSL in gpu/shaders.js mirrors these functions line for line; tests compare the two.
3
+ // Working buffers are Float32Array RGBA, rows top-down, values 0..1.
4
+
5
+ import { oklab, LUMA } from '../color.js';
6
+
7
+ /**
8
+ * Resample to w × h. 'box' averages up to 16×16 evenly spaced source texels per output pixel
9
+ * (alpha-weighted); 'nearest' takes the centre texel. Integer maths, identical on the GPU.
10
+ */
11
+ export function resample(src, w, h, mode = 'box', crop = null) {
12
+ const ox = crop ? crop.x : 0, oy = crop ? crop.y : 0;
13
+ const sw = crop ? crop.w : src.width, sh = crop ? crop.h : src.height, d = src.data, stride = src.width;
14
+ const out = new Float32Array(w * h * 4);
15
+ const nx = mode === 'nearest' ? 1 : Math.min(16, Math.max(1, Math.ceil(sw / w)));
16
+ const ny = mode === 'nearest' ? 1 : Math.min(16, Math.max(1, Math.ceil(sh / h)));
17
+ const xs = new Int32Array(w * nx), ys = new Int32Array(h * ny);
18
+ for (let x = 0; x < w; x++) for (let i = 0; i < nx; i++) xs[x * nx + i] = ox + Math.min(sw - 1, Math.floor(((2 * (x * nx + i) + 1) * sw) / (2 * w * nx)));
19
+ for (let y = 0; y < h; y++) for (let j = 0; j < ny; j++) ys[y * ny + j] = oy + Math.min(sh - 1, Math.floor(((2 * (y * ny + j) + 1) * sh) / (2 * h * ny)));
20
+ for (let y = 0; y < h; y++) {
21
+ for (let x = 0; x < w; x++) {
22
+ let r = 0, g = 0, b = 0, a = 0;
23
+ for (let j = 0; j < ny; j++) {
24
+ const row = ys[y * ny + j] * stride;
25
+ for (let i = 0; i < nx; i++) {
26
+ const o = (row + xs[x * nx + i]) * 4;
27
+ const al = d[o + 3] / 255;
28
+ r += d[o] / 255 * al; g += d[o + 1] / 255 * al; b += d[o + 2] / 255 * al; a += al;
29
+ }
30
+ }
31
+ const o = (y * w + x) * 4;
32
+ if (a > 0) { out[o] = r / a; out[o + 1] = g / a; out[o + 2] = b / a; }
33
+ out[o + 3] = a / (nx * ny);
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /** Brightness, contrast, saturation, hue, gamma — in that order, in place. */
40
+ export function adjust(buf, { brightness = 0, contrast = 0, saturation = 0, hue = 0, gamma = 1 }) {
41
+ if (!brightness && !contrast && !saturation && !hue && gamma === 1) return buf;
42
+ const f = contrast >= 0 ? 1 / (1 - 0.99 * contrast) : 1 + contrast;
43
+ const ig = 1 / gamma;
44
+ // luminance-preserving hue rotation: the CSS hue-rotate() matrix (rows sum to 1, so greys stay grey)
45
+ const hk = Math.cos(hue * Math.PI / 180), hq = Math.sin(hue * Math.PI / 180);
46
+ const M = [
47
+ 0.213 + 0.787 * hk - 0.213 * hq, 0.715 - 0.715 * hk - 0.715 * hq, 0.072 - 0.072 * hk + 0.928 * hq,
48
+ 0.213 - 0.213 * hk + 0.143 * hq, 0.715 + 0.285 * hk + 0.140 * hq, 0.072 - 0.072 * hk - 0.283 * hq,
49
+ 0.213 - 0.213 * hk - 0.787 * hq, 0.715 - 0.715 * hk + 0.715 * hq, 0.072 + 0.928 * hk + 0.072 * hq,
50
+ ];
51
+ for (let o = 0; o < buf.length; o += 4) {
52
+ let r = buf[o] + brightness, g = buf[o + 1] + brightness, b = buf[o + 2] + brightness;
53
+ r = (r - 0.5) * f + 0.5; g = (g - 0.5) * f + 0.5; b = (b - 0.5) * f + 0.5;
54
+ const l = r * LUMA[0] + g * LUMA[1] + b * LUMA[2];
55
+ const k = 1 + saturation;
56
+ r = l + (r - l) * k; g = l + (g - l) * k; b = l + (b - l) * k;
57
+ if (hue) {
58
+ const r2 = M[0] * r + M[1] * g + M[2] * b, g2 = M[3] * r + M[4] * g + M[5] * b, b2 = M[6] * r + M[7] * g + M[8] * b;
59
+ r = r2; g = g2; b = b2;
60
+ }
61
+ buf[o] = Math.pow(clamp01(r), ig); buf[o + 1] = Math.pow(clamp01(g), ig); buf[o + 2] = Math.pow(clamp01(b), ig);
62
+ }
63
+ return buf;
64
+ }
65
+
66
+ /** Binary alpha: below the threshold becomes fully transparent, the rest fully opaque. */
67
+ export function alphaCut(buf, threshold) {
68
+ for (let o = 3; o < buf.length; o += 4) buf[o] = buf[o] >= threshold && buf[o] > 0 ? 1 : 0;
69
+ return buf;
70
+ }
71
+
72
+ export const clamp01 = v => (v < 0 ? 0 : v > 1 ? 1 : v);
73
+
74
+ /** Precompute what nearest() needs for a palette. */
75
+ export function paletteSpace(palette, distance) {
76
+ const n = palette.length;
77
+ const coords = new Float64Array(n * 3);
78
+ const rgb = new Float64Array(n * 3);
79
+ palette.forEach((c, i) => {
80
+ rgb.set([c[0] / 255, c[1] / 255, c[2] / 255], i * 3);
81
+ coords.set(distance === 1 ? [c[0] / 255, c[1] / 255, c[2] / 255] : oklab(c[0] / 255, c[1] / 255, c[2] / 255), i * 3);
82
+ });
83
+ return { n, coords, rgb, distance };
84
+ }
85
+
86
+ const W_RGB = [0.299, 0.587, 0.114];
87
+
88
+ /** Index of the nearest palette colour (ties: lowest index). */
89
+ export function nearest(sp, r, g, b) {
90
+ let p0, p1, p2, w0 = 1, w1 = 1, w2 = 1;
91
+ if (sp.distance === 1) { p0 = r; p1 = g; p2 = b; w0 = W_RGB[0]; w1 = W_RGB[1]; w2 = W_RGB[2]; }
92
+ else [p0, p1, p2] = oklab(r, g, b);
93
+ let best = 0, bd = Infinity;
94
+ const c = sp.coords;
95
+ for (let i = 0; i < sp.n; i++) {
96
+ const d0 = p0 - c[i * 3], d1 = p1 - c[i * 3 + 1], d2 = p2 - c[i * 3 + 2];
97
+ const dist = w0 * d0 * d0 + w1 * d1 * d1 + w2 * d2 * d2;
98
+ if (dist < bd) { bd = dist; best = i; }
99
+ }
100
+ return best;
101
+ }
102
+
103
+ /** No dithering / ordered dithering → RGBA bytes. */
104
+ export function quantizeOrdered(buf, w, h, plan) {
105
+ const out = new Uint8ClampedArray(w * h * 4);
106
+ const sp = plan.quantize ? paletteSpace(plan.palette, plan.distance) : null;
107
+ const map = plan.kind === 'ordered' ? plan.map : null;
108
+ const amt = map ? plan.spread * plan.settings.dither.strength : 0;
109
+ for (let y = 0; y < h; y++) {
110
+ for (let x = 0; x < w; x++) {
111
+ const o = (y * w + x) * 4;
112
+ if (buf[o + 3] < 0.5) continue; // transparent stays 0,0,0,0
113
+ let r = buf[o], g = buf[o + 1], b = buf[o + 2];
114
+ if (sp) {
115
+ if (map) {
116
+ const t = (map.values[(y % map.h) * map.w + (x % map.w)] + 0.5) / map.levels - 0.5;
117
+ r = clamp01(r + t * amt); g = clamp01(g + t * amt); b = clamp01(b + t * amt);
118
+ }
119
+ const i = nearest(sp, r, g, b);
120
+ out[o] = plan.palette[i][0]; out[o + 1] = plan.palette[i][1]; out[o + 2] = plan.palette[i][2];
121
+ } else {
122
+ out[o] = Math.round(r * 255); out[o + 1] = Math.round(g * 255); out[o + 2] = Math.round(b * 255);
123
+ }
124
+ out[o + 3] = 255;
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+
130
+ /** Error diffusion (left → right, top → bottom) → RGBA bytes. */
131
+ export function quantizeDiffusion(buf, w, h, plan) {
132
+ const out = new Uint8ClampedArray(w * h * 4);
133
+ const sp = paletteSpace(plan.palette, plan.distance);
134
+ const acc = new Float32Array(w * h * 3);
135
+ const strength = plan.settings.dither.strength;
136
+ const kernel = plan.kernel;
137
+ for (let y = 0; y < h; y++) {
138
+ for (let x = 0; x < w; x++) {
139
+ const p = y * w + x, o = p * 4;
140
+ if (buf[o + 3] < 0.5) continue;
141
+ const r = clamp01(buf[o] + acc[p * 3] * strength);
142
+ const g = clamp01(buf[o + 1] + acc[p * 3 + 1] * strength);
143
+ const b = clamp01(buf[o + 2] + acc[p * 3 + 2] * strength);
144
+ const i = nearest(sp, r, g, b);
145
+ const er = r - sp.rgb[i * 3], eg = g - sp.rgb[i * 3 + 1], eb = b - sp.rgb[i * 3 + 2];
146
+ for (const [dx, dy, wt] of kernel) {
147
+ const xx = x + dx, yy = y + dy;
148
+ if (xx < 0 || xx >= w || yy >= h) continue;
149
+ const q = (yy * w + xx) * 3;
150
+ acc[q] += er * wt; acc[q + 1] += eg * wt; acc[q + 2] += eb * wt;
151
+ }
152
+ out[o] = plan.palette[i][0]; out[o + 1] = plan.palette[i][1]; out[o + 2] = plan.palette[i][2]; out[o + 3] = 255;
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ const key = (d, o) => (d[o + 3] === 0 ? -1 : (d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
159
+ const N4 = [[0, -1], [1, 0], [0, 1], [-1, 0]];
160
+
161
+ /** Replace pixels that share no colour with any 4-neighbour by their most common neighbour. */
162
+ export function removeOrphans(px, w, h) {
163
+ const src = px.slice();
164
+ for (let y = 0; y < h; y++) {
165
+ for (let x = 0; x < w; x++) {
166
+ const o = (y * w + x) * 4, c = key(src, o);
167
+ const ks = [], os = [];
168
+ for (const [dx, dy] of N4) {
169
+ const xx = x + dx, yy = y + dy;
170
+ if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
171
+ const oo = (yy * w + xx) * 4;
172
+ ks.push(key(src, oo)); os.push(oo);
173
+ }
174
+ if (ks.length < 2 || ks.includes(c)) continue;
175
+ let best = 0, bc = 0;
176
+ for (let i = 0; i < ks.length; i++) {
177
+ let n = 0; for (let j = 0; j < ks.length; j++) if (ks[j] === ks[i]) n++;
178
+ if (n > bc) { bc = n; best = i; }
179
+ }
180
+ px.set(src.subarray(os[best], os[best] + 4), o);
181
+ }
182
+ }
183
+ return px;
184
+ }
185
+
186
+ /** 3×3 majority: a colour that fills at least 5 of the 9 cells wins. */
187
+ export function smoothMajority(px, w, h) {
188
+ const src = px.slice();
189
+ for (let y = 0; y < h; y++) {
190
+ for (let x = 0; x < w; x++) {
191
+ const o = (y * w + x) * 4, c = key(src, o);
192
+ const ks = [], os = [];
193
+ for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
194
+ const xx = Math.min(w - 1, Math.max(0, x + dx)), yy = Math.min(h - 1, Math.max(0, y + dy));
195
+ const oo = (yy * w + xx) * 4; ks.push(key(src, oo)); os.push(oo);
196
+ }
197
+ for (let i = 0; i < 9; i++) {
198
+ if (ks[i] === c) continue;
199
+ let n = 0; for (let j = 0; j < 9; j++) if (ks[j] === ks[i]) n++;
200
+ if (n >= 5) { px.set(src.subarray(os[i], os[i] + 4), o); break; }
201
+ }
202
+ }
203
+ }
204
+ return px;
205
+ }
206
+
207
+ /** Outline around (outer) or inside (inner) the opaque shape. */
208
+ export function outline(px, w, h, { mode, color, corners }) {
209
+ if (mode === 'none') return px;
210
+ const src = px.slice();
211
+ const opaque = (x, y) => x >= 0 && y >= 0 && x < w && y < h && src[(y * w + x) * 4 + 3] > 0;
212
+ const inside = (x, y) => x >= 0 && y >= 0 && x < w && y < h;
213
+ const nb = corners ? [[0, -1], [1, 0], [0, 1], [-1, 0], [-1, -1], [1, -1], [1, 1], [-1, 1]] : N4;
214
+ for (let y = 0; y < h; y++) {
215
+ for (let x = 0; x < w; x++) {
216
+ const me = opaque(x, y);
217
+ let hit = false;
218
+ for (const [dx, dy] of nb) {
219
+ const xx = x + dx, yy = y + dy;
220
+ if (!inside(xx, yy)) continue;
221
+ if (mode === 'outer' ? (!me && opaque(xx, yy)) : (me && !opaque(xx, yy))) { hit = true; break; }
222
+ }
223
+ if (hit) { const o = (y * w + x) * 4; px[o] = color[0]; px[o + 1] = color[1]; px[o + 2] = color[2]; px[o + 3] = 255; }
224
+ }
225
+ }
226
+ return px;
227
+ }
@@ -1,3 +1,3 @@
1
- // Generated by scripts/gen-bluenoise.mjs: 64×64 void-and-cluster blue noise, one byte per cell (0..255).
2
- export const BLUE_NOISE_SIZE = 64;
3
- export const BLUE_NOISE_B64 = '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';
1
+ // Generated by scripts/gen-bluenoise.mjs: 64×64 void-and-cluster blue noise, one byte per cell (0..255).
2
+ export const BLUE_NOISE_SIZE = 64;
3
+ export const BLUE_NOISE_B64 = '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';
@@ -1,27 +1,27 @@
1
- // Error-diffusion kernels as push offsets [dx, dy, weight] (weights already divided).
2
- // `skew` is the wavefront slope the GPU needs: pixels with x + skew*y = k only depend on
3
- // earlier waves, so each wave is one draw (see docs/how-it-works.md).
4
-
5
- const k = (div, list) => list.map(([dx, dy, w]) => [dx, dy, w / div]);
6
-
7
- export const KERNELS = {
8
- 'floyd-steinberg': k(16, [[1, 0, 7], [-1, 1, 3], [0, 1, 5], [1, 1, 1]]),
9
- 'atkinson': k(8, [[1, 0, 1], [2, 0, 1], [-1, 1, 1], [0, 1, 1], [1, 1, 1], [0, 2, 1]]),
10
- 'sierra-lite': k(4, [[1, 0, 2], [-1, 1, 1], [0, 1, 1]]),
11
- 'sierra': k(32, [[1, 0, 5], [2, 0, 3], [-2, 1, 2], [-1, 1, 4], [0, 1, 5], [1, 1, 4], [2, 1, 2], [-1, 2, 2], [0, 2, 3], [1, 2, 2]]),
12
- 'burkes': k(32, [[1, 0, 8], [2, 0, 4], [-2, 1, 2], [-1, 1, 4], [0, 1, 8], [1, 1, 4], [2, 1, 2]]),
13
- 'jarvis': k(48, [[1, 0, 7], [2, 0, 5], [-2, 1, 3], [-1, 1, 5], [0, 1, 7], [1, 1, 5], [2, 1, 3], [-2, 2, 1], [-1, 2, 3], [0, 2, 5], [1, 2, 3], [2, 2, 1]]),
14
- 'stucki': k(42, [[1, 0, 8], [2, 0, 4], [-2, 1, 2], [-1, 1, 4], [0, 1, 8], [1, 1, 4], [2, 1, 2], [-2, 2, 1], [-1, 2, 2], [0, 2, 4], [1, 2, 2], [2, 2, 1]]),
15
- };
16
-
17
- export const MAX_KERNEL = 12;
18
-
19
- /** Smallest integer s so that every push offset lands on a later wave x + s*y. */
20
- export function kernelSkew(kernel) {
21
- let s = 1;
22
- for (const [dx, dy] of kernel) if (dy > 0) s = Math.max(s, Math.floor(-dx / dy) + 1);
23
- return s;
24
- }
25
-
26
- /** Number of waves (GPU draws) for an image of w × h. */
27
- export const waveCount = (w, h, skew) => (w - 1) + skew * (h - 1) + 1;
1
+ // Error-diffusion kernels as push offsets [dx, dy, weight] (weights already divided).
2
+ // `skew` is the wavefront slope the GPU needs: pixels with x + skew*y = k only depend on
3
+ // earlier waves, so each wave is one draw (see docs/how-it-works.md).
4
+
5
+ const k = (div, list) => list.map(([dx, dy, w]) => [dx, dy, w / div]);
6
+
7
+ export const KERNELS = {
8
+ 'floyd-steinberg': k(16, [[1, 0, 7], [-1, 1, 3], [0, 1, 5], [1, 1, 1]]),
9
+ 'atkinson': k(8, [[1, 0, 1], [2, 0, 1], [-1, 1, 1], [0, 1, 1], [1, 1, 1], [0, 2, 1]]),
10
+ 'sierra-lite': k(4, [[1, 0, 2], [-1, 1, 1], [0, 1, 1]]),
11
+ 'sierra': k(32, [[1, 0, 5], [2, 0, 3], [-2, 1, 2], [-1, 1, 4], [0, 1, 5], [1, 1, 4], [2, 1, 2], [-1, 2, 2], [0, 2, 3], [1, 2, 2]]),
12
+ 'burkes': k(32, [[1, 0, 8], [2, 0, 4], [-2, 1, 2], [-1, 1, 4], [0, 1, 8], [1, 1, 4], [2, 1, 2]]),
13
+ 'jarvis': k(48, [[1, 0, 7], [2, 0, 5], [-2, 1, 3], [-1, 1, 5], [0, 1, 7], [1, 1, 5], [2, 1, 3], [-2, 2, 1], [-1, 2, 3], [0, 2, 5], [1, 2, 3], [2, 2, 1]]),
14
+ 'stucki': k(42, [[1, 0, 8], [2, 0, 4], [-2, 1, 2], [-1, 1, 4], [0, 1, 8], [1, 1, 4], [2, 1, 2], [-2, 2, 1], [-1, 2, 2], [0, 2, 4], [1, 2, 2], [2, 2, 1]]),
15
+ };
16
+
17
+ export const MAX_KERNEL = 12;
18
+
19
+ /** Smallest integer s so that every push offset lands on a later wave x + s*y. */
20
+ export function kernelSkew(kernel) {
21
+ let s = 1;
22
+ for (const [dx, dy] of kernel) if (dy > 0) s = Math.max(s, Math.floor(-dx / dy) + 1);
23
+ return s;
24
+ }
25
+
26
+ /** Number of waves (GPU draws) for an image of w × h. */
27
+ export const waveCount = (w, h, skew) => (w - 1) + skew * (h - 1) + 1;