/ challenges-old / string-solutions
Sign in
challenges-old / string-solutions
string-solutions / web / viewer.js
160 lines · 6.7 KBBlameHistoryRaw
1// Lists this repository's submissions and animates the exported HDF5 displacement fields (read with h5wasm).
2import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0.7/dist/esm/hdf5_hl.js";
4const manifest = await (await fetch("manifest.json")).json();
6// ---------------------------------------------------------------- submissions table
7const subsDiv = document.getElementById("submissions");
8const fieldSolutions = [];
9for (const [cid, ch] of Object.entries(manifest.challenges)) {
10 const h3 = document.createElement("h3");
11 h3.textContent = cid;
12 subsDiv.appendChild(h3);
13 for (const sub of ch.submissions) {
14 const p = document.createElement("p");
15 p.innerHTML = `<b>${esc(sub.submission_id)}</b> by ${esc(sub.submitter.name)}${sub.date ? " (" + esc(sub.date) + ")" : ""}. ${esc(sub.description || "")}` +
16 (sub.code_url ? ` <a href="${esc(sub.code_url)}">code</a>` : "");
17 subsDiv.appendChild(p);
18 const rows = sub.solutions.map((s) => {
19 if (s.fields) fieldSolutions.push({ label: `${sub.submission_id}/${s.solution_id}`, path: s.fields.path });
20 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>` +
21 `<td class="num">${s.timing_seconds ?? ""}</td><td class="muted">${esc((s.more_accurate_than || []).join(", "))}</td>` +
22 `<td><div class="cplayer" data-src="${esc(s.output)}"></div><br><a href="${esc(s.output)}">wav</a>${s.fields ? ` <a href="${esc(s.fields.path)}">fields</a>` : ""}</td></tr>`;
23 }).join("");
24 const wrap = document.createElement("div");
25 wrap.className = "wrap";
26 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>`;
27 subsDiv.appendChild(wrap);
28 }
31const gains = manifest.playback_gain_db || {};
32const firstGain = Object.values(gains).find((g) => typeof g === "number");
33ChallengePlayer.controls(document.getElementById("playback"), { gainDb: typeof firstGain === "number" ? firstGain : 20, limiter: true });
34ChallengePlayer.mount();
36// ---------------------------------------------------------------- field viewer
37const select = document.getElementById("fieldSelect");
38const status = document.getElementById("fieldStatus");
39const canvas = document.getElementById("canvas");
40const ctx = canvas.getContext("2d");
41const playBtn = document.getElementById("playBtn");
42const speed = document.getElementById("speed");
43const speedLabel = document.getElementById("speedLabel");
44const timeSlider = document.getElementById("timeSlider");
45const timeLabel = document.getElementById("timeLabel");
47for (const f of fieldSolutions) {
48 const o = document.createElement("option");
49 o.value = f.path;
50 o.textContent = f.label;
51 select.appendChild(o);
54let field = null; // {x, t, u (Float32Array nt*nx), nx, nt, umax}
55let frame = 0;
56let playing = false;
57let lastTs = null;
58let simTime = 0;
60async function loadField(path) {
61 status.textContent = `Loading ${path}…`;
62 playing = false;
63 playBtn.textContent = "Play";
64 try {
65 const { FS } = await h5wasm.ready;
66 const buf = new Uint8Array(await (await fetch(path)).arrayBuffer());
67 const name = "current.h5";
68 try { FS.unlink(name); } catch (_) {}
69 FS.writeFile(name, buf);
70 const file = new h5wasm.File(name, "r");
71 const x = Float64Array.from(file.get("x").value);
72 const t = Float64Array.from(file.get("t").value);
73 const u = Float32Array.from(file.get("u").value);
74 file.close();
75 let umax = 0;
76 for (let i = 0; i < u.length; i++) umax = Math.max(umax, Math.abs(u[i]));
77 field = { x, t, u, nx: x.length, nt: t.length, umax };
78 timeSlider.max = String(field.nt - 1);
79 frame = 0;
80 simTime = 0;
81 status.textContent = `${path}: ${field.nx} points × ${field.nt} frames, max |u| = ${(umax * 1e3).toFixed(3)} mm`;
82 draw();
83 } catch (e) {
84 status.textContent = `Could not load ${path}: ${e}`;
85 }
88function draw() {
89 const dpr = window.devicePixelRatio || 1;
90 const w = canvas.clientWidth, h = canvas.clientHeight;
91 if (canvas.width !== Math.round(w * dpr)) { canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr); }
92 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
93 ctx.clearRect(0, 0, w, h);
94 const css = getComputedStyle(document.documentElement);
95 ctx.strokeStyle = css.getPropertyValue("--rest");
96 ctx.beginPath(); ctx.moveTo(20, h / 2); ctx.lineTo(w - 20, h / 2); ctx.stroke();
97 if (!field) return;
98 const { x, u, nx, umax } = field;
99 const L = x[nx - 1];
100 const scale = umax > 0 ? (0.42 * h) / umax : 0;
101 ctx.strokeStyle = css.getPropertyValue("--string");
102 ctx.lineWidth = 2;
103 ctx.beginPath();
104 for (let i = 0; i < nx; i++) {
105 const px = 20 + (x[i] / L) * (w - 40);
106 const py = h / 2 - u[frame * nx + i] * scale;
107 if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
108 }
109 ctx.stroke();
110 // pickup marker at x_m = 0.54 m
111 const xm = 20 + (0.54 / L) * (w - 40);
112 ctx.strokeStyle = css.getPropertyValue("--accent");
113 ctx.setLineDash([4, 4]);
114 ctx.beginPath(); ctx.moveTo(xm, 10); ctx.lineTo(xm, h - 10); ctx.stroke();
115 ctx.setLineDash([]);
116 ctx.fillStyle = css.getPropertyValue("--muted");
117 ctx.font = "12px system-ui, sans-serif";
118 ctx.fillText(`t = ${(field.t[frame] * 1e3).toFixed(1)} ms`, 24, 16);
119 ctx.fillText("pickup", xm + 4, h - 14);
120 timeSlider.value = String(frame);
121 timeLabel.textContent = `${(field.t[frame] * 1e3).toFixed(1)} ms`;
124function tick(ts) {
125 if (!playing || !field) return;
126 if (lastTs !== null) {
127 const factor = Math.pow(10, parseFloat(speed.value));
128 simTime += ((ts - lastTs) / 1000) * factor;
129 const tEnd = field.t[field.nt - 1];
130 if (simTime > tEnd) simTime = 0;
131 const dt = field.t[1] - field.t[0];
132 frame = Math.min(field.nt - 1, Math.round(simTime / dt));
133 draw();
134 }
135 lastTs = ts;
136 requestAnimationFrame(tick);
139playBtn.addEventListener("click", () => {
140 playing = !playing;
141 playBtn.textContent = playing ? "Pause" : "Play";
142 lastTs = null;
143 if (playing) requestAnimationFrame(tick);
144});
145timeSlider.addEventListener("input", () => {
146 if (!field) return;
147 frame = parseInt(timeSlider.value, 10);
148 simTime = field.t[frame];
149 draw();
150});
151function updateSpeedLabel() { speedLabel.textContent = `${Math.pow(10, parseFloat(speed.value)).toPrecision(2)} × real time`; }
152speed.addEventListener("input", updateSpeedLabel);
153updateSpeedLabel();
154select.addEventListener("change", () => loadField(select.value));
155window.addEventListener("resize", draw);
157if (fieldSolutions.length) loadField(fieldSolutions[0].path);
158else status.textContent = "No solution in this repository exports a displacement field.";
160function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); }
↑↓ move↵openescclose