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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -2
  3. package/dist/editor.css +1 -1
  4. package/dist/editor.css.map +2 -2
  5. package/dist/editor.esm.js +60 -60
  6. package/dist/editor.esm.js.map +4 -4
  7. package/dist/fonts/README.md +6 -6
  8. package/dist/fonts/geist-LICENSE.txt +93 -93
  9. package/dist/image-to-pixel.bundle.esm.js +16 -16
  10. package/dist/image-to-pixel.bundle.esm.js.map +3 -3
  11. package/dist/image-to-pixel.cjs +2 -2
  12. package/dist/image-to-pixel.cjs.map +2 -2
  13. package/dist/image-to-pixel.esm.js +2 -2
  14. package/dist/image-to-pixel.esm.js.map +2 -2
  15. package/dist/image-to-pixel.js +2 -2
  16. package/dist/image-to-pixel.js.map +2 -2
  17. package/dist/image-to-pixel.min.js +11 -11
  18. package/dist/image-to-pixel.min.js.map +3 -3
  19. package/dist/video.esm.js +2 -2
  20. package/dist/video.esm.js.map +4 -4
  21. package/lib/core/color.js +69 -69
  22. package/lib/core/cpu/pipeline.js +21 -21
  23. package/lib/core/cpu/stages.js +227 -227
  24. package/lib/core/dither/bluenoise-data.js +3 -3
  25. package/lib/core/dither/kernels.js +27 -27
  26. package/lib/core/dither/matrices.js +95 -95
  27. package/lib/core/engine.js +158 -158
  28. package/lib/core/export/canvas.js +22 -22
  29. package/lib/core/export/crc32.js +15 -15
  30. package/lib/core/export/gif.js +153 -153
  31. package/lib/core/export/png.js +94 -94
  32. package/lib/core/export/scale.js +103 -103
  33. package/lib/core/export/sheet.js +95 -95
  34. package/lib/core/export/zip.js +55 -55
  35. package/lib/core/gpu/backend.js +159 -159
  36. package/lib/core/gpu/shaders.js +258 -258
  37. package/lib/core/palette/builtin.js +36 -36
  38. package/lib/core/palette/extract.js +69 -69
  39. package/lib/core/palette/lospec.js +24 -24
  40. package/lib/core/palette/parse.js +112 -112
  41. package/lib/core/plan.js +58 -58
  42. package/lib/core/presets.js +42 -42
  43. package/lib/core/settings.js +274 -274
  44. package/lib/core/source.js +89 -89
  45. package/lib/core/video/audio.js +159 -0
  46. package/lib/core/video/frames.js +38 -0
  47. package/lib/core/video/mux-mp4.js +213 -24
  48. package/lib/core/video/mux-webm.js +63 -22
  49. package/lib/editor/controls.js +139 -139
  50. package/lib/editor/dom.js +65 -65
  51. package/lib/editor/exporter.js +43 -3
  52. package/lib/editor/icons.js +4 -0
  53. package/lib/editor/index.js +218 -9
  54. package/lib/editor/inspector.js +19 -4
  55. package/lib/editor/library.js +8 -2
  56. package/lib/editor/persist.js +18 -18
  57. package/lib/editor/samples.js +90 -90
  58. package/lib/editor/stage.js +239 -239
  59. package/lib/editor/store.js +80 -80
  60. package/lib/editor/styles/editor.css +34 -1
  61. package/lib/editor/styles/fonts/README.md +6 -6
  62. package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
  63. package/lib/editor/styles/tokens.css +89 -89
  64. package/lib/editor/ui.js +97 -97
  65. package/lib/editor/workspace.js +62 -0
  66. package/lib/index.js +31 -29
  67. package/lib/video.js +37 -7
  68. package/package.json +1 -1
  69. package/types/editor.d.ts +34 -34
  70. package/types/index.d.ts +196 -196
  71. package/types/video.d.ts +61 -5
@@ -1,103 +1,103 @@
1
- // Pure pixel helpers (no DOM): integer upscaling, grid lines, colour counting.
2
-
3
- import { parseColor, toHex } from '../color.js';
4
-
5
- /**
6
- * Nearest-neighbour integer upscale.
7
- * @param {{ width: number, height: number, data: ArrayLike<number> }} img
8
- * @param {number} k scale factor (integer ≥ 1)
9
- * @param {{ grid?: boolean, gridColor?: string, gridAlpha?: number }} [opts] grid: 1px lines between source pixels
10
- */
11
- export function upscale(img, k = 1, opts = {}) {
12
- k = Math.max(1, Math.round(k));
13
- const w = img.width * k, h = img.height * k;
14
- const out = new Uint8ClampedArray(w * h * 4);
15
- const src = img.data, sw = img.width;
16
- for (let y = 0; y < h; y++) {
17
- const sy = (y / k) | 0;
18
- for (let x = 0; x < w; x++) {
19
- const so = (sy * sw + ((x / k) | 0)) * 4, o = (y * w + x) * 4;
20
- out[o] = src[so]; out[o + 1] = src[so + 1]; out[o + 2] = src[so + 2]; out[o + 3] = src[so + 3];
21
- }
22
- }
23
- if (opts.grid && k >= 3) {
24
- const [r, g, b] = parseColor(opts.gridColor || '#000000');
25
- const a = opts.gridAlpha ?? 0.35;
26
- for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
27
- if (x % k && y % k) continue;
28
- const o = (y * w + x) * 4;
29
- const sa = out[o + 3] / 255, oa = a + sa * (1 - a);
30
- out[o] = (r * a + out[o] * sa * (1 - a)) / oa;
31
- out[o + 1] = (g * a + out[o + 1] * sa * (1 - a)) / oa;
32
- out[o + 2] = (b * a + out[o + 2] * sa * (1 - a)) / oa;
33
- out[o + 3] = oa * 255;
34
- }
35
- }
36
- return { width: w, height: h, data: out };
37
- }
38
-
39
- /**
40
- * Nearest-neighbour resize to any size (used to export at the source's resolution, like 2.x's
41
- * "full resolution" download). Pixels may differ by one in size when the ratio isn't whole.
42
- */
43
- export function resizeNearest(img, width, height) {
44
- width = Math.max(1, Math.round(width)); height = Math.max(1, Math.round(height));
45
- const out = new Uint8ClampedArray(width * height * 4);
46
- const xs = new Int32Array(width);
47
- for (let x = 0; x < width; x++) xs[x] = Math.min(img.width - 1, Math.floor((x + 0.5) * img.width / width));
48
- for (let y = 0; y < height; y++) {
49
- const sy = Math.min(img.height - 1, Math.floor((y + 0.5) * img.height / height));
50
- for (let x = 0; x < width; x++) {
51
- const so = (sy * img.width + xs[x]) * 4, o = (y * width + x) * 4;
52
- out[o] = img.data[so]; out[o + 1] = img.data[so + 1]; out[o + 2] = img.data[so + 2]; out[o + 3] = img.data[so + 3];
53
- }
54
- }
55
- return { width, height, data: out };
56
- }
57
-
58
- /** Distinct opaque colours in an image ('#rrggbb'), in order of first appearance. */
59
- export function uniqueColors(img, limit = Infinity) {
60
- const seen = new Set();
61
- const d = img.data;
62
- for (let o = 0; o < d.length; o += 4) {
63
- if (d[o + 3] === 0) continue;
64
- seen.add((d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
65
- if (seen.size > limit) break;
66
- }
67
- return [...seen].map(v => toHex([v >> 16, (v >> 8) & 255, v & 255]));
68
- }
69
-
70
- /** True when the image has at least one non-opaque pixel. */
71
- export function hasTransparency(img) {
72
- for (let o = 3; o < img.data.length; o += 4) if (img.data[o] < 255) return true;
73
- return false;
74
- }
75
-
76
- /**
77
- * Detect the pixel-grid size of an upscaled pixel-art image (e.g. a sprite saved at 4×).
78
- * Returns the largest block size b such that the image is made of b×b solid blocks
79
- * (allowing a small tolerance for JPEG noise), or 1.
80
- */
81
- export function detectGrid(img, tolerance = 8) {
82
- const { width: w, height: h, data: d } = img;
83
- const same = (a, b) => Math.abs(d[a] - d[b]) <= tolerance && Math.abs(d[a + 1] - d[b + 1]) <= tolerance && Math.abs(d[a + 2] - d[b + 2]) <= tolerance && Math.abs(d[a + 3] - d[b + 3]) <= tolerance;
84
- // run lengths along rows and columns; the block size divides all of them (mostly)
85
- const gcd = (a, b) => (b ? gcd(b, a % b) : a);
86
- let g = 0;
87
- const stepY = Math.max(1, Math.floor(h / 64)), stepX = Math.max(1, Math.floor(w / 64));
88
- for (let y = 0; y < h; y += stepY) {
89
- let run = 1;
90
- for (let x = 1; x < w; x++) {
91
- if (same((y * w + x) * 4, (y * w + x - 1) * 4)) run++;
92
- else { g = gcd(g, run); run = 1; if (g === 1) return 1; }
93
- }
94
- }
95
- for (let x = 0; x < w; x += stepX) {
96
- let run = 1;
97
- for (let y = 1; y < h; y++) {
98
- if (same((y * w + x) * 4, ((y - 1) * w + x) * 4)) run++;
99
- else { g = gcd(g, run); run = 1; if (g === 1) return 1; }
100
- }
101
- }
102
- return g || 1;
103
- }
1
+ // Pure pixel helpers (no DOM): integer upscaling, grid lines, colour counting.
2
+
3
+ import { parseColor, toHex } from '../color.js';
4
+
5
+ /**
6
+ * Nearest-neighbour integer upscale.
7
+ * @param {{ width: number, height: number, data: ArrayLike<number> }} img
8
+ * @param {number} k scale factor (integer ≥ 1)
9
+ * @param {{ grid?: boolean, gridColor?: string, gridAlpha?: number }} [opts] grid: 1px lines between source pixels
10
+ */
11
+ export function upscale(img, k = 1, opts = {}) {
12
+ k = Math.max(1, Math.round(k));
13
+ const w = img.width * k, h = img.height * k;
14
+ const out = new Uint8ClampedArray(w * h * 4);
15
+ const src = img.data, sw = img.width;
16
+ for (let y = 0; y < h; y++) {
17
+ const sy = (y / k) | 0;
18
+ for (let x = 0; x < w; x++) {
19
+ const so = (sy * sw + ((x / k) | 0)) * 4, o = (y * w + x) * 4;
20
+ out[o] = src[so]; out[o + 1] = src[so + 1]; out[o + 2] = src[so + 2]; out[o + 3] = src[so + 3];
21
+ }
22
+ }
23
+ if (opts.grid && k >= 3) {
24
+ const [r, g, b] = parseColor(opts.gridColor || '#000000');
25
+ const a = opts.gridAlpha ?? 0.35;
26
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
27
+ if (x % k && y % k) continue;
28
+ const o = (y * w + x) * 4;
29
+ const sa = out[o + 3] / 255, oa = a + sa * (1 - a);
30
+ out[o] = (r * a + out[o] * sa * (1 - a)) / oa;
31
+ out[o + 1] = (g * a + out[o + 1] * sa * (1 - a)) / oa;
32
+ out[o + 2] = (b * a + out[o + 2] * sa * (1 - a)) / oa;
33
+ out[o + 3] = oa * 255;
34
+ }
35
+ }
36
+ return { width: w, height: h, data: out };
37
+ }
38
+
39
+ /**
40
+ * Nearest-neighbour resize to any size (used to export at the source's resolution, like 2.x's
41
+ * "full resolution" download). Pixels may differ by one in size when the ratio isn't whole.
42
+ */
43
+ export function resizeNearest(img, width, height) {
44
+ width = Math.max(1, Math.round(width)); height = Math.max(1, Math.round(height));
45
+ const out = new Uint8ClampedArray(width * height * 4);
46
+ const xs = new Int32Array(width);
47
+ for (let x = 0; x < width; x++) xs[x] = Math.min(img.width - 1, Math.floor((x + 0.5) * img.width / width));
48
+ for (let y = 0; y < height; y++) {
49
+ const sy = Math.min(img.height - 1, Math.floor((y + 0.5) * img.height / height));
50
+ for (let x = 0; x < width; x++) {
51
+ const so = (sy * img.width + xs[x]) * 4, o = (y * width + x) * 4;
52
+ out[o] = img.data[so]; out[o + 1] = img.data[so + 1]; out[o + 2] = img.data[so + 2]; out[o + 3] = img.data[so + 3];
53
+ }
54
+ }
55
+ return { width, height, data: out };
56
+ }
57
+
58
+ /** Distinct opaque colours in an image ('#rrggbb'), in order of first appearance. */
59
+ export function uniqueColors(img, limit = Infinity) {
60
+ const seen = new Set();
61
+ const d = img.data;
62
+ for (let o = 0; o < d.length; o += 4) {
63
+ if (d[o + 3] === 0) continue;
64
+ seen.add((d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
65
+ if (seen.size > limit) break;
66
+ }
67
+ return [...seen].map(v => toHex([v >> 16, (v >> 8) & 255, v & 255]));
68
+ }
69
+
70
+ /** True when the image has at least one non-opaque pixel. */
71
+ export function hasTransparency(img) {
72
+ for (let o = 3; o < img.data.length; o += 4) if (img.data[o] < 255) return true;
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Detect the pixel-grid size of an upscaled pixel-art image (e.g. a sprite saved at 4×).
78
+ * Returns the largest block size b such that the image is made of b×b solid blocks
79
+ * (allowing a small tolerance for JPEG noise), or 1.
80
+ */
81
+ export function detectGrid(img, tolerance = 8) {
82
+ const { width: w, height: h, data: d } = img;
83
+ const same = (a, b) => Math.abs(d[a] - d[b]) <= tolerance && Math.abs(d[a + 1] - d[b + 1]) <= tolerance && Math.abs(d[a + 2] - d[b + 2]) <= tolerance && Math.abs(d[a + 3] - d[b + 3]) <= tolerance;
84
+ // run lengths along rows and columns; the block size divides all of them (mostly)
85
+ const gcd = (a, b) => (b ? gcd(b, a % b) : a);
86
+ let g = 0;
87
+ const stepY = Math.max(1, Math.floor(h / 64)), stepX = Math.max(1, Math.floor(w / 64));
88
+ for (let y = 0; y < h; y += stepY) {
89
+ let run = 1;
90
+ for (let x = 1; x < w; x++) {
91
+ if (same((y * w + x) * 4, (y * w + x - 1) * 4)) run++;
92
+ else { g = gcd(g, run); run = 1; if (g === 1) return 1; }
93
+ }
94
+ }
95
+ for (let x = 0; x < w; x += stepX) {
96
+ let run = 1;
97
+ for (let y = 1; y < h; y++) {
98
+ if (same((y * w + x) * 4, ((y - 1) * w + x) * 4)) run++;
99
+ else { g = gcd(g, run); run = 1; if (g === 1) return 1; }
100
+ }
101
+ }
102
+ return g || 1;
103
+ }
@@ -1,95 +1,95 @@
1
- // Sprite sheets (many frames → one image + atlas JSON) and tilesets (one image → unique tiles + map).
2
-
3
- const blank = (w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4) });
4
-
5
- function blit(dst, src, dx, dy, sx = 0, sy = 0, sw = src.width, sh = src.height) {
6
- for (let y = 0; y < sh; y++) {
7
- const so = ((sy + y) * src.width + sx) * 4, o = ((dy + y) * dst.width + dx) * 4;
8
- dst.data.set(src.data.subarray ? src.data.subarray(so, so + sw * 4) : Array.prototype.slice.call(src.data, so, so + sw * 4), o);
9
- }
10
- }
11
-
12
- /**
13
- * Pack frames into a grid. Frames may differ in size; cells use the largest frame.
14
- * @param {{ width: number, height: number, data: ArrayLike<number>, name?: string, delay?: number }[]} frames
15
- * @param {{ columns?: number, padding?: number, name?: string }} [opts] columns: 0 = roughly square
16
- * @returns {{ image: { width, height, data }, atlas: object }}
17
- */
18
- export function packSpriteSheet(frames, opts = {}) {
19
- if (!frames.length) throw new Error('No frames');
20
- const pad = Math.max(0, opts.padding | 0);
21
- const cw = Math.max(...frames.map(f => f.width)), ch = Math.max(...frames.map(f => f.height));
22
- const cols = Math.max(1, Math.min(frames.length, opts.columns || Math.ceil(Math.sqrt(frames.length))));
23
- const rows = Math.ceil(frames.length / cols);
24
- const W = cols * cw + (cols - 1) * pad, H = rows * ch + (rows - 1) * pad;
25
- const image = blank(W, H);
26
- const out = {};
27
- frames.forEach((f, i) => {
28
- const x = (i % cols) * (cw + pad), y = Math.floor(i / cols) * (ch + pad);
29
- blit(image, f, x, y);
30
- out[f.name || `frame-${String(i).padStart(3, '0')}`] = {
31
- frame: { x, y, w: f.width, h: f.height },
32
- sourceSize: { w: f.width, h: f.height },
33
- ...(f.delay != null ? { duration: Math.round(f.delay) } : {}),
34
- };
35
- });
36
- // the "hash" flavour of the TexturePacker / Aseprite JSON format, which most engines read
37
- const atlas = { frames: out, meta: { app: 'image-to-pixel', image: (opts.name || 'sheet') + '.png', size: { w: W, h: H }, scale: '1', columns: cols, rows, padding: pad } };
38
- return { image, atlas };
39
- }
40
-
41
- /**
42
- * Cut an image into tiles, drop duplicates (optionally counting mirrored tiles as duplicates)
43
- * and return the tileset image and a tile map.
44
- * @param {{ width, height, data }} img
45
- * @param {{ tileWidth?: number, tileHeight?: number, columns?: number, flips?: boolean }} [opts]
46
- * @returns {{ image, map: { width: number, height: number, tiles: number[], flips?: number[] }, count: number }}
47
- */
48
- export function buildTileset(img, opts = {}) {
49
- const tw = Math.max(1, opts.tileWidth || 8), th = Math.max(1, opts.tileHeight || tw);
50
- const mw = Math.ceil(img.width / tw), mh = Math.ceil(img.height / th);
51
- const tiles = [], keys = new Map(), map = [], flipsOut = [];
52
- const read = (tx, ty) => {
53
- const t = blank(tw, th);
54
- for (let y = 0; y < th; y++) for (let x = 0; x < tw; x++) {
55
- const sx = tx * tw + x, sy = ty * th + y;
56
- if (sx >= img.width || sy >= img.height) continue;
57
- const so = (sy * img.width + sx) * 4;
58
- t.data.set([img.data[so], img.data[so + 1], img.data[so + 2], img.data[so + 3]], (y * tw + x) * 4);
59
- }
60
- return t;
61
- };
62
- const key = t => {
63
- let s = '';
64
- for (let i = 0; i < t.data.length; i += 4) s += t.data[i + 3] === 0 ? '.' : String.fromCharCode(t.data[i] + 1, t.data[i + 1] + 1, t.data[i + 2] + 1);
65
- return s;
66
- };
67
- const flip = (t, fx, fy) => {
68
- const o = blank(tw, th);
69
- for (let y = 0; y < th; y++) for (let x = 0; x < tw; x++) {
70
- const si = ((fy ? th - 1 - y : y) * tw + (fx ? tw - 1 - x : x)) * 4;
71
- o.data.set(t.data.subarray(si, si + 4), (y * tw + x) * 4);
72
- }
73
- return o;
74
- };
75
- for (let ty = 0; ty < mh; ty++) {
76
- for (let tx = 0; tx < mw; tx++) {
77
- const t = read(tx, ty);
78
- let id = keys.get(key(t)), f = 0;
79
- if (id === undefined && opts.flips) {
80
- for (const [fx, fy, code] of [[1, 0, 1], [0, 1, 2], [1, 1, 3]]) {
81
- const k = keys.get(key(flip(t, fx, fy)));
82
- if (k !== undefined) { id = k; f = code; break; }
83
- }
84
- }
85
- if (id === undefined) { id = tiles.length; tiles.push(t); keys.set(key(t), id); }
86
- map.push(id); flipsOut.push(f);
87
- }
88
- }
89
- const sheet = packSpriteSheet(tiles, { columns: opts.columns || Math.min(16, tiles.length) });
90
- return {
91
- image: sheet.image,
92
- map: { width: mw, height: mh, tileWidth: tw, tileHeight: th, tiles: map, ...(opts.flips ? { flips: flipsOut } : {}) },
93
- count: tiles.length,
94
- };
95
- }
1
+ // Sprite sheets (many frames → one image + atlas JSON) and tilesets (one image → unique tiles + map).
2
+
3
+ const blank = (w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4) });
4
+
5
+ function blit(dst, src, dx, dy, sx = 0, sy = 0, sw = src.width, sh = src.height) {
6
+ for (let y = 0; y < sh; y++) {
7
+ const so = ((sy + y) * src.width + sx) * 4, o = ((dy + y) * dst.width + dx) * 4;
8
+ dst.data.set(src.data.subarray ? src.data.subarray(so, so + sw * 4) : Array.prototype.slice.call(src.data, so, so + sw * 4), o);
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Pack frames into a grid. Frames may differ in size; cells use the largest frame.
14
+ * @param {{ width: number, height: number, data: ArrayLike<number>, name?: string, delay?: number }[]} frames
15
+ * @param {{ columns?: number, padding?: number, name?: string }} [opts] columns: 0 = roughly square
16
+ * @returns {{ image: { width, height, data }, atlas: object }}
17
+ */
18
+ export function packSpriteSheet(frames, opts = {}) {
19
+ if (!frames.length) throw new Error('No frames');
20
+ const pad = Math.max(0, opts.padding | 0);
21
+ const cw = Math.max(...frames.map(f => f.width)), ch = Math.max(...frames.map(f => f.height));
22
+ const cols = Math.max(1, Math.min(frames.length, opts.columns || Math.ceil(Math.sqrt(frames.length))));
23
+ const rows = Math.ceil(frames.length / cols);
24
+ const W = cols * cw + (cols - 1) * pad, H = rows * ch + (rows - 1) * pad;
25
+ const image = blank(W, H);
26
+ const out = {};
27
+ frames.forEach((f, i) => {
28
+ const x = (i % cols) * (cw + pad), y = Math.floor(i / cols) * (ch + pad);
29
+ blit(image, f, x, y);
30
+ out[f.name || `frame-${String(i).padStart(3, '0')}`] = {
31
+ frame: { x, y, w: f.width, h: f.height },
32
+ sourceSize: { w: f.width, h: f.height },
33
+ ...(f.delay != null ? { duration: Math.round(f.delay) } : {}),
34
+ };
35
+ });
36
+ // the "hash" flavour of the TexturePacker / Aseprite JSON format, which most engines read
37
+ const atlas = { frames: out, meta: { app: 'image-to-pixel', image: (opts.name || 'sheet') + '.png', size: { w: W, h: H }, scale: '1', columns: cols, rows, padding: pad } };
38
+ return { image, atlas };
39
+ }
40
+
41
+ /**
42
+ * Cut an image into tiles, drop duplicates (optionally counting mirrored tiles as duplicates)
43
+ * and return the tileset image and a tile map.
44
+ * @param {{ width, height, data }} img
45
+ * @param {{ tileWidth?: number, tileHeight?: number, columns?: number, flips?: boolean }} [opts]
46
+ * @returns {{ image, map: { width: number, height: number, tiles: number[], flips?: number[] }, count: number }}
47
+ */
48
+ export function buildTileset(img, opts = {}) {
49
+ const tw = Math.max(1, opts.tileWidth || 8), th = Math.max(1, opts.tileHeight || tw);
50
+ const mw = Math.ceil(img.width / tw), mh = Math.ceil(img.height / th);
51
+ const tiles = [], keys = new Map(), map = [], flipsOut = [];
52
+ const read = (tx, ty) => {
53
+ const t = blank(tw, th);
54
+ for (let y = 0; y < th; y++) for (let x = 0; x < tw; x++) {
55
+ const sx = tx * tw + x, sy = ty * th + y;
56
+ if (sx >= img.width || sy >= img.height) continue;
57
+ const so = (sy * img.width + sx) * 4;
58
+ t.data.set([img.data[so], img.data[so + 1], img.data[so + 2], img.data[so + 3]], (y * tw + x) * 4);
59
+ }
60
+ return t;
61
+ };
62
+ const key = t => {
63
+ let s = '';
64
+ for (let i = 0; i < t.data.length; i += 4) s += t.data[i + 3] === 0 ? '.' : String.fromCharCode(t.data[i] + 1, t.data[i + 1] + 1, t.data[i + 2] + 1);
65
+ return s;
66
+ };
67
+ const flip = (t, fx, fy) => {
68
+ const o = blank(tw, th);
69
+ for (let y = 0; y < th; y++) for (let x = 0; x < tw; x++) {
70
+ const si = ((fy ? th - 1 - y : y) * tw + (fx ? tw - 1 - x : x)) * 4;
71
+ o.data.set(t.data.subarray(si, si + 4), (y * tw + x) * 4);
72
+ }
73
+ return o;
74
+ };
75
+ for (let ty = 0; ty < mh; ty++) {
76
+ for (let tx = 0; tx < mw; tx++) {
77
+ const t = read(tx, ty);
78
+ let id = keys.get(key(t)), f = 0;
79
+ if (id === undefined && opts.flips) {
80
+ for (const [fx, fy, code] of [[1, 0, 1], [0, 1, 2], [1, 1, 3]]) {
81
+ const k = keys.get(key(flip(t, fx, fy)));
82
+ if (k !== undefined) { id = k; f = code; break; }
83
+ }
84
+ }
85
+ if (id === undefined) { id = tiles.length; tiles.push(t); keys.set(key(t), id); }
86
+ map.push(id); flipsOut.push(f);
87
+ }
88
+ }
89
+ const sheet = packSpriteSheet(tiles, { columns: opts.columns || Math.min(16, tiles.length) });
90
+ return {
91
+ image: sheet.image,
92
+ map: { width: mw, height: mh, tileWidth: tw, tileHeight: th, tiles: map, ...(opts.flips ? { flips: flipsOut } : {}) },
93
+ count: tiles.length,
94
+ };
95
+ }
@@ -1,55 +1,55 @@
1
- // Store-only zip writer (PNGs and GIFs are already compressed, so deflating again buys little).
2
-
3
- import { crc32 } from './crc32.js';
4
-
5
- const enc = new TextEncoder();
6
-
7
- /**
8
- * @param {{ name: string, data: Uint8Array | string, date?: Date }[]} entries
9
- * @returns {Uint8Array}
10
- */
11
- export function createZip(entries) {
12
- const files = [], central = [];
13
- let offset = 0;
14
- const used = new Set();
15
- for (const e of entries) {
16
- let name = e.name.replace(/\\/g, '/').replace(/^\/+/, '');
17
- // avoid duplicate names inside one archive
18
- if (used.has(name)) { const m = /^(.*?)(\.[^.]*)?$/.exec(name); let i = 2; while (used.has(`${m[1]}-${i}${m[2] || ''}`)) i++; name = `${m[1]}-${i}${m[2] || ''}`; }
19
- used.add(name);
20
- const nameBytes = enc.encode(name);
21
- const data = typeof e.data === 'string' ? enc.encode(e.data) : e.data;
22
- const crc = crc32(data);
23
- const d = e.date || new Date();
24
- const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
25
- const date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
26
-
27
- const local = new Uint8Array(30 + nameBytes.length);
28
- const lv = new DataView(local.buffer);
29
- lv.setUint32(0, 0x04034b50, true); lv.setUint16(4, 20, true); lv.setUint16(6, 0x0800, true); // UTF-8 names
30
- lv.setUint16(8, 0, true); lv.setUint16(10, time, true); lv.setUint16(12, date, true);
31
- lv.setUint32(14, crc, true); lv.setUint32(18, data.length, true); lv.setUint32(22, data.length, true);
32
- lv.setUint16(26, nameBytes.length, true); lv.setUint16(28, 0, true);
33
- local.set(nameBytes, 30);
34
-
35
- const cen = new Uint8Array(46 + nameBytes.length);
36
- const cv = new DataView(cen.buffer);
37
- cv.setUint32(0, 0x02014b50, true); cv.setUint16(4, 20, true); cv.setUint16(6, 20, true); cv.setUint16(8, 0x0800, true);
38
- cv.setUint16(10, 0, true); cv.setUint16(12, time, true); cv.setUint16(14, date, true);
39
- cv.setUint32(16, crc, true); cv.setUint32(20, data.length, true); cv.setUint32(24, data.length, true);
40
- cv.setUint16(28, nameBytes.length, true); cv.setUint32(42, offset, true);
41
- cen.set(nameBytes, 46);
42
-
43
- files.push(local, data); central.push(cen);
44
- offset += local.length + data.length;
45
- }
46
- const cenSize = central.reduce((s, c) => s + c.length, 0);
47
- const end = new Uint8Array(22);
48
- const ev = new DataView(end.buffer);
49
- ev.setUint32(0, 0x06054b50, true); ev.setUint16(8, entries.length, true); ev.setUint16(10, entries.length, true);
50
- ev.setUint32(12, cenSize, true); ev.setUint32(16, offset, true);
51
- const out = new Uint8Array(offset + cenSize + 22);
52
- let o = 0;
53
- for (const p of [...files, ...central, end]) { out.set(p, o); o += p.length; }
54
- return out;
55
- }
1
+ // Store-only zip writer (PNGs and GIFs are already compressed, so deflating again buys little).
2
+
3
+ import { crc32 } from './crc32.js';
4
+
5
+ const enc = new TextEncoder();
6
+
7
+ /**
8
+ * @param {{ name: string, data: Uint8Array | string, date?: Date }[]} entries
9
+ * @returns {Uint8Array}
10
+ */
11
+ export function createZip(entries) {
12
+ const files = [], central = [];
13
+ let offset = 0;
14
+ const used = new Set();
15
+ for (const e of entries) {
16
+ let name = e.name.replace(/\\/g, '/').replace(/^\/+/, '');
17
+ // avoid duplicate names inside one archive
18
+ if (used.has(name)) { const m = /^(.*?)(\.[^.]*)?$/.exec(name); let i = 2; while (used.has(`${m[1]}-${i}${m[2] || ''}`)) i++; name = `${m[1]}-${i}${m[2] || ''}`; }
19
+ used.add(name);
20
+ const nameBytes = enc.encode(name);
21
+ const data = typeof e.data === 'string' ? enc.encode(e.data) : e.data;
22
+ const crc = crc32(data);
23
+ const d = e.date || new Date();
24
+ const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
25
+ const date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
26
+
27
+ const local = new Uint8Array(30 + nameBytes.length);
28
+ const lv = new DataView(local.buffer);
29
+ lv.setUint32(0, 0x04034b50, true); lv.setUint16(4, 20, true); lv.setUint16(6, 0x0800, true); // UTF-8 names
30
+ lv.setUint16(8, 0, true); lv.setUint16(10, time, true); lv.setUint16(12, date, true);
31
+ lv.setUint32(14, crc, true); lv.setUint32(18, data.length, true); lv.setUint32(22, data.length, true);
32
+ lv.setUint16(26, nameBytes.length, true); lv.setUint16(28, 0, true);
33
+ local.set(nameBytes, 30);
34
+
35
+ const cen = new Uint8Array(46 + nameBytes.length);
36
+ const cv = new DataView(cen.buffer);
37
+ cv.setUint32(0, 0x02014b50, true); cv.setUint16(4, 20, true); cv.setUint16(6, 20, true); cv.setUint16(8, 0x0800, true);
38
+ cv.setUint16(10, 0, true); cv.setUint16(12, time, true); cv.setUint16(14, date, true);
39
+ cv.setUint32(16, crc, true); cv.setUint32(20, data.length, true); cv.setUint32(24, data.length, true);
40
+ cv.setUint16(28, nameBytes.length, true); cv.setUint32(42, offset, true);
41
+ cen.set(nameBytes, 46);
42
+
43
+ files.push(local, data); central.push(cen);
44
+ offset += local.length + data.length;
45
+ }
46
+ const cenSize = central.reduce((s, c) => s + c.length, 0);
47
+ const end = new Uint8Array(22);
48
+ const ev = new DataView(end.buffer);
49
+ ev.setUint32(0, 0x06054b50, true); ev.setUint16(8, entries.length, true); ev.setUint16(10, entries.length, true);
50
+ ev.setUint32(12, cenSize, true); ev.setUint32(16, offset, true);
51
+ const out = new Uint8Array(offset + cenSize + 22);
52
+ let o = 0;
53
+ for (const p of [...files, ...central, end]) { out.set(p, o); o += p.length; }
54
+ return out;
55
+ }