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>