// 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 = `${rows}
SolutionMethodTime (s)Claimed more accurate thanOutput
`; 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])); }