// Lists this repository's submissions and animates the exported HDF5 displacement fields as a 3D surface. import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0.7/dist/esm/hdf5_hl.js"; const manifest = await (await fetch("manifest.json")).json(); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); // ---------------------------------------------------------------- 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 && s.fields.available !== false) fieldSolutions.push({ label: `${sub.submission_id}/${s.solution_id}`, path: s.fields.path }); const fieldsLink = s.fields ? (s.fields.available === false ? ' (fields stored in LFS, not published here)' : ` fields`) : ""; return `${esc(s.solution_id)}${esc(s.method || "")}
${esc(JSON.stringify(s.parameters || {}))}` + `${s.timing_seconds ?? ""}${esc((s.more_accurate_than || []).join(", "))}` + `

wav${fieldsLink}`; }).join(""); const wrap = document.createElement("div"); wrap.className = "wrap"; wrap.innerHTML = `${rows}
SolutionMethodTime (s)Claimed more accurate thanOutput
`; subsDiv.appendChild(wrap); } } const gains = manifest.playback_gain_db || {}; const firstGain = Object.values(gains).find((g) => typeof g === "number"); ChallengePlayer.controls(document.getElementById("playback"), { gainDb: typeof firstGain === "number" ? firstGain : 20, limiter: true }); ChallengePlayer.mount(); // ---------------------------------------------------------------- 3D viewer const view = document.getElementById("view"); const status = document.getElementById("fieldStatus"); const select = document.getElementById("fieldSelect"); const playBtn = document.getElementById("playBtn"); const speed = document.getElementById("speed"); const speedLabel = document.getElementById("speedLabel"); const exag = document.getElementById("exag"); const exagLabel = document.getElementById("exagLabel"); const timeSlider = document.getElementById("timeSlider"); const derigid = document.getElementById("derigid"); const timeLabel = document.getElementById("timeLabel"); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio || 1); view.appendChild(renderer.domElement); const scene = new THREE.Scene(); const dark = matchMedia("(prefers-color-scheme: dark)").matches; scene.background = new THREE.Color(dark ? 0x151515 : 0xffffff); const camera = new THREE.PerspectiveCamera(40, 2, 0.01, 100); camera.position.set(0.35, -0.5, 0.35); camera.up.set(0, 0, 1); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dl = new THREE.DirectionalLight(0xffffff, 0.9); dl.position.set(0.5, -0.8, 1.0); scene.add(dl); let field = null; // {x, y, t, u, nx, ny, nt, umax} let mesh = null; let frame = 0; let playing = false; let lastTs = null; let simTime = 0; function resize() { const w = view.clientWidth, h = view.clientHeight; renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); } window.addEventListener("resize", () => { resize(); render(); }); resize(); function buildMesh() { if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); } const { x, y, nx, ny } = field; const Lx = x[nx - 1], Ly = y[ny - 1]; const geo = new THREE.PlaneGeometry(Lx, Ly, nx - 1, ny - 1); geo.translate(0, 0, 0); const colors = new Float32Array(nx * ny * 3); geo.setAttribute("color", new THREE.BufferAttribute(colors, 3)); const mat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, metalness: 0.1, roughness: 0.7 }); mesh = new THREE.Mesh(geo, mat); scene.add(mesh); // a thin frame at rest height for reference const edges = new THREE.EdgesGeometry(new THREE.PlaneGeometry(Lx, Ly, 1, 1)); const frameLine = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: dark ? 0x555555 : 0xbbbbbb })); mesh.add(frameLine); // strike marker const marker = new THREE.Mesh(new THREE.SphereGeometry(0.004, 12, 12), new THREE.MeshBasicMaterial({ color: 0xd04040 })); marker.position.set(0.09144 - Lx / 2, 0.10668 - Ly / 2, 0.004); mesh.add(marker); } function updateSurface() { if (!field || !mesh) return; const { u, nx, ny, x, y, plane, xm, ym } = field; const rigidOff = derigid.checked; const umax = rigidOff ? field.emax : field.umax; const pos = mesh.geometry.attributes.position; const col = mesh.geometry.attributes.color; const scale = umax > 0 ? (0.03 * Math.pow(10, parseFloat(exag.value)) / umax) * 0.1 : 0; // exaggerated height const base = frame * nx * ny; const c0 = plane[3 * frame], gx = plane[3 * frame + 1], gy = plane[3 * frame + 2]; // PlaneGeometry vertices run row by row from +y (top) to -y; row r corresponds to y index (ny-1-r) for (let r = 0; r < ny; r++) { const j = ny - 1 - r; for (let i = 0; i < nx; i++) { let v = u[base + j * nx + i]; if (rigidOff) v -= c0 + gx * (x[i] - xm) + gy * (y[j] - ym); const k = r * nx + i; const z = v * scale; pos.setZ(k, z); const s = umax > 0 ? Math.max(-1, Math.min(1, v / umax)) : 0; // blue (negative) to white to red (positive) col.setXYZ(k, s > 0 ? 1 : 1 + s * 0.8, 1 - Math.abs(s) * 0.7, s < 0 ? 1 : 1 - s * 0.8); } } pos.needsUpdate = true; col.needsUpdate = true; mesh.geometry.computeVertexNormals(); timeSlider.value = String(frame); timeLabel.textContent = `${(field.t[frame] * 1e3).toFixed(0)} ms`; } function render() { renderer.render(scene, camera); } controls.addEventListener("change", render); async function loadField(path) { status.textContent = `Loading ${path} (this may take a while, the file is large)…`; playing = false; playBtn.textContent = "Play"; try { const { FS } = await h5wasm.ready; const resp = await fetch(path); const buf = new Uint8Array(await resp.arrayBuffer()); if (buf.length < 200 && new TextDecoder().decode(buf).startsWith("version https://git-lfs")) { status.textContent = `${path} is a git LFS pointer; the object was not published with this site.`; return; } 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 y = Float64Array.from(file.get("y").value); const t = Float64Array.from(file.get("t").value); const u = Float32Array.from(file.get("u").value); file.close(); // best-fit plane per frame (the free plate's rigid translation and rotations), so that the // elastic deformation can be shown on its own const nx = x.length, ny = y.length, nt = t.length; const xm = x.reduce((a, b) => a + b, 0) / nx, ym = y.reduce((a, b) => a + b, 0) / ny; let sxx = 0, syy = 0; for (let i = 0; i < nx; i++) sxx += (x[i] - xm) ** 2; for (let j = 0; j < ny; j++) syy += (y[j] - ym) ** 2; sxx *= ny; syy *= nx; const plane = new Float64Array(nt * 3); // c, gx, gy per frame for (let k = 0; k < nt; k++) { let s0 = 0, sx = 0, sy = 0; const base = k * nx * ny; for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) { const v = u[base + j * nx + i]; s0 += v; sx += v * (x[i] - xm); sy += v * (y[j] - ym); } plane[3 * k] = s0 / (nx * ny); plane[3 * k + 1] = sx / sxx; plane[3 * k + 2] = sy / syy; } let umax = 0, emax = 0; for (let k = 0; k < nt; k++) { const base = k * nx * ny; for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) { const v = u[base + j * nx + i]; const e = v - plane[3 * k] - plane[3 * k + 1] * (x[i] - xm) - plane[3 * k + 2] * (y[j] - ym); if (Math.abs(v) > umax) umax = Math.abs(v); if (Math.abs(e) > emax) emax = Math.abs(e); } } field = { x, y, t, u, nx, ny, nt, umax, emax, plane, xm, ym }; timeSlider.max = String(field.nt - 1); frame = 0; simTime = 0; buildMesh(); updateSurface(); render(); status.textContent = `${path}: ${field.nx} × ${field.ny} points × ${field.nt} frames, max |u| = ${(umax * 1e3).toFixed(2)} mm including rigid motion, ${(emax * 1e3).toFixed(3)} mm elastic`; } catch (e) { status.textContent = `Could not load ${path}: ${e}`; } } 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)); updateSurface(); render(); } 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]; updateSurface(); render(); }); const updSpeed = () => { speedLabel.textContent = `${Math.pow(10, parseFloat(speed.value)).toPrecision(2)} × real time`; }; const updExag = () => { exagLabel.textContent = `×${Math.pow(10, parseFloat(exag.value)).toPrecision(2)}`; updateSurface(); render(); }; speed.addEventListener("input", updSpeed); exag.addEventListener("input", updExag); derigid.addEventListener("change", () => { updateSurface(); render(); }); updSpeed(); exagLabel.textContent = `×${Math.pow(10, parseFloat(exag.value)).toPrecision(2)}`; select.addEventListener("change", () => loadField(select.value)); for (const f of fieldSolutions) { const o = document.createElement("option"); o.value = f.path; o.textContent = f.label; select.appendChild(o); } render(); if (fieldSolutions.length) loadField(fieldSolutions[0].path); else status.textContent = "No solution in this repository exports a displacement field that is published with this site.";