Files
meson-mfem/web/demo.js

196 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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();