@typeonce/ritmo-tools 0.17.0 → 0.19.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 (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +304 -24
  3. package/dist/cli.js +5 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/internal/version.d.ts +1 -1
  6. package/dist/internal/version.js +1 -1
  7. package/dist/music/cli/commands.d.ts +15 -0
  8. package/dist/music/cli/commands.d.ts.map +1 -0
  9. package/dist/music/cli/commands.js +180 -0
  10. package/dist/music/cli/commands.js.map +1 -0
  11. package/dist/music/cli/io.d.ts +11 -0
  12. package/dist/music/cli/io.d.ts.map +1 -0
  13. package/dist/music/cli/io.js +28 -0
  14. package/dist/music/cli/io.js.map +1 -0
  15. package/dist/music/cli/serve.d.ts +13 -0
  16. package/dist/music/cli/serve.d.ts.map +1 -0
  17. package/dist/music/cli/serve.js +168 -0
  18. package/dist/music/cli/serve.js.map +1 -0
  19. package/dist/music/core/audio.d.ts +22 -0
  20. package/dist/music/core/audio.d.ts.map +1 -0
  21. package/dist/music/core/audio.js +95 -0
  22. package/dist/music/core/audio.js.map +1 -0
  23. package/dist/music/core/check.d.ts +51 -0
  24. package/dist/music/core/check.d.ts.map +1 -0
  25. package/dist/music/core/check.js +113 -0
  26. package/dist/music/core/check.js.map +1 -0
  27. package/dist/music/core/instruments.d.ts +179 -0
  28. package/dist/music/core/instruments.d.ts.map +1 -0
  29. package/dist/music/core/instruments.js +531 -0
  30. package/dist/music/core/instruments.js.map +1 -0
  31. package/dist/music/core/listen.d.ts +32 -0
  32. package/dist/music/core/listen.d.ts.map +1 -0
  33. package/dist/music/core/listen.js +350 -0
  34. package/dist/music/core/listen.js.map +1 -0
  35. package/dist/music/core/midi.d.ts +4 -0
  36. package/dist/music/core/midi.d.ts.map +1 -0
  37. package/dist/music/core/midi.js +76 -0
  38. package/dist/music/core/midi.js.map +1 -0
  39. package/dist/music/core/render.d.ts +37 -0
  40. package/dist/music/core/render.d.ts.map +1 -0
  41. package/dist/music/core/render.js +204 -0
  42. package/dist/music/core/render.js.map +1 -0
  43. package/dist/music/core/review.d.ts +41 -0
  44. package/dist/music/core/review.d.ts.map +1 -0
  45. package/dist/music/core/review.js +139 -0
  46. package/dist/music/core/review.js.map +1 -0
  47. package/dist/music/core/song.d.ts +144 -0
  48. package/dist/music/core/song.d.ts.map +1 -0
  49. package/dist/music/core/song.js +291 -0
  50. package/dist/music/core/song.js.map +1 -0
  51. package/dist/music/page/assets/index-BfO_jOqP.js +4 -0
  52. package/dist/music/page/assets/index-Dw14kxkL.css +1 -0
  53. package/dist/music/page/index.html +19 -0
  54. package/dist/pixel/cli/commands.d.ts.map +1 -1
  55. package/dist/pixel/cli/commands.js +122 -46
  56. package/dist/pixel/cli/commands.js.map +1 -1
  57. package/dist/pixel/cli/io.d.ts +5 -1
  58. package/dist/pixel/cli/io.d.ts.map +1 -1
  59. package/dist/pixel/cli/io.js +16 -1
  60. package/dist/pixel/cli/io.js.map +1 -1
  61. package/dist/pixel/core/animation.d.ts +23 -8
  62. package/dist/pixel/core/animation.d.ts.map +1 -1
  63. package/dist/pixel/core/animation.js +41 -18
  64. package/dist/pixel/core/animation.js.map +1 -1
  65. package/dist/pixel/core/grid.d.ts +3 -2
  66. package/dist/pixel/core/grid.d.ts.map +1 -1
  67. package/dist/pixel/core/grid.js +14 -6
  68. package/dist/pixel/core/grid.js.map +1 -1
  69. package/dist/pixel/core/preview.d.ts +8 -3
  70. package/dist/pixel/core/preview.d.ts.map +1 -1
  71. package/dist/pixel/core/preview.js +39 -25
  72. package/dist/pixel/core/preview.js.map +1 -1
  73. package/dist/pixel/core/project.d.ts +27 -0
  74. package/dist/pixel/core/project.d.ts.map +1 -0
  75. package/dist/pixel/core/project.js +117 -0
  76. package/dist/pixel/core/project.js.map +1 -0
  77. package/dist/pixel/core/recipe.d.ts.map +1 -1
  78. package/dist/pixel/core/recipe.js +43 -6
  79. package/dist/pixel/core/recipe.js.map +1 -1
  80. package/dist/pixel/core/report.d.ts +21 -3
  81. package/dist/pixel/core/report.d.ts.map +1 -1
  82. package/dist/pixel/core/report.js +52 -22
  83. package/dist/pixel/core/report.js.map +1 -1
  84. package/dist/pixel/core/sheet.d.ts +68 -8
  85. package/dist/pixel/core/sheet.d.ts.map +1 -1
  86. package/dist/pixel/core/sheet.js +250 -32
  87. package/dist/pixel/core/sheet.js.map +1 -1
  88. package/dist/pixel/core/spritesheet.d.ts +43 -35
  89. package/dist/pixel/core/spritesheet.d.ts.map +1 -1
  90. package/dist/pixel/core/spritesheet.js +63 -59
  91. package/dist/pixel/core/spritesheet.js.map +1 -1
  92. package/dist/sound/cli/commands.d.ts +25 -0
  93. package/dist/sound/cli/commands.d.ts.map +1 -0
  94. package/dist/sound/cli/commands.js +137 -0
  95. package/dist/sound/cli/commands.js.map +1 -0
  96. package/dist/sound/cli/io.d.ts +24 -0
  97. package/dist/sound/cli/io.d.ts.map +1 -0
  98. package/dist/sound/cli/io.js +51 -0
  99. package/dist/sound/cli/io.js.map +1 -0
  100. package/dist/sound/cli/serve.d.ts +11 -0
  101. package/dist/sound/cli/serve.d.ts.map +1 -0
  102. package/dist/sound/cli/serve.js +147 -0
  103. package/dist/sound/cli/serve.js.map +1 -0
  104. package/dist/sound/core/measure.d.ts +67 -0
  105. package/dist/sound/core/measure.d.ts.map +1 -0
  106. package/dist/sound/core/measure.js +138 -0
  107. package/dist/sound/core/measure.js.map +1 -0
  108. package/dist/sound/core/recipe.d.ts +143 -0
  109. package/dist/sound/core/recipe.d.ts.map +1 -0
  110. package/dist/sound/core/recipe.js +292 -0
  111. package/dist/sound/core/recipe.js.map +1 -0
  112. package/dist/sound/core/render.d.ts +37 -0
  113. package/dist/sound/core/render.d.ts.map +1 -0
  114. package/dist/sound/core/render.js +317 -0
  115. package/dist/sound/core/render.js.map +1 -0
  116. package/dist/sound/core/review.d.ts +52 -0
  117. package/dist/sound/core/review.d.ts.map +1 -0
  118. package/dist/sound/core/review.js +89 -0
  119. package/dist/sound/core/review.js.map +1 -0
  120. package/dist/sound/core/takes.d.ts +29 -0
  121. package/dist/sound/core/takes.d.ts.map +1 -0
  122. package/dist/sound/core/takes.js +109 -0
  123. package/dist/sound/core/takes.js.map +1 -0
  124. package/dist/sound/core/wav.d.ts +4 -0
  125. package/dist/sound/core/wav.d.ts.map +1 -0
  126. package/dist/sound/core/wav.js +28 -0
  127. package/dist/sound/core/wav.js.map +1 -0
  128. package/dist/sound/page/assets/index-Cjun5b92.js +6 -0
  129. package/dist/sound/page/assets/index-DZm59wkQ.css +1 -0
  130. package/dist/sound/page/index.html +19 -0
  131. package/package.json +10 -4
@@ -48,18 +48,69 @@ export interface SheetFrame {
48
48
  readonly y: number;
49
49
  };
50
50
  }
51
- /** Everything that makes a sheet into an animation, beyond the recipe every frame shares. */
52
- export interface Sheet {
53
- readonly frames: ReadonlyArray<SheetFrame>;
54
- readonly anchor: Anchor;
51
+ /**
52
+ * One animation on a sheet: a name a game plays it by, its frames (indices
53
+ * into the sheet's frames, in the order they play), whether it is exported,
54
+ * and how it plays.
55
+ */
56
+ export interface SheetAnimation {
57
+ readonly name: string;
58
+ readonly frames: ReadonlyArray<number>;
59
+ readonly include: boolean;
55
60
  /** Frames per second. */
56
61
  readonly fps: number;
57
62
  readonly loop: Loop;
58
- /** Empty pixels kept around the frames, on every side. */
63
+ }
64
+ /**
65
+ * Everything that makes a sheet into animations, beyond the recipe every
66
+ * frame shares. Every frame belongs to exactly one animation: change frames
67
+ * and animations with `withFrames`, `insertFrame`, `removeFrame`,
68
+ * `splitAnimation`, `joinAnimation`, and `renameAnimation`, which keep that so.
69
+ */
70
+ export interface Sheet {
71
+ readonly frames: ReadonlyArray<SheetFrame>;
72
+ readonly animations: ReadonlyArray<SheetAnimation>;
73
+ readonly anchor: Anchor;
74
+ /** Empty pixels kept around each animation's frames, on every side. */
59
75
  readonly padding: number;
60
76
  }
77
+ export declare const DEFAULT_FPS = 8;
61
78
  export declare const defaultSheet: Sheet;
62
79
  export declare const frameOf: (box: FrameBox) => SheetFrame;
80
+ /** An animation of these frames, exported, at the default speed, looping. */
81
+ export declare const animationOf: (name: string, frames: ReadonlyArray<number>) => SheetAnimation;
82
+ /** Boxes in reading order: row by row, left to right, a row level with another after the one on its left. */
83
+ export declare const readingOrder: (boxes: ReadonlyArray<FrameBox>) => Array<FrameBox>;
84
+ /**
85
+ * The animations a sheet's frames make, by default: one per row. Image
86
+ * generators often put two blocks side by side (walking on the left,
87
+ * throwing on the right), and where their rows do not line up the blocks
88
+ * show; a row across both is split where the blocks meet, when its two
89
+ * parts hold as many frames as the blocks' rows (a row of a different count,
90
+ * like an explosion under both, stays whole). Named "animation-1", ... in
91
+ * reading order.
92
+ */
93
+ export declare const groupFrames: (boxes: ReadonlyArray<FrameBox>) => Array<SheetAnimation>;
94
+ /** The sheet with these frames, grouped into animations by `groupFrames` (anchor and margin kept). */
95
+ export declare const withFrames: (sheet: Sheet, frames: ReadonlyArray<SheetFrame>) => Sheet;
96
+ /**
97
+ * The sheet with a frame added at `index` (later frames move up one), in
98
+ * the animation of the frame before it, right after that frame; the first
99
+ * frame of all joins the first animation, and a sheet with none gets one.
100
+ */
101
+ export declare const insertFrame: (sheet: Sheet, index: number, frame: SheetFrame) => Sheet;
102
+ /** The sheet without the frame at `index` (later frames move down one); an animation left with no frames is dropped. */
103
+ export declare const removeFrame: (sheet: Sheet, index: number) => Sheet;
104
+ /** The animation at `animation` cut in two before its frame at position `at` (1 or more): the second part is a new animation after it. */
105
+ export declare const splitAnimation: (sheet: Sheet, animation: number, at: number) => Sheet;
106
+ /** The animation at `animation` and the one after it as one, keeping the first one's name and settings. */
107
+ export declare const joinAnimation: (sheet: Sheet, animation: number) => Sheet;
108
+ /** A name an animation can take: letters, digits, "-", "_", and ".", as games look it up. */
109
+ export declare const isAnimationName: (name: string) => boolean;
110
+ /** The animation renamed; a name that is taken gets a number, and a name that is not `isAnimationName` leaves it as it is. */
111
+ export declare const renameAnimation: (sheet: Sheet, animation: number, name: string) => Sheet;
112
+ /** The animation with some of its settings changed (whether it is exported, its speed, how it loops). */
113
+ export declare const setAnimation: (sheet: Sheet, animation: number, change: Partial<Pick<SheetAnimation, "include" | "fps" | "loop">>) => Sheet;
63
114
  /**
64
115
  * The background's colors: those covering much of the picture's border (a
65
116
  * painted grid behind the frames gives two, its fill and its lines).
@@ -101,6 +152,13 @@ export declare const clearSheet: (image: RgbaImage, colors: ReadonlyArray<Rgb>,
101
152
  * per frame, row by row, left to right. Boxes reach halfway into the gaps
102
153
  * around them, so a frame's loose bits (sparks, dust) stay inside it.
103
154
  * Returns one box around everything when the sheet has a single frame.
155
+ *
156
+ * The sheet is cut into rows of art, each row into frames, and each frame's
157
+ * box is cut into rows again when it holds frames stacked one over another:
158
+ * a sheet made of blocks side by side whose rows do not line up (walking on
159
+ * the left, throwing on the right) has no empty row across its whole width
160
+ * between two of a block's rows. Every cut is judged at the sheet's size, so
161
+ * a gap inside a frame (a body and the bomb it throws) never splits it.
104
162
  */
105
163
  export declare const findFrames: (image: RgbaImage) => Array<FrameBox>;
106
164
  /** The part of a picture inside a box (clamped to the picture). */
@@ -143,9 +201,11 @@ export declare const layOut: (arts: ReadonlyArray<PixelArt>, offsets: ReadonlyAr
143
201
  /** The order frames play in: start to end, or there and back without repeating the ends. */
144
202
  export declare const playOrder: (count: number, loop: Loop) => Array<number>;
145
203
  /**
146
- * Whether boxes found in a sheet look like frames of one animation rather
147
- * than parts of one picture: at least two, holding about as much art each
148
- * (the least at least a fifth of the most) and about as wide.
204
+ * Whether boxes found in a sheet look like frames of an animation rather
205
+ * than parts of one picture: at least two, and most of them (three in four)
206
+ * alike, holding about as much art as the typical frame and about as wide.
207
+ * The rest may be faint (the sparks and smoke at the end of an explosion):
208
+ * one such frame does not make a sheet a picture.
149
209
  */
150
210
  export declare const looksAnimated: (image: RgbaImage, boxes: ReadonlyArray<FrameBox>) => boolean;
151
211
  //# sourceMappingURL=sheet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sheet.d.ts","sourceRoot":"","sources":["../../../src/pixel/core/sheet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sheet.d.ts","sourceRoot":"","sources":["../../../src/pixel/core/sheet.ts"],"names":[],"mappings":"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"}
@@ -23,8 +23,170 @@ export const anchors = [
23
23
  { id: "center", label: "Center", hint: "Each frame's body is centered on the same spot: for flying or floating things." },
24
24
  { id: "box", label: "Box", hint: "Frames are placed as they sit in their boxes, unmoved." }
25
25
  ];
26
- export const defaultSheet = { frames: [], anchor: "ground", fps: 8, loop: "forward", padding: 1 };
26
+ export const DEFAULT_FPS = 8;
27
+ export const defaultSheet = { frames: [], animations: [], anchor: "ground", padding: 1 };
27
28
  export const frameOf = (box) => ({ box, include: true, nudge: { x: 0, y: 0 } });
29
+ /** An animation of these frames, exported, at the default speed, looping. */
30
+ export const animationOf = (name, frames) => ({ name, frames, include: true, fps: DEFAULT_FPS, loop: "forward" });
31
+ /** Two boxes on one row: their middles at about one height and their heights alike (a quarter of the smaller apart). */
32
+ const sameRow = (a, b) => {
33
+ const least = Math.min(a.height, b.height);
34
+ return Math.abs(a.y + a.height / 2 - (b.y + b.height / 2)) <= 0.25 * least && Math.max(a.height, b.height) <= 1.5 * least;
35
+ };
36
+ /** Boxes in rows: each row's indices left to right, the rows top to bottom (a row level with another, left first). */
37
+ const rowsOf = (boxes) => {
38
+ const rows = [];
39
+ boxes.forEach((box, i) => {
40
+ const row = rows.find((other) => sameRow(boxes[other[0]], box));
41
+ if (row === undefined)
42
+ rows.push([i]);
43
+ else
44
+ row.push(i);
45
+ });
46
+ for (const row of rows)
47
+ row.sort((a, b) => boxes[a].x - boxes[b].x);
48
+ const top = (row) => boxes[row[0]];
49
+ return rows.sort((a, b) => {
50
+ const first = top(a);
51
+ const second = top(b);
52
+ return Math.abs(first.y - second.y) <= 0.25 * Math.min(first.height, second.height) ? first.x - second.x : first.y - second.y;
53
+ });
54
+ };
55
+ /** Boxes in reading order: row by row, left to right, a row level with another after the one on its left. */
56
+ export const readingOrder = (boxes) => rowsOf(boxes).flatMap((row) => row.map((i) => boxes[i]));
57
+ /**
58
+ * The animations a sheet's frames make, by default: one per row. Image
59
+ * generators often put two blocks side by side (walking on the left,
60
+ * throwing on the right), and where their rows do not line up the blocks
61
+ * show; a row across both is split where the blocks meet, when its two
62
+ * parts hold as many frames as the blocks' rows (a row of a different count,
63
+ * like an explosion under both, stays whole). Named "animation-1", ... in
64
+ * reading order.
65
+ */
66
+ export const groupFrames = (boxes) => {
67
+ const rows = rowsOf(boxes);
68
+ const left = (row) => Math.min(...row.map((i) => boxes[i].x));
69
+ const right = (row) => Math.max(...row.map((i) => boxes[i].x + boxes[i].width));
70
+ const span = (row) => ({
71
+ top: Math.min(...row.map((i) => boxes[i].y)),
72
+ bottom: Math.max(...row.map((i) => boxes[i].y + boxes[i].height))
73
+ });
74
+ const widths = boxes.map((box) => box.width).sort((a, b) => a - b);
75
+ const reach = 0.5 * (widths[Math.floor(widths.length / 2)] ?? 0);
76
+ // Where two rows sit side by side: the blocks' meeting line, and how many frames each block's rows hold.
77
+ const seams = [];
78
+ for (const a of rows) {
79
+ for (const b of rows) {
80
+ if (a === b)
81
+ continue;
82
+ const sa = span(a);
83
+ const sb = span(b);
84
+ const overlap = Math.min(sa.bottom, sb.bottom) - Math.max(sa.top, sb.top);
85
+ if (overlap > 0.3 * Math.min(sa.bottom - sa.top, sb.bottom - sb.top) && right(a) <= left(b) + reach) {
86
+ seams.push({ x: (right(a) + left(b)) / 2, before: a.length, after: b.length });
87
+ }
88
+ }
89
+ }
90
+ const parts = rows.flatMap((row) => {
91
+ for (const seam of seams) {
92
+ const at = row.findIndex((i) => boxes[i].x + boxes[i].width / 2 > seam.x);
93
+ if (at !== seam.before || row.length - at !== seam.after)
94
+ continue;
95
+ const edge = boxes[row[at]].x;
96
+ if (Math.abs(edge - seam.x) <= reach)
97
+ return [row.slice(0, at), row.slice(at)];
98
+ }
99
+ return [row];
100
+ });
101
+ return parts.map((frames, i) => animationOf(`animation-${i + 1}`, frames));
102
+ };
103
+ /** The sheet with these frames, grouped into animations by `groupFrames` (anchor and margin kept). */
104
+ export const withFrames = (sheet, frames) => ({
105
+ ...sheet,
106
+ frames,
107
+ animations: groupFrames(frames.map((frame) => frame.box))
108
+ });
109
+ /** A name no other animation has: `name`, or `name-2`, `name-3`, ... */
110
+ const freeName = (animations, name, skip = -1) => {
111
+ const taken = new Set(animations.filter((_, i) => i !== skip).map((animation) => animation.name));
112
+ if (!taken.has(name))
113
+ return name;
114
+ let n = 2;
115
+ while (taken.has(`${name}-${n}`))
116
+ n++;
117
+ return `${name}-${n}`;
118
+ };
119
+ /**
120
+ * The sheet with a frame added at `index` (later frames move up one), in
121
+ * the animation of the frame before it, right after that frame; the first
122
+ * frame of all joins the first animation, and a sheet with none gets one.
123
+ */
124
+ export const insertFrame = (sheet, index, frame) => {
125
+ const frames = [...sheet.frames];
126
+ frames.splice(index, 0, frame);
127
+ const shifted = sheet.animations.map((animation) => ({ ...animation, frames: animation.frames.map((i) => (i >= index ? i + 1 : i)) }));
128
+ if (shifted.length === 0)
129
+ return { ...sheet, frames, animations: [animationOf("animation-1", [index])] };
130
+ const before = index - 1;
131
+ const owner = shifted.findIndex((animation) => animation.frames.includes(before));
132
+ const animations = shifted.map((animation, a) => {
133
+ if (owner < 0 ? a !== 0 : a !== owner)
134
+ return animation;
135
+ const list = [...animation.frames];
136
+ list.splice(owner < 0 ? 0 : list.indexOf(before) + 1, 0, index);
137
+ return { ...animation, frames: list };
138
+ });
139
+ return { ...sheet, frames, animations };
140
+ };
141
+ /** The sheet without the frame at `index` (later frames move down one); an animation left with no frames is dropped. */
142
+ export const removeFrame = (sheet, index) => ({
143
+ ...sheet,
144
+ frames: sheet.frames.filter((_, i) => i !== index),
145
+ animations: sheet.animations
146
+ .map((animation) => ({ ...animation, frames: animation.frames.filter((i) => i !== index).map((i) => (i > index ? i - 1 : i)) }))
147
+ .filter((animation) => animation.frames.length > 0)
148
+ });
149
+ /** The animation at `animation` cut in two before its frame at position `at` (1 or more): the second part is a new animation after it. */
150
+ export const splitAnimation = (sheet, animation, at) => {
151
+ const current = sheet.animations[animation];
152
+ if (current === undefined || at < 1 || at >= current.frames.length)
153
+ return sheet;
154
+ const animations = [...sheet.animations];
155
+ const second = { ...current, frames: current.frames.slice(at), name: "" };
156
+ animations.splice(animation, 1, { ...current, frames: current.frames.slice(0, at) }, { ...second, name: freeName(animations, current.name) });
157
+ return { ...sheet, animations };
158
+ };
159
+ /** The animation at `animation` and the one after it as one, keeping the first one's name and settings. */
160
+ export const joinAnimation = (sheet, animation) => {
161
+ const current = sheet.animations[animation];
162
+ const next = sheet.animations[animation + 1];
163
+ if (current === undefined || next === undefined)
164
+ return sheet;
165
+ const animations = [...sheet.animations];
166
+ animations.splice(animation, 2, { ...current, frames: [...current.frames, ...next.frames] });
167
+ return { ...sheet, animations };
168
+ };
169
+ /** A name an animation can take: letters, digits, "-", "_", and ".", as games look it up. */
170
+ export const isAnimationName = (name) => /^[A-Za-z0-9_.-]+$/.test(name);
171
+ /** The animation renamed; a name that is taken gets a number, and a name that is not `isAnimationName` leaves it as it is. */
172
+ export const renameAnimation = (sheet, animation, name) => {
173
+ const current = sheet.animations[animation];
174
+ const wanted = name.trim().replace(/\s+/g, "-");
175
+ if (current === undefined || !isAnimationName(wanted))
176
+ return sheet;
177
+ const animations = [...sheet.animations];
178
+ animations[animation] = { ...current, name: freeName(sheet.animations, wanted, animation) };
179
+ return { ...sheet, animations };
180
+ };
181
+ /** The animation with some of its settings changed (whether it is exported, its speed, how it loops). */
182
+ export const setAnimation = (sheet, animation, change) => {
183
+ const current = sheet.animations[animation];
184
+ if (current === undefined)
185
+ return sheet;
186
+ const animations = [...sheet.animations];
187
+ animations[animation] = { ...current, ...change, fps: Math.min(60, Math.max(0.5, change.fps ?? current.fps)) };
188
+ return { ...sheet, animations };
189
+ };
28
190
  // ---------------------------------------------------------------------------
29
191
  // Clearing the background
30
192
  const distance = (x, y) => Math.hypot(x.l - y.l, x.a - y.a, x.b - y.b);
@@ -299,37 +461,70 @@ const LABEL_BAND = 0.4;
299
461
  * per frame, row by row, left to right. Boxes reach halfway into the gaps
300
462
  * around them, so a frame's loose bits (sparks, dust) stay inside it.
301
463
  * Returns one box around everything when the sheet has a single frame.
464
+ *
465
+ * The sheet is cut into rows of art, each row into frames, and each frame's
466
+ * box is cut into rows again when it holds frames stacked one over another:
467
+ * a sheet made of blocks side by side whose rows do not line up (walking on
468
+ * the left, throwing on the right) has no empty row across its whole width
469
+ * between two of a block's rows. Every cut is judged at the sheet's size, so
470
+ * a gap inside a frame (a body and the bomb it throws) never splits it.
302
471
  */
303
472
  export const findFrames = (image) => {
304
- const { width, height } = image;
305
- const whole = { x: 0, y: 0, width, height };
306
- const { rows } = counts(image, whole);
473
+ const least = Math.max(2, 0.01 * image.width);
474
+ const spacing = [];
475
+ const top = framesIn(image, { x: 0, y: 0, width: image.width, height: image.height }, least, spacing);
476
+ // Below the top, frames split only at gaps not much narrower than the narrowest gap that
477
+ // already split two frames: one frame's own gaps (a body and its bomb) are narrower.
478
+ const usual = spacing.length === 0 ? least : Math.max(least, 0.6 * Math.min(...spacing));
479
+ const again = (box, depth) => {
480
+ if (depth >= DEPTH || bandsIn(image, box).kept.length < 2)
481
+ return [box];
482
+ const inner = framesIn(image, box, usual, []);
483
+ return inner.length > 1 ? inner.flatMap((part) => again(part, depth + 1)) : [box];
484
+ };
485
+ return readingOrder(top.flatMap((box) => again(box, 0)));
486
+ };
487
+ /** How many times a box is cut into rows again: blocks within blocks are rare. */
488
+ const DEPTH = 3;
489
+ /** The rows of art in one part of the sheet, and those tall enough to be frames (the rest are labels). */
490
+ const bandsIn = (image, area) => {
491
+ const { rows } = counts(image, area);
307
492
  // A row with a pixel or two in it is noise left by the clearing, not art.
308
- const noise = Math.max(1, Math.round(width * 0.001));
309
- const bands = runsOf(rows.map((n) => n > noise), Math.max(2, Math.round(height * 0.01))).filter((band) => band.to - band.from >= 3);
493
+ const noise = Math.max(1, Math.round(image.width * 0.001));
494
+ const bands = runsOf(rows.map((n) => n > noise), Math.max(2, Math.round(image.height * 0.01)))
495
+ .filter((band) => band.to - band.from >= 3)
496
+ .map((band) => ({ from: band.from + area.y, to: band.to + area.y }));
497
+ const tallest = Math.max(0, ...bands.map((band) => band.to - band.from));
498
+ return { bands, kept: bands.filter((band) => band.to - band.from >= LABEL_BAND * tallest) };
499
+ };
500
+ /**
501
+ * The frames inside one part of the sheet, split only at gaps at least
502
+ * `least` wide. Adds the width of each gap it split frames at to `spacing`.
503
+ */
504
+ const framesIn = (image, area, least, spacing) => {
505
+ const { bands, kept } = bandsIn(image, area);
310
506
  if (bands.length === 0)
311
507
  return [];
312
- const tallest = Math.max(...bands.map((band) => band.to - band.from));
313
- const kept = bands.filter((band) => band.to - band.from >= LABEL_BAND * tallest);
508
+ const bottomOf = area.y + area.height;
314
509
  const boxes = [];
315
- kept.forEach((band, b) => {
510
+ kept.forEach((band) => {
316
511
  // The band's own space, halfway to the bands around it (labels included).
317
512
  const above = bands[bands.indexOf(band) - 1];
318
513
  const below = bands[bands.indexOf(band) + 1];
319
- const marginTop = Math.round((band.from - (above?.to ?? Math.max(0, band.from - (band.to - band.from) * 0.1))) / 2);
320
- const marginBottom = Math.round(((below?.from ?? Math.min(height, band.to + (band.to - band.from) * 0.1)) - band.to) / 2);
321
- const top = Math.max(0, band.from - Math.min(marginTop, Math.round(0.15 * (band.to - band.from))));
322
- const bottom = Math.min(height, band.to + Math.min(marginBottom, Math.round(0.15 * (band.to - band.from))));
323
- void b;
324
- const area = { x: 0, y: band.from, width, height: band.to - band.from };
325
- const { columns } = counts(image, area);
326
- const pieces = runsOf(columns.map((n) => n > 0), 1);
514
+ const marginTop = Math.round((band.from - (above?.to ?? Math.max(area.y, band.from - (band.to - band.from) * 0.1))) / 2);
515
+ const marginBottom = Math.round(((below?.from ?? Math.min(bottomOf, band.to + (band.to - band.from) * 0.1)) - band.to) / 2);
516
+ const top = Math.max(area.y, band.from - Math.min(marginTop, Math.round(0.15 * (band.to - band.from))));
517
+ const bottom = Math.min(bottomOf, band.to + Math.min(marginBottom, Math.round(0.15 * (band.to - band.from))));
518
+ const { columns } = counts(image, { x: area.x, y: band.from, width: area.width, height: band.to - band.from });
519
+ const pieces = runsOf(columns.map((n) => n > 0), 1).map((piece) => ({ from: piece.from + area.x, to: piece.to + area.x }));
327
520
  if (pieces.length === 0)
328
521
  return;
329
522
  // Gaps between the pieces; the frames are split at the widest ones.
330
523
  const gaps = pieces.slice(1).map((piece, i) => ({ at: i, size: piece.from - pieces[i].to }));
331
- const splits = widestGaps(gaps.map((gap) => gap.size), width);
524
+ const splits = widestGaps(gaps.map((gap) => gap.size), least);
332
525
  const cuts = new Set(splits.map((i) => gaps[i].at));
526
+ for (const i of splits)
527
+ spacing.push(gaps[i].size);
333
528
  let start = pieces[0].from;
334
529
  const groups = [];
335
530
  pieces.forEach((piece, i) => {
@@ -354,18 +549,37 @@ export const findFrames = (image) => {
354
549
  if (at < 0 || inside[i].from - inside[i - 1].to > inside[at].from - inside[at - 1].to)
355
550
  at = i;
356
551
  }
357
- if (at < 0 || inside[at].from - inside[at - 1].to < Math.max(2, 0.01 * width))
552
+ if (at < 0 || inside[at].from - inside[at - 1].to < least)
358
553
  break;
359
554
  groups.splice(wide, 1, { from: run.from, to: inside[at - 1].to }, { from: inside[at].from, to: run.to });
360
555
  }
361
- // A group with almost nothing in it is left-over noise: it joins its neighbour.
362
- const mass = (run) => columns.slice(run.from, run.to).reduce((sum, n) => sum + n, 0);
556
+ // A group with almost nothing in it is noise when it sits against a frame, and joins
557
+ // it; set apart like a frame, it is one (the last wisp of smoke of an explosion).
558
+ const mass = (run) => columns.slice(run.from - area.x, run.to - area.x).reduce((sum, n) => sum + n, 0);
363
559
  const biggest = Math.max(...groups.map(mass));
364
- const solid = groups.filter((run) => mass(run) >= 0.02 * biggest);
560
+ const spaced = groups.slice(1).map((run, i) => run.from - groups[i].to);
561
+ const usualGap = [...spaced].sort((a, b) => a - b)[Math.floor(spaced.length / 2)] ?? 0;
562
+ const solid = [];
563
+ groups.forEach((run, i) => {
564
+ const before = i > 0 ? run.from - groups[i - 1].to : Number.POSITIVE_INFINITY;
565
+ const after = i + 1 < groups.length ? groups[i + 1].from - run.to : Number.POSITIVE_INFINITY;
566
+ if (mass(run) >= 0.02 * biggest || Math.min(before, after) >= 0.5 * usualGap) {
567
+ solid.push(run);
568
+ return;
569
+ }
570
+ // Joins the nearer neighbour: the one before when it is already kept, else the next.
571
+ const last = solid[solid.length - 1];
572
+ if (last !== undefined && before <= after)
573
+ solid[solid.length - 1] = { from: last.from, to: run.to };
574
+ else if (i + 1 < groups.length)
575
+ groups[i + 1] = { from: run.from, to: groups[i + 1].to };
576
+ else if (last !== undefined)
577
+ solid[solid.length - 1] = { from: last.from, to: run.to };
578
+ });
365
579
  const typicalGap = solid.length > 1 ? solid.slice(1).reduce((sum, run, i) => sum + (run.from - solid[i].to), 0) / (solid.length - 1) : Math.round(0.05 * (band.to - band.from));
366
580
  solid.forEach((run, i) => {
367
- const left = i === 0 ? Math.max(0, run.from - Math.round(typicalGap / 2)) : Math.round((solid[i - 1].to + run.from) / 2);
368
- const right = i === solid.length - 1 ? Math.min(width, run.to + Math.round(typicalGap / 2)) : Math.round((run.to + solid[i + 1].from) / 2);
581
+ const left = i === 0 ? Math.max(area.x, run.from - Math.round(typicalGap / 2)) : Math.round((solid[i - 1].to + run.from) / 2);
582
+ const right = i === solid.length - 1 ? Math.min(area.x + area.width, run.to + Math.round(typicalGap / 2)) : Math.round((run.to + solid[i + 1].from) / 2);
369
583
  boxes.push({ x: left, y: top, width: right - left, height: bottom - top });
370
584
  });
371
585
  });
@@ -375,11 +589,10 @@ export const findFrames = (image) => {
375
589
  * Which gaps (by index) separate frames: the widest ones, down to the
376
590
  * biggest drop in width. Gaps inside a frame (between a body and its
377
591
  * sparks) are much narrower than the gaps between frames. A gap narrower
378
- * than 1% of the sheet never splits.
592
+ * than `least` (1% of the sheet at the top) never splits.
379
593
  */
380
- const widestGaps = (gaps, width) => {
594
+ const widestGaps = (gaps, least) => {
381
595
  const order = gaps.map((size, i) => ({ size, i })).sort((a, b) => b.size - a.size);
382
- const least = Math.max(2, 0.01 * width);
383
596
  const wide = order.filter((gap) => gap.size >= least);
384
597
  if (wide.length === 0)
385
598
  return [];
@@ -551,9 +764,11 @@ export const playOrder = (count, loop) => {
551
764
  return [...forward, ...forward.slice(1, -1).reverse()];
552
765
  };
553
766
  /**
554
- * Whether boxes found in a sheet look like frames of one animation rather
555
- * than parts of one picture: at least two, holding about as much art each
556
- * (the least at least a fifth of the most) and about as wide.
767
+ * Whether boxes found in a sheet look like frames of an animation rather
768
+ * than parts of one picture: at least two, and most of them (three in four)
769
+ * alike, holding about as much art as the typical frame and about as wide.
770
+ * The rest may be faint (the sparks and smoke at the end of an explosion):
771
+ * one such frame does not make a sheet a picture.
557
772
  */
558
773
  export const looksAnimated = (image, boxes) => {
559
774
  if (boxes.length < 2)
@@ -567,7 +782,10 @@ export const looksAnimated = (image, boxes) => {
567
782
  });
568
783
  return columns.reduce((sum, n) => sum + n, 0);
569
784
  });
570
- const widths = boxes.map((box) => box.width);
571
- return Math.min(...masses) >= 0.2 * Math.max(...masses) && Math.min(...widths) >= 0.4 * Math.max(...widths);
785
+ const median = (values) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)];
786
+ const typicalMass = median(masses);
787
+ const typicalWidth = median(boxes.map((box) => box.width));
788
+ const alike = boxes.filter((box, i) => masses[i] >= 0.2 * typicalMass && masses[i] <= 5 * typicalMass && box.width >= 0.4 * typicalWidth && box.width <= 2.5 * typicalWidth).length;
789
+ return alike >= 2 && alike >= 0.75 * boxes.length;
572
790
  };
573
791
  //# sourceMappingURL=sheet.js.map