@oidoid/void 0.1.0-4 → 0.1.0-7

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 (307) hide show
  1. package/dist/meta.json +1 -0
  2. package/dist/package.json +66 -0
  3. package/dist/public/favicon/favicon16.png +0 -0
  4. package/dist/public/favicon/favicon16.webp +0 -0
  5. package/dist/public/favicon/favicon192.png +0 -0
  6. package/dist/public/favicon/favicon192.webp +0 -0
  7. package/dist/public/favicon/favicon32.png +0 -0
  8. package/dist/public/favicon/favicon32.webp +0 -0
  9. package/dist/public/favicon/favicon48.png +0 -0
  10. package/dist/public/favicon/favicon48.webp +0 -0
  11. package/dist/public/favicon/favicon64.png +0 -0
  12. package/dist/public/favicon/favicon64.webp +0 -0
  13. package/dist/public/index.js +98 -0
  14. package/dist/public/index.js.map +7 -0
  15. package/dist/public/preload-atlas.png +0 -0
  16. package/dist/public/preload-atlas.webp +0 -0
  17. package/dist/public/void-v0.1.0-6+20251020.7cdf98d.html +111 -0
  18. package/dist/public/void-v0.1.0-6+20251020.e4e9eef.html +111 -0
  19. package/dist/public/void-v0.1.0-7+20251021.7cdf98d.html +111 -0
  20. package/dist/schema/config-file.d.ts +32 -0
  21. package/dist/schema/config-file.js +41 -0
  22. package/dist/schema/config-file.js.map +1 -0
  23. package/dist/schema/config-file.v0.json +68 -0
  24. package/dist/src/audio.d.ts +7 -0
  25. package/dist/src/audio.js +25 -0
  26. package/dist/src/audio.js.map +1 -0
  27. package/dist/src/demo/assets/manifest.json +41 -0
  28. package/dist/src/demo/assets/preload-atlas.json +193 -0
  29. package/dist/src/demo/ents/clock-ent.d.ts +11 -0
  30. package/dist/src/demo/ents/clock-ent.js +26 -0
  31. package/dist/src/demo/ents/clock-ent.js.map +1 -0
  32. package/dist/src/demo/ents/render-toggle-ent.d.ts +12 -0
  33. package/dist/src/demo/ents/render-toggle-ent.js +41 -0
  34. package/dist/src/demo/ents/render-toggle-ent.js.map +1 -0
  35. package/dist/src/demo/ents/work-counter-ent.d.ts +10 -0
  36. package/dist/src/demo/ents/work-counter-ent.js +20 -0
  37. package/dist/src/demo/ents/work-counter-ent.js.map +1 -0
  38. package/dist/src/demo/game.d.ts +12 -0
  39. package/dist/src/demo/game.js +135 -0
  40. package/dist/src/demo/game.js.map +1 -0
  41. package/dist/src/demo/index.d.ts +1 -0
  42. package/dist/src/demo/index.js +8 -0
  43. package/dist/src/demo/index.js.map +1 -0
  44. package/dist/src/demo/tsconfig.json +15 -0
  45. package/dist/src/demo/types/tag.d.ts +2 -0
  46. package/dist/src/demo/types/tag.js +2 -0
  47. package/dist/src/demo/types/tag.js.map +1 -0
  48. package/dist/src/demo/void.json +11 -0
  49. package/dist/src/ents/button-ent.d.ts +39 -0
  50. package/dist/src/ents/button-ent.js +128 -0
  51. package/dist/src/ents/button-ent.js.map +1 -0
  52. package/dist/src/ents/cursor-ent.d.ts +18 -0
  53. package/dist/src/ents/cursor-ent.js +63 -0
  54. package/dist/src/ents/cursor-ent.js.map +1 -0
  55. package/dist/src/ents/ent.d.ts +7 -0
  56. package/dist/src/ents/ent.js +2 -0
  57. package/dist/src/ents/ent.js.map +1 -0
  58. package/dist/src/ents/follow-cam-ent.d.ts +25 -0
  59. package/dist/src/ents/follow-cam-ent.js +87 -0
  60. package/dist/src/ents/follow-cam-ent.js.map +1 -0
  61. package/dist/src/ents/nine-patch-ent.d.ts +43 -0
  62. package/dist/src/ents/nine-patch-ent.js +143 -0
  63. package/dist/src/ents/nine-patch-ent.js.map +1 -0
  64. package/dist/src/ents/text-ent.d.ts +23 -0
  65. package/dist/src/ents/text-ent.js +109 -0
  66. package/dist/src/ents/text-ent.js.map +1 -0
  67. package/dist/src/ents/zoo.d.ts +17 -0
  68. package/dist/src/ents/zoo.js +45 -0
  69. package/dist/src/ents/zoo.js.map +1 -0
  70. package/dist/src/graphics/atlas-parser.d.ts +4 -0
  71. package/dist/src/graphics/atlas-parser.js +27 -0
  72. package/dist/src/graphics/atlas-parser.js.map +1 -0
  73. package/dist/src/graphics/atlas.d.ts +49 -0
  74. package/dist/src/graphics/atlas.js +7 -0
  75. package/dist/src/graphics/atlas.js.map +1 -0
  76. package/dist/src/graphics/cam.d.ts +89 -0
  77. package/dist/src/graphics/cam.js +220 -0
  78. package/dist/src/graphics/cam.js.map +1 -0
  79. package/dist/src/graphics/gl.d.ts +24 -0
  80. package/dist/src/graphics/gl.js +70 -0
  81. package/dist/src/graphics/gl.js.map +1 -0
  82. package/dist/src/graphics/layer.d.ts +21 -0
  83. package/dist/src/graphics/layer.js +23 -0
  84. package/dist/src/graphics/layer.js.map +1 -0
  85. package/dist/src/graphics/renderer.d.ts +36 -0
  86. package/dist/src/graphics/renderer.js +180 -0
  87. package/dist/src/graphics/renderer.js.map +1 -0
  88. package/dist/src/graphics/sprite-frag.glsl.d.ts +1 -0
  89. package/dist/src/graphics/sprite-frag.glsl.js +35 -0
  90. package/dist/src/graphics/sprite-frag.glsl.js.map +1 -0
  91. package/dist/src/graphics/sprite-vert.glsl.d.ts +1 -0
  92. package/dist/src/graphics/sprite-vert.glsl.js +68 -0
  93. package/dist/src/graphics/sprite-vert.glsl.js.map +1 -0
  94. package/dist/src/graphics/sprite.d.ts +108 -0
  95. package/dist/src/graphics/sprite.js +301 -0
  96. package/dist/src/graphics/sprite.js.map +1 -0
  97. package/dist/src/index.d.ts +30 -0
  98. package/dist/src/index.js +33 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/input/context-menu.d.ts +8 -0
  101. package/dist/src/input/context-menu.js +25 -0
  102. package/dist/src/input/context-menu.js.map +1 -0
  103. package/dist/src/input/gamepad.d.ts +18 -0
  104. package/dist/src/input/gamepad.js +49 -0
  105. package/dist/src/input/gamepad.js.map +1 -0
  106. package/dist/src/input/input.d.ts +148 -0
  107. package/dist/src/input/input.js +383 -0
  108. package/dist/src/input/input.js.map +1 -0
  109. package/dist/src/input/keyboard.d.ts +17 -0
  110. package/dist/src/input/keyboard.js +46 -0
  111. package/dist/src/input/keyboard.js.map +1 -0
  112. package/dist/src/input/pointer.d.ts +53 -0
  113. package/dist/src/input/pointer.js +162 -0
  114. package/dist/src/input/pointer.js.map +1 -0
  115. package/dist/src/input/wheel.d.ts +12 -0
  116. package/dist/src/input/wheel.js +30 -0
  117. package/dist/src/input/wheel.js.map +1 -0
  118. package/dist/src/looper.d.ts +17 -0
  119. package/dist/src/looper.js +51 -0
  120. package/dist/src/looper.js.map +1 -0
  121. package/dist/src/mem/pool.d.ts +31 -0
  122. package/dist/src/mem/pool.js +98 -0
  123. package/dist/src/mem/pool.js.map +1 -0
  124. package/dist/src/random/random.d.ts +8 -0
  125. package/dist/src/random/random.js +23 -0
  126. package/dist/src/random/random.js.map +1 -0
  127. package/dist/src/storage/local-storage.d.ts +3 -0
  128. package/dist/src/storage/local-storage.js +11 -0
  129. package/dist/src/storage/local-storage.js.map +1 -0
  130. package/dist/src/text/font.d.ts +6 -0
  131. package/dist/src/text/font.js +20 -0
  132. package/dist/src/text/font.js.map +1 -0
  133. package/dist/src/text/mem-prop-5x6.json +573 -0
  134. package/dist/src/text/text-layout.d.ts +19 -0
  135. package/dist/src/text/text-layout.js +85 -0
  136. package/dist/src/text/text-layout.js.map +1 -0
  137. package/dist/src/tsconfig.json +15 -0
  138. package/dist/src/types/geo.d.ts +38 -0
  139. package/dist/src/types/geo.js +61 -0
  140. package/dist/src/types/geo.js.map +1 -0
  141. package/dist/src/types/json.d.ts +31 -0
  142. package/dist/src/types/json.js +2 -0
  143. package/dist/src/types/json.js.map +1 -0
  144. package/dist/src/types/time.d.ts +19 -0
  145. package/dist/src/types/time.js +10 -0
  146. package/dist/src/types/time.js.map +1 -0
  147. package/dist/src/types/void-version.d.ts +7 -0
  148. package/dist/src/types/void-version.js +2 -0
  149. package/dist/src/types/void-version.js.map +1 -0
  150. package/dist/src/utils/async-util.d.ts +11 -0
  151. package/dist/src/utils/async-util.js +36 -0
  152. package/dist/src/utils/async-util.js.map +1 -0
  153. package/dist/src/utils/canvas-util.d.ts +7 -0
  154. package/dist/src/utils/canvas-util.js +128 -0
  155. package/dist/src/utils/canvas-util.js.map +1 -0
  156. package/dist/src/utils/color-util.d.ts +1 -0
  157. package/dist/src/utils/color-util.js +4 -0
  158. package/dist/src/utils/color-util.js.map +1 -0
  159. package/dist/src/utils/debug.d.ts +19 -0
  160. package/dist/src/utils/debug.js +35 -0
  161. package/dist/src/utils/debug.js.map +1 -0
  162. package/dist/src/utils/delay-interval.d.ts +7 -0
  163. package/dist/src/utils/delay-interval.js +29 -0
  164. package/dist/src/utils/delay-interval.js.map +1 -0
  165. package/dist/src/utils/dom-util.d.ts +3 -0
  166. package/dist/src/utils/dom-util.js +29 -0
  167. package/dist/src/utils/dom-util.js.map +1 -0
  168. package/dist/src/utils/fetch-util.d.ts +3 -0
  169. package/dist/src/utils/fetch-util.js +24 -0
  170. package/dist/src/utils/fetch-util.js.map +1 -0
  171. package/dist/src/utils/math.d.ts +6 -0
  172. package/dist/src/utils/math.js +18 -0
  173. package/dist/src/utils/math.js.map +1 -0
  174. package/dist/src/utils/vibrate.d.ts +1 -0
  175. package/dist/src/utils/vibrate.js +4 -0
  176. package/dist/src/utils/vibrate.js.map +1 -0
  177. package/dist/src/void.d.ts +48 -0
  178. package/dist/src/void.js +102 -0
  179. package/dist/src/void.js.map +1 -0
  180. package/dist/tools/atlas-pack/aseprite.d.ts +58 -0
  181. package/dist/tools/atlas-pack/aseprite.js +16 -0
  182. package/dist/tools/atlas-pack/aseprite.js.map +1 -0
  183. package/dist/tools/atlas-pack/atlas-json-parser.d.ts +21 -0
  184. package/dist/tools/atlas-pack/atlas-json-parser.js +116 -0
  185. package/dist/tools/atlas-pack/atlas-json-parser.js.map +1 -0
  186. package/dist/tools/atlas-pack/atlas-pack.d.ts +2 -0
  187. package/dist/tools/atlas-pack/atlas-pack.js +22 -0
  188. package/dist/tools/atlas-pack/atlas-pack.js.map +1 -0
  189. package/dist/tools/bundle/bundle.d.ts +3 -0
  190. package/dist/tools/bundle/bundle.js +48 -0
  191. package/dist/tools/bundle/bundle.js.map +1 -0
  192. package/dist/tools/bundle/html-plugin.d.ts +3 -0
  193. package/dist/tools/bundle/html-plugin.js +90 -0
  194. package/dist/tools/bundle/html-plugin.js.map +1 -0
  195. package/dist/tools/tsconfig-base.json +48 -0
  196. package/dist/tools/tsconfig.json +19 -0
  197. package/dist/tools/types/config.d.ts +27 -0
  198. package/dist/tools/types/config.js +31 -0
  199. package/dist/tools/types/config.js.map +1 -0
  200. package/dist/tools/types/package-json.d.ts +4 -0
  201. package/dist/tools/types/package-json.js +2 -0
  202. package/dist/tools/types/package-json.js.map +1 -0
  203. package/dist/tools/utils/argv.d.ts +12 -0
  204. package/dist/tools/utils/argv.js +19 -0
  205. package/dist/tools/utils/argv.js.map +1 -0
  206. package/dist/tools/utils/exec.d.ts +3 -0
  207. package/dist/tools/utils/exec.js +15 -0
  208. package/dist/tools/utils/exec.js.map +1 -0
  209. package/dist/tools/utils/file-util.d.ts +1 -0
  210. package/dist/tools/utils/file-util.js +13 -0
  211. package/dist/tools/utils/file-util.js.map +1 -0
  212. package/dist/tools/utils/html-parser.d.ts +1 -0
  213. package/dist/tools/utils/html-parser.js +10 -0
  214. package/dist/tools/utils/html-parser.js.map +1 -0
  215. package/dist/tools/void.d.ts +15 -0
  216. package/dist/tools/void.js +31 -0
  217. package/dist/tools/void.js.map +1 -0
  218. package/package.json +31 -41
  219. package/readme.md +1 -1
  220. package/schema/config-file.test.ts +55 -0
  221. package/schema/config-file.ts +69 -0
  222. package/schema/config-file.v0.json +68 -0
  223. package/tools/atlas-pack/aseprite.ts +60 -0
  224. package/tools/atlas-pack/atlas-json-parser.test.ts +780 -0
  225. package/tools/atlas-pack/atlas-json-parser.ts +159 -0
  226. package/tools/atlas-pack/atlas-pack.ts +51 -0
  227. package/tools/bundle/bundle.ts +64 -0
  228. package/tools/bundle/html-plugin.ts +135 -0
  229. package/tools/types/config.ts +65 -0
  230. package/tools/types/package-json.ts +4 -0
  231. package/tools/utils/argv.test.ts +41 -0
  232. package/tools/utils/argv.ts +29 -0
  233. package/tools/utils/exec.ts +22 -0
  234. package/tools/utils/file-util.ts +11 -0
  235. package/tools/utils/html-parser.ts +9 -0
  236. package/tools/void.ts +55 -0
  237. package/dist/atlas/anim.d.ts +0 -30
  238. package/dist/atlas/anim.js +0 -16
  239. package/dist/atlas/anim.js.map +0 -1
  240. package/dist/atlas/aseprite.d.ts +0 -37
  241. package/dist/atlas/aseprite.js +0 -2
  242. package/dist/atlas/aseprite.js.map +0 -1
  243. package/dist/atlas/atlas-parser.d.ts +0 -52
  244. package/dist/atlas/atlas-parser.js +0 -109
  245. package/dist/atlas/atlas-parser.js.map +0 -1
  246. package/dist/atlas/atlas.d.ts +0 -4
  247. package/dist/atlas/atlas.js +0 -2
  248. package/dist/atlas/atlas.js.map +0 -1
  249. package/dist/audio/synth.d.ts +0 -4
  250. package/dist/audio/synth.js +0 -21
  251. package/dist/audio/synth.js.map +0 -1
  252. package/dist/graphics/bitmap.d.ts +0 -14
  253. package/dist/graphics/bitmap.js +0 -14
  254. package/dist/graphics/bitmap.js.map +0 -1
  255. package/dist/graphics/cam.d.ts +0 -16
  256. package/dist/graphics/cam.js +0 -42
  257. package/dist/graphics/cam.js.map +0 -1
  258. package/dist/graphics/frag.glsl.d.ts +0 -1
  259. package/dist/graphics/frag.glsl.js +0 -15
  260. package/dist/graphics/frag.glsl.js.map +0 -1
  261. package/dist/graphics/frame-listener.d.ts +0 -16
  262. package/dist/graphics/frame-listener.js +0 -83
  263. package/dist/graphics/frame-listener.js.map +0 -1
  264. package/dist/graphics/renderer.d.ts +0 -12
  265. package/dist/graphics/renderer.js +0 -184
  266. package/dist/graphics/renderer.js.map +0 -1
  267. package/dist/graphics/vert.glsl.d.ts +0 -1
  268. package/dist/graphics/vert.glsl.js +0 -46
  269. package/dist/graphics/vert.glsl.js.map +0 -1
  270. package/dist/index.d.ts +0 -31
  271. package/dist/index.js +0 -79
  272. package/dist/index.js.map +0 -1
  273. package/dist/input/gamepad-poller.d.ts +0 -8
  274. package/dist/input/gamepad-poller.js +0 -38
  275. package/dist/input/gamepad-poller.js.map +0 -1
  276. package/dist/input/input.d.ts +0 -44
  277. package/dist/input/input.js +0 -175
  278. package/dist/input/input.js.map +0 -1
  279. package/dist/input/keyboard-poller.d.ts +0 -7
  280. package/dist/input/keyboard-poller.js +0 -30
  281. package/dist/input/keyboard-poller.js.map +0 -1
  282. package/dist/input/pointer-poller.d.ts +0 -12
  283. package/dist/input/pointer-poller.js +0 -67
  284. package/dist/input/pointer-poller.js.map +0 -1
  285. package/dist/sprite/sprite.d.ts +0 -51
  286. package/dist/sprite/sprite.js +0 -161
  287. package/dist/sprite/sprite.js.map +0 -1
  288. package/dist/storage/json-storage.d.ts +0 -4
  289. package/dist/storage/json-storage.js +0 -13
  290. package/dist/storage/json-storage.js.map +0 -1
  291. package/dist/test/tsconfig.json +0 -14
  292. package/dist/text/font.d.ts +0 -6
  293. package/dist/text/font.js +0 -18
  294. package/dist/text/font.js.map +0 -1
  295. package/dist/text/text-layout.d.ts +0 -11
  296. package/dist/text/text-layout.js +0 -73
  297. package/dist/text/text-layout.js.map +0 -1
  298. package/dist/tsconfig.json +0 -13
  299. package/dist/types/2d.d.ts +0 -9
  300. package/dist/types/2d.js +0 -2
  301. package/dist/types/2d.js.map +0 -1
  302. package/dist/void.js +0 -60
  303. package/dist/void.js.map +0 -7
  304. package/dist/void.meta.json +0 -289
  305. package/src/atlas/anim.js +0 -17
  306. package/tools/atlas-parser.js +0 -120
  307. package/tools/void.js +0 -143
@@ -0,0 +1,159 @@
1
+ import {
2
+ type Anim,
3
+ type AtlasJSON,
4
+ animCels,
5
+ animMillis,
6
+ celMillis,
7
+ type TagFormat
8
+ } from '../../src/graphics/atlas.ts'
9
+ import {type Box, boxEq, type XY} from '../../src/types/geo.ts'
10
+ import {
11
+ type Aseprite,
12
+ AsepriteDirection,
13
+ type AsepriteFrame,
14
+ type AsepriteFrameMap,
15
+ type AsepriteFrameTag,
16
+ type AsepriteSlice,
17
+ type AsepriteTagSpan
18
+ } from './aseprite.ts'
19
+
20
+ export function parseAtlasJSON(ase: Readonly<Aseprite>): AtlasJSON {
21
+ const anim: {[tag: string]: Anim} = {}
22
+ const cels: number[] = []
23
+ for (const span of ase.meta.frameTags) {
24
+ const tag = parseTag(span.name)
25
+ if (anim[tag]) throw Error(`atlas tag "${tag}" duplicate`)
26
+ const id = Object.keys(anim).length
27
+ anim[tag] = parseAnim(id, span, ase.frames, ase.meta.slices)
28
+ for (const cel of parseAnimFrames(span, ase.frames).map(parseCel))
29
+ cels.push(cel.x, cel.y)
30
+ }
31
+ for (const slice of ase.meta.slices)
32
+ if (!anim[parseTag(slice.name)])
33
+ throw Error(`atlas hitbox "${slice.name}" has no animation`)
34
+ return {anim, celXY: cels}
35
+ }
36
+
37
+ /** @internal */
38
+ export function parseAnim(
39
+ id: number,
40
+ span: Readonly<AsepriteTagSpan>,
41
+ map: Readonly<AsepriteFrameMap>,
42
+ slices: readonly Readonly<AsepriteSlice>[]
43
+ ): Anim {
44
+ const cels = parseAnimFrames(span, map)
45
+ if (!cels[0]) throw Error(`no atlas frame "${span.name}"`)
46
+ const {hitbox, hurtbox} = parseHitboxes(span, slices)
47
+ return {
48
+ cels: cels.length,
49
+ h: cels[0].sourceSize.h,
50
+ hitbox,
51
+ hurtbox,
52
+ id,
53
+ w: cels[0].sourceSize.w
54
+ }
55
+ }
56
+
57
+ /**
58
+ * every frame is guaranteed to be present for at least `celMillis`. cels are
59
+ * duplicated until cel duration is at least met. cels are unpacked until a full
60
+ * period is defined for the direction. no warns for overflowing past on second
61
+ * or uneven periods.
62
+ * @internal
63
+ */
64
+ export function parseAnimFrames(
65
+ span: AsepriteTagSpan,
66
+ map: AsepriteFrameMap
67
+ ): AsepriteFrame[] {
68
+ const cels = []
69
+ let animDuration = 0
70
+ const len = span.to - span.from + 1
71
+ const peak = len - 1
72
+ const cycle = Math.max(1, 2 * peak)
73
+ const end =
74
+ span.direction === AsepriteDirection.Forward ||
75
+ span.direction === AsepriteDirection.Reverse
76
+ ? len
77
+ : cycle
78
+ const indexByDir: {[dir in AsepriteDirection]: (i: number) => number} = {
79
+ forward: i => span.from + (i % len),
80
+ pingpong: i => span.from + peak - Math.abs((i % cycle) - peak),
81
+ pingpong_reverse: i => span.to - (peak - Math.abs((i % cycle) - peak)),
82
+ reverse: i => span.to - (i % len)
83
+ }
84
+ const frameIndex = indexByDir[span.direction as AsepriteDirection]
85
+ for (
86
+ let i = 0;
87
+ i < end && cels.length < animCels && animDuration < animMillis;
88
+ i++
89
+ ) {
90
+ const frameTag = `${span.name}--${frameIndex(i)}` as AsepriteFrameTag
91
+ const frame = map[frameTag]
92
+ if (!frame) throw Error(`no atlas frame "${frameTag}"`)
93
+ for (
94
+ let celDuration = 0;
95
+ celDuration < frame.duration &&
96
+ cels.length < animCels &&
97
+ animDuration < animMillis;
98
+ celDuration += celMillis, animDuration += celMillis
99
+ ) {
100
+ cels.push(frame)
101
+
102
+ if (span.from === span.to) return cels // optimize for long single cel.
103
+ }
104
+ }
105
+ return cels
106
+ }
107
+
108
+ /** @internal */
109
+ export function parseCel(frame: Readonly<AsepriteFrame>): XY {
110
+ return {
111
+ x: frame.frame.x + (frame.frame.w - frame.sourceSize.w) / 2,
112
+ y: frame.frame.y + (frame.frame.h - frame.sourceSize.h) / 2
113
+ }
114
+ }
115
+
116
+ /** @internal */
117
+ export function parseHitboxes(
118
+ span: Readonly<AsepriteTagSpan>,
119
+ slices: readonly Readonly<AsepriteSlice>[]
120
+ ): {hitbox: Box | undefined; hurtbox: Box | undefined} {
121
+ let hitbox
122
+ let hurtbox
123
+ // https://github.com/aseprite/aseprite/issues/3524
124
+ for (const slice of slices) {
125
+ if (slice.name !== span.name) continue
126
+
127
+ if (!slice.keys[0]) continue
128
+
129
+ for (const k of slice.keys)
130
+ if (!boxEq(k.bounds, slice.keys[0].bounds))
131
+ throw Error(
132
+ `atlas tag "${span.name}" hitbox bounds varies across frames`
133
+ )
134
+
135
+ const red = slice.color === '#ff0000ff'
136
+ const green = slice.color === '#00ff00ff'
137
+ const blue = slice.color === '#0000ffff' // default Aseprite slice color.
138
+ if (!red && !green && !blue)
139
+ throw Error(
140
+ `atlas tag "${span.name}" hitbox color ${slice.color} unsupported`
141
+ )
142
+
143
+ if (hitbox && (red || blue))
144
+ throw Error(`atlas tag "${span.name}" has multiple hitboxes`)
145
+
146
+ if (hurtbox && (green || blue))
147
+ throw Error(`atlas tag "${span.name}" has multiple hurtboxes`)
148
+
149
+ if (red || blue) hitbox = slice.keys[0].bounds
150
+ if (green || blue) hurtbox = slice.keys[0].bounds
151
+ }
152
+ return {hitbox, hurtbox}
153
+ }
154
+
155
+ function parseTag(tag: string): TagFormat {
156
+ if (!tag.includes('--'))
157
+ throw Error(`atlas tag "${tag}" not in <filestem>--<animation> format`)
158
+ return tag as TagFormat
159
+ }
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import type {AtlasConfig} from '../../schema/config-file.ts'
4
+ import {exec} from '../utils/exec.ts'
5
+ import {globAll} from '../utils/file-util.ts'
6
+ import {parseAtlasJSON} from './atlas-json-parser.ts'
7
+
8
+ // to-do: separate executable?
9
+ export async function packAtlas(config: Readonly<AtlasConfig>): Promise<void> {
10
+ const filenames = await globAll(path.join(config.dir, '**.aseprite'))
11
+ if (!filenames.length) return
12
+
13
+ const webp = config.image.endsWith('.webp')
14
+ const sheet = webp ? config.image.replace('.webp', '.png') : config.image
15
+ const json = await exec(
16
+ 'aseprite',
17
+ '--batch',
18
+ '--color-mode=indexed',
19
+ '--filename-format={title}--{tag}--{frame}',
20
+ '--list-slices',
21
+ '--list-tags',
22
+ '--merge-duplicates',
23
+ `--sheet=${sheet}`,
24
+ '--sheet-pack',
25
+ '--tagname-format={title}--{tag}',
26
+ ...filenames
27
+ )
28
+
29
+ if (webp)
30
+ await exec(
31
+ 'cwebp',
32
+ '-exact',
33
+ '-lossless',
34
+ '-mt',
35
+ '-quiet',
36
+ '-z',
37
+ '9',
38
+ sheet,
39
+ '-o',
40
+ config.image
41
+ )
42
+
43
+ await fs.writeFile(
44
+ config.json,
45
+ JSON.stringify(parseAtlasJSON(JSON.parse(json)))
46
+ )
47
+
48
+ try {
49
+ await exec('biome', 'check', '--fix', config.json)
50
+ } catch {}
51
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs'
2
+ import esbuild from 'esbuild'
3
+ import type {AtlasConfig} from '../../schema/config-file.ts'
4
+ import * as V from '../../src/index.ts'
5
+ import type {VoidVersion} from '../../src/types/void-version.ts'
6
+ import {packAtlas} from '../atlas-pack/atlas-pack.ts'
7
+ import type {Config} from '../types/config.ts'
8
+ import {HTMLPlugin} from './html-plugin.ts'
9
+
10
+ export async function bundle(
11
+ config: Readonly<Config>,
12
+ srcFilenames: readonly string[],
13
+ voidVersion: Readonly<VoidVersion>
14
+ ): Promise<void> {
15
+ const opts: esbuild.BuildOptions = {
16
+ banner: config.watch
17
+ ? {
18
+ js: "new EventSource('/esbuild').addEventListener('change', () => location.reload());"
19
+ }
20
+ : {},
21
+ bundle: true,
22
+ define: {
23
+ // define on globalThis to avoid ReferenceError in unit tests.
24
+ 'globalThis.voidVersion': JSON.stringify(voidVersion)
25
+ },
26
+ entryPoints: [...srcFilenames],
27
+ format: 'esm',
28
+ logLevel: 'info', // print the port and build demarcations.
29
+ metafile: true,
30
+ minify: config.minify,
31
+ outdir: config.out.dir,
32
+ plugins: [HTMLPlugin(config)],
33
+ sourcemap: 'linked',
34
+ target: 'es2024' // https://esbuild.github.io/content-types/#tsconfig-json
35
+ }
36
+
37
+ if (config.preloadAtlas) await packAtlas(config.preloadAtlas)
38
+ if (config.preloadAtlas && config.watch) {
39
+ fs.watch(config.preloadAtlas.dir, {recursive: true}, (ev, type) =>
40
+ onWatch(config.preloadAtlas!, ev, type)
41
+ )
42
+ const ctx = await esbuild.context(opts)
43
+ await Promise.all([
44
+ ctx.watch(),
45
+ ctx.serve({port: 1234, servedir: config.out.dir})
46
+ ])
47
+ } else {
48
+ const build = await esbuild.build(opts)
49
+ if (config.meta)
50
+ await fs.promises.writeFile(config.meta, JSON.stringify(build.metafile))
51
+ }
52
+ }
53
+
54
+ const onWatch = V.debounce(
55
+ async (
56
+ config: Readonly<AtlasConfig>,
57
+ ev: fs.WatchEventType,
58
+ file: string | null
59
+ ) => {
60
+ console.log(`asset ${file} ${ev}.`)
61
+ await packAtlas(config)
62
+ },
63
+ 500 as V.Millis
64
+ )
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import type esbuild from 'esbuild'
4
+ import {minify} from 'html-minifier-next'
5
+ import type {Config} from '../types/config.ts'
6
+ import {exec} from '../utils/exec.ts'
7
+ import {parseHTML} from '../utils/html-parser.ts'
8
+
9
+ type Manifest = {
10
+ icons?:
11
+ | {
12
+ src?: string | undefined
13
+ size?: string | undefined
14
+ type?: string | undefined
15
+ }[]
16
+ | undefined
17
+ start_url?: string | undefined
18
+ version?: string | undefined
19
+ }
20
+
21
+ export function HTMLPlugin(config: Readonly<Config>): esbuild.Plugin {
22
+ return {
23
+ name: 'HTMLPlugin',
24
+ setup(build) {
25
+ build.onEnd(async _result => {
26
+ const doc = await parseHTML(config.entry)
27
+
28
+ const scripts = doc.querySelectorAll<HTMLScriptElement>(
29
+ "script[type='module'][src$='.ts']"
30
+ )
31
+ for (const script of scripts) {
32
+ // to-do: test.
33
+ const filename = path.basename(
34
+ script.getAttribute('src')!.replace(/\.ts$/, '.js')
35
+ )
36
+ if (config.oneFile) {
37
+ script.removeAttribute('src')
38
+ script.textContent = await fs.readFile(
39
+ path.join(config.out.dir, filename),
40
+ {encoding: 'utf8'}
41
+ )
42
+ } else script.src = filename
43
+ }
44
+
45
+ if (config.oneFile) {
46
+ const imgs = doc.querySelectorAll<HTMLImageElement>('img[src]')
47
+ for (const img of imgs) {
48
+ const filename = img.getAttribute('src')!
49
+ const b64 = await fs.readFile(path.join(config.out.dir, filename), {
50
+ encoding: 'base64'
51
+ })
52
+ const ext = filename.match(/\.([^.]+)$/)?.[1]
53
+ if (!ext) throw Error('no asset extension')
54
+ img.src = `data:image/${ext};base64,${b64}`
55
+ }
56
+ }
57
+
58
+ const manifestEl = doc.querySelector<HTMLLinkElement>(
59
+ 'link[href][rel="manifest"]'
60
+ )
61
+ if (manifestEl) {
62
+ const manifestFilename = path.resolve(
63
+ path.dirname(config.entry),
64
+ manifestEl.getAttribute('href')!
65
+ )
66
+ const manifest: Manifest = JSON.parse(
67
+ await fs.readFile(manifestFilename, 'utf8')
68
+ )
69
+ if (config.version) manifest.version = config.version
70
+
71
+ for (const icon of manifest.icons ?? []) {
72
+ if (!icon.src || !icon.type) continue
73
+ const iconFilename = `${path.dirname(manifestFilename)}/${icon.src}`
74
+ const file = await fs.readFile(iconFilename)
75
+ if (config.oneFile)
76
+ icon.src = `data:${icon.type};base64,${file.toString('base64')}`
77
+ else icon.src = path.relative(config.out.dir, iconFilename)
78
+ }
79
+ if (config.watch) manifest.start_url = 'http://localhost:1234'
80
+
81
+ if (config.oneFile)
82
+ manifestEl.href = `data:application/json,${encodeURIComponent(
83
+ JSON.stringify(manifest)
84
+ )}`
85
+ else
86
+ await fs.writeFile(
87
+ path.join(config.out.dir, path.basename(manifestFilename)),
88
+ JSON.stringify(manifest)
89
+ )
90
+ }
91
+
92
+ if (config.oneFile) {
93
+ const icons = doc.querySelectorAll<HTMLLinkElement>(
94
+ 'link[href][rel="icon"][type]'
95
+ )
96
+ for (const icon of icons) {
97
+ const filename = path.resolve(
98
+ config.out.dir,
99
+ icon.getAttribute('href')!
100
+ )
101
+ const file = await fs.readFile(filename)
102
+ icon.href = `data:${icon.type};base64,${file.toString('base64')}`
103
+ }
104
+ }
105
+
106
+ let html = `<!doctype html>\n${doc.documentElement.outerHTML}`
107
+
108
+ if (config.minify)
109
+ html = await minify(html, {
110
+ caseSensitive: true,
111
+ collapseBooleanAttributes: true,
112
+ html5: true,
113
+ minifyCSS: true,
114
+ removeAttributeQuotes: true,
115
+ removeComments: true,
116
+ removeEmptyAttributes: true
117
+ })
118
+ else
119
+ try {
120
+ html = await exec(
121
+ 'biome',
122
+ 'check',
123
+ '--fix',
124
+ `--stdin-file-path=${config.entry}`,
125
+ {stdin: html}
126
+ )
127
+ } catch {}
128
+
129
+ // to-do: test.
130
+ const outHTMLFilename = path.join(config.out.dir, config.out.filename)
131
+ await fs.writeFile(outHTMLFilename, html)
132
+ })
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,65 @@
1
+ import path from 'node:path'
2
+ import type {AtlasConfig, ConfigFile} from '../../schema/config-file.ts'
3
+ import type {Argv} from '../utils/argv.ts'
4
+ import type {PackageJSON} from './package-json.ts'
5
+
6
+ export type Config = {
7
+ $schema: string
8
+ entry: string
9
+ meta: string | undefined
10
+ out: {dir: string; filename: string}
11
+ preloadAtlas: AtlasConfig | undefined
12
+
13
+ /** config directory name. */
14
+ dirname: string
15
+ /** config filename. */
16
+ filename: string
17
+
18
+ minify: boolean
19
+ oneFile: boolean
20
+ watch: boolean
21
+
22
+ /** package publish date. */
23
+ published: string | undefined
24
+ /** package version. */
25
+ version: string | undefined
26
+
27
+ /** Git short hash. */
28
+ hash: string
29
+ }
30
+
31
+ export function Config(
32
+ configFile: Readonly<ConfigFile>,
33
+ argv: Readonly<Argv>,
34
+ packageJSON: Readonly<PackageJSON>,
35
+ hash: string
36
+ ): Config {
37
+ let fileStem = path.basename(configFile.entry).replace(/\.[^.]+$/, '')
38
+ if (configFile.out.name && !argv.opts['--watch'])
39
+ fileStem = configFile.out.name
40
+ let fileSuffix = '.html'
41
+ if (!argv.opts['--watch']) {
42
+ const version = packageJSON.version ? `v${packageJSON.version}` : ''
43
+ const published =
44
+ packageJSON.version && packageJSON.published
45
+ ? `+${packageJSON.published}`
46
+ : ''
47
+ const hashStr = packageJSON.version && hash ? `.${hash}` : ''
48
+ fileSuffix = `-${version}${published}${hashStr}.html`
49
+ }
50
+ return {
51
+ $schema: configFile.$schema,
52
+ entry: configFile.entry,
53
+ meta: configFile.meta,
54
+ out: {dir: configFile.out.dir, filename: `${fileStem}${fileSuffix}`},
55
+ preloadAtlas: configFile.preloadAtlas,
56
+ dirname: configFile.dirname,
57
+ filename: configFile.filename,
58
+ minify: argv.opts['--minify'] ?? false,
59
+ oneFile: argv.opts['--one-file'] ?? false,
60
+ watch: argv.opts['--watch'] ?? false,
61
+ published: packageJSON.published,
62
+ version: packageJSON.version,
63
+ hash
64
+ }
65
+ }
@@ -0,0 +1,4 @@
1
+ export type PackageJSON = {
2
+ version?: string | undefined
3
+ published?: string | undefined
4
+ }
@@ -0,0 +1,41 @@
1
+ import assert from 'node:assert/strict'
2
+ import {test} from 'node:test'
3
+ import {Argv} from './argv.ts'
4
+
5
+ declare module './argv.ts' {
6
+ interface Opts {
7
+ '--config'?: string
8
+ '--minify'?: true
9
+ '--watch'?: true
10
+ }
11
+ }
12
+
13
+ test('parses empty.', () => {
14
+ assert.deepEqual<Argv>(Argv(['/usr/local/bin/node', 'tools/void.ts']), {
15
+ args: [],
16
+ opts: {},
17
+ posargs: []
18
+ })
19
+ })
20
+
21
+ test('parses nonempty.', () => {
22
+ assert.deepEqual<Argv>(
23
+ Argv([
24
+ '/usr/local/bin/node',
25
+ 'tools/void.ts',
26
+ '--config=config',
27
+ '--minify',
28
+ '--watch',
29
+ 'a.aseprite',
30
+ 'b.aseprite',
31
+ '--',
32
+ 'posarg0',
33
+ 'posarg1'
34
+ ]),
35
+ {
36
+ args: ['a.aseprite', 'b.aseprite'],
37
+ opts: {'--config': 'config', '--minify': true, '--watch': true},
38
+ posargs: ['posarg0', 'posarg1']
39
+ }
40
+ )
41
+ })
@@ -0,0 +1,29 @@
1
+ export type Argv = {
2
+ /** all the arguments not starting with `--` before `--`. */
3
+ args: string[]
4
+ /** all options starting with `--` before `--` and their optional value. */
5
+ opts: Opts
6
+ /** everything after `--`. */
7
+ posargs: string[]
8
+ }
9
+
10
+ /** string-string|bool map. Eg, `'--config'?: string; '--minify'?: string | true` */
11
+ // biome-ignore lint/suspicious/noEmptyInterface: declaration merging.
12
+ export interface Opts {}
13
+
14
+ export function Argv(argv: readonly string[]): Argv {
15
+ const args = []
16
+ const posargs = []
17
+ const opts: {[k: string]: string | true} = {}
18
+ for (let i = 2; i < argv.length; i++) {
19
+ if (argv[i] === '--') {
20
+ posargs.push(...argv.slice(i + 1))
21
+ break
22
+ }
23
+ if (argv[i]!.startsWith('--')) {
24
+ const [k, v] = argv[i]!.split(/=(.*)/).slice(0, 2)
25
+ opts[k!] = v ?? true
26
+ } else args.push(argv[i]!)
27
+ }
28
+ return {args, opts, posargs}
29
+ }
@@ -0,0 +1,22 @@
1
+ import {execFile} from 'node:child_process'
2
+ import util from 'node:util'
3
+
4
+ export async function exec(
5
+ exe: string,
6
+ ...args:
7
+ | readonly [...string[], {stdin?: string | undefined}]
8
+ | readonly string[]
9
+ ): Promise<string> {
10
+ const opts = (typeof args.at(-1) === 'object' ? args.at(-1) : undefined) as
11
+ | {stdin?: string | undefined}
12
+ | undefined
13
+ args = (opts ? args.slice(0, -1) : args) as string[]
14
+ const promise = util.promisify(execFile)(exe, args, {})
15
+ if (promise.child.stdin && opts?.stdin != null) {
16
+ promise.child.stdin.write(opts.stdin)
17
+ promise.child.stdin.end()
18
+ }
19
+ const {stdout, stderr} = await promise
20
+ process.stderr.write(stderr)
21
+ return stdout
22
+ }
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs/promises'
2
+
3
+ export async function globAll(glob: string): Promise<string[]> {
4
+ const filenames = []
5
+ try {
6
+ for await (const filename of fs.glob(glob)) filenames.push(filename)
7
+ } catch (err) {
8
+ throw Error(`glob \`${glob}\` unreadable`, {cause: err})
9
+ }
10
+ return filenames
11
+ }
@@ -0,0 +1,9 @@
1
+ import {JSDOM} from 'jsdom'
2
+
3
+ export async function parseHTML(filename: string): Promise<Document> {
4
+ try {
5
+ return (await JSDOM.fromFile(filename)).window.document
6
+ } catch (err) {
7
+ throw Error(`HTML ${filename} unparsable`, {cause: err})
8
+ }
9
+ }
package/tools/void.ts ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // void.ts --config=<void.json> [--minify] [--one-file] [--watch]
3
+ // compiles images into an atlas and bundles an HTML entrypoint.
4
+
5
+ import path from 'node:path'
6
+ import voidPackageJSON from '../package.json' with {type: 'json'}
7
+ import {parseConfigFile} from '../schema/config-file.ts'
8
+ import type {VoidVersion} from '../src/types/void-version.ts'
9
+ import {bundle} from './bundle/bundle.ts'
10
+ import {Config} from './types/config.ts'
11
+ import type {PackageJSON} from './types/package-json.ts'
12
+ import {Argv} from './utils/argv.ts'
13
+ import {exec} from './utils/exec.ts'
14
+ import {parseHTML} from './utils/html-parser.ts'
15
+
16
+ declare module './utils/argv.ts' {
17
+ interface Opts {
18
+ '--config'?: string
19
+ '--minify'?: true
20
+ /** inline everything into a single HTML file output. */
21
+ '--one-file'?: true
22
+ /**
23
+ * run development server on http://localhost:1234 and reload on code
24
+ * change.
25
+ */
26
+ '--watch'?: true
27
+ }
28
+ }
29
+
30
+ export async function build(args: readonly string[]): Promise<void> {
31
+ const argv = Argv(args)
32
+ const configFile = await parseConfigFile(argv.opts['--config'] ?? 'void.json')
33
+ const hash = (await exec('git', 'rev-parse', '--short', 'HEAD')).trim()
34
+ const packageJSON: PackageJSON = JSON.parse(
35
+ (await exec('npm', 'pkg', 'get', 'version', 'published')) || '{}'
36
+ )
37
+ const config = Config(configFile, argv, packageJSON, hash)
38
+
39
+ const doc = await parseHTML(config.entry)
40
+ const srcFilenames = [
41
+ ...doc.querySelectorAll<HTMLScriptElement>(
42
+ "script[type='module'][src$='.ts']"
43
+ )
44
+ ].map(el => path.resolve(path.dirname(config.entry), el.getAttribute('src')!))
45
+
46
+ const voidVersion: VoidVersion = {
47
+ published: voidPackageJSON.published,
48
+ // imported JSON doesn't treeshake. define as a constant.
49
+ version: voidPackageJSON.version
50
+ }
51
+
52
+ await bundle(config, srcFilenames, voidVersion)
53
+ }
54
+
55
+ if (import.meta.main) await build(process.argv)
@@ -1,30 +0,0 @@
1
- /**
2
- * @template {AnimTag} T
3
- * @typedef {object} Anim
4
- * @prop {readonly Readonly<import('../types/2d.js').XY>[]} cels
5
- * @prop {Readonly<import('../types/2d.js').Box>} hitbox
6
- * @prop {number} id A multiple of 16 (maxAnimCels).
7
- * @prop {number} w
8
- * @prop {number} h
9
- * @prop {T & AnimTag} tag
10
- */
11
- /**
12
- * `--tagname-format={title}--{tag}`.
13
- * @typedef {`${string}--${string}`} AnimTag
14
- */
15
- export const maxAnimCels: 16;
16
- export type Anim<T extends `${string}--${string}`> = {
17
- cels: readonly Readonly<import('../types/2d.js').XY>[];
18
- hitbox: Readonly<import('../types/2d.js').Box>;
19
- /**
20
- * A multiple of 16 (maxAnimCels).
21
- */
22
- id: number;
23
- w: number;
24
- h: number;
25
- tag: T & AnimTag;
26
- };
27
- /**
28
- * `--tagname-format={title}--{tag}`.
29
- */
30
- export type AnimTag = `${string}--${string}`;