Mirai's Miscellaneous Misadventures
M43 / index.html
1<!-- license: AGPLv3 or later -->
2<!-- copyright 2023 zamfofex -->
3
4<!doctype html>
5<html lang="en">
6<meta charset="utf-8">
7
8<title> Random Character Adventures </title>
9
10<style>
11
12*, ::before, ::after
13{
14 touch-action: none;
15}
16
17html, body
18{
19 width: 100%;
20 height: 100%;
21 display: flex;
22}
23
24body
25{
26 margin: 0;
27 background: #000;
28}
29
30canvas
31{
32 width: 100%;
33 height: 100%;
34 image-rendering: crisp-edges;
35 image-rendering: pixelated;
36 object-fit: contain;
37 outline: none;
38}
39
40</style>
41
42<script type="module">
43
44let canvas = document.querySelector("canvas")
45let context = canvas.getContext("2d")
46
47let global = v =>
48{
49 if (typeof v === "number") return v
50 return v.value
51}
52
53let cache = new Map()
54let pending = []
55
56let schedule = f =>
57{
58 if (pending.length === 0)
59 f()
60 else
61 pending.push(f)
62}
63
64let pointer = a =>
65{
66 if (a < 0) a = 0x10000 + a
67 return a
68}
69
70let rch = [0x11, 0x44, 0x77, 0x99, 0xCC, 0xFF]
71let gch = [0x11, 0x33, 0x55, 0x77, 0x99, 0xBB, 0xDD, 0xFF]
72let bch = [0x22, 0x55, 0x88, 0xBB, 0xEE]
73
74let createTexture = getView => (data0, asset) =>
75{
76 let view = getView()
77 asset = pointer(asset)
78
79 let width = view.getUint32(asset, true)
80 let height = view.getUint32(asset + 4, true)
81 let colors = view.getUint32(asset + 8, true)
82
83 if (width === 0 || height === 0)
84 {
85 cache.delete(asset)
86 return
87 }
88
89 let image = new ImageData(width, height)
90 let {data} = image
91
92 for (let x = 0 ; x < width ; x++)
93 for (let y = 0 ; y < height ; y++)
94 {
95 let o = x + y * width
96 let color = view.getUint8(colors + o)
97
98 if (color === 0) continue
99
100 if (color < 0x10)
101 {
102 let ch = color * 0x11
103 data[o * 4 + 0] = ch
104 data[o * 4 + 1] = ch
105 data[o * 4 + 2] = ch
106 data[o * 4 + 3] = 0xFF
107 }
108 else
109 {
110 color -= 0x10
111 let r = rch[Math.floor(color / 40) % 6]
112 let g = gch[Math.floor(color / 5) % 8]
113 let b = bch[Math.floor(color / 1) % 5]
114
115 data[o * 4 + 0] = r
116 data[o * 4 + 1] = g
117 data[o * 4 + 2] = b
118 data[o * 4 + 3] = 0xFF
119 }
120 }
121
122 pending.push(async () => cache.set(asset, await createImageBitmap(image)))
123 return asset
124}
125
126let stamp = (data, x, y, asset) => schedule(() => context.drawImage(cache.get(asset), x, y))
127
128let invalidate = (data, asset) => schedule(() => cache.delete(asset))
129
130let main = async () =>
131{
132 let run = (start, tick, size) =>
133 {
134 let chapter = allocate(size)
135 let past = performance.now()
136 table.get(start)(chapter, engine)
137
138 let step = async () =>
139 {
140 if (pending.length !== 0)
141 {
142 for (let f of pending) await f()
143 pending = []
144 }
145
146 table.get(tick)(chapter, left, right)
147
148 let now = performance.now()
149 setTimeout(step, (100 / 3) + past - now)
150 past = now
151 }
152 step()
153 }
154
155 let memcpy = (a, b, n) =>
156 {
157 new Uint8Array(memory.buffer, pointer(a), n).set(new Uint8Array(memory.buffer, pointer(b), n))
158 return a
159 }
160
161 let memset = (a, b, n) =>
162 {
163 new Uint8Array(memory.buffer, pointer(a), n).set(Array(n).fill(pointer(b)))
164 return a
165 }
166
167 let memcmp = (a, b, n) =>
168 {
169 a = new Uint8Array(memory.buffer, pointer(a), n)
170 b = new Uint8Array(memory.buffer, pointer(b), n)
171 for (let i = 0 ; i < n ; i++)
172 {
173 if (a[i] !== b[i])
174 return a[i] - b[i]
175 }
176 return 0
177 }
178
179 let getView = () => new DataView(memory.buffer)
180
181 let imports =
182 {
183 env:
184 {
185 memcpy, memmove: memcpy, memset, memcmp,
186 mimimi_wasm_javascript_texture: createTexture(getView),
187 mimimi_wasm_javascript_invalidate: invalidate,
188 mimimi_wasm_javascript_stamp: stamp,
189 mimimi_wasm: run,
190 }
191 }
192
193 let body = await fetch("mimimi-loader.wasm")
194
195 let exports
196 try { exports = (await WebAssembly.instantiateStreaming(body, imports)).instance.exports }
197 catch (e) { exports = (await WebAssembly.instantiate(await body.arrayBuffer(), imports)).instance.exports }
198
199 let {memory, __indirect_function_table: table, __heap_base: base} = exports
200 let {mimimi_wasm_texture, mimimi_wasm_invalidate, mimimi_wasm_stamp} = exports
201 let {main} = exports
202
203 let f = table.grow(3)
204 table.set(f + 0, mimimi_wasm_texture)
205 table.set(f + 1, mimimi_wasm_invalidate)
206 table.set(f + 2, mimimi_wasm_stamp)
207
208 let offset = global(base)
209 let allocate = size =>
210 {
211 let result = offset
212 let available = memory.buffer.byteLength - offset
213 if (available < size) memory.grow(Math.ceil((size - available) / 0x10000))
214 offset += size
215 return result
216 }
217
218 let engine = allocate(24)
219 let view = getView()
220 view.setUint32(engine + 4, f + 0, true)
221 view.setUint32(engine + 8, f + 1, true)
222 view.setUint32(engine + 12, f + 2, true)
223 view.setUint32(engine + 16, 512, true)
224 view.setUint32(engine + 20, 256, true)
225
226 let left = 0
227 let right = 0
228
229 addEventListener("keydown", ({code}) =>
230 {
231 if (code === "ArrowLeft") left = 1
232 if (code === "ArrowRight") right = 1
233 if (code === "KeyA") left = 1
234 if (code === "KeyD") right = 1
235 })
236
237 addEventListener("keyup", ({code}) =>
238 {
239 if (code === "ArrowLeft") left = 0
240 if (code === "ArrowRight") right = 0
241 if (code === "KeyA") left = 0
242 if (code === "KeyD") right = 0
243 })
244
245 addEventListener("pointerdown", ({offsetX, button}) =>
246 {
247 if (button !== 0) return
248
249 if (offsetX < innerWidth / 2)
250 left = 1
251 else
252 right = 1
253 })
254
255 addEventListener("pointerup", () => { left = 0 ; right = 0 })
256
257 main()
258}
259
260main()
261
262</script>
263
264<canvas width="512" height="256"></canvas>