/ challenges-old / string-solutions
Sign in
challenges-old / string-solutions
string-solutions / web / viewer.js
155 lines · 6.4 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><audio controls preload="none" src="${esc(s.output)}"></audio><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 }
31// ---------------------------------------------------------------- field viewer
32const select = document.getElementById("fieldSelect");
33const status = document.getElementById("fieldStatus");
34const canvas = document.getElementById("canvas");
35const ctx = canvas.getContext("2d");
36const playBtn = document.getElementById("playBtn");
37const speed = document.getElementById("speed");
38const speedLabel = document.getElementById("speedLabel");
39const timeSlider = document.getElementById("timeSlider");
40const timeLabel = document.getElementById("timeLabel");
42for (const f of fieldSolutions) {
43 const o = document.createElement("option");
44 o.value = f.path;
45 o.textContent = f.label;
46 select.appendChild(o);
49let field = null; // {x, t, u (Float32Array nt*nx), nx, nt, umax}
50let frame = 0;
51let playing = false;
52let lastTs = null;
53let simTime = 0;
55async function loadField(path) {
56 status.textContent = `Loading ${path}…`;
57 playing = false;
58 playBtn.textContent = "Play";
59 try {
60 const { FS } = await h5wasm.ready;
61 const buf = new Uint8Array(await (await fetch(path)).arrayBuffer());
62 const name = "current.h5";
63 try { FS.unlink(name); } catch (_) {}
64 FS.writeFile(name, buf);
65 const file = new h5wasm.File(name, "r");
66 const x = Float64Array.from(file.get("x").value);
67 const t = Float64Array.from(file.get("t").value);
68 const u = Float32Array.from(file.get("u").value);
69 file.close();
70 let umax = 0;
71 for (let i = 0; i < u.length; i++) umax = Math.max(umax, Math.abs(u[i]));
72 field = { x, t, u, nx: x.length, nt: t.length, umax };
73 timeSlider.max = String(field.nt - 1);
74 frame = 0;
75 simTime = 0;
76 status.textContent = `${path}: ${field.nx} points × ${field.nt} frames, max |u| = ${(umax * 1e3).toFixed(3)} mm`;
77 draw();
78 } catch (e) {
79 status.textContent = `Could not load ${path}: ${e}`;
80 }
83function draw() {
84 const dpr = window.devicePixelRatio || 1;
85 const w = canvas.clientWidth, h = canvas.clientHeight;
86 if (canvas.width !== Math.round(w * dpr)) { canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr); }
87 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
88 ctx.clearRect(0, 0, w, h);
89 const css = getComputedStyle(document.documentElement);
90 ctx.strokeStyle = css.getPropertyValue("--rest");
91 ctx.beginPath(); ctx.moveTo(20, h / 2); ctx.lineTo(w - 20, h / 2); ctx.stroke();
92 if (!field) return;
93 const { x, u, nx, umax } = field;
94 const L = x[nx - 1];
95 const scale = umax > 0 ? (0.42 * h) / umax : 0;
96 ctx.strokeStyle = css.getPropertyValue("--string");
97 ctx.lineWidth = 2;
98 ctx.beginPath();
99 for (let i = 0; i < nx; i++) {
100 const px = 20 + (x[i] / L) * (w - 40);
101 const py = h / 2 - u[frame * nx + i] * scale;
102 if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
103 }
104 ctx.stroke();
105 // pickup marker at x_m = 0.54 m
106 const xm = 20 + (0.54 / L) * (w - 40);
107 ctx.strokeStyle = css.getPropertyValue("--accent");
108 ctx.setLineDash([4, 4]);
109 ctx.beginPath(); ctx.moveTo(xm, 10); ctx.lineTo(xm, h - 10); ctx.stroke();
110 ctx.setLineDash([]);
111 ctx.fillStyle = css.getPropertyValue("--muted");
112 ctx.font = "12px system-ui, sans-serif";
113 ctx.fillText(`t = ${(field.t[frame] * 1e3).toFixed(1)} ms`, 24, 16);
114 ctx.fillText("pickup", xm + 4, h - 14);
115 timeSlider.value = String(frame);
116 timeLabel.textContent = `${(field.t[frame] * 1e3).toFixed(1)} ms`;
119function tick(ts) {
120 if (!playing || !field) return;
121 if (lastTs !== null) {
122 const factor = Math.pow(10, parseFloat(speed.value));
123 simTime += ((ts - lastTs) / 1000) * factor;
124 const tEnd = field.t[field.nt - 1];
125 if (simTime > tEnd) simTime = 0;
126 const dt = field.t[1] - field.t[0];
127 frame = Math.min(field.nt - 1, Math.round(simTime / dt));
128 draw();
129 }
130 lastTs = ts;
131 requestAnimationFrame(tick);
134playBtn.addEventListener("click", () => {
135 playing = !playing;
136 playBtn.textContent = playing ? "Pause" : "Play";
137 lastTs = null;
138 if (playing) requestAnimationFrame(tick);
139});
140timeSlider.addEventListener("input", () => {
141 if (!field) return;
142 frame = parseInt(timeSlider.value, 10);
143 simTime = field.t[frame];
144 draw();
145});
146function updateSpeedLabel() { speedLabel.textContent = `${Math.pow(10, parseFloat(speed.value)).toPrecision(2)} × real time`; }
147speed.addEventListener("input", updateSpeedLabel);
148updateSpeedLabel();
149select.addEventListener("change", () => loadField(select.value));
150window.addEventListener("resize", draw);
152if (fieldSolutions.length) loadField(fieldSolutions[0].path);
153else status.textContent = "No solution in this repository exports a displacement field.";
155function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); }
↑↓ move↵openescclose