/** * bootstrap.js — ES module entry point. * * Initialises the WASM module, wires canvas elements to Rust-exported types, * keeps canvas drawing buffers in sync with their CSS size, and provides a * draggable resize handle for the patch bay panel. */ import init, { AudioEngine, OscilloscopeView, SpectrumView, PatchBay, SynthParams, } from "./pkg/synth_visualiser.js"; // ── Canvas buffer sizing ────────────────────────────────────────────────────── // A has two independent sizes: // - CSS display size (width/height CSS properties) — how large it appears // - Drawing buffer (width/height HTML attributes) — actual pixel resolution // // We must keep them in sync; if the buffer is smaller than the display size the // browser stretches it and everything looks blurry / oversized. function fitCanvas(canvas) { const w = Math.round(canvas.clientWidth); const h = Math.round(canvas.clientHeight); if (w > 0 && h > 0 && (canvas.width !== w || canvas.height !== h)) { canvas.width = w; canvas.height = h; } } // ── Resize handle ───────────────────────────────────────────────────────────── function initResizeHandle() { const handle = document.getElementById("resize-handle"); const panel = document.getElementById("patchbay-panel"); let dragging = false; let startY = 0; let startH = 0; handle.addEventListener("pointerdown", e => { dragging = true; startY = e.clientY; startH = panel.offsetHeight; handle.setPointerCapture(e.pointerId); handle.classList.add("active"); }); handle.addEventListener("pointermove", e => { if (!dragging) return; // Dragging up (negative dy) increases the panel height const dy = e.clientY - startY; const h = Math.max(80, startH - dy); panel.style.height = h + "px"; }); handle.addEventListener("pointerup", () => { dragging = false; handle.classList.remove("active"); }); handle.addEventListener("pointercancel",() => { dragging = false; handle.classList.remove("active"); }); } // ── Bootstrap ───────────────────────────────────────────────────────────────── const loader = document.getElementById("loader"); const status = document.getElementById("status"); const srLabel = document.getElementById("sample-rate"); const frameTime = document.getElementById("frame-time"); async function bootstrap() { initResizeHandle(); try { await init(); const engine = new AudioEngine(); await engine.attach(); const analyser = engine.analyser_node(); const oscilloscope = new OscilloscopeView("oscilloscope-canvas", analyser); const spectrum = new SpectrumView("spectrum-canvas", analyser); const patchbay = new PatchBay("patchbay-canvas"); // Fit all canvas buffers to their current CSS layout size before we // ask Rust for canvas.width() to position the default modules. const pbCanvas = document.getElementById("patchbay-canvas"); const oscCanvas = document.getElementById("oscilloscope-canvas"); const specCanvas = document.getElementById("spectrum-canvas"); const allCanvases = [oscCanvas, specCanvas, pbCanvas]; allCanvases.forEach(fitCanvas); // Seed a default patch using the now-correct canvas width. const cw = pbCanvas.width || pbCanvas.clientWidth || 800; patchbay.add_module("vco", cw * 0.12, 80); patchbay.add_module("adsr", cw * 0.32, 80); patchbay.add_module("svf", cw * 0.55, 80); patchbay.add_module("vca", cw * 0.76, 80); // Keep canvas buffers in sync whenever the panel is resized. const ro = new ResizeObserver(() => allCanvases.forEach(fitCanvas)); allCanvases.forEach(c => ro.observe(c)); // Patch bay pointer events pbCanvas.addEventListener("pointerdown", e => patchbay.on_pointer_down(e.offsetX, e.offsetY)); pbCanvas.addEventListener("pointermove", e => patchbay.on_pointer_move(e.offsetX, e.offsetY)); pbCanvas.addEventListener("pointerup", e => patchbay.on_pointer_up(e.offsetX, e.offsetY)); pbCanvas.addEventListener("dblclick", e => patchbay.on_double_click(e.offsetX, e.offsetY)); const params = new SynthParams(); engine.set_params(params.to_json()); srLabel.textContent = `SR: ${engine.sample_rate()} Hz`; status.textContent = "Running"; engine.start(); let last = performance.now(); function frame(now) { oscilloscope.draw(); spectrum.draw(); patchbay.draw(); frameTime.textContent = `frame: ${(now - last).toFixed(1)} ms`; last = now; requestAnimationFrame(frame); } requestAnimationFrame(frame); loader.classList.add("hidden"); } catch (err) { console.error("[bootstrap] Fatal:", err); loader.textContent = `Error: ${err.message ?? err}`; } } bootstrap();