Download player.html from epoch14/fly-sim: direct link, hf CLI and curl.
- Browser
- Download file 6.79 kB
-
https://huggingface.co/spaces/epoch14/fly-sim/resolve/main/player.html
- Command line
-
hf download hf://spaces/epoch14/fly-sim/player.html
-
curl -L -o player.html https://huggingface.co/spaces/epoch14/fly-sim/resolve/main/player.html
6.79 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>FlySim — trajectory player</title> | |
| <style> | |
| :root { color-scheme: dark; } | |
| body { margin:0; font:16px/1.6 system-ui, sans-serif; background:#0d1117; color:#e6edf3; } | |
| main { max-width: 960px; margin: 0 auto; padding: 32px 20px 64px; } | |
| h1 { font-size: 1.5rem; margin: 0 0 6px; } | |
| .sub { color:#8b949e; margin: 0 0 20px; } | |
| .controls { display:flex; gap:14px; align-items:center; margin: 12px 0; flex-wrap:wrap; } | |
| button { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px; | |
| padding:6px 14px; cursor:pointer; font-size:0.95rem; } | |
| button:hover { background:#30363d; } | |
| select { background:#161b22; color:#e6edf3; border:1px solid #30363d; border-radius:6px; padding:6px 10px; } | |
| label { font-size:0.9rem; color:#b6c2cf; } | |
| input[type=range] { accent-color:#58a6ff; } | |
| #t { font-variant-numeric: tabular-nums; color:#8b949e; min-width:110px; } | |
| canvas { width:100%; background:#0a0d12; border:1px solid #21262d; border-radius:10px; } | |
| .legend { display:flex; gap:18px; font-size:0.9rem; color:#b6c2cf; margin-top:8px; flex-wrap:wrap; } | |
| .dot { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:5px; } | |
| a { color:#58a6ff; text-decoration:none; } | |
| .dead { text-decoration: line-through; opacity: 0.4; } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>FlySim trajectory player</h1> | |
| <p class="sub">Real committed simulation data, replayed in your browser. No server — the JSON | |
| comes straight from the run logs (<a href="https://huggingface.co/datasets/epoch14/fly-sim-runs">fly-sim-runs</a>).</p> | |
| <div class="controls"> | |
| <button id="play">⏸ pause</button> | |
| <label>run: | |
| <select id="run"> | |
| <option value="data/colony60.json">colony — 3 walkers, 60 s (M1)</option> | |
| <option value="data/flight_left.json">flight — connectome-left drive (M0)</option> | |
| <option value="data/flight_right.json">flight — connectome-right drive (M0)</option> | |
| <option value="data/flight_straight.json">flight — straight drive (M0)</option> | |
| </select> | |
| </label> | |
| <label>speed: <input type="range" id="speed" min="0.25" max="16" step="0.25" value="4"> <span id="sp">4×</span></label> | |
| <span id="t">t = 0.0 s</span> | |
| </div> | |
| <canvas id="cv" width="960" height="600"></canvas> | |
| <div class="legend" id="legend"></div> | |
| <p class="sub" id="note"></p> | |
| </main> | |
| <script> | |
| const FLIES = [ | |
| {c: "#ff7b72", name: "fly0"}, | |
| {c: "#58a6ff", name: "fly1"}, | |
| {c: "#3fb950", name: "fly2"}, | |
| ]; | |
| let data = null, idx = 0, playing = true, speed = 4, lastStep = 0; | |
| const cv = document.getElementById("cv"), ctx = cv.getContext("2d"); | |
| const legend = document.getElementById("legend"); | |
| function bounds(frames) { | |
| let xs = [], ys = []; | |
| for (const f of frames) | |
| for (let k = 0; k < data.n; k++) { | |
| const p = f["f" + k]; if (p) { xs.push(p[0]); ys.push(p[1]); } | |
| const g = f["g" + k]; if (g) { xs.push(g[0]); ys.push(g[1]); } | |
| } | |
| const cx = (Math.min(...xs) + Math.max(...xs)) / 2; | |
| const cy = (Math.min(...ys) + Math.max(...ys)) / 2; | |
| const span = Math.max(Math.max(...xs) - Math.min(...xs), | |
| Math.max(...ys) - Math.min(...ys)) * 1.15 + 10; | |
| return {cx, cy, s: Math.min(cv.width, cv.height) / span}; | |
| } | |
| function w2c(x, y, b) { | |
| return [cv.width / 2 + (x - b.cx) * b.s, cv.height / 2 - (y - b.cy) * b.s]; | |
| } | |
| function draw() { | |
| ctx.fillStyle = "#0a0d12"; ctx.fillRect(0, 0, cv.width, cv.height); | |
| if (!data) return; | |
| const b = bounds(data.frames); | |
| // grid | |
| ctx.strokeStyle = "#161b22"; ctx.lineWidth = 1; | |
| for (let gx = 0; gx < cv.width; gx += 40) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, cv.height); ctx.stroke(); } | |
| for (let gy = 0; gy < cv.height; gy += 40) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(cv.width, gy); ctx.stroke(); } | |
| const f = data.frames[Math.min(idx, data.frames.length - 1)]; | |
| // goal (ghost) | |
| for (let k = 0; k < data.n; k++) { | |
| const g = f["g" + k]; | |
| if (g) { | |
| const [gx, gy] = w2c(g[0], g[1], b); | |
| ctx.strokeStyle = "#8b949e88"; ctx.setLineDash([4, 4]); ctx.lineWidth = 1.5; | |
| ctx.beginPath(); ctx.arc(gx, gy, 7, 0, 2 * Math.PI); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| } | |
| // flies | |
| for (let k = 0; k < data.n; k++) { | |
| const p = f["f" + k]; if (!p) continue; | |
| const [x, y] = w2c(p[0], p[1], b); | |
| const dead = f["a" + k] === false; | |
| // trail | |
| ctx.strokeStyle = FLIES[k].c + "55"; ctx.lineWidth = 1.5; | |
| ctx.beginPath(); | |
| for (let j = Math.max(0, idx - 400); j <= idx; j++) { | |
| const q = data.frames[j] && data.frames[j]["f" + k]; | |
| if (!q) continue; | |
| const [tx, ty] = w2c(q[0], q[1], b); | |
| j === 0 || !data.frames[j - 1] ? ctx.moveTo(tx, ty) : ctx.lineTo(tx, ty); | |
| } | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.arc(x, y, dead ? 4 : 6.5, 0, 2 * Math.PI); | |
| ctx.fillStyle = dead ? "#555" : FLIES[k].c; | |
| ctx.fill(); | |
| if (f["e" + k] !== undefined) { | |
| ctx.fillStyle = "#8b949e"; ctx.font = "11px system-ui"; | |
| ctx.fillText("E=" + f["e" + k], x + 10, y - 8); | |
| if (dead) { ctx.fillStyle = "#f85149aa"; ctx.fillText("dead", x + 10, y + 12); } | |
| } | |
| } | |
| document.getElementById("t").textContent = "t = " + f.t + " s"; | |
| } | |
| function legendRender() { | |
| legend.innerHTML = data.frames[0] && data.frames[0]["g0"] !== undefined | |
| ? FLIES.map((fl, k) => `<span><span class="dot" style="background:${fl.c}"></span>${fl.name} <span style="color:#6e7681">(dashed = connectome goal)</span></span>`).join("") | |
| : FLIES.slice(0, data.n).map(fl => `<span><span class="dot" style="background:${fl.c}"></span>${fl.name}</span>`).join(""); | |
| } | |
| function load(url) { | |
| data = null; | |
| fetch(url).then(r => r.json()).then(d => { | |
| data = d; idx = 0; | |
| legendRender(); | |
| document.getElementById("note").textContent = | |
| `${d.duration_s} s of simulated time · ${d.frames.length} replay frames · kind: ${d.kind}`; | |
| }); | |
| } | |
| function step(ts) { | |
| if (playing && data) { | |
| if (!lastStep) lastStep = ts; | |
| const dt = (ts - lastStep) / 1000; lastStep = ts; | |
| const fps = 60 * speed; | |
| idx += Math.max(1, Math.round(dt * fps * 0.0167 * 60)); | |
| if (idx >= data.frames.length) idx = 0; | |
| } else lastStep = ts; | |
| draw(); | |
| requestAnimationFrame(step); | |
| } | |
| document.getElementById("play").onclick = e => { | |
| playing = !playing; | |
| e.target.textContent = playing ? "⏸ pause" : "▶ play"; | |
| }; | |
| document.getElementById("speed").oninput = e => { | |
| speed = +e.target.value; | |
| document.getElementById("sp").textContent = speed + "×"; | |
| }; | |
| document.getElementById("run").onchange = e => load(e.target.value); | |
| load("data/colony60.json"); | |
| requestAnimationFrame(step); | |
| </script> | |
| </body> | |
| </html> | |