"use strict"; const elements = { form: document.querySelector("#solve-form"), cells: document.querySelector("#cells"), cellsValue: document.querySelector("#cells-value"), order: document.querySelector("#order"), button: document.querySelector("#solve-button"), status: document.querySelector("#runtime-status"), statusDot: document.querySelector("#status-dot"), capabilities: document.querySelector("#capability-list"), canvas: document.querySelector("#mesh-canvas"), runIndex: document.querySelector("#run-index"), dofs: document.querySelector("#metric-dofs"), iterations: document.querySelector("#metric-iterations"), error: document.querySelector("#metric-error"), time: document.querySelector("#metric-time"), log: document.querySelector("#runtime-log"), footerVersion: document.querySelector("#footer-version"), }; let mfemModule; let completedRuns = 0; let lastCells = Number(elements.cells.value); function setStatus(message, state = "loading") { elements.status.textContent = message; elements.statusDot.dataset.state = state; } function appendLog(message) { const timestamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", }); const prior = elements.log.textContent.startsWith("Waiting") ? "" : `${elements.log.textContent}\n`; elements.log.textContent = `${prior}[${timestamp}] ${message}`; } function drawMesh(cells, solved = false) { const canvas = elements.canvas; const context = canvas.getContext("2d"); const ratio = Math.max(1, window.devicePixelRatio || 1); const bounds = canvas.getBoundingClientRect(); const width = Math.max(320, Math.round(bounds.width)); const height = Math.max(260, Math.round(bounds.height)); canvas.width = width * ratio; canvas.height = height * ratio; context.scale(ratio, ratio); const gradient = context.createRadialGradient( width * 0.5, height * 0.46, 10, width * 0.5, height * 0.5, width * 0.66, ); gradient.addColorStop(0, solved ? "rgba(125, 249, 197, 0.30)" : "rgba(89, 199, 255, 0.16)"); gradient.addColorStop(0.55, solved ? "rgba(35, 124, 126, 0.20)" : "rgba(27, 72, 104, 0.13)"); gradient.addColorStop(1, "rgba(8, 14, 24, 0)"); context.fillStyle = gradient; context.fillRect(0, 0, width, height); const padding = Math.max(30, Math.min(width, height) * 0.11); const side = Math.min(width - 2 * padding, height - 2 * padding); const left = (width - side) / 2; const top = (height - side) / 2; const step = side / cells; context.strokeStyle = solved ? "rgba(151, 255, 215, 0.30)" : "rgba(138, 207, 236, 0.22)"; context.lineWidth = 1; context.beginPath(); for (let index = 0; index <= cells; index += 1) { const offset = Math.round(index * step) + 0.5; context.moveTo(left + offset, top); context.lineTo(left + offset, top + side); context.moveTo(left, top + offset); context.lineTo(left + side, top + offset); } context.stroke(); context.strokeStyle = solved ? "#7df9c5" : "#59c7ff"; context.lineWidth = 2; context.strokeRect(left, top, side, side); } function renderCapabilities(capabilities) { const entries = [ ["Wasm", capabilities.wasm], ["MFEM", true], ["libCEED", capabilities.ceed], ["zlib", capabilities.zlib], ["MPI", capabilities.mpi], ]; elements.capabilities.replaceChildren( ...entries.map(([name, enabled]) => { const badge = document.createElement("span"); badge.className = enabled ? "capability enabled" : "capability disabled"; badge.textContent = enabled ? name : `${name} off`; return badge; }), ); elements.footerVersion.textContent = `MFEM ${capabilities.mfemVersion} · WebAssembly`; } function formatScientific(value) { return Number(value).toExponential(3); } function formatTime(seconds) { const milliseconds = Number(seconds) * 1000; return milliseconds < 1000 ? `${milliseconds.toFixed(1)} ms` : `${Number(seconds).toFixed(2)} s`; } async function runSolve() { const cells = Number(elements.cells.value); const order = Number(elements.order.value); elements.button.disabled = true; elements.button.classList.add("working"); setStatus("Solving finite-element system", "working"); appendLog(`Starting ${cells}×${cells} mesh at polynomial order ${order}.`); await new Promise((resolve) => window.setTimeout(resolve, 20)); try { const raw = mfemModule.ccall( "mfem_demo_solve", "string", ["number", "number"], [cells, order], ); const result = JSON.parse(raw); if (!result.ok) { throw new Error(result.error || "The MFEM solve failed."); } completedRuns += 1; lastCells = cells; elements.runIndex.textContent = `Run ${String(completedRuns).padStart(2, "0")}`; elements.dofs.textContent = Number(result.trueDofs).toLocaleString(); elements.iterations.textContent = Number(result.iterations).toLocaleString(); elements.error.textContent = formatScientific(result.l2Error); elements.time.textContent = formatTime(result.elapsedSeconds); drawMesh(cells, true); setStatus("Solve complete", "ready"); appendLog( `Converged in ${result.iterations} iterations with residual ${formatScientific(result.finalNorm)}.`, ); } catch (error) { setStatus("Solve failed", "error"); appendLog(error instanceof Error ? error.message : String(error)); } finally { elements.button.disabled = false; elements.button.classList.remove("working"); } } elements.cells.addEventListener("input", () => { elements.cellsValue.value = elements.cells.value; drawMesh(Number(elements.cells.value), false); }); elements.form.addEventListener("submit", (event) => { event.preventDefault(); void runSolve(); }); window.addEventListener("resize", () => drawMesh(lastCells, completedRuns > 0)); async function initialize() { drawMesh(lastCells, false); try { if (typeof MFEMDemoModule !== "function") { throw new Error("The generated MFEM WebAssembly loader was not found."); } mfemModule = await MFEMDemoModule({ locateFile: (path) => new URL(path, window.location.href).href, print: (message) => appendLog(message), printErr: (message) => appendLog(`Runtime: ${message}`), }); const capabilities = JSON.parse( mfemModule.ccall("mfem_demo_capabilities", "string", [], []), ); renderCapabilities(capabilities); elements.button.disabled = false; setStatus("Runtime ready", "ready"); appendLog(`Loaded MFEM ${capabilities.mfemVersion} from WebAssembly.`); } catch (error) { setStatus("Runtime failed to load", "error"); appendLog(error instanceof Error ? error.message : String(error)); } } void initialize();