Mirai's Miscellaneous Misadventures

M46 / index.html

<!-- license: AGPLv3 or later -->
<!-- copyright 2023 zamfofex -->

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">

<title> Random Character Adventures </title>

<style>

*, ::before, ::after
{
	touch-action: none;
}

html, body
{
	width: 100%;
	height: 100%;
	display: flex;
}

body
{
	margin: 0;
	background: #000;
}

canvas
{
	width: 100%;
	height: 100%;
	image-rendering: crisp-edges;
	image-rendering: pixelated;
	object-fit: contain;
	outline: none;
}

</style>

<script type="module">

let canvas = document.querySelector("canvas")
let context = canvas.getContext("2d")

let global = v =>
{
	if (typeof v === "number") return v
	return v.value
}

let cache = new Map()
let pending = []

let schedule = f =>
{
	if (pending.length === 0)
		f()
	else
		pending.push(f)
}

let pointer = a =>
{
	if (a < 0) a = 0x10000 + a
	return a
}

let rch = [0x11, 0x44, 0x77, 0x99, 0xCC, 0xFF]
let gch = [0x11, 0x33, 0x55, 0x77, 0x99, 0xBB, 0xDD, 0xFF]
let bch = [0x22, 0x55, 0x88, 0xBB, 0xEE]

let createTexture = getView => (data0, asset) =>
{
	let view = getView()
	asset = pointer(asset)
	
	let width = view.getUint32(asset, true)
	let height = view.getUint32(asset + 4, true)
	let colors = view.getUint32(asset + 8, true)
	
	if (width === 0 || height === 0)
	{
		cache.delete(asset)
		return
	}
	
	let image = new ImageData(width, height)
	let {data} = image
	
	for (let x = 0 ; x < width ; x++)
	for (let y = 0 ; y < height ; y++)
	{
		let o = x + y * width
		let color = view.getUint8(colors + o)
		
		if (color === 0) continue
		
		if (color < 0x10)
		{
			let ch = color * 0x11
			data[o * 4 + 0] = ch
			data[o * 4 + 1] = ch
			data[o * 4 + 2] = ch
			data[o * 4 + 3] = 0xFF
		}
		else
		{
			color -= 0x10
			let r = rch[Math.floor(color / 40) % 6]
			let g = gch[Math.floor(color / 5) % 8]
			let b = bch[Math.floor(color / 1) % 5]
			
			data[o * 4 + 0] = r
			data[o * 4 + 1] = g
			data[o * 4 + 2] = b
			data[o * 4 + 3] = 0xFF
		}
	}
	
	pending.push(async () => cache.set(asset, await createImageBitmap(image)))
	return asset
}

let stamp = (data, x, y, asset) => schedule(() => context.drawImage(cache.get(asset), x, y))

let invalidate = (data, asset) => schedule(() => cache.delete(asset))

let main = async () =>
{
	let run = (start, tick, size) =>
	{
		let chapter = allocate(size)
		let past = performance.now()
		table.get(start)(chapter, engine)
		
		let step = async () =>
		{
			if (pending.length !== 0)
			{
				for (let f of pending) await f()
				pending = []
			}
			
			table.get(tick)(chapter, left, right)
			
			let now = performance.now()
			setTimeout(step, (100 / 3) + past - now)
			past = now
		}
		step()
	}
	
	let memcpy = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, pointer(a), n).set(new Uint8Array(memory.buffer, pointer(b), n))
		return a
	}
	
	let memset = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, pointer(a), n).set(Array(n).fill(pointer(b)))
		return a
	}
	
	let memcmp = (a, b, n) =>
	{
		a = new Uint8Array(memory.buffer, pointer(a), n)
		b = new Uint8Array(memory.buffer, pointer(b), n)
		for (let i = 0 ; i < n ; i++)
		{
			if (a[i] !== b[i])
				return a[i] - b[i]
		}
		return 0
	}
	
	let getView = () => new DataView(memory.buffer)
	
	let imports =
	{
		env:
		{
			memcpy, memmove: memcpy, memset, memcmp,
			mimimi_wasm_javascript_texture: createTexture(getView),
			mimimi_wasm_javascript_invalidate: invalidate,
			mimimi_wasm_javascript_stamp: stamp,
			mimimi_wasm: run,
		}
	}
	
	let body = await fetch("mimimi-loader.wasm")
	
	let exports
	try { exports = (await WebAssembly.instantiateStreaming(body, imports)).instance.exports }
	catch (e) { exports = (await WebAssembly.instantiate(await body.arrayBuffer(), imports)).instance.exports }
	
	let {memory, __indirect_function_table: table, __heap_base: base} = exports
	let {mimimi_wasm_texture, mimimi_wasm_invalidate, mimimi_wasm_stamp} = exports
	let {main} = exports
	
	let f = table.grow(3)
	table.set(f + 0, mimimi_wasm_texture)
	table.set(f + 1, mimimi_wasm_invalidate)
	table.set(f + 2, mimimi_wasm_stamp)
	
	let offset = global(base)
	let allocate = size =>
	{
		let result = offset
		let available = memory.buffer.byteLength - offset
		if (available < size) memory.grow(Math.ceil((size - available) / 0x10000))
		offset += size
		return result
	}
	
	let engine = allocate(24)
	let view = getView()
	view.setUint32(engine + 4, f + 0, true)
	view.setUint32(engine + 8, f + 1, true)
	view.setUint32(engine + 12, f + 2, true)
	view.setUint32(engine + 16, 512, true)
	view.setUint32(engine + 20, 256, true)
	
	let left = 0
	let right = 0
	
	addEventListener("keydown", ({code}) =>
	{
		if (code === "ArrowLeft") left = 1
		if (code === "ArrowRight") right = 1
		if (code === "KeyA") left = 1
		if (code === "KeyD") right = 1
	})
	
	addEventListener("keyup", ({code}) =>
	{
		if (code === "ArrowLeft") left = 0
		if (code === "ArrowRight") right = 0
		if (code === "KeyA") left = 0
		if (code === "KeyD") right = 0
	})
	
	addEventListener("contextmenu", event => event.preventDefault())
	
	addEventListener("pointerdown", event =>
	{
		if (event.pointerType === "mouse") return
		if (event.button !== 0) return
		
		if (event.offsetX < innerWidth / 2)
			left = 1
		else
			right = 1
	})
	
	addEventListener("pointerup", event =>
	{
		if (event.pointerType === "mouse") return
		if (event.button !== 0) return
		
		left = 0
		right = 0
	})
	
	addEventListener("mousedown", ({button}) =>
	{
		if (button === 0) left = 1
		if (button === 2) right = 1
	})
	
	addEventListener("mouseup", ({button}) =>
	{
		if (button === 0) left = 0
		if (button === 2) right = 0
	})
	
	main()
}

main()

</script>

<canvas width="512" height="256"></canvas>