Mirai's Miscellaneous Misadventures
M55 / index.html
1<!-- license: AGPLv3 or later -->
2<!-- copyright 2024 zamfofex -->
3
4<!doctype html>
5<html lang="en">
6<meta charset="utf-8">
7<meta name="viewport" content="width=device-width">
8
9<title> Mirai's Miscellaneous Misadventures </title>
10
11<style>
12
13*, ::before, ::after {
14 touch-action: none;
15}
16
17html, body {
18 width: 100%;
19 height: 100%;
20 display: flex;
21}
22
23body {
24 margin: 0;
25 background: #000;
26}
27
28canvas {
29 image-rendering: crisp-edges;
30 image-rendering: pixelated;
31 object-fit: contain;
32 outline: none;
33}
34
35</style>
36
37<script type="module">
38
39let canvas = document.querySelector("canvas")
40let context = canvas.getContext("2d")
41
42let rch = [0x11, 0x44, 0x77, 0x99, 0xCC, 0xFF]
43let gch = [0x11, 0x33, 0x55, 0x77, 0x99, 0xBB, 0xDD, 0xFF]
44let bch = [0x22, 0x55, 0x88, 0xBB, 0xEE]
45
46let image = new ImageData(1, 1)
47
48let time = -Infinity
49
50let tick = () =>
51{
52 time += 100 / 3
53 let now = performance.now()
54 if (now > time) console.log(`lag: ${now - time} milliseconds`), time = now
55 setTimeout(tick, time - now)
56
57 let pad = {}
58 try { pad = getGamepadInputs() }
59 catch (e) { }
60
61 let address = mimimi_wasm_tick(left || pad.left, right || pad.right, innerWidth, innerHeight)
62
63 let view = new DataView(memory.buffer)
64
65 let width = view.getInt32(address, true)
66 let height = view.getInt32(address + 4, true)
67 let colors = view.getUint32(address + 8, true)
68
69 for (let [i, {oscillator, gain}] of notes.entries()) {
70 let frequency = view.getUint8(address + 12 + i * 2)
71 let volume = view.getUint8(address + 12 + i * 2 + 1)
72 oscillator.frequency.value = 440 * 2 ** ((frequency / 2 - 69) / 12)
73 gain.gain.value = volume / 256
74 }
75
76 audio.resume()
77
78 if (image.width !== width || image.height !== height) {
79 image = new ImageData(width, height)
80 }
81
82 let {data} = image
83
84 for (let y = 0 ; y < height ; y++) {
85
86 for (let x = 0 ; x < width ; x++) {
87
88 let o = x + y * width
89 let color = view.getUint8(colors + o)
90
91 if (color === 0) continue
92
93 if (color < 0x10) {
94 let ch = color * 0x11
95 data[o * 4 + 0] = ch
96 data[o * 4 + 1] = ch
97 data[o * 4 + 2] = ch
98 data[o * 4 + 3] = 0xFF
99 }
100 else {
101 color -= 0x10
102 data[o * 4 + 0] = rch[Math.floor(color / 40) % 6]
103 data[o * 4 + 1] = gch[Math.floor(color / 5) % 8]
104 data[o * 4 + 2] = bch[Math.floor(color / 1) % 5]
105 data[o * 4 + 3] = 0xFF
106 }
107 }
108 }
109
110 canvas.width = width
111 canvas.height = height
112 context.putImageData(image, 0, 0)
113}
114
115let mimimi_wasm = () =>
116{
117 let data = malloc(4096 + 16)
118 if (data < 0) data += 0x10000
119
120 let view = new DataView(memory.buffer)
121 view.setUint32(data, data + 8, true)
122 view.setUint32(data + 4, 4096, true)
123
124 tick()
125}
126
127let imports = {
128 env: {mimimi_wasm},
129 wasi_snapshot_preview1: {proc_exit: () => { throw new Error() }},
130}
131
132let response = await fetch("mimimi.wasm")
133let buffer = await response.arrayBuffer()
134let {instance} = await WebAssembly.instantiate(buffer, imports)
135let {memory, _initialize, malloc, main} = instance.exports
136let {mimimi_wasm_tick} = instance.exports
137
138let left = 0
139let right = 0
140
141let leftKeys = ["ArrowLeft", "KeyA", "Numpad4", "KeyH", "Convert"]
142let rightKeys = ["ArrowRight", "KeyD", "Numpad6", "KeyL", "NonConvert"]
143
144addEventListener("keydown", ({code}) =>
145{
146 if (leftKeys.includes(code)) left = 1
147 if (rightKeys.includes(code)) right = 1
148})
149
150addEventListener("keyup", ({code}) =>
151{
152 if (leftKeys.includes(code)) left = 0
153 if (rightKeys.includes(code)) right = 0
154})
155
156addEventListener("contextmenu", event => event.preventDefault())
157
158addEventListener("pointerdown", event =>
159{
160 if (event.pointerType === "mouse") return
161 if (event.button !== 0) return
162 if (event.offsetX < innerWidth / 2) left = 1
163 else right = 1
164})
165
166addEventListener("pointerup", event =>
167{
168 if (event.pointerType === "mouse") return
169 if (event.button !== 0) return
170 left = 0
171 right = 0
172})
173
174addEventListener("mousedown", ({button}) =>
175{
176 if (button === 0) left = 1
177 if (button === 2) right = 1
178})
179
180addEventListener("mouseup", ({button}) =>
181{
182 if (button === 0) left = 0
183 if (button === 2) right = 0
184})
185
186let leftButtons = [2, 4, 6, 8, 10, 12, 13, 14]
187let rightButtons = [0, 1, 3, 5, 7, 9, 11, 15]
188let controlAxes = [0, 2]
189
190let getGamepadInputs = () =>
191{
192 let left = false
193 let right = false
194 for (let {mapping, buttons, axes} of navigator.getGamepads()) {
195
196 for (let i of leftButtons) {
197 if (buttons[i] && buttons[i].pressed) {
198 left = true
199 break
200 }
201 }
202
203 for (let i of rightButtons) {
204 if (buttons[i] && buttons[i].pressed) {
205 right = true
206 break
207 }
208 }
209
210 for (let i of controlAxes) {
211 if (i >= axes.length) continue
212 if (axes[i] < -0.25) left = true
213 if (axes[i] > 0.25) right = true
214 }
215 }
216
217 return {left, right}
218}
219
220let audio = new AudioContext()
221let notes = []
222
223for (let i = 0 ; i < 256 ; i++) {
224
225 let oscillator = audio.createOscillator()
226 let gain = audio.createGain()
227
228 gain.gain.value = 0
229
230 oscillator.connect(gain)
231 gain.connect(audio.destination)
232 oscillator.start()
233
234 notes.push({oscillator, gain})
235}
236
237_initialize()
238main()
239
240</script>
241
242<canvas width="512" height="256"></canvas>