kinda/web/sim.js
2026-07-08 16:47:24 -07:00

174 lines
4.1 KiB
JavaScript

const EVENT_UP = 0;
const EVENT_DOWN = 1;
const EVENT_SELECT = 2;
const EVENT_BACK = 3;
const canvas = document.getElementById('display');
const context = canvas.getContext('2d');
const status = document.getElementById('status');
const buttons = Array.from(document.querySelectorAll('[data-event]'));
let wasm;
let lastPointerTime = -Infinity;
async function loadWasm() {
const imports = {};
const response = fetch('./pkg/kinda.wasm');
if ('instantiateStreaming' in WebAssembly) {
try {
return await WebAssembly.instantiateStreaming(response, imports);
} catch (error) {
console.warn('Falling back to ArrayBuffer WebAssembly load', error);
}
}
const bytes = await (await fetch('./pkg/kinda.wasm')).arrayBuffer();
return WebAssembly.instantiate(bytes, imports);
}
function render() {
const width = wasm.kinda_sim_width();
const height = wasm.kinda_sim_height();
const pixelPointer = wasm.kinda_sim_pixels();
const pixelLength = wasm.kinda_sim_pixel_len();
const pixels = new Uint8Array(wasm.memory.buffer, pixelPointer, pixelLength);
canvas.width = width;
canvas.height = height;
const image = context.createImageData(width, height);
for (let source = 0, target = 0; source < pixels.length; source += 1, target += 4) {
const value = Math.round(18 + (pixels[source] * 230) / 255);
image.data[target] = value;
image.data[target + 1] = value;
image.data[target + 2] = value;
image.data[target + 3] = 255;
}
context.putImageData(image, 0, 0);
}
function sendInput(eventCode) {
if (!wasm) {
return;
}
wasm.kinda_sim_input(eventCode);
render();
}
function sendTouch(clientX, clientY) {
if (!wasm) {
return;
}
const bounds = canvas.getBoundingClientRect();
const x = Math.max(
0,
Math.min(canvas.width - 1, Math.floor(((clientX - bounds.left) * canvas.width) / bounds.width))
);
const y = Math.max(
0,
Math.min(canvas.height - 1, Math.floor(((clientY - bounds.top) * canvas.height) / bounds.height))
);
wasm.kinda_sim_touch(x, y);
render();
}
function capturePointer(event) {
if (!canvas.setPointerCapture || event.pointerId === undefined) {
return;
}
try {
canvas.setPointerCapture(event.pointerId);
} catch (_) {
// Synthetic and some browser-generated events do not have an active pointer.
}
}
function handlePointerDown(event) {
event.preventDefault();
lastPointerTime = event.timeStamp;
capturePointer(event);
sendTouch(event.clientX, event.clientY);
}
function handleMouseDown(event) {
if (event.timeStamp - lastPointerTime < 50) {
return;
}
event.preventDefault();
sendTouch(event.clientX, event.clientY);
}
function handleTouchStart(event) {
if (event.timeStamp - lastPointerTime < 50) {
return;
}
const touch = event.changedTouches[0];
if (!touch) {
return;
}
event.preventDefault();
sendTouch(touch.clientX, touch.clientY);
}
function keyboardEventCode(event) {
if (event.key === 'ArrowUp') {
return EVENT_UP;
}
if (event.key === 'ArrowDown') {
return EVENT_DOWN;
}
if (event.key === 'Enter') {
return EVENT_SELECT;
}
if (event.key === 'Escape' || event.key === 'Backspace') {
return EVENT_BACK;
}
return null;
}
buttons.forEach((button) => {
button.disabled = true;
button.addEventListener('click', () => sendInput(Number(button.dataset.event)));
});
canvas.addEventListener('pointerdown', handlePointerDown);
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
window.addEventListener('keydown', (event) => {
const eventCode = keyboardEventCode(event);
if (eventCode === null) {
return;
}
event.preventDefault();
sendInput(eventCode);
});
try {
const instance = await loadWasm();
wasm = instance.instance.exports;
wasm.kinda_sim_init();
render();
buttons.forEach((button) => {
button.disabled = false;
});
status.textContent = 'Ready. Press buttons or click/touch the display.';
} catch (error) {
console.error(error);
status.textContent = error instanceof Error ? error.message : 'Could not load simulator.';
}