fly-sim / player.html
epoch14's picture
player: all 3 drives
35338ac verified
Raw History Blame Contribute Delete
6.79 kB
<!DOCTYPE html>
<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>