image-to-pixel 3.0.0 → 3.1.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/LICENSE +21 -21
- package/README.md +2 -2
- package/dist/editor.css +1 -1
- package/dist/editor.css.map +2 -2
- package/dist/editor.esm.js +60 -60
- package/dist/editor.esm.js.map +4 -4
- package/dist/fonts/README.md +6 -6
- package/dist/fonts/geist-LICENSE.txt +93 -93
- package/dist/image-to-pixel.bundle.esm.js +16 -16
- package/dist/image-to-pixel.bundle.esm.js.map +3 -3
- package/dist/image-to-pixel.cjs +2 -2
- package/dist/image-to-pixel.cjs.map +2 -2
- package/dist/image-to-pixel.esm.js +2 -2
- package/dist/image-to-pixel.esm.js.map +2 -2
- package/dist/image-to-pixel.js +2 -2
- package/dist/image-to-pixel.js.map +2 -2
- package/dist/image-to-pixel.min.js +11 -11
- package/dist/image-to-pixel.min.js.map +3 -3
- package/dist/video.esm.js +2 -2
- package/dist/video.esm.js.map +4 -4
- package/lib/core/color.js +69 -69
- package/lib/core/cpu/pipeline.js +21 -21
- package/lib/core/cpu/stages.js +227 -227
- package/lib/core/dither/bluenoise-data.js +3 -3
- package/lib/core/dither/kernels.js +27 -27
- package/lib/core/dither/matrices.js +95 -95
- package/lib/core/engine.js +158 -158
- package/lib/core/export/canvas.js +22 -22
- package/lib/core/export/crc32.js +15 -15
- package/lib/core/export/gif.js +153 -153
- package/lib/core/export/png.js +94 -94
- package/lib/core/export/scale.js +103 -103
- package/lib/core/export/sheet.js +95 -95
- package/lib/core/export/zip.js +55 -55
- package/lib/core/gpu/backend.js +159 -159
- package/lib/core/gpu/shaders.js +258 -258
- package/lib/core/palette/builtin.js +36 -36
- package/lib/core/palette/extract.js +69 -69
- package/lib/core/palette/lospec.js +24 -24
- package/lib/core/palette/parse.js +112 -112
- package/lib/core/plan.js +58 -58
- package/lib/core/presets.js +42 -42
- package/lib/core/settings.js +274 -274
- package/lib/core/source.js +89 -89
- package/lib/core/video/audio.js +159 -0
- package/lib/core/video/frames.js +38 -0
- package/lib/core/video/mux-mp4.js +213 -24
- package/lib/core/video/mux-webm.js +63 -22
- package/lib/editor/controls.js +139 -139
- package/lib/editor/dom.js +65 -65
- package/lib/editor/exporter.js +43 -3
- package/lib/editor/icons.js +4 -0
- package/lib/editor/index.js +218 -9
- package/lib/editor/inspector.js +19 -4
- package/lib/editor/library.js +8 -2
- package/lib/editor/persist.js +18 -18
- package/lib/editor/samples.js +90 -90
- package/lib/editor/stage.js +239 -239
- package/lib/editor/store.js +80 -80
- package/lib/editor/styles/editor.css +34 -1
- package/lib/editor/styles/fonts/README.md +6 -6
- package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
- package/lib/editor/styles/tokens.css +89 -89
- package/lib/editor/ui.js +97 -97
- package/lib/editor/workspace.js +62 -0
- package/lib/index.js +31 -29
- package/lib/video.js +37 -7
- package/package.json +1 -1
- package/types/editor.d.ts +34 -34
- package/types/index.d.ts +196 -196
- package/types/video.d.ts +61 -5
|
@@ -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;
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
// Threshold maps for ordered dithering.
|
|
2
|
-
// A map is { w, h, levels, values: Uint8Array } and the threshold of a cell is (value + 0.5) / levels.
|
|
3
|
-
// The GPU reads the same bytes from a texture, so CPU and GPU agree exactly.
|
|
4
|
-
|
|
5
|
-
import { BLUE_NOISE_B64, BLUE_NOISE_SIZE } from './bluenoise-data.js';
|
|
6
|
-
|
|
7
|
-
function fromB64(b64) {
|
|
8
|
-
if (typeof atob === 'function') return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
9
|
-
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Bayer index matrix of size n (power of two). */
|
|
13
|
-
export function bayer(n) {
|
|
14
|
-
let m = [[0]];
|
|
15
|
-
for (let s = 1; s < n; s *= 2) {
|
|
16
|
-
const next = [];
|
|
17
|
-
for (let y = 0; y < s * 2; y++) next.push(new Array(s * 2));
|
|
18
|
-
for (let y = 0; y < s; y++) for (let x = 0; x < s; x++) {
|
|
19
|
-
const v = 4 * m[y][x];
|
|
20
|
-
next[y][x] = v; next[y][x + s] = v + 2; next[y + s][x] = v + 3; next[y + s][x + s] = v + 1;
|
|
21
|
-
}
|
|
22
|
-
m = next;
|
|
23
|
-
}
|
|
24
|
-
return m;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Turn arbitrary numbers into ranks 0..n-1 (ties keep reading order). */
|
|
28
|
-
export function toRanks(rows) {
|
|
29
|
-
const h = rows.length, w = rows[0].length;
|
|
30
|
-
const cells = [];
|
|
31
|
-
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) cells.push({ i: y * w + x, v: rows[y][x] });
|
|
32
|
-
cells.sort((a, b) => a.v - b.v || a.i - b.i);
|
|
33
|
-
const out = new Uint8Array(w * h);
|
|
34
|
-
cells.forEach((c, r) => { out[c.i] = r; });
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Clustered-dot screen: cells ranked by a dot function, two dots per tile at 45°. */
|
|
39
|
-
function clustered(n) {
|
|
40
|
-
const rows = [];
|
|
41
|
-
for (let y = 0; y < n; y++) {
|
|
42
|
-
const row = [];
|
|
43
|
-
for (let x = 0; x < n; x++) {
|
|
44
|
-
const u = (x + 0.5) / n * 2 * Math.PI, v = (y + 0.5) / n * 2 * Math.PI;
|
|
45
|
-
row.push(-(Math.cos(u) + Math.cos(v)) + (x * n + y) * 1e-9);
|
|
46
|
-
}
|
|
47
|
-
rows.push(row);
|
|
48
|
-
}
|
|
49
|
-
return rows;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const CLUSTER4 = [[7, 13, 11, 4], [12, 16, 14, 8], [10, 15, 6, 2], [5, 9, 3, 1]]; // classic editor's matrix
|
|
53
|
-
const cache = new Map();
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Threshold map for an ordered method id ('bayer4', 'bluenoise', 'custom' …).
|
|
57
|
-
* @param {string} method
|
|
58
|
-
* @param {number[][]|null} [custom] matrix for 'custom'
|
|
59
|
-
*/
|
|
60
|
-
export function thresholdMap(method, custom = null) {
|
|
61
|
-
if (method === 'custom') {
|
|
62
|
-
const m = custom && custom.length ? custom : bayer(4);
|
|
63
|
-
return { w: m[0].length, h: m.length, levels: m.length * m[0].length, values: toRanks(m) };
|
|
64
|
-
}
|
|
65
|
-
if (cache.has(method)) return cache.get(method);
|
|
66
|
-
let map;
|
|
67
|
-
const sq = rows => ({ w: rows[0].length, h: rows.length, levels: rows.length * rows[0].length, values: toRanks(rows) });
|
|
68
|
-
switch (method) {
|
|
69
|
-
case 'bayer2': map = sq(bayer(2)); break;
|
|
70
|
-
case 'bayer4': map = sq(bayer(4)); break;
|
|
71
|
-
case 'bayer8': map = sq(bayer(8)); break;
|
|
72
|
-
case 'bayer16': map = sq(bayer(16)); break;
|
|
73
|
-
case 'cluster4': map = sq(CLUSTER4); break;
|
|
74
|
-
case 'cluster8': map = sq(clustered(8)); break;
|
|
75
|
-
case 'lines': map = sq([[0], [2], [1], [3]]); break;
|
|
76
|
-
case 'bluenoise': map = { w: BLUE_NOISE_SIZE, h: BLUE_NOISE_SIZE, levels: 256, values: fromB64(BLUE_NOISE_B64) }; break;
|
|
77
|
-
default: map = sq(bayer(4));
|
|
78
|
-
}
|
|
79
|
-
cache.set(method, map);
|
|
80
|
-
return map;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Parse a custom matrix typed by a person: rows on lines, numbers separated by spaces or commas.
|
|
85
|
-
* Accepts an optional "/ 16" style divisor line, which is ignored (only the order matters).
|
|
86
|
-
*/
|
|
87
|
-
export function parseMatrix(text) {
|
|
88
|
-
const rows = String(text).split(/\r?\n|;/).map(l => l.trim()).filter(l => l && !/^\//.test(l))
|
|
89
|
-
.map(l => l.replace(/[[\]]/g, '').split(/[\s,]+/).filter(Boolean).map(Number));
|
|
90
|
-
if (!rows.length) throw new Error('Matrix is empty');
|
|
91
|
-
const w = rows[0].length;
|
|
92
|
-
if (rows.some(r => r.length !== w || r.some(v => !Number.isFinite(v)))) throw new Error('Every row needs the same number of values');
|
|
93
|
-
if (w > 16 || rows.length > 16) throw new Error('Matrices are limited to 16×16');
|
|
94
|
-
return rows;
|
|
95
|
-
}
|
|
1
|
+
// Threshold maps for ordered dithering.
|
|
2
|
+
// A map is { w, h, levels, values: Uint8Array } and the threshold of a cell is (value + 0.5) / levels.
|
|
3
|
+
// The GPU reads the same bytes from a texture, so CPU and GPU agree exactly.
|
|
4
|
+
|
|
5
|
+
import { BLUE_NOISE_B64, BLUE_NOISE_SIZE } from './bluenoise-data.js';
|
|
6
|
+
|
|
7
|
+
function fromB64(b64) {
|
|
8
|
+
if (typeof atob === 'function') return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
9
|
+
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Bayer index matrix of size n (power of two). */
|
|
13
|
+
export function bayer(n) {
|
|
14
|
+
let m = [[0]];
|
|
15
|
+
for (let s = 1; s < n; s *= 2) {
|
|
16
|
+
const next = [];
|
|
17
|
+
for (let y = 0; y < s * 2; y++) next.push(new Array(s * 2));
|
|
18
|
+
for (let y = 0; y < s; y++) for (let x = 0; x < s; x++) {
|
|
19
|
+
const v = 4 * m[y][x];
|
|
20
|
+
next[y][x] = v; next[y][x + s] = v + 2; next[y + s][x] = v + 3; next[y + s][x + s] = v + 1;
|
|
21
|
+
}
|
|
22
|
+
m = next;
|
|
23
|
+
}
|
|
24
|
+
return m;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Turn arbitrary numbers into ranks 0..n-1 (ties keep reading order). */
|
|
28
|
+
export function toRanks(rows) {
|
|
29
|
+
const h = rows.length, w = rows[0].length;
|
|
30
|
+
const cells = [];
|
|
31
|
+
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) cells.push({ i: y * w + x, v: rows[y][x] });
|
|
32
|
+
cells.sort((a, b) => a.v - b.v || a.i - b.i);
|
|
33
|
+
const out = new Uint8Array(w * h);
|
|
34
|
+
cells.forEach((c, r) => { out[c.i] = r; });
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Clustered-dot screen: cells ranked by a dot function, two dots per tile at 45°. */
|
|
39
|
+
function clustered(n) {
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (let y = 0; y < n; y++) {
|
|
42
|
+
const row = [];
|
|
43
|
+
for (let x = 0; x < n; x++) {
|
|
44
|
+
const u = (x + 0.5) / n * 2 * Math.PI, v = (y + 0.5) / n * 2 * Math.PI;
|
|
45
|
+
row.push(-(Math.cos(u) + Math.cos(v)) + (x * n + y) * 1e-9);
|
|
46
|
+
}
|
|
47
|
+
rows.push(row);
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CLUSTER4 = [[7, 13, 11, 4], [12, 16, 14, 8], [10, 15, 6, 2], [5, 9, 3, 1]]; // classic editor's matrix
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Threshold map for an ordered method id ('bayer4', 'bluenoise', 'custom' …).
|
|
57
|
+
* @param {string} method
|
|
58
|
+
* @param {number[][]|null} [custom] matrix for 'custom'
|
|
59
|
+
*/
|
|
60
|
+
export function thresholdMap(method, custom = null) {
|
|
61
|
+
if (method === 'custom') {
|
|
62
|
+
const m = custom && custom.length ? custom : bayer(4);
|
|
63
|
+
return { w: m[0].length, h: m.length, levels: m.length * m[0].length, values: toRanks(m) };
|
|
64
|
+
}
|
|
65
|
+
if (cache.has(method)) return cache.get(method);
|
|
66
|
+
let map;
|
|
67
|
+
const sq = rows => ({ w: rows[0].length, h: rows.length, levels: rows.length * rows[0].length, values: toRanks(rows) });
|
|
68
|
+
switch (method) {
|
|
69
|
+
case 'bayer2': map = sq(bayer(2)); break;
|
|
70
|
+
case 'bayer4': map = sq(bayer(4)); break;
|
|
71
|
+
case 'bayer8': map = sq(bayer(8)); break;
|
|
72
|
+
case 'bayer16': map = sq(bayer(16)); break;
|
|
73
|
+
case 'cluster4': map = sq(CLUSTER4); break;
|
|
74
|
+
case 'cluster8': map = sq(clustered(8)); break;
|
|
75
|
+
case 'lines': map = sq([[0], [2], [1], [3]]); break;
|
|
76
|
+
case 'bluenoise': map = { w: BLUE_NOISE_SIZE, h: BLUE_NOISE_SIZE, levels: 256, values: fromB64(BLUE_NOISE_B64) }; break;
|
|
77
|
+
default: map = sq(bayer(4));
|
|
78
|
+
}
|
|
79
|
+
cache.set(method, map);
|
|
80
|
+
return map;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse a custom matrix typed by a person: rows on lines, numbers separated by spaces or commas.
|
|
85
|
+
* Accepts an optional "/ 16" style divisor line, which is ignored (only the order matters).
|
|
86
|
+
*/
|
|
87
|
+
export function parseMatrix(text) {
|
|
88
|
+
const rows = String(text).split(/\r?\n|;/).map(l => l.trim()).filter(l => l && !/^\//.test(l))
|
|
89
|
+
.map(l => l.replace(/[[\]]/g, '').split(/[\s,]+/).filter(Boolean).map(Number));
|
|
90
|
+
if (!rows.length) throw new Error('Matrix is empty');
|
|
91
|
+
const w = rows[0].length;
|
|
92
|
+
if (rows.some(r => r.length !== w || r.some(v => !Number.isFinite(v)))) throw new Error('Every row needs the same number of values');
|
|
93
|
+
if (w > 16 || rows.length > 16) throw new Error('Matrices are limited to 16×16');
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
package/lib/core/engine.js
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
// The public engine: createPixelator() picks a backend and turns sources into pixel art.
|
|
2
|
-
|
|
3
|
-
import { makePlan } from './plan.js';
|
|
4
|
-
import { runCPU } from './cpu/pipeline.js';
|
|
5
|
-
import { createGPUBackend } from './gpu/backend.js';
|
|
6
|
-
import { toFrame } from './source.js';
|
|
7
|
-
import { normalizeSettings, LEGACY_DITHER } from './settings.js';
|
|
8
|
-
import { uniqueColors } from './export/scale.js';
|
|
9
|
-
import { toCanvas } from './export/canvas.js';
|
|
10
|
-
import { encodePNG } from './export/png.js';
|
|
11
|
-
|
|
12
|
-
const isVideo = s => typeof HTMLVideoElement !== 'undefined' && s instanceof HTMLVideoElement;
|
|
13
|
-
const isVideoFrame = s => typeof VideoFrame !== 'undefined' && s instanceof VideoFrame;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Create an engine. Reuse one for many images: the GPU backend compiles its shaders once.
|
|
17
|
-
* @param {{ backend?: 'auto' | 'webgl' | 'cpu', diffusion?: 'auto' | 'gpu' | 'cpu', maxSourceSize?: number }} [options]
|
|
18
|
-
* diffusion: where error diffusion runs. 'auto' uses the GPU unless WebGL is software-rendered.
|
|
19
|
-
*/
|
|
20
|
-
export async function createPixelator(options = {}) {
|
|
21
|
-
const want = options.backend || 'auto';
|
|
22
|
-
let gpu = null, gpuError = null;
|
|
23
|
-
if (want !== 'cpu' && typeof document !== 'undefined') {
|
|
24
|
-
try { gpu = await createGPUBackend(); } catch (e) {
|
|
25
|
-
gpuError = e;
|
|
26
|
-
if (want === 'webgl') throw e;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const scratch = typeof document !== 'undefined' ? document.createElement('canvas') : null;
|
|
30
|
-
let last = { source: null, frame: null };
|
|
31
|
-
let queue = Promise.resolve();
|
|
32
|
-
|
|
33
|
-
async function frameFor(source) {
|
|
34
|
-
if (source === last.source && last.frame && !isVideo(source) && !isVideoFrame(source)) {
|
|
35
|
-
last.frame.dirty = false;
|
|
36
|
-
return last.frame;
|
|
37
|
-
}
|
|
38
|
-
const frame = await toFrame(source, { maxSize: options.maxSourceSize, canvas: scratch });
|
|
39
|
-
last = { source, frame };
|
|
40
|
-
return frame;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function processNow(source, settings) {
|
|
44
|
-
const t0 = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
45
|
-
const frame = await frameFor(source);
|
|
46
|
-
const plan = makePlan(settings, frame);
|
|
47
|
-
let data, backend = 'cpu';
|
|
48
|
-
const diffusionOnCPU = plan.kind === 'diffusion' && (options.diffusion === 'cpu' || (options.diffusion !== 'gpu' && gpu && gpu.software));
|
|
49
|
-
if (gpu && frame.canvas && !diffusionOnCPU) {
|
|
50
|
-
data = await gpu.run(plan, frame);
|
|
51
|
-
backend = 'webgl';
|
|
52
|
-
} else {
|
|
53
|
-
data = runCPU(plan, frame);
|
|
54
|
-
}
|
|
55
|
-
const ms = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - t0;
|
|
56
|
-
return makeResult(plan, data, backend, ms);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
/** 'webgl' or 'cpu' */
|
|
61
|
-
get backend() { return gpu ? 'webgl' : 'cpu'; },
|
|
62
|
-
/** GPU renderer string (WebGL backend only). */
|
|
63
|
-
get renderer() { return gpu ? gpu.renderer : ''; },
|
|
64
|
-
/** Why the GPU backend is not in use, if it isn't. */
|
|
65
|
-
get fallbackReason() { return gpuError ? String(gpuError.message || gpuError) : ''; },
|
|
66
|
-
/**
|
|
67
|
-
* Process one source. Calls are queued, so it is safe to call from input handlers.
|
|
68
|
-
* @returns {Promise<PixelResult>}
|
|
69
|
-
*/
|
|
70
|
-
process(source, settings = {}) {
|
|
71
|
-
const run = queue.then(() => processNow(source, settings));
|
|
72
|
-
queue = run.catch(() => { });
|
|
73
|
-
return run;
|
|
74
|
-
},
|
|
75
|
-
/** Forget the cached source (call after drawing new content into the same canvas). */
|
|
76
|
-
invalidate() { last = { source: null, frame: null }; },
|
|
77
|
-
destroy() { if (gpu) gpu.destroy(); gpu = null; },
|
|
78
|
-
/** The underlying ShaderMate instance (GPU backend only), for debugging. */
|
|
79
|
-
get shader() { return gpu ? gpu.shader : null; },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @typedef {object} PixelResult
|
|
85
|
-
* @property {number} width
|
|
86
|
-
* @property {number} height
|
|
87
|
-
* @property {Uint8ClampedArray} data RGBA, rows top-down
|
|
88
|
-
* @property {string[]} palette colours actually available ('#rrggbb')
|
|
89
|
-
* @property {object} settings normalised settings used
|
|
90
|
-
* @property {'webgl'|'cpu'} backend
|
|
91
|
-
* @property {number} ms processing time
|
|
92
|
-
*/
|
|
93
|
-
export function makeResult(plan, data, backend = 'cpu', ms = 0) {
|
|
94
|
-
const result = {
|
|
95
|
-
width: plan.width,
|
|
96
|
-
height: plan.height,
|
|
97
|
-
data,
|
|
98
|
-
palette: plan.paletteHex ? plan.paletteHex.slice() : null,
|
|
99
|
-
settings: plan.settings,
|
|
100
|
-
kind: plan.kind,
|
|
101
|
-
waves: plan.waves,
|
|
102
|
-
/** The framed part of the source, as fractions of its width and height. */
|
|
103
|
-
crop: { x: plan.crop.x / plan.srcWidth, y: plan.crop.y / plan.srcHeight, w: plan.crop.w / plan.srcWidth, h: plan.crop.h / plan.srcHeight },
|
|
104
|
-
backend,
|
|
105
|
-
ms,
|
|
106
|
-
/** Distinct colours actually used in the image. */
|
|
107
|
-
usedColors() { return uniqueColors(this); },
|
|
108
|
-
toImageData() { return new ImageData(new Uint8ClampedArray(this.data), this.width, this.height); },
|
|
109
|
-
/** Canvas at `scale` × (nearest-neighbour), optional grid lines. */
|
|
110
|
-
toCanvas(scale = 1, opts = {}) { return toCanvas(this, scale, opts); },
|
|
111
|
-
/** PNG bytes (indexed when ≤ 256 colours unless `indexed: false`). */
|
|
112
|
-
toPNG(opts = {}) { return encodePNG(this, opts); },
|
|
113
|
-
async toBlob(opts = {}) { return new Blob([await encodePNG(this, opts)], { type: 'image/png' }); },
|
|
114
|
-
};
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let shared = null;
|
|
119
|
-
/** One-shot helper with a shared engine. `pixelate(source, settings)`. */
|
|
120
|
-
export async function pixelate(source, settings) {
|
|
121
|
-
// classic API: pixelate({ image, width, dither, strength, palette, resolution }) → canvas
|
|
122
|
-
if (settings === undefined && source && typeof source === 'object' && 'image' in source && 'width' in source) {
|
|
123
|
-
return pixelateClassic(source);
|
|
124
|
-
}
|
|
125
|
-
shared ||= createPixelator();
|
|
126
|
-
return (await shared).process(source, settings);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Compatibility with image-to-pixel.js 2.x. Returns a canvas like the old function did. */
|
|
130
|
-
async function pixelateClassic(opts) {
|
|
131
|
-
const { image, width, dither = 'none', strength = 0, palette = null, resolution = 'original' } = opts;
|
|
132
|
-
let pal = { source: 'none' };
|
|
133
|
-
if (Array.isArray(palette)) pal = { source: 'custom', colors: palette };
|
|
134
|
-
else if (typeof palette === 'string') {
|
|
135
|
-
const { fetchLospec } = await import('./palette/lospec.js');
|
|
136
|
-
const p = await fetchLospec(palette);
|
|
137
|
-
pal = { source: 'custom', colors: p.colors, name: p.name };
|
|
138
|
-
}
|
|
139
|
-
const settings = normalizeSettings({
|
|
140
|
-
size: { width },
|
|
141
|
-
palette: pal,
|
|
142
|
-
dither: { method: LEGACY_DITHER[String(dither).toLowerCase()] || 'none', strength: strength / 100 },
|
|
143
|
-
alpha: { threshold: 0 },
|
|
144
|
-
});
|
|
145
|
-
shared ||= createPixelator();
|
|
146
|
-
const engine = await shared;
|
|
147
|
-
const res = await engine.process(image, settings);
|
|
148
|
-
if (resolution === 'original') {
|
|
149
|
-
const frame = await toFrame(image);
|
|
150
|
-
const c = document.createElement('canvas');
|
|
151
|
-
c.width = frame.width; c.height = frame.height;
|
|
152
|
-
const ctx = c.getContext('2d');
|
|
153
|
-
ctx.imageSmoothingEnabled = false;
|
|
154
|
-
ctx.drawImage(res.toCanvas(1), 0, 0, c.width, c.height);
|
|
155
|
-
return c;
|
|
156
|
-
}
|
|
157
|
-
return res.toCanvas(1);
|
|
158
|
-
}
|
|
1
|
+
// The public engine: createPixelator() picks a backend and turns sources into pixel art.
|
|
2
|
+
|
|
3
|
+
import { makePlan } from './plan.js';
|
|
4
|
+
import { runCPU } from './cpu/pipeline.js';
|
|
5
|
+
import { createGPUBackend } from './gpu/backend.js';
|
|
6
|
+
import { toFrame } from './source.js';
|
|
7
|
+
import { normalizeSettings, LEGACY_DITHER } from './settings.js';
|
|
8
|
+
import { uniqueColors } from './export/scale.js';
|
|
9
|
+
import { toCanvas } from './export/canvas.js';
|
|
10
|
+
import { encodePNG } from './export/png.js';
|
|
11
|
+
|
|
12
|
+
const isVideo = s => typeof HTMLVideoElement !== 'undefined' && s instanceof HTMLVideoElement;
|
|
13
|
+
const isVideoFrame = s => typeof VideoFrame !== 'undefined' && s instanceof VideoFrame;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an engine. Reuse one for many images: the GPU backend compiles its shaders once.
|
|
17
|
+
* @param {{ backend?: 'auto' | 'webgl' | 'cpu', diffusion?: 'auto' | 'gpu' | 'cpu', maxSourceSize?: number }} [options]
|
|
18
|
+
* diffusion: where error diffusion runs. 'auto' uses the GPU unless WebGL is software-rendered.
|
|
19
|
+
*/
|
|
20
|
+
export async function createPixelator(options = {}) {
|
|
21
|
+
const want = options.backend || 'auto';
|
|
22
|
+
let gpu = null, gpuError = null;
|
|
23
|
+
if (want !== 'cpu' && typeof document !== 'undefined') {
|
|
24
|
+
try { gpu = await createGPUBackend(); } catch (e) {
|
|
25
|
+
gpuError = e;
|
|
26
|
+
if (want === 'webgl') throw e;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const scratch = typeof document !== 'undefined' ? document.createElement('canvas') : null;
|
|
30
|
+
let last = { source: null, frame: null };
|
|
31
|
+
let queue = Promise.resolve();
|
|
32
|
+
|
|
33
|
+
async function frameFor(source) {
|
|
34
|
+
if (source === last.source && last.frame && !isVideo(source) && !isVideoFrame(source)) {
|
|
35
|
+
last.frame.dirty = false;
|
|
36
|
+
return last.frame;
|
|
37
|
+
}
|
|
38
|
+
const frame = await toFrame(source, { maxSize: options.maxSourceSize, canvas: scratch });
|
|
39
|
+
last = { source, frame };
|
|
40
|
+
return frame;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function processNow(source, settings) {
|
|
44
|
+
const t0 = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
45
|
+
const frame = await frameFor(source);
|
|
46
|
+
const plan = makePlan(settings, frame);
|
|
47
|
+
let data, backend = 'cpu';
|
|
48
|
+
const diffusionOnCPU = plan.kind === 'diffusion' && (options.diffusion === 'cpu' || (options.diffusion !== 'gpu' && gpu && gpu.software));
|
|
49
|
+
if (gpu && frame.canvas && !diffusionOnCPU) {
|
|
50
|
+
data = await gpu.run(plan, frame);
|
|
51
|
+
backend = 'webgl';
|
|
52
|
+
} else {
|
|
53
|
+
data = runCPU(plan, frame);
|
|
54
|
+
}
|
|
55
|
+
const ms = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - t0;
|
|
56
|
+
return makeResult(plan, data, backend, ms);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
/** 'webgl' or 'cpu' */
|
|
61
|
+
get backend() { return gpu ? 'webgl' : 'cpu'; },
|
|
62
|
+
/** GPU renderer string (WebGL backend only). */
|
|
63
|
+
get renderer() { return gpu ? gpu.renderer : ''; },
|
|
64
|
+
/** Why the GPU backend is not in use, if it isn't. */
|
|
65
|
+
get fallbackReason() { return gpuError ? String(gpuError.message || gpuError) : ''; },
|
|
66
|
+
/**
|
|
67
|
+
* Process one source. Calls are queued, so it is safe to call from input handlers.
|
|
68
|
+
* @returns {Promise<PixelResult>}
|
|
69
|
+
*/
|
|
70
|
+
process(source, settings = {}) {
|
|
71
|
+
const run = queue.then(() => processNow(source, settings));
|
|
72
|
+
queue = run.catch(() => { });
|
|
73
|
+
return run;
|
|
74
|
+
},
|
|
75
|
+
/** Forget the cached source (call after drawing new content into the same canvas). */
|
|
76
|
+
invalidate() { last = { source: null, frame: null }; },
|
|
77
|
+
destroy() { if (gpu) gpu.destroy(); gpu = null; },
|
|
78
|
+
/** The underlying ShaderMate instance (GPU backend only), for debugging. */
|
|
79
|
+
get shader() { return gpu ? gpu.shader : null; },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} PixelResult
|
|
85
|
+
* @property {number} width
|
|
86
|
+
* @property {number} height
|
|
87
|
+
* @property {Uint8ClampedArray} data RGBA, rows top-down
|
|
88
|
+
* @property {string[]} palette colours actually available ('#rrggbb')
|
|
89
|
+
* @property {object} settings normalised settings used
|
|
90
|
+
* @property {'webgl'|'cpu'} backend
|
|
91
|
+
* @property {number} ms processing time
|
|
92
|
+
*/
|
|
93
|
+
export function makeResult(plan, data, backend = 'cpu', ms = 0) {
|
|
94
|
+
const result = {
|
|
95
|
+
width: plan.width,
|
|
96
|
+
height: plan.height,
|
|
97
|
+
data,
|
|
98
|
+
palette: plan.paletteHex ? plan.paletteHex.slice() : null,
|
|
99
|
+
settings: plan.settings,
|
|
100
|
+
kind: plan.kind,
|
|
101
|
+
waves: plan.waves,
|
|
102
|
+
/** The framed part of the source, as fractions of its width and height. */
|
|
103
|
+
crop: { x: plan.crop.x / plan.srcWidth, y: plan.crop.y / plan.srcHeight, w: plan.crop.w / plan.srcWidth, h: plan.crop.h / plan.srcHeight },
|
|
104
|
+
backend,
|
|
105
|
+
ms,
|
|
106
|
+
/** Distinct colours actually used in the image. */
|
|
107
|
+
usedColors() { return uniqueColors(this); },
|
|
108
|
+
toImageData() { return new ImageData(new Uint8ClampedArray(this.data), this.width, this.height); },
|
|
109
|
+
/** Canvas at `scale` × (nearest-neighbour), optional grid lines. */
|
|
110
|
+
toCanvas(scale = 1, opts = {}) { return toCanvas(this, scale, opts); },
|
|
111
|
+
/** PNG bytes (indexed when ≤ 256 colours unless `indexed: false`). */
|
|
112
|
+
toPNG(opts = {}) { return encodePNG(this, opts); },
|
|
113
|
+
async toBlob(opts = {}) { return new Blob([await encodePNG(this, opts)], { type: 'image/png' }); },
|
|
114
|
+
};
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let shared = null;
|
|
119
|
+
/** One-shot helper with a shared engine. `pixelate(source, settings)`. */
|
|
120
|
+
export async function pixelate(source, settings) {
|
|
121
|
+
// classic API: pixelate({ image, width, dither, strength, palette, resolution }) → canvas
|
|
122
|
+
if (settings === undefined && source && typeof source === 'object' && 'image' in source && 'width' in source) {
|
|
123
|
+
return pixelateClassic(source);
|
|
124
|
+
}
|
|
125
|
+
shared ||= createPixelator();
|
|
126
|
+
return (await shared).process(source, settings);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Compatibility with image-to-pixel.js 2.x. Returns a canvas like the old function did. */
|
|
130
|
+
async function pixelateClassic(opts) {
|
|
131
|
+
const { image, width, dither = 'none', strength = 0, palette = null, resolution = 'original' } = opts;
|
|
132
|
+
let pal = { source: 'none' };
|
|
133
|
+
if (Array.isArray(palette)) pal = { source: 'custom', colors: palette };
|
|
134
|
+
else if (typeof palette === 'string') {
|
|
135
|
+
const { fetchLospec } = await import('./palette/lospec.js');
|
|
136
|
+
const p = await fetchLospec(palette);
|
|
137
|
+
pal = { source: 'custom', colors: p.colors, name: p.name };
|
|
138
|
+
}
|
|
139
|
+
const settings = normalizeSettings({
|
|
140
|
+
size: { width },
|
|
141
|
+
palette: pal,
|
|
142
|
+
dither: { method: LEGACY_DITHER[String(dither).toLowerCase()] || 'none', strength: strength / 100 },
|
|
143
|
+
alpha: { threshold: 0 },
|
|
144
|
+
});
|
|
145
|
+
shared ||= createPixelator();
|
|
146
|
+
const engine = await shared;
|
|
147
|
+
const res = await engine.process(image, settings);
|
|
148
|
+
if (resolution === 'original') {
|
|
149
|
+
const frame = await toFrame(image);
|
|
150
|
+
const c = document.createElement('canvas');
|
|
151
|
+
c.width = frame.width; c.height = frame.height;
|
|
152
|
+
const ctx = c.getContext('2d');
|
|
153
|
+
ctx.imageSmoothingEnabled = false;
|
|
154
|
+
ctx.drawImage(res.toCanvas(1), 0, 0, c.width, c.height);
|
|
155
|
+
return c;
|
|
156
|
+
}
|
|
157
|
+
return res.toCanvas(1);
|
|
158
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// DOM helpers: canvases and downloads.
|
|
2
|
-
|
|
3
|
-
import { upscale } from './scale.js';
|
|
4
|
-
|
|
5
|
-
/** RGBA image → <canvas>, upscaled by an integer factor with optional grid lines. */
|
|
6
|
-
export function toCanvas(img, scale = 1, opts = {}) {
|
|
7
|
-
const up = scale > 1 || opts.grid ? upscale(img, scale, opts) : img;
|
|
8
|
-
const c = opts.canvas || document.createElement('canvas');
|
|
9
|
-
c.width = up.width; c.height = up.height;
|
|
10
|
-
c.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(up.data), up.width, up.height), 0, 0);
|
|
11
|
-
return c;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Trigger a browser download for a Blob or bytes. */
|
|
15
|
-
export function downloadBlob(data, filename, type = 'application/octet-stream') {
|
|
16
|
-
const blob = data instanceof Blob ? data : new Blob([data], { type });
|
|
17
|
-
const url = URL.createObjectURL(blob);
|
|
18
|
-
const a = document.createElement('a');
|
|
19
|
-
a.href = url; a.download = filename; a.rel = 'noopener';
|
|
20
|
-
document.body.appendChild(a); a.click(); a.remove();
|
|
21
|
-
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
22
|
-
}
|
|
1
|
+
// DOM helpers: canvases and downloads.
|
|
2
|
+
|
|
3
|
+
import { upscale } from './scale.js';
|
|
4
|
+
|
|
5
|
+
/** RGBA image → <canvas>, upscaled by an integer factor with optional grid lines. */
|
|
6
|
+
export function toCanvas(img, scale = 1, opts = {}) {
|
|
7
|
+
const up = scale > 1 || opts.grid ? upscale(img, scale, opts) : img;
|
|
8
|
+
const c = opts.canvas || document.createElement('canvas');
|
|
9
|
+
c.width = up.width; c.height = up.height;
|
|
10
|
+
c.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(up.data), up.width, up.height), 0, 0);
|
|
11
|
+
return c;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Trigger a browser download for a Blob or bytes. */
|
|
15
|
+
export function downloadBlob(data, filename, type = 'application/octet-stream') {
|
|
16
|
+
const blob = data instanceof Blob ? data : new Blob([data], { type });
|
|
17
|
+
const url = URL.createObjectURL(blob);
|
|
18
|
+
const a = document.createElement('a');
|
|
19
|
+
a.href = url; a.download = filename; a.rel = 'noopener';
|
|
20
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
21
|
+
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
22
|
+
}
|