// Lists this repository's submissions and animates the exported HDF5 displacement fields (read with h5wasm).
import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0.7/dist/esm/hdf5_hl.js";
const manifest = await (await fetch("manifest.json")).json();
// ---------------------------------------------------------------- submissions table
const subsDiv = document.getElementById("submissions");
const fieldSolutions = [];
for (const [cid, ch] of Object.entries(manifest.challenges)) {
const h3 = document.createElement("h3");
h3.textContent = cid;
subsDiv.appendChild(h3);
for (const sub of ch.submissions) {
const p = document.createElement("p");
p.innerHTML = `${esc(sub.submission_id)} by ${esc(sub.submitter.name)}${sub.date ? " (" + esc(sub.date) + ")" : ""}. ${esc(sub.description || "")}` +
(sub.code_url ? ` code` : "");
subsDiv.appendChild(p);
const rows = sub.solutions.map((s) => {
if (s.fields) fieldSolutions.push({ label: `${sub.submission_id}/${s.solution_id}`, path: s.fields.path });
return `
| ${esc(s.solution_id)} | ${esc(s.method || "")}
${esc(JSON.stringify(s.parameters || {}))} | ` +
`${s.timing_seconds ?? ""} | ${esc((s.more_accurate_than || []).join(", "))} | ` +
` wav${s.fields ? ` fields` : ""} |
`;
}).join("");
const wrap = document.createElement("div");
wrap.className = "wrap";
wrap.innerHTML = `| Solution | Method | Time (s) | Claimed more accurate than | Output |
${rows}
`;
subsDiv.appendChild(wrap);
}
}
// ---------------------------------------------------------------- field viewer
const select = document.getElementById("fieldSelect");
const status = document.getElementById("fieldStatus");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const playBtn = document.getElementById("playBtn");
const speed = document.getElementById("speed");
const speedLabel = document.getElementById("speedLabel");
const timeSlider = document.getElementById("timeSlider");
const timeLabel = document.getElementById("timeLabel");
for (const f of fieldSolutions) {
const o = document.createElement("option");
o.value = f.path;
o.textContent = f.label;
select.appendChild(o);
}
let field = null; // {x, t, u (Float32Array nt*nx), nx, nt, umax}
let frame = 0;
let playing = false;
let lastTs = null;
let simTime = 0;
async function loadField(path) {
status.textContent = `Loading ${path}…`;
playing = false;
playBtn.textContent = "Play";
try {
const { FS } = await h5wasm.ready;
const buf = new Uint8Array(await (await fetch(path)).arrayBuffer());
const name = "current.h5";
try { FS.unlink(name); } catch (_) {}
FS.writeFile(name, buf);
const file = new h5wasm.File(name, "r");
const x = Float64Array.from(file.get("x").value);
const t = Float64Array.from(file.get("t").value);
const u = Float32Array.from(file.get("u").value);
file.close();
let umax = 0;
for (let i = 0; i < u.length; i++) umax = Math.max(umax, Math.abs(u[i]));
field = { x, t, u, nx: x.length, nt: t.length, umax };
timeSlider.max = String(field.nt - 1);
frame = 0;
simTime = 0;
status.textContent = `${path}: ${field.nx} points × ${field.nt} frames, max |u| = ${(umax * 1e3).toFixed(3)} mm`;
draw();
} catch (e) {
status.textContent = `Could not load ${path}: ${e}`;
}
}
function draw() {
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = canvas.clientHeight;
if (canvas.width !== Math.round(w * dpr)) { canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr); }
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const css = getComputedStyle(document.documentElement);
ctx.strokeStyle = css.getPropertyValue("--rest");
ctx.beginPath(); ctx.moveTo(20, h / 2); ctx.lineTo(w - 20, h / 2); ctx.stroke();
if (!field) return;
const { x, u, nx, umax } = field;
const L = x[nx - 1];
const scale = umax > 0 ? (0.42 * h) / umax : 0;
ctx.strokeStyle = css.getPropertyValue("--string");
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < nx; i++) {
const px = 20 + (x[i] / L) * (w - 40);
const py = h / 2 - u[frame * nx + i] * scale;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// pickup marker at x_m = 0.54 m
const xm = 20 + (0.54 / L) * (w - 40);
ctx.strokeStyle = css.getPropertyValue("--accent");
ctx.setLineDash([4, 4]);
ctx.beginPath(); ctx.moveTo(xm, 10); ctx.lineTo(xm, h - 10); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = css.getPropertyValue("--muted");
ctx.font = "12px system-ui, sans-serif";
ctx.fillText(`t = ${(field.t[frame] * 1e3).toFixed(1)} ms`, 24, 16);
ctx.fillText("pickup", xm + 4, h - 14);
timeSlider.value = String(frame);
timeLabel.textContent = `${(field.t[frame] * 1e3).toFixed(1)} ms`;
}
function tick(ts) {
if (!playing || !field) return;
if (lastTs !== null) {
const factor = Math.pow(10, parseFloat(speed.value));
simTime += ((ts - lastTs) / 1000) * factor;
const tEnd = field.t[field.nt - 1];
if (simTime > tEnd) simTime = 0;
const dt = field.t[1] - field.t[0];
frame = Math.min(field.nt - 1, Math.round(simTime / dt));
draw();
}
lastTs = ts;
requestAnimationFrame(tick);
}
playBtn.addEventListener("click", () => {
playing = !playing;
playBtn.textContent = playing ? "Pause" : "Play";
lastTs = null;
if (playing) requestAnimationFrame(tick);
});
timeSlider.addEventListener("input", () => {
if (!field) return;
frame = parseInt(timeSlider.value, 10);
simTime = field.t[frame];
draw();
});
function updateSpeedLabel() { speedLabel.textContent = `${Math.pow(10, parseFloat(speed.value)).toPrecision(2)} × real time`; }
speed.addEventListener("input", updateSpeedLabel);
updateSpeedLabel();
select.addEventListener("change", () => loadField(select.value));
window.addEventListener("resize", draw);
if (fieldSolutions.length) loadField(fieldSolutions[0].path);
else status.textContent = "No solution in this repository exports a displacement field.";
function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); }