1// Lists this repository's submissions and animates the exported HDF5 displacement fields as a 3D surface.
2import * as THREE from "three";
3import { OrbitControls } from "three/addons/controls/OrbitControls.js";
4import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0.7/dist/esm/hdf5_hl.js";
6const manifest = await (await fetch("manifest.json")).json();
7const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
9// ---------------------------------------------------------------- submissions table
10const subsDiv = document.getElementById("submissions");
11const fieldSolutions = [];
12for (const [cid, ch] of Object.entries(manifest.challenges)) {
13 const h3 = document.createElement("h3");
14 h3.textContent = cid;
15 subsDiv.appendChild(h3);
16 for (const sub of ch.submissions) {
17 const p = document.createElement("p");
18 p.innerHTML = `<b>${esc(sub.submission_id)}</b> by ${esc(sub.submitter.name)}${sub.date ? " (" + esc(sub.date) + ")" : ""}. ${esc(sub.description || "")}` +
19 (sub.code_url ? ` <a href="${esc(sub.code_url)}">code</a>` : "");
20 subsDiv.appendChild(p);
21 const rows = sub.solutions.map((s) => {
22 if (s.fields && s.fields.available !== false) fieldSolutions.push({ label: `${sub.submission_id}/${s.solution_id}`, path: s.fields.path });
23 const fieldsLink = s.fields ? (s.fields.available === false ? ' <span class="muted">(fields stored in LFS, not published here)</span>' : ` <a href="${esc(s.fields.path)}">fields</a>`) : "";
24 return `<tr><td>${esc(s.solution_id)}</td><td>${esc(s.method || "")}<br><span class="muted"><code>${esc(JSON.stringify(s.parameters || {}))}</code></span></td>` +
25 `<td class="num">${s.timing_seconds ?? ""}</td><td class="muted">${esc((s.more_accurate_than || []).join(", "))}</td>` +
26 `<td><div class="cplayer" data-src="${esc(s.output)}"></div><br><a href="${esc(s.output)}">wav</a>${fieldsLink}</td></tr>`;
27 }).join("");
28 const wrap = document.createElement("div");
29 wrap.className = "wrap";
30 wrap.innerHTML = `<table><tr><th>Solution</th><th>Method</th><th>Time (s)</th><th>Claimed more accurate than</th><th>Output</th></tr>${rows}</table>`;
31 subsDiv.appendChild(wrap);
32 }
33}
35const gains = manifest.playback_gain_db || {};
36const firstGain = Object.values(gains).find((g) => typeof g === "number");
37ChallengePlayer.controls(document.getElementById("playback"), { gainDb: typeof firstGain === "number" ? firstGain : 20, limiter: true });
38ChallengePlayer.mount();
40// ---------------------------------------------------------------- 3D viewer
41const view = document.getElementById("view");
42const status = document.getElementById("fieldStatus");
43const select = document.getElementById("fieldSelect");
44const playBtn = document.getElementById("playBtn");
45const speed = document.getElementById("speed");
46const speedLabel = document.getElementById("speedLabel");
47const exag = document.getElementById("exag");
48const exagLabel = document.getElementById("exagLabel");
49const timeSlider = document.getElementById("timeSlider");
50const timeLabel = document.getElementById("timeLabel");
52const renderer = new THREE.WebGLRenderer({ antialias: true });
53renderer.setPixelRatio(window.devicePixelRatio || 1);
54view.appendChild(renderer.domElement);
55const scene = new THREE.Scene();
56const dark = matchMedia("(prefers-color-scheme: dark)").matches;
57scene.background = new THREE.Color(dark ? 0x151515 : 0xffffff);
58const camera = new THREE.PerspectiveCamera(40, 2, 0.01, 100);
59camera.position.set(0.35, -0.5, 0.35);
60camera.up.set(0, 0, 1);
61const controls = new OrbitControls(camera, renderer.domElement);
62controls.target.set(0, 0, 0);
63scene.add(new THREE.AmbientLight(0xffffff, 0.6));
64const dl = new THREE.DirectionalLight(0xffffff, 0.9);
65dl.position.set(0.5, -0.8, 1.0);
66scene.add(dl);
68let field = null; // {x, y, t, u, nx, ny, nt, umax}
69let mesh = null;
70let frame = 0;
71let playing = false;
72let lastTs = null;
73let simTime = 0;
75function resize() {
76 const w = view.clientWidth, h = view.clientHeight;
77 renderer.setSize(w, h, false);
78 camera.aspect = w / h;
79 camera.updateProjectionMatrix();
80}
81window.addEventListener("resize", () => { resize(); render(); });
82resize();
84function buildMesh() {
85 if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); }
86 const { x, y, nx, ny } = field;
87 const Lx = x[nx - 1], Ly = y[ny - 1];
88 const geo = new THREE.PlaneGeometry(Lx, Ly, nx - 1, ny - 1);
89 geo.translate(0, 0, 0);
90 const colors = new Float32Array(nx * ny * 3);
91 geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
92 const mat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, metalness: 0.1, roughness: 0.7 });
93 mesh = new THREE.Mesh(geo, mat);
94 scene.add(mesh);
95 // a thin frame at rest height for reference
96 const edges = new THREE.EdgesGeometry(new THREE.PlaneGeometry(Lx, Ly, 1, 1));
97 const frameLine = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: dark ? 0x555555 : 0xbbbbbb }));
98 mesh.add(frameLine);
99 // strike marker
100 const marker = new THREE.Mesh(new THREE.SphereGeometry(0.004, 12, 12), new THREE.MeshBasicMaterial({ color: 0xd04040 }));
101 marker.position.set(0.09144 - Lx / 2, 0.10668 - Ly / 2, 0.004);
102 mesh.add(marker);
103}
105function updateSurface() {
106 if (!field || !mesh) return;
107 const { u, nx, ny, umax } = field;
108 const pos = mesh.geometry.attributes.position;
109 const col = mesh.geometry.attributes.color;
110 const scale = umax > 0 ? (0.03 * Math.pow(10, parseFloat(exag.value)) / umax) * 0.1 : 0; // exaggerated height
111 const base = frame * nx * ny;
112 // PlaneGeometry vertices run row by row from +y (top) to -y; row r corresponds to y index (ny-1-r)
113 for (let r = 0; r < ny; r++) {
114 const j = ny - 1 - r;
115 for (let i = 0; i < nx; i++) {
116 const v = u[base + j * nx + i];
117 const k = r * nx + i;
118 const z = v * scale;
119 pos.setZ(k, z);
120 const s = umax > 0 ? Math.max(-1, Math.min(1, v / umax)) : 0;
121 // blue (negative) to white to red (positive)
122 col.setXYZ(k, s > 0 ? 1 : 1 + s * 0.8, 1 - Math.abs(s) * 0.7, s < 0 ? 1 : 1 - s * 0.8);
123 }
124 }
125 pos.needsUpdate = true;
126 col.needsUpdate = true;
127 mesh.geometry.computeVertexNormals();
128 timeSlider.value = String(frame);
129 timeLabel.textContent = `${(field.t[frame] * 1e3).toFixed(0)} ms`;
130}
132function render() { renderer.render(scene, camera); }
133controls.addEventListener("change", render);
135async function loadField(path) {
136 status.textContent = `Loading ${path} (this may take a while, the file is large)…`;
137 playing = false;
138 playBtn.textContent = "Play";
139 try {
140 const { FS } = await h5wasm.ready;
141 const resp = await fetch(path);
142 const buf = new Uint8Array(await resp.arrayBuffer());
143 if (buf.length < 200 && new TextDecoder().decode(buf).startsWith("version https://git-lfs")) {
144 status.textContent = `${path} is a git LFS pointer; the object was not published with this site.`;
145 return;
146 }
147 const name = "current.h5";
148 try { FS.unlink(name); } catch (_) {}
149 FS.writeFile(name, buf);
150 const file = new h5wasm.File(name, "r");
151 const x = Float64Array.from(file.get("x").value);
152 const y = Float64Array.from(file.get("y").value);
153 const t = Float64Array.from(file.get("t").value);
154 const u = Float32Array.from(file.get("u").value);
155 file.close();
156 let umax = 0;
157 for (let i = 0; i < u.length; i++) { const a = Math.abs(u[i]); if (a > umax) umax = a; }
158 field = { x, y, t, u, nx: x.length, ny: y.length, nt: t.length, umax };
159 timeSlider.max = String(field.nt - 1);
160 frame = 0;
161 simTime = 0;
162 buildMesh();
163 updateSurface();
164 render();
165 status.textContent = `${path}: ${field.nx} × ${field.ny} points × ${field.nt} frames, max |u| = ${(umax * 1e3).toFixed(3)} mm`;
166 } catch (e) {
167 status.textContent = `Could not load ${path}: ${e}`;
168 }
169}
171function tick(ts) {
172 if (!playing || !field) return;
173 if (lastTs !== null) {
174 const factor = Math.pow(10, parseFloat(speed.value));
175 simTime += ((ts - lastTs) / 1000) * factor;
176 const tEnd = field.t[field.nt - 1];
177 if (simTime > tEnd) simTime = 0;
178 const dt = field.t[1] - field.t[0];
179 frame = Math.min(field.nt - 1, Math.round(simTime / dt));
180 updateSurface();
181 render();
182 }
183 lastTs = ts;
184 requestAnimationFrame(tick);
185}
187playBtn.addEventListener("click", () => {
188 playing = !playing;
189 playBtn.textContent = playing ? "Pause" : "Play";
190 lastTs = null;
191 if (playing) requestAnimationFrame(tick);
192});
193timeSlider.addEventListener("input", () => {
194 if (!field) return;
195 frame = parseInt(timeSlider.value, 10);
196 simTime = field.t[frame];
197 updateSurface();
198 render();
199});
200const updSpeed = () => { speedLabel.textContent = `${Math.pow(10, parseFloat(speed.value)).toPrecision(2)} × real time`; };
201const updExag = () => { exagLabel.textContent = `×${Math.pow(10, parseFloat(exag.value)).toPrecision(2)}`; updateSurface(); render(); };
202speed.addEventListener("input", updSpeed);
203exag.addEventListener("input", updExag);
204updSpeed();
205exagLabel.textContent = `×${Math.pow(10, parseFloat(exag.value)).toPrecision(2)}`;
206select.addEventListener("change", () => loadField(select.value));
208for (const f of fieldSolutions) {
209 const o = document.createElement("option");
210 o.value = f.path;
211 o.textContent = f.label;
212 select.appendChild(o);
213}
214render();
215if (fieldSolutions.length) loadField(fieldSolutions[0].path);
216else status.textContent = "No solution in this repository exports a displacement field that is published with this site.";