diff options
Diffstat (limited to 'index.htm')
-rw-r--r-- | index.htm | 687 |
1 files changed, 460 insertions, 227 deletions
@@ -8,275 +8,508 @@ </head> <body style="margin: 0; height: 100%; overflow: hidden; overflow-x: hidden;"> <canvas style="margin: 0; width: 100%; height: 100%;" id="frame" oncontextmenu="event.preventDefault()"></canvas> + <script type="worklet"> + registerProcessor( + "Sound_Node", + class Sound_Node extends AudioWorkletProcessor { + constructor(options) { + super(); + + this.frame_width = 0; + this.frame_height = 0; + + let event_handlers = { + wasm : (ev) => { + WebAssembly.instantiate( + ev.wasm, + { + wasi_snapshot_preview1 : { + clock_time_get : () => { return Date.now(); }, + args_sizes_get : () => { throw new Error("Unexpected args_sizes_get call"); }, + args_get : () => { throw new Error("Unexpected args_get call"); }, + proc_exit : () => { throw new Error("Unexpected proc_exit call"); }, + fd_close : () => { throw new Error("Unexpected fd_close call"); }, + fd_write : () => { throw new Error("Unexpected fd_write call"); }, + fd_seek : () => { throw new Error("Unexpected fd_seek call"); }, + }, + env : { + p_clipboard_write_impl : (size, text) => { + let text_buffer = new ArrayBuffer(size); + new Uint8Array(text_buffer).set(new Uint8Array(this.memory_buffer, text, size)); + + this.port.postMessage({ + id : "clipboard", + text_buffer : text_buffer, + }); + }, + p_time_impl : Date.now, + + floor : Math.floor, + ceil : Math.ceil, + sqrt : Math.sqrt, + pow : Math.pow, + log : Math.log, + log2 : Math.log2, + log10 : Math.log10, + exp : Math.exp, + sin : Math.sin, + cos : Math.cos, + tan : Math.tan, + asin : Math.asin, + acos : Math.acos, + atan : Math.atan, + atan2 : Math.atan2, + }, + } + ).then((program) => { + program.exports.js_main(options.processorOptions.href); + + this.memory_buffer = program.exports.memory.buffer; + this.sound_buffer_address = program.exports.js_sound_buffer(); + this.pixels_address = program.exports.js_pixels(); + this.frames = new Float32Array(this.memory_buffer, this.sound_buffer_address, program.exports.js_max_num_audio_frames()); + + let bytes = new Uint8Array(this.memory_buffer); + let title_address = program.exports.js_title(); + let len = 0; + while (bytes[title_address + len] != 0) + ++len; + let title_buffer = new ArrayBuffer(len); + new Uint8Array(title_buffer).set(bytes.subarray(title_address, title_address + len)); + + this.port.postMessage({ + id : "init", + title_buffer : title_buffer, + }); + + this.program = program; + }); + }, + + resize : (ev) => { + this.frame_width = ev.width; + this.frame_height = ev.height; + }, + + mousedown : (ev) => { + if (this.program === undefined) + return; + + this.program.exports.js_mousedown(ev.buttons); + }, + + mouseup : (ev) => { + if (this.program === undefined) + return; + + this.program.exports.js_mouseup(ev.buttons); + }, + + mousemove : (ev) => { + if (this.program === undefined) + return; + + this.program.exports.js_mousemove(ev.x, ev.y); + }, + + keydown : (ev) => { + if (this.program === undefined) + return; + + this.program.exports.js_keydown(ev.key, ev.mod, ev.ch); + }, + + keyup : (ev) => { + if (this.program === undefined) + return; + + this.program.exports.js_keyup(ev.key, ev.mod); + }, + + paste : (ev) => { + if (this.program === undefined) + return; + + let address = this.program.exports.js_clipboard_buffer(ev.bytes.length); + let len = this.program.exports.js_clipboard_size(); + + new Uint8Array(this.memory_buffer, address, len).set(ev.bytes.subarray(0, len)); + + this.program.exports.js_keydown(ev.key, ev.mod, ev.ch); + }, + }; + + this.port.onmessage = (ev) => { + if (ev.data.id in event_handlers) + event_handlers[ev.data.id](ev.data); + }; + } + + process(input, output, parameters) { + if (this.program === undefined) + return true; + + let num_channels = output[0].length; + let num_samples = output[0][0].length; + let num_frames = num_samples * num_channels; + + this.program.exports.js_frame(this.frame_width, this.frame_height, num_samples); + + let pixels_size = this.frame_width * this.frame_height * 4; + let pixels_buffer = new ArrayBuffer(pixels_size); + new Uint8Array(pixels_buffer).set(new Uint8Array(this.memory_buffer, this.pixels_address, pixels_size)); + + this.port.postMessage({ + id : "frame", + pixels_buffer : pixels_buffer, + }); + + for (let j = 0; j < num_channels; ++j) + for (let i = 0; i < num_samples; ++i) + output[0][j][i] = this.frames[j * num_samples + i]; + + return true; + } + } + ); + </script> <script type="text/javascript"> - function string_from_memory(bytes, offset) { - let len = 0; - while (bytes[offset + len] != 0) - ++len; - return new TextDecoder("utf8").decode(bytes.subarray(offset, offset + len)); + const KEY_PASTE = 118; + + let key_map = { + "Backspace" : 8, + "Tab" : 9, + "Enter" : 10, + "ControlLeft" : 11, + "ControlRight" : 12, + "ShiftLeft" : 13, + "ShiftRight" : 14, + "AltLeft" : 15, + "AltRight" : 16, + "ArrowLeft" : 17, + "ArrowRight" : 18, + "ArrowUp" : 19, + "ArrowDown" : 20, + "Pause" : 21, + "Insert" : 22, + "Home" : 23, + "End" : 24, + "PageUp" : 25, + "PageDown" : 26, + "Escape" : 27, + "PrintScreen" : 28, + "Space" : 32, + "MetaLeft" : 33, + "MetaRight" : 34, + "Quote" : 39, + "Comma" : 44, + "Minus" : 45, + "Period" : 46, + "Slash" : 47, + "Digit0" : 48, + "Digit1" : 49, + "Digit2" : 50, + "Digit3" : 51, + "Digit4" : 52, + "Digit5" : 53, + "Digit6" : 54, + "Digit7" : 55, + "Digit8" : 56, + "Digit9" : 57, + "Semicolon" : 59, + "Equal" : 61, + "BracketLeft" : 91, + "Backslash" : 92, + "BracketRight" : 93, + "Backquote" : 96, + "KeyA" : 97, + "KeyB" : 98, + "KeyC" : 99, + "KeyD" : 100, + "KeyE" : 101, + "KeyF" : 102, + "KeyG" : 103, + "KeyH" : 104, + "KeyI" : 105, + "KeyJ" : 106, + "KeyK" : 107, + "KeyL" : 108, + "KeyM" : 109, + "KeyN" : 110, + "KeyO" : 111, + "KeyP" : 112, + "KeyQ" : 113, + "KeyR" : 114, + "KeyS" : 115, + "KeyT" : 116, + "KeyU" : 117, + "KeyV" : KEY_PASTE, + "KeyW" : 119, + "KeyX" : 120, + "KeyY" : 121, + "KeyZ" : 122, + "Delete" : 127, + "F1" : 145, + "F2" : 146, + "F3" : 147, + "F4" : 148, + "F5" : 149, + "F6" : 150, + "F7" : 151, + "F8" : 152, + "F9" : 153, + "F10" : 154, + "F11" : 155, + "F12" : 156, + "F13" : 157, + "F14" : 158, + "F15" : 159, + "F16" : 160, + "F17" : 161, + "F18" : 162, + "F19" : 163, + "F20" : 164, + "F21" : 165, + "F22" : 166, + "F23" : 167, + "F24" : 168, + }; + + function key_from_code(code) { + if (code in key_map) + return key_map[code]; + return 0; + }; + + function mod_from_event(ev) { + let mod = 0; + if (ev.ctrlKey) mod |= 1; + if (ev.shiftKey) mod |= 2; + if (ev.altKey) mod |= 4; + if (ev.metaKey) mod |= 8; + return mod; + }; + + function char_from_event(ev) { + if (ev.key.length != 1) + return 0; + return ev.key.charCodeAt(0); } async function run(attrs) { - let wait_for_events = false; - let sleep_duration = 0; + let sound_sample_rate = 44100; // TODO + let sound_num_channels = 2; // TODO + + let frame_width = 0; + let frame_height = 0; let canvas; - let program; - let memory; - let pixels_address; let context; + let pixels_buffer; let animation_frame; - canvas = document.getElementById(attrs.id); + let sound_ready = false; + let sound_init; + let sound_context; + let sound_node; + + let paste_mod = 0; + let paste_ch = 0; + + canvas = attrs.canvas; context = canvas.getContext("2d"); - program = await WebAssembly.instantiateStreaming( - fetch("index.wasm"), - { - "wasi_snapshot_preview1" : { - clock_time_get : () => { return Date.now(); }, - args_sizes_get : () => { throw new Error("Unexpected args_sizes_get call"); }, - args_get : () => { throw new Error("Unexpected args_get call"); }, - proc_exit : () => { throw new Error("Unexpected proc_exit call"); }, - fd_close : () => { throw new Error("Unexpected fd_close call"); }, - fd_write : () => { throw new Error("Unexpected fd_write call"); }, - fd_seek : () => { throw new Error("Unexpected fd_seek call"); }, - }, - "env" : { - p_init : () => { - program.instance.exports.js_init(); - memory = new Uint8Array(program.instance.exports.memory.buffer); - pixels_address = program.instance.exports.js_pixels(); - document.title = string_from_memory(memory, program.instance.exports.js_title()); - }, - p_render_frame_impl : () => { - let data = new ImageData( - new Uint8ClampedArray( - memory.subarray( - pixels_address, - pixels_address + 4 * canvas.width * canvas.height - ) - ), - canvas.width, - canvas.height - ); - - context.putImageData(data, 0, 0); - }, - p_clipboard_write : (size, text) => { - navigator.clipboard.writeText( - new TextDecoder().decode(memory.subarray(text, text + size)) - ).catch((e) => { console.error(e); }); - }, - p_cleanup : () => {}, - p_wait_events_impl : () => { wait_for_events = true; }, - p_sleep_for : (time) => { sleep_duration += time; }, - p_time : Date.now, - - floor : Math.floor, - ceil : Math.ceil, - sqrt : Math.sqrt, - pow : Math.pow, - log : Math.log, - log2 : Math.log2, - log10 : Math.log10, - exp : Math.exp, - sin : Math.sin, - cos : Math.cos, - tan : Math.tan, - asin : Math.asin, - acos : Math.acos, - atan : Math.atan, - atan2 : Math.atan2, - - memset : (dst, val, num) => { program.instance.exports.js_memset(dst, val, num); }, - memcpy : (dst, src, num) => { program.instance.exports.js_memcpy(dst, src, num); }, - }, - } - ); + context.fillStyle = "#000"; + context.fillRect(0, 0, canvas.width, canvas.height); - program.instance.exports.js_main(document.location.href); + context.fillStyle = "#fff"; + context.font = "1.7em serif"; + context.textAlign = "center"; + context.textBaseline = "middle"; + context.fillText("Click to run", canvas.width / 2.0, canvas.height / 2.0); - animation_frame = (time) => { + animation_frame = () => { if (attrs.fit_window) { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } - wait_for_events = false; - program.instance.exports.js_frame(canvas.width, canvas.height); - - if (!wait_for_events) { - if (sleep_duration > 0) { - let timeout = sleep_duration; - sleep_duration = 0; - setTimeout(() => { - window.requestAnimationFrame(animation_frame); - }, timeout); - } else - window.requestAnimationFrame(animation_frame); + if (frame_width != canvas.width || frame_height != canvas.height) { + frame_width = canvas.width; + frame_height = canvas.height; + + sound_node.port.postMessage({ + id : "resize", + width : frame_width, + height : frame_height, + }); + } + + let num_bytes = 4 * canvas.width * canvas.height; + + if (pixels_buffer !== undefined && pixels_buffer.byteLength >= num_bytes) { + let pixels = new Uint8ClampedArray(pixels_buffer, 0, num_bytes); + + let data = new ImageData( + pixels, + canvas.width, + canvas.height + ); + + context.putImageData(data, 0, 0); + } else { + context.fillStyle = "#000"; + context.fillRect(0, 0, canvas.width, canvas.height); } - }; - window.addEventListener("resize", (ev) => { window.requestAnimationFrame(animation_frame); - }); + }; + + sound_init = async () => { + sound_context = new AudioContext({ + sampleRate : sound_sample_rate, + }); + + let blob = new Blob( + [ document.querySelector("script[type=worklet]").innerText ], + { type : "application/javascript", } + ); + + await sound_context.audioWorklet.addModule(URL.createObjectURL(blob)); + + sound_node = new AudioWorkletNode( + sound_context, + "Sound_Node", + { numberOfInputs : 0, + outputChannelCount : [ sound_num_channels ], + processorOptions : { href : document.location.href }, } + ); + + sound_node.connect(sound_context.destination); + + let message_handlers = { + init : (ev) => { + document.title = new TextDecoder("utf8").decode(new Uint8Array(ev.title_buffer)); + sound_ready = true; + + window.requestAnimationFrame(animation_frame); + }, + + clipboard : (ev) => { + navigator.clipboard.writeText(new TextDecoder("utf8").decode(new Uint8Array(ev.text_buffer))); + }, + + frame : (ev) => { + pixels_buffer = ev.pixels_buffer; + } + }; + + sound_node.port.onmessage = (ev) => { + if (ev.data.id in message_handlers) + message_handlers[ev.data.id](ev.data); + }; + + sound_node.port.postMessage({ + id : "wasm", + wasm : await WebAssembly.compileStreaming(attrs.wasm), + }); + }; canvas.addEventListener("mousedown", (ev) => { - program.instance.exports.js_mousedown(ev.buttons); - window.requestAnimationFrame(animation_frame); + if (!sound_ready) { + sound_init(); + sound_init = () => {}; + return; + } + + sound_node.port.postMessage({ + id : "mousedown", + buttons : ev.buttons, + }); }); canvas.addEventListener("mouseup", (ev) => { - program.instance.exports.js_mouseup(ev.buttons); - window.requestAnimationFrame(animation_frame); - }); + if (!sound_ready) + return; - canvas.addEventListener("mousemove", (ev) => { - program.instance.exports.js_mousemove(ev.clientX, ev.clientY); - window.requestAnimationFrame(animation_frame); + sound_node.port.postMessage({ + id : "mouseup", + buttons : ev.buttons, + }); }); - let key_from_code = (code) => { - switch (code) { - case "Backspace": return 8; - case "Tab": return 9; - case "Enter": return 10; - case "ControlLeft": return 11; - case "ControlRight": return 12; - case "ShiftLeft": return 13; - case "ShiftRight": return 14; - case "AltLeft": return 15; - case "AltRight": return 16; - case "ArrowLeft": return 17; - case "ArrowRight": return 18; - case "ArrowUp": return 19; - case "ArrowDown": return 20; - case "Pause": return 21; - case "Insert": return 22; - case "Home": return 23; - case "End": return 24; - case "PageUp": return 25; - case "PageDown": return 26; - case "Escape": return 27; - case "PrintScreen": return 28; - case "Space": return 32; - case "MetaLeft": return 33; - case "MetaRight": return 34; - case "Quote": return 39; - case "Comma": return 44; - case "Minus": return 45; - case "Period": return 46; - case "Slash": return 47; - case "Digit0": return 48; - case "Digit1": return 49; - case "Digit2": return 50; - case "Digit3": return 51; - case "Digit4": return 52; - case "Digit5": return 53; - case "Digit6": return 54; - case "Digit7": return 55; - case "Digit8": return 56; - case "Digit9": return 57; - case "Semicolon": return 59; - case "Equal": return 61; - case "BracketLeft": return 91; - case "Backslash": return 92; - case "BracketRight": return 93; - case "Backquote": return 96; - case "KeyA": return 97; - case "KeyB": return 98; - case "KeyC": return 99; - case "KeyD": return 100; - case "KeyE": return 101; - case "KeyF": return 102; - case "KeyG": return 103; - case "KeyH": return 104; - case "KeyI": return 105; - case "KeyJ": return 106; - case "KeyK": return 107; - case "KeyL": return 108; - case "KeyM": return 109; - case "KeyN": return 110; - case "KeyO": return 111; - case "KeyP": return 112; - case "KeyQ": return 113; - case "KeyR": return 114; - case "KeyS": return 115; - case "KeyT": return 116; - case "KeyU": return 117; - case "KeyV": return 118; - case "KeyW": return 119; - case "KeyX": return 120; - case "KeyY": return 121; - case "KeyZ": return 122; - case "Delete": return 127; - case "F1": return 145; - case "F2": return 146; - case "F3": return 147; - case "F4": return 148; - case "F5": return 149; - case "F6": return 150; - case "F7": return 151; - case "F8": return 152; - case "F9": return 153; - case "F10": return 154; - case "F11": return 155; - case "F12": return 156; - case "F13": return 157; - case "F14": return 158; - case "F15": return 159; - case "F16": return 160; - case "F17": return 161; - case "F18": return 162; - case "F19": return 163; - case "F20": return 164; - case "F21": return 165; - case "F22": return 166; - case "F23": return 167; - case "F24": return 168; - } - return 0; - }; + canvas.addEventListener("mousemove", (ev) => { + if (!sound_ready) + return; - let mod_from_event = (ev) => { - let mod = 0; - if (ev.ctrlKey) mod |= 1; - if (ev.shiftKey) mod |= 2; - if (ev.altKey) mod |= 4; - if (ev.metaKey) mod |= 8; - return mod; - }; + sound_node.port.postMessage({ + id : "mousemove", + x : ev.clientX, + y : ev.clientY, + }); + }); window.addEventListener("keydown", (ev) => { - ev.preventDefault(); - if (!ev.repeat) { - let mod = mod_from_event(ev); - let key = key_from_code(ev.code); - if (key != 0) - program.instance.exports.js_keydown(key, mod); - window.requestAnimationFrame(animation_frame); - } + if (!sound_ready) + return; + if (ev.repeat) + return; + let key = key_from_code(ev.code); + if (key == 0) + return; + let mod = mod_from_event(ev); + let ch = char_from_event(ev); + + if (ev.ctrlKey && key == KEY_PASTE) { + paste_mod = mod; + paste_ch = ch; + } else + sound_node.port.postMessage({ + id : "keydown", + mod : mod, + key : key, + ch : ch, + }); }); window.addEventListener("keyup", (ev) => { ev.preventDefault(); - if (!ev.repeat) { - let mod = mod_from_event(ev); - let key = key_from_code(ev.code); - if (key != 0) - program.instance.exports.js_keyup(key, mod); - window.requestAnimationFrame(animation_frame); - } + if (!sound_ready) + return; + if (ev.repeat) + return; + let key = key_from_code(ev.code); + if (key == 0) + return; + let mod = mod_from_event(ev); + + sound_node.port.postMessage({ + id : "keyup", + key : key, + mod : mod, + }); }); - window.requestAnimationFrame(animation_frame); + window.addEventListener("paste", (ev) => { + ev.preventDefault(); + if (!sound_ready) + return; + + sound_node.port.postMessage({ + id : "paste", + bytes : new TextEncoder("utf-8").encode(ev.clipboardData.getData("text")), + mod : paste_mod, + key : KEY_PASTE, + ch : paste_ch, + }); + }); } run({ - id : "frame", + canvas : document.getElementById("frame"), + wasm : fetch("index.wasm"), fit_window : true, }).catch((e) => console.error(e)); </script> </body> -</html> +</html> |