@xprite/pixelate 0.0.0-stage → 0.1.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 (110) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +270 -2
  4. package/README.zh-CN.md +240 -0
  5. package/dist/analyze.d.ts +8 -0
  6. package/dist/analyze.js +217 -0
  7. package/dist/base.d.ts +18 -0
  8. package/dist/base.js +42 -0
  9. package/dist/cell-color-picker.d.ts +14 -0
  10. package/dist/cell-color-picker.js +278 -0
  11. package/dist/color-lab.d.ts +3 -0
  12. package/dist/color-lab.js +31 -0
  13. package/dist/color-step.d.ts +33 -0
  14. package/dist/color-step.js +62 -0
  15. package/dist/contours.d.ts +4 -0
  16. package/dist/contours.js +66 -0
  17. package/dist/denoise.d.ts +3 -0
  18. package/dist/denoise.js +37 -0
  19. package/dist/deskew.d.ts +24 -0
  20. package/dist/deskew.js +202 -0
  21. package/dist/grid-ac-rl-consensus.d.ts +127 -0
  22. package/dist/grid-ac-rl-consensus.js +497 -0
  23. package/dist/grid-arbitrate.d.ts +52 -0
  24. package/dist/grid-arbitrate.js +366 -0
  25. package/dist/grid-cut-snap.d.ts +15 -0
  26. package/dist/grid-cut-snap.js +77 -0
  27. package/dist/grid-description-length.d.ts +47 -0
  28. package/dist/grid-description-length.js +697 -0
  29. package/dist/grid-detect.d.ts +8 -0
  30. package/dist/grid-detect.js +665 -0
  31. package/dist/grid-edge-coherence.d.ts +4 -0
  32. package/dist/grid-edge-coherence.js +158 -0
  33. package/dist/grid-edge-path.d.ts +9 -0
  34. package/dist/grid-edge-path.js +201 -0
  35. package/dist/grid-frame-lattice.d.ts +25 -0
  36. package/dist/grid-frame-lattice.js +420 -0
  37. package/dist/grid-frequency-spectrum.d.ts +11 -0
  38. package/dist/grid-frequency-spectrum.js +89 -0
  39. package/dist/grid-period-boundary.d.ts +13 -0
  40. package/dist/grid-period-boundary.js +255 -0
  41. package/dist/grid-period-comb.d.ts +4 -0
  42. package/dist/grid-period-comb.js +230 -0
  43. package/dist/grid-period-distance.d.ts +7 -0
  44. package/dist/grid-period-distance.js +233 -0
  45. package/dist/grid-phase-field.d.ts +11 -0
  46. package/dist/grid-phase-field.js +446 -0
  47. package/dist/grid-pitch-lattice.d.ts +4 -0
  48. package/dist/grid-pitch-lattice.js +255 -0
  49. package/dist/grid-pitch-search.d.ts +11 -0
  50. package/dist/grid-pitch-search.js +233 -0
  51. package/dist/grid-sample.d.ts +4 -0
  52. package/dist/grid-sample.js +164 -0
  53. package/dist/grid-sampling.d.ts +13 -0
  54. package/dist/grid-sampling.js +132 -0
  55. package/dist/grid-strip-shift.d.ts +44 -0
  56. package/dist/grid-strip-shift.js +223 -0
  57. package/dist/grid-test-images.d.ts +69 -0
  58. package/dist/grid-test-images.js +547 -0
  59. package/dist/grid-warp.d.ts +38 -0
  60. package/dist/grid-warp.js +252 -0
  61. package/dist/grid.d.ts +8 -0
  62. package/dist/grid.js +43 -0
  63. package/dist/index.d.ts +16 -0
  64. package/dist/index.js +14 -0
  65. package/dist/line-guided/cv-lab.d.ts +4 -0
  66. package/dist/line-guided/cv-lab.js +134 -0
  67. package/dist/line-guided/cv-ops.d.ts +29 -0
  68. package/dist/line-guided/cv-ops.js +759 -0
  69. package/dist/line-guided/pixeloe-entry.d.ts +15 -0
  70. package/dist/line-guided/pixeloe-entry.js +122 -0
  71. package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
  72. package/dist/line-guided/pixeloe-legacy.js +301 -0
  73. package/dist/line-guided/xdog.d.ts +7 -0
  74. package/dist/line-guided/xdog.js +84 -0
  75. package/dist/most-frequent.d.ts +3 -0
  76. package/dist/most-frequent.js +56 -0
  77. package/dist/outline-prepass.d.ts +37 -0
  78. package/dist/outline-prepass.js +56 -0
  79. package/dist/palette-snap.d.ts +4 -0
  80. package/dist/palette-snap.js +371 -0
  81. package/dist/palette.d.ts +21 -0
  82. package/dist/palette.js +280 -0
  83. package/dist/pixelate.d.ts +15 -0
  84. package/dist/pixelate.js +214 -0
  85. package/dist/pixelation-algorithm-registry.d.ts +34 -0
  86. package/dist/pixelation-algorithm-registry.js +716 -0
  87. package/dist/pixeloe-prepass.d.ts +12 -0
  88. package/dist/pixeloe-prepass.js +527 -0
  89. package/dist/quantization.d.ts +6 -0
  90. package/dist/quantization.js +145 -0
  91. package/dist/remove-background.d.ts +40 -0
  92. package/dist/remove-background.js +229 -0
  93. package/dist/resize.d.ts +3 -0
  94. package/dist/resize.js +86 -0
  95. package/dist/sampling.d.ts +7 -0
  96. package/dist/sampling.js +301 -0
  97. package/dist/settings.d.ts +36 -0
  98. package/dist/settings.js +181 -0
  99. package/dist/types.d.ts +255 -0
  100. package/dist/types.js +87 -0
  101. package/licenses/PixelOE-Apache-2.0.txt +201 -0
  102. package/licenses/pixel-art-fixer-MIT.txt +21 -0
  103. package/licenses/proper-pixel-art-MIT.txt +21 -0
  104. package/licenses/tensorhead-dithering-MIT.txt +21 -0
  105. package/package.json +62 -4
  106. package/wasm/ac-rl-detect-browser-worker.js +45 -0
  107. package/wasm/ac-rl-detect-worker.mjs +51 -0
  108. package/wasm/pixelfixer_detect.d.ts +65 -0
  109. package/wasm/pixelfixer_detect.js +334 -0
  110. package/wasm/pixelfixer_detect_bg.wasm +0 -0
@@ -0,0 +1,547 @@
1
+ /** Deterministic synthetic images shared by the grid detection tests. */
2
+ function rng(seed) {
3
+ let state = seed >>> 0 || 1;
4
+ return () => {
5
+ state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
6
+ return state / 4294967296;
7
+ };
8
+ }
9
+ function blank(width, height) {
10
+ return { width, height, data: new Uint8ClampedArray(width * height * 4) };
11
+ }
12
+ const PALETTE = [
13
+ [86, 40, 90],
14
+ [232, 232, 240],
15
+ [30, 30, 40],
16
+ [200, 60, 50],
17
+ [240, 190, 70],
18
+ [70, 120, 200],
19
+ [60, 170, 90],
20
+ [140, 100, 70],
21
+ [250, 140, 160],
22
+ [110, 110, 130],
23
+ [20, 200, 220],
24
+ [180, 180, 60],
25
+ ];
26
+ /** A flat-coloured blocky sprite: background, then overlapping rectangles and ellipses. */
27
+ export function sprite(size, seed = 7) {
28
+ const random = rng(seed);
29
+ const image = blank(size, size);
30
+ const paint = (x, y, color) => {
31
+ const o = (y * size + x) * 4;
32
+ image.data[o] = color[0];
33
+ image.data[o + 1] = color[1];
34
+ image.data[o + 2] = color[2];
35
+ image.data[o + 3] = 255;
36
+ };
37
+ for (let i = 0; i < size * size; i++)
38
+ paint(i % size, Math.floor(i / size), PALETTE[0]);
39
+ for (let shape = 0; shape < 40; shape++) {
40
+ const color = PALETTE[1 + Math.floor(random() * (PALETTE.length - 1))];
41
+ const w = 3 + Math.floor(random() * size * 0.3), h = 3 + Math.floor(random() * size * 0.3);
42
+ const x0 = Math.floor(random() * (size - w)), y0 = Math.floor(random() * (size - h));
43
+ const round = random() < 0.5;
44
+ for (let y = y0; y < y0 + h; y++)
45
+ for (let x = x0; x < x0 + w; x++) {
46
+ if (round) {
47
+ const dx = (x - x0 + 0.5) / w - 0.5, dy = (y - y0 + 0.5) / h - 0.5;
48
+ if (dx * dx + dy * dy > 0.25)
49
+ continue;
50
+ }
51
+ paint(x, y, color);
52
+ }
53
+ }
54
+ return image;
55
+ }
56
+ /**
57
+ * Detailed pixel art: a dithered two-tone sky, outlined blobs with a lit and a shaded tone,
58
+ * single-pixel highlights and scattered speckles, so many neighbouring cells share a colour while
59
+ * others change every pixel.
60
+ */
61
+ export function detailedSprite(width, height, seed = 7) {
62
+ const random = rng(seed);
63
+ const image = blank(width, height);
64
+ const paint = (x, y, color) => {
65
+ if (x < 0 || y < 0 || x >= width || y >= height)
66
+ return;
67
+ const o = (y * width + x) * 4;
68
+ image.data[o] = color[0];
69
+ image.data[o + 1] = color[1];
70
+ image.data[o + 2] = color[2];
71
+ image.data[o + 3] = 255;
72
+ };
73
+ const tone = (color, factor) => color.map((v) => v * factor);
74
+ const sky = PALETTE[1 + Math.floor(random() * (PALETTE.length - 1))];
75
+ const band = height * (0.3 + random() * 0.4);
76
+ for (let y = 0; y < height; y++)
77
+ for (let x = 0; x < width; x++) {
78
+ const lower = y > band || (y > band - 3 && (x + y) % 2 === 0);
79
+ paint(x, y, lower ? tone(sky, 0.7) : sky);
80
+ }
81
+ const outline = PALETTE[2];
82
+ const blobs = 6 + Math.floor(random() * 6);
83
+ for (let shape = 0; shape < blobs; shape++) {
84
+ const color = PALETTE[3 + Math.floor(random() * (PALETTE.length - 3))];
85
+ const rx = 3 + random() * width * 0.18, ry = 3 + random() * height * 0.18;
86
+ const cx = random() * width, cy = random() * height;
87
+ for (let y = Math.floor(cy - ry - 1); y <= cy + ry + 1; y++)
88
+ for (let x = Math.floor(cx - rx - 1); x <= cx + rx + 1; x++) {
89
+ const d = Math.hypot((x + 0.5 - cx) / rx, (y + 0.5 - cy) / ry);
90
+ if (d > 1)
91
+ continue;
92
+ const edge = d > 1 - 1.2 / Math.min(rx, ry);
93
+ const lit = (x - cx) / rx + (y - cy) / ry < -0.3;
94
+ paint(x, y, edge ? outline : lit ? color : tone(color, 0.65));
95
+ }
96
+ }
97
+ for (let i = 0; i < width * height * 0.02; i++)
98
+ paint(Math.floor(random() * width), Math.floor(random() * height), PALETTE[1]);
99
+ return image;
100
+ }
101
+ /**
102
+ * Nearest resample onto `cuts` (output positions of every native cell boundary, first 0, last the
103
+ * output length): output pixel centre x + 0.5 takes the native cell whose span contains it.
104
+ */
105
+ function resampleOnCuts(source, cutsX, cutsY) {
106
+ const out = blank(Math.round(cutsX[cutsX.length - 1]), Math.round(cutsY[cutsY.length - 1]));
107
+ const index = (cuts, length) => {
108
+ const map = new Int32Array(length);
109
+ let cell = 0;
110
+ for (let x = 0; x < length; x++) {
111
+ while (cell < cuts.length - 2 && x + 0.5 >= cuts[cell + 1])
112
+ cell++;
113
+ map[x] = cell;
114
+ }
115
+ return map;
116
+ };
117
+ const columns = index(cutsX, out.width), rows = index(cutsY, out.height);
118
+ for (let y = 0; y < out.height; y++)
119
+ for (let x = 0; x < out.width; x++)
120
+ for (let c = 0; c < 4; c++)
121
+ out.data[(y * out.width + x) * 4 + c] =
122
+ source.data[(rows[y] * source.width + columns[x]) * 4 + c];
123
+ return out;
124
+ }
125
+ const evenCuts = (cells, length) => Array.from({ length: cells + 1 }, (_, i) => (i * length) / cells);
126
+ /** Non-integer square nearest upscale: cells alternate between two integer widths. */
127
+ export function fractionalUpscale(source, scale) {
128
+ return resampleOnCuts(source, evenCuts(source.width, Math.round(source.width * scale)), evenCuts(source.height, Math.round(source.height * scale)));
129
+ }
130
+ /** Cuts whose cell size swells and shrinks smoothly (two sine waves) by up to `drift` of a cell. */
131
+ function driftCuts(cells, length, drift, random) {
132
+ const p0 = random() * 2 * Math.PI, p1 = random() * 2 * Math.PI;
133
+ const sizes = Array.from({ length: cells }, (_, i) => {
134
+ const t = i / Math.max(cells - 1, 1);
135
+ return (1 +
136
+ drift *
137
+ (0.7 * Math.sin(2 * Math.PI * 1.3 * t + p0) + 0.3 * Math.sin(2 * Math.PI * 3.1 * t + p1)));
138
+ });
139
+ const total = sizes.reduce((a, b) => a + b, 0);
140
+ const cuts = [0];
141
+ for (const size of sizes)
142
+ cuts.push(cuts[cuts.length - 1] + (size * length) / total);
143
+ return cuts;
144
+ }
145
+ /** Nearest upscale whose cell size drifts smoothly across the image (a non-uniform grid). */
146
+ export function driftUpscale(source, scale, drift, seed = 17) {
147
+ const random = rng(seed);
148
+ return resampleOnCuts(source, driftCuts(source.width, Math.round(source.width * scale), drift, random), driftCuts(source.height, Math.round(source.height * scale), drift, random));
149
+ }
150
+ /** Nearest upscale whose every inner cut moves by up to `jitter` cells, as pixel-bench's jitter. */
151
+ export function jitterUpscale(source, scale, jitter, seed = 23) {
152
+ const random = rng(seed);
153
+ const cuts = (cells) => {
154
+ const length = Math.round(cells * scale);
155
+ const base = length / cells;
156
+ const out = Array.from({ length: cells + 1 }, (_, i) => i === 0 || i === cells ? i * base : i * base + (random() * 2 - 1) * jitter * base);
157
+ for (let i = 1; i < out.length; i++)
158
+ out[i] = Math.max(out[i], out[i - 1]);
159
+ return out;
160
+ };
161
+ return resampleOnCuts(source, cuts(source.width), cuts(source.height));
162
+ }
163
+ export function checkerboard(cell, cells) {
164
+ const size = cell * cells;
165
+ const image = blank(size, size);
166
+ for (let y = 0; y < size; y++)
167
+ for (let x = 0; x < size; x++) {
168
+ const o = (y * size + x) * 4;
169
+ const v = (Math.floor(x / cell) + Math.floor(y / cell)) % 2 ? 255 : 0;
170
+ image.data[o] = image.data[o + 1] = image.data[o + 2] = v;
171
+ image.data[o + 3] = 255;
172
+ }
173
+ return image;
174
+ }
175
+ /** Nearest-neighbour upscale by a possibly fractional factor. */
176
+ export function upscaleNearest(source, fx, fy = fx) {
177
+ const out = blank(Math.round(source.width * fx), Math.round(source.height * fy));
178
+ for (let y = 0; y < out.height; y++)
179
+ for (let x = 0; x < out.width; x++) {
180
+ const sx = Math.min(source.width - 1, Math.floor(x / fx)), sy = Math.min(source.height - 1, Math.floor(y / fy));
181
+ for (let c = 0; c < 4; c++)
182
+ out.data[(y * out.width + x) * 4 + c] = source.data[(sy * source.width + sx) * 4 + c];
183
+ }
184
+ return out;
185
+ }
186
+ /** Writes into out[o..o+3] the bilinear sample of `source` at source coordinates (sx, sy). */
187
+ function sampleBilinear(source, sx, sy, out, o) {
188
+ const x0 = Math.floor(sx), y0 = Math.floor(sy);
189
+ const x1 = Math.min(source.width - 1, x0 + 1), y1 = Math.min(source.height - 1, y0 + 1);
190
+ const fx = sx - x0, fy = sy - y0;
191
+ for (let c = 0; c < 4; c++) {
192
+ const g = (xx, yy) => source.data[(yy * source.width + xx) * 4 + c];
193
+ out[o + c] =
194
+ (g(x0, y0) * (1 - fx) + g(x1, y0) * fx) * (1 - fy) +
195
+ (g(x0, y1) * (1 - fx) + g(x1, y1) * fx) * fy;
196
+ }
197
+ }
198
+ export function upscaleBilinear(source, scale) {
199
+ const out = blank(source.width * scale, source.height * scale);
200
+ for (let y = 0; y < out.height; y++)
201
+ for (let x = 0; x < out.width; x++)
202
+ sampleBilinear(source, x / scale, y / scale, out.data, (y * out.width + x) * 4);
203
+ return out;
204
+ }
205
+ /** Bilinear resample with pixel-centre alignment (as image libraries do), by a possibly fractional factor. */
206
+ export function resampleBilinear(source, scale) {
207
+ const out = blank(Math.round(source.width * scale), Math.round(source.height * scale));
208
+ const fx = source.width / out.width, fy = source.height / out.height;
209
+ const clamp = (value, max) => Math.max(0, Math.min(max, value));
210
+ for (let y = 0; y < out.height; y++)
211
+ for (let x = 0; x < out.width; x++)
212
+ sampleBilinear(source, clamp((x + 0.5) * fx - 0.5, source.width - 1), clamp((y + 0.5) * fy - 0.5, source.height - 1), out.data, (y * out.width + x) * 4);
213
+ return out;
214
+ }
215
+ function bilinearPlanes(source, scale) {
216
+ const width = Math.round(source.width * scale), height = Math.round(source.height * scale);
217
+ const data = new Float32Array(width * height * 4);
218
+ const fx = source.width / width, fy = source.height / height;
219
+ for (let y = 0; y < height; y++) {
220
+ const sy = Math.max(0, Math.min(source.height - 1, (y + 0.5) * fy - 0.5));
221
+ const y0 = Math.floor(sy), y1 = Math.min(source.height - 1, y0 + 1), ty = sy - y0;
222
+ for (let x = 0; x < width; x++) {
223
+ const sx = Math.max(0, Math.min(source.width - 1, (x + 0.5) * fx - 0.5));
224
+ const x0 = Math.floor(sx), x1 = Math.min(source.width - 1, x0 + 1), tx = sx - x0;
225
+ for (let c = 0; c < 4; c++) {
226
+ const g = (xx, yy) => source.data[(yy * source.width + xx) * 4 + c];
227
+ data[(y * width + x) * 4 + c] =
228
+ (g(x0, y0) * (1 - tx) + g(x1, y0) * tx) * (1 - ty) +
229
+ (g(x0, y1) * (1 - tx) + g(x1, y1) * tx) * ty;
230
+ }
231
+ }
232
+ }
233
+ return { width, height, data };
234
+ }
235
+ /** Separable Gaussian blur (radius 4 sigma) with replicated borders. */
236
+ function gaussPlanes(planes, sigma, channels = 4) {
237
+ const { width, height } = planes;
238
+ const radius = Math.max(1, Math.ceil(4 * sigma));
239
+ const kernel = Array.from({ length: 2 * radius + 1 }, (_, i) => Math.exp(-((i - radius) ** 2) / (2 * sigma * sigma)));
240
+ const norm = kernel.reduce((a, b) => a + b, 0);
241
+ const pass = (input, horizontal) => {
242
+ const out = new Float32Array(input.length);
243
+ for (let y = 0; y < height; y++)
244
+ for (let x = 0; x < width; x++)
245
+ for (let c = 0; c < channels; c++) {
246
+ let sum = 0;
247
+ for (let d = -radius; d <= radius; d++) {
248
+ const xx = horizontal ? Math.max(0, Math.min(width - 1, x + d)) : x, yy = horizontal ? y : Math.max(0, Math.min(height - 1, y + d));
249
+ sum += kernel[d + radius] * input[(yy * width + xx) * channels + c];
250
+ }
251
+ out[(y * width + x) * channels + c] = sum / norm;
252
+ }
253
+ return out;
254
+ };
255
+ return { width, height, data: pass(pass(planes.data, true), false) };
256
+ }
257
+ /** Bilinear sample of every pixel at (x + dx, y + dy), with replicated borders. */
258
+ function displacePlanes(planes, dx, dy) {
259
+ const { width, height, data } = planes;
260
+ const out = new Float32Array(data.length);
261
+ for (let y = 0; y < height; y++)
262
+ for (let x = 0; x < width; x++) {
263
+ const i = y * width + x;
264
+ const sx = Math.max(0, Math.min(width - 1, x + dx[i])), sy = Math.max(0, Math.min(height - 1, y + dy[i]));
265
+ const x0 = Math.floor(sx), y0 = Math.floor(sy), x1 = Math.min(width - 1, x0 + 1), y1 = Math.min(height - 1, y0 + 1), tx = sx - x0, ty = sy - y0;
266
+ for (let c = 0; c < 4; c++) {
267
+ const g = (xx, yy) => data[(yy * width + xx) * 4 + c];
268
+ out[i * 4 + c] =
269
+ (g(x0, y0) * (1 - tx) + g(x1, y0) * tx) * (1 - ty) +
270
+ (g(x0, y1) * (1 - tx) + g(x1, y1) * tx) * ty;
271
+ }
272
+ }
273
+ return { width, height, data: out };
274
+ }
275
+ /** Smooth random field over width x height with peak magnitude 1: blurred noise at half size. */
276
+ function smoothField(width, height, sigma, random) {
277
+ const hw = Math.max(2, width >> 1), hh = Math.max(2, height >> 1);
278
+ const noise = new Float32Array(hw * hh);
279
+ for (let i = 0; i < noise.length; i++)
280
+ noise[i] = Math.sqrt(-2 * Math.log(1 - random())) * Math.cos(2 * Math.PI * random());
281
+ const small = gaussPlanes({ width: hw, height: hh, data: noise }, sigma, 1).data;
282
+ const field = new Float32Array(width * height);
283
+ let peak = 0;
284
+ for (let y = 0; y < height; y++) {
285
+ const sy = Math.max(0, Math.min(hh - 1, ((y + 0.5) * hh) / height - 0.5));
286
+ const y0 = Math.floor(sy), y1 = Math.min(hh - 1, y0 + 1), ty = sy - y0;
287
+ for (let x = 0; x < width; x++) {
288
+ const sx = Math.max(0, Math.min(hw - 1, ((x + 0.5) * hw) / width - 0.5));
289
+ const x0 = Math.floor(sx), x1 = Math.min(hw - 1, x0 + 1), tx = sx - x0;
290
+ const v = (small[y0 * hw + x0] * (1 - tx) + small[y0 * hw + x1] * tx) * (1 - ty) +
291
+ (small[y1 * hw + x0] * (1 - tx) + small[y1 * hw + x1] * tx) * ty;
292
+ field[y * width + x] = v;
293
+ peak = Math.max(peak, Math.abs(v));
294
+ }
295
+ }
296
+ for (let i = 0; i < field.length; i++)
297
+ field[i] /= peak || 1;
298
+ return field;
299
+ }
300
+ function quantise(planes) {
301
+ return { width: planes.width, height: planes.height, data: Uint8ClampedArray.from(planes.data) };
302
+ }
303
+ /** pixel-bench's soft_bilinear: a centred bilinear upscale, then a Gaussian blur. */
304
+ export function softBilinear(source, scale, blur = 0.5) {
305
+ return quantise(gaussPlanes(bilinearPlanes(source, scale), blur));
306
+ }
307
+ /**
308
+ * pixel-bench's mush: a centred bilinear upscale displaced by a smooth random field of up to
309
+ * `amplitude` pixels (correlated over about a cell), a soft-focus blend and a Gaussian blur.
310
+ */
311
+ export function mushWarp(source, scale, seed = 19, amplitude = 1.6, blur = 0.7) {
312
+ const random = rng(seed);
313
+ const base = bilinearPlanes(source, scale);
314
+ const sigma = Math.max(scale * 0.6, 1);
315
+ const dx = smoothField(base.width, base.height, sigma, random), dy = smoothField(base.width, base.height, sigma, random);
316
+ for (let i = 0; i < dx.length; i++) {
317
+ dx[i] *= amplitude;
318
+ dy[i] *= amplitude;
319
+ }
320
+ const moved = displacePlanes(base, dx, dy);
321
+ const focus = gaussPlanes(moved, 0.6);
322
+ for (let i = 0; i < moved.data.length; i++)
323
+ moved.data[i] = 0.6 * moved.data[i] + 0.4 * focus.data[i];
324
+ return quantise(gaussPlanes(moved, blur));
325
+ }
326
+ /**
327
+ * pixel-bench's warp on a nearest upscale: a smooth shear that moves every row along x and every
328
+ * column along y by two sine waves of up to `amplitude` pixels.
329
+ */
330
+ export function warpUpscale(source, scale, amplitude, seed = 29) {
331
+ const random = rng(seed);
332
+ const up = upscaleNearest(source, scale);
333
+ const { width, height } = up;
334
+ const phases = Array.from({ length: 4 }, () => random() * 2 * Math.PI);
335
+ const wave = (t, a, b) => amplitude * (Math.sin(2 * Math.PI * 2.5 * t + a) + 0.5 * Math.sin(2 * Math.PI * 5.75 * t + b));
336
+ const out = blank(width, height);
337
+ for (let y = 0; y < height; y++) {
338
+ const dx = wave(y / height, phases[0], phases[1]);
339
+ for (let x = 0; x < width; x++) {
340
+ const dy = wave(x / width, phases[2], phases[3]);
341
+ const sx = Math.max(0, Math.min(width - 1, Math.round(x + dx))), sy = Math.max(0, Math.min(height - 1, Math.round(y + dy)));
342
+ out.data.set(up.data.subarray((sy * width + sx) * 4, (sy * width + sx + 1) * 4), (y * width + x) * 4);
343
+ }
344
+ }
345
+ return out;
346
+ }
347
+ /**
348
+ * pixel-bench's row_jitter: a nearest upscale whose every row of cells moves along x, and every
349
+ * column of cells along y, by up to `amplitude` pixels.
350
+ */
351
+ export function rowJitterUpscale(source, scale, amplitude, seed = 31) {
352
+ const random = rng(seed);
353
+ const rowShifts = Array.from({ length: source.height }, () => (random() * 2 - 1) * amplitude);
354
+ const columnShifts = Array.from({ length: source.width }, () => (random() * 2 - 1) * amplitude);
355
+ const out = blank(Math.round(source.width * scale), Math.round(source.height * scale));
356
+ const cell = (position, cells) => Math.max(0, Math.min(cells - 1, Math.floor(position / scale)));
357
+ for (let y = 0; y < out.height; y++)
358
+ for (let x = 0; x < out.width; x++) {
359
+ const sx = cell(x + 0.5 + rowShifts[cell(y + 0.5, source.height)], source.width), sy = cell(y + 0.5 + columnShifts[cell(x + 0.5, source.width)], source.height);
360
+ out.data.set(source.data.subarray((sy * source.width + sx) * 4, (sy * source.width + sx + 1) * 4), (y * out.width + x) * 4);
361
+ }
362
+ return out;
363
+ }
364
+ /**
365
+ * pixel-bench's block_reset: a nearest upscale split into `blocks` by `blocks` parts, each of which
366
+ * samples the source at its own phase, up to half a cell either way on both axes.
367
+ */
368
+ export function blockResetUpscale(source, scale, blocks, seed = 37) {
369
+ const random = rng(seed);
370
+ const phase = () => (random() - 0.5) * scale;
371
+ const shifts = Array.from({ length: blocks * blocks }, () => [phase(), phase()]);
372
+ const out = blank(Math.round(source.width * scale), Math.round(source.height * scale));
373
+ const cell = (position, cells) => Math.max(0, Math.min(cells - 1, Math.floor(position / scale)));
374
+ const block = (position, length) => Math.min(blocks - 1, Math.floor((position * blocks) / length));
375
+ for (let y = 0; y < out.height; y++)
376
+ for (let x = 0; x < out.width; x++) {
377
+ const [dx, dy] = shifts[block(y, out.height) * blocks + block(x, out.width)];
378
+ const sx = cell(x + 0.5 + dx, source.width), sy = cell(y + 0.5 + dy, source.height);
379
+ out.data.set(source.data.subarray((sy * source.width + sx) * 4, (sy * source.width + sx + 1) * 4), (y * out.width + x) * 4);
380
+ }
381
+ return out;
382
+ }
383
+ /**
384
+ * pixel-bench's cell_texture: a nearest upscale under a smooth multiplicative texture (correlated
385
+ * over a fifth of a cell) of standard deviation `amplitude`.
386
+ */
387
+ export function texturedUpscale(source, scale, amplitude, seed = 37) {
388
+ const out = upscaleNearest(source, scale);
389
+ const field = smoothField(out.width, out.height, scale / 5, rng(seed));
390
+ let squares = 0;
391
+ for (const value of field)
392
+ squares += value * value;
393
+ const deviation = Math.sqrt(squares / field.length) || 1;
394
+ for (let i = 0; i < field.length; i++)
395
+ for (let c = 0; c < 3; c++)
396
+ out.data[i * 4 + c] *= 1 + (amplitude * field[i]) / deviation;
397
+ return out;
398
+ }
399
+ /** A soft "AI upscale": centred bilinear, a further blur and mild noise. */
400
+ export function mush(source, scale, seed = 13) {
401
+ return addNoise(boxBlur(resampleBilinear(source, scale), 1), 6, seed);
402
+ }
403
+ /** Separable box blur with the given radius (radius 1 smears each edge over about 3 pixels). */
404
+ export function boxBlur(source, radius) {
405
+ const { width, height } = source;
406
+ const pass = (input, horizontal) => {
407
+ const out = new Uint8ClampedArray(input.length);
408
+ for (let y = 0; y < height; y++)
409
+ for (let x = 0; x < width; x++)
410
+ for (let c = 0; c < 4; c++) {
411
+ let sum = 0, n = 0;
412
+ for (let d = -radius; d <= radius; d++) {
413
+ const xx = horizontal ? x + d : x, yy = horizontal ? y : y + d;
414
+ if (xx < 0 || yy < 0 || xx >= width || yy >= height)
415
+ continue;
416
+ sum += input[(yy * width + xx) * 4 + c];
417
+ n++;
418
+ }
419
+ out[(y * width + x) * 4 + c] = sum / n;
420
+ }
421
+ return out;
422
+ };
423
+ return { width, height, data: pass(pass(source.data, true), false) };
424
+ }
425
+ export function gradient(width, height) {
426
+ const image = blank(width, height);
427
+ for (let y = 0; y < height; y++)
428
+ for (let x = 0; x < width; x++) {
429
+ const o = (y * width + x) * 4;
430
+ image.data[o] = (x / width) * 255;
431
+ image.data[o + 1] = (y / height) * 255;
432
+ image.data[o + 2] = 128;
433
+ image.data[o + 3] = 255;
434
+ }
435
+ return image;
436
+ }
437
+ /** Smooth, photo-like colour field: low-frequency waves plus a few soft blobs, no hard edges. */
438
+ export function smoothPhoto(width, height) {
439
+ const image = blank(width, height);
440
+ for (let y = 0; y < height; y++)
441
+ for (let x = 0; x < width; x++) {
442
+ const u = x / width, v = y / height, o = (y * width + x) * 4;
443
+ const blob = Math.exp(-(((u - 0.4) ** 2 + (v - 0.55) ** 2) / 0.03));
444
+ image.data[o] = 128 + 90 * Math.sin(u * 7 + v * 3) + 30 * Math.sin(u * 23) + 50 * blob;
445
+ image.data[o + 1] = 128 + 90 * Math.sin(v * 5 - u * 2) + 30 * Math.cos(v * 19);
446
+ image.data[o + 2] = 128 + 80 * Math.cos(u * 4 + v * 6) - 40 * blob;
447
+ image.data[o + 3] = 255;
448
+ }
449
+ return image;
450
+ }
451
+ /** Textured "photo": noise at several scales, so it has edges everywhere. */
452
+ export function texturePhoto(width, height, seed = 3) {
453
+ const random = rng(seed);
454
+ const image = blank(width, height);
455
+ const base = smoothPhoto(width, height);
456
+ for (let i = 0; i < width * height; i++)
457
+ for (let c = 0; c < 3; c++)
458
+ image.data[i * 4 + c] = base.data[i * 4 + c] + (random() - 0.5) * 70;
459
+ for (let i = 0; i < width * height; i++)
460
+ image.data[i * 4 + 3] = 255;
461
+ return image;
462
+ }
463
+ export function addNoise(source, amplitude, seed = 11) {
464
+ const random = rng(seed);
465
+ const data = new Uint8ClampedArray(source.data);
466
+ for (let i = 0; i < data.length; i += 4)
467
+ for (let c = 0; c < 3; c++)
468
+ data[i + c] += (random() - 0.5) * 2 * amplitude;
469
+ return { width: source.width, height: source.height, data };
470
+ }
471
+ /** Cells of 15 to 17 pixels with a margin, like a hand-resized sprite. */
472
+ export function jitterWithMargin(source, margin = 3, seed = 5) {
473
+ const random = rng(seed);
474
+ const widths = Array.from({ length: source.width }, () => 15 + Math.floor(random() * 3));
475
+ const heights = Array.from({ length: source.height }, () => 15 + Math.floor(random() * 3));
476
+ const width = widths.reduce((a, b) => a + b, 0) + margin * 2;
477
+ const height = heights.reduce((a, b) => a + b, 0) + margin * 2;
478
+ const out = blank(width, height);
479
+ const edge = (source.data[0] << 0) | 0;
480
+ void edge;
481
+ for (let i = 0; i < width * height; i++) {
482
+ out.data[i * 4] = 255;
483
+ out.data[i * 4 + 1] = 255;
484
+ out.data[i * 4 + 2] = 255;
485
+ out.data[i * 4 + 3] = 255;
486
+ }
487
+ let y = margin;
488
+ for (let sy = 0; sy < source.height; sy++) {
489
+ let x = margin;
490
+ for (let sx = 0; sx < source.width; sx++) {
491
+ for (let yy = y; yy < y + heights[sy]; yy++)
492
+ for (let xx = x; xx < x + widths[sx]; xx++)
493
+ for (let c = 0; c < 4; c++)
494
+ out.data[(yy * width + xx) * 4 + c] = source.data[(sy * source.width + sx) * 4 + c];
495
+ x += widths[sx];
496
+ }
497
+ y += heights[sy];
498
+ }
499
+ return out;
500
+ }
501
+ /** A smooth backdrop with hard-edged, anti-aliased shapes at arbitrary positions (no pixel grid). */
502
+ export function vectorShapes(width, height, seed = 21) {
503
+ const random = rng(seed);
504
+ const image = smoothPhoto(width, height);
505
+ for (let shape = 0; shape < 30; shape++) {
506
+ const cx = random() * width, cy = random() * height, rx = 15 + random() * 80, ry = 15 + random() * 80;
507
+ const color = [random() * 255, random() * 255, random() * 255];
508
+ for (let y = Math.max(0, Math.floor(cy - ry - 1)); y < Math.min(height, cy + ry + 1); y++)
509
+ for (let x = Math.max(0, Math.floor(cx - rx - 1)); x < Math.min(width, cx + rx + 1); x++) {
510
+ const d = Math.hypot((x - cx) / rx, (y - cy) / ry);
511
+ const cover = Math.max(0, Math.min(1, (1 - d) * Math.min(rx, ry)));
512
+ for (let c = 0; c < 3; c++) {
513
+ const o = (y * width + x) * 4 + c;
514
+ image.data[o] = image.data[o] * (1 - cover) + color[c] * cover;
515
+ }
516
+ }
517
+ }
518
+ return image;
519
+ }
520
+ /** Cuts whose every cell is an independent 1 ± `spread` share of the pitch, as generators draw. */
521
+ function wanderCuts(cells, pitch, spread, random) {
522
+ const cuts = [0];
523
+ for (let i = 0; i < cells; i++)
524
+ cuts.push(cuts[i] + pitch * (1 + spread * (2 * random() - 1)));
525
+ return cuts;
526
+ }
527
+ /**
528
+ * A generated sprite: `art` on a clear margin of `margin` cells, drawn with cells of 1 ± `spread`
529
+ * of `pitch`, each on its own, then softened by a box blur of `blur` pixels. No lattice fits it.
530
+ */
531
+ export function generatedSprite(art, pitch, spread, margin = 6, blur = 1, seed = 29) {
532
+ const random = rng(seed);
533
+ const padded = blank(art.width + 2 * margin, art.height + 2 * margin);
534
+ for (let y = 0; y < art.height; y++)
535
+ padded.data.set(art.data.subarray(y * art.width * 4, (y + 1) * art.width * 4), ((y + margin) * padded.width + margin) * 4);
536
+ const drawn = resampleOnCuts(padded, wanderCuts(padded.width, pitch, spread, random), wanderCuts(padded.height, pitch, spread, random));
537
+ return blur ? boxBlur(drawn, blur) : drawn;
538
+ }
539
+ /** A sprite on a clear background: `sprite` with its background colour made transparent. */
540
+ export function clearSprite(size, seed = 7) {
541
+ const image = sprite(size, seed);
542
+ const [r, g, b] = [image.data[0], image.data[1], image.data[2]];
543
+ for (let o = 0; o < image.data.length; o += 4)
544
+ if (image.data[o] === r && image.data[o + 1] === g && image.data[o + 2] === b)
545
+ image.data.fill(0, o, o + 4);
546
+ return image;
547
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A smoothly warped lattice along one axis: u(x) is the (continuous) cell index at raster position
3
+ * x, so cuts sit where u crosses an integer. A uniform grid is the linear case; an upscale whose cell
4
+ * size drifts across the image bends u gently.
5
+ *
6
+ * Every measured edge is a cut, but most cuts show no edge (neighbouring cells of one colour), so
7
+ * the hard part is how many cells each gap between edges spans. A single global pitch gains or loses
8
+ * a cell over long flat runs once the cell size drifts. Instead the gaps are labelled jointly
9
+ * (Viterbi): the state is the local pitch on a log scale, a gap of g pixels spans round(g / pitch)
10
+ * cells, its quantisation misfit is the observation cost, and the pitch may wander from gap to gap
11
+ * like a random walk whose variance grows with the cells between the two gaps' centres. u is then fitted by least
12
+ * squares to the labelled edges, piecewise linear on evenly spaced knots under a second-difference
13
+ * penalty, so the pitch may drift but not jump and flat stretches continue the neighbouring pitch.
14
+ */
15
+ export interface AxisWarp {
16
+ readonly spacing: number;
17
+ /** u at x = m * spacing. */
18
+ readonly values: Float64Array;
19
+ }
20
+ export interface WarpEdge {
21
+ readonly position: number;
22
+ readonly weight: number;
23
+ }
24
+ export declare function warpAt(warp: AxisWarp, x: number): number;
25
+ /** Local pitch (pixels per cell) at x. */
26
+ export declare function warpPitch(warp: AxisWarp, x: number): number;
27
+ /** Position in [from, to] where u reaches `index` (u is increasing there); NaN when it does not. */
28
+ export declare function warpInverse(warp: AxisWarp, index: number, from: number, to: number): number;
29
+ /** Solves the symmetric positive definite pentadiagonal system (band Cholesky). */
30
+ export declare function solveBanded(diag: Float64Array, off1: Float64Array, off2: Float64Array, rhs: Float64Array): Float64Array;
31
+ /**
32
+ * Fits u over [0, length] to `edges` with local pitches near `proposed`, none below `minPitch`
33
+ * (where the edges of narrower cells would merge); null with too few edges. `crisp` edges sit on
34
+ * whole pixels of a nearest-neighbour upscale.
35
+ * u(0) is shifted to the integer nearest zero, so a grid that starts on the image border starts at
36
+ * index 0.
37
+ */
38
+ export declare function fitWarp(edges: readonly WarpEdge[], length: number, proposed: number, minPitch: number, crisp: boolean): AxisWarp | null;