2026-05-14 20:15:23 +09:00
|
|
|
function requestRender() {
|
|
|
|
|
renderDirty = true;
|
|
|
|
|
}
|
|
|
|
|
function drawFrame(forceStats = false) {
|
|
|
|
|
render();
|
|
|
|
|
updateStats(forceStats);
|
|
|
|
|
renderTooltip();
|
|
|
|
|
renderDirty = false;
|
|
|
|
|
lastRenderAt = performance.now();
|
|
|
|
|
}
|
|
|
|
|
function scheduleLoop() {
|
|
|
|
|
if (loopScheduled) return;
|
|
|
|
|
loopScheduled = true;
|
|
|
|
|
const delay = document.hidden
|
|
|
|
|
? LoopConfig.hiddenPollMs
|
|
|
|
|
: running ? 0 : LoopConfig.idlePollMs;
|
|
|
|
|
if (delay > 0) {
|
|
|
|
|
loopTimer = setTimeout(() => {
|
|
|
|
|
loopTimer = null;
|
|
|
|
|
requestAnimationFrame(loop);
|
|
|
|
|
}, delay);
|
|
|
|
|
} else {
|
|
|
|
|
requestAnimationFrame(loop);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function loop(now = performance.now()) {
|
|
|
|
|
loopScheduled = false;
|
|
|
|
|
if (running && !document.hidden && now - lastSimTickAt >= LoopConfig.simTickMs) {
|
|
|
|
|
lastSimTickAt = now;
|
|
|
|
|
const steps = Number(els.speed.value);
|
|
|
|
|
const stepBudget = LoopConfig.stepBudgetMs * Math.max(1, steps);
|
|
|
|
|
const loopStart = performance.now();
|
|
|
|
|
let completedSteps = 0;
|
|
|
|
|
for (let i = 0; i < steps; i++) {
|
|
|
|
|
sim.step();
|
|
|
|
|
completedSteps++;
|
|
|
|
|
if (completedSteps > 0 && performance.now() - loopStart > stepBudget) break;
|
|
|
|
|
}
|
|
|
|
|
if (completedSteps > 0) requestRender();
|
|
|
|
|
}
|
|
|
|
|
const shouldRender = !document.hidden &&
|
|
|
|
|
(renderDirty || (running && now - lastRenderAt >= LoopConfig.renderMs));
|
|
|
|
|
if (shouldRender) drawFrame();
|
|
|
|
|
scheduleLoop();
|
|
|
|
|
}
|
|
|
|
|
function reset() {
|
|
|
|
|
const size = Number(els.worldSize.value);
|
2026-09-16 13:51:43 +09:00
|
|
|
const initialPopulation = Number(els.agentCount.value);
|
2026-05-14 20:15:23 +09:00
|
|
|
els.canvas.width = size;
|
|
|
|
|
els.canvas.height = size;
|
|
|
|
|
renderImage = null;
|
|
|
|
|
renderImageSize = 0;
|
2026-09-16 13:51:43 +09:00
|
|
|
sim = new Simulation(size, initialPopulation);
|
2026-05-14 20:15:23 +09:00
|
|
|
setLegend();
|
|
|
|
|
drawFrame(true);
|
|
|
|
|
renderStateGraph();
|
|
|
|
|
}
|
|
|
|
|
function refreshStateGraph() {
|
|
|
|
|
renderStateGraph();
|
|
|
|
|
clearStateGraphInfo();
|
|
|
|
|
}
|
|
|
|
|
els.toggleRun.addEventListener("click", () => {
|
|
|
|
|
running = !running;
|
|
|
|
|
els.toggleRun.textContent = running ? "Pause" : "Run";
|
|
|
|
|
lastSimTickAt = 0;
|
|
|
|
|
if (running && loopTimer) {
|
|
|
|
|
clearTimeout(loopTimer);
|
|
|
|
|
loopTimer = null;
|
|
|
|
|
loopScheduled = false;
|
|
|
|
|
scheduleLoop();
|
|
|
|
|
} else if (running) {
|
|
|
|
|
scheduleLoop();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
els.stepOnce.addEventListener("click", () => {
|
|
|
|
|
sim.step();
|
|
|
|
|
requestRender();
|
|
|
|
|
drawFrame(true);
|
|
|
|
|
renderStateGraph();
|
|
|
|
|
});
|
|
|
|
|
els.resetWorld.addEventListener("click", reset);
|
|
|
|
|
for (const control of [els.worldSize, els.agentCount]) control.addEventListener("change", reset);
|
|
|
|
|
els.viewMode.addEventListener("change", () => {
|
|
|
|
|
setLegend();
|
|
|
|
|
requestRender();
|
|
|
|
|
drawFrame();
|
|
|
|
|
});
|
|
|
|
|
els.canvas.addEventListener("mousemove", showTooltip);
|
|
|
|
|
els.canvas.addEventListener("mouseleave", hideTooltip);
|
|
|
|
|
for (const control of [els.historyFilter, els.historySort, els.historyMetric]) {
|
|
|
|
|
control?.addEventListener("change", refreshStateGraph);
|
|
|
|
|
}
|
|
|
|
|
els.toggleActiveStates?.addEventListener("click", () => {
|
|
|
|
|
graphState.showActive = !graphState.showActive;
|
|
|
|
|
els.toggleActiveStates.setAttribute("aria-pressed", String(graphState.showActive));
|
|
|
|
|
refreshStateGraph();
|
|
|
|
|
});
|
|
|
|
|
els.togglePastStates?.addEventListener("click", () => {
|
|
|
|
|
graphState.showPast = !graphState.showPast;
|
|
|
|
|
els.togglePastStates.setAttribute("aria-pressed", String(graphState.showPast));
|
|
|
|
|
refreshStateGraph();
|
|
|
|
|
});
|
|
|
|
|
els.stateGraph?.addEventListener("mousemove", updateStateGraphInfo);
|
|
|
|
|
els.stateGraph?.addEventListener("mouseleave", clearStateGraphInfo);
|
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
|
|
|
lastSimTickAt = 0;
|
|
|
|
|
requestRender();
|
|
|
|
|
if (!document.hidden && loopTimer) {
|
|
|
|
|
clearTimeout(loopTimer);
|
|
|
|
|
loopTimer = null;
|
|
|
|
|
loopScheduled = false;
|
|
|
|
|
scheduleLoop();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
reset();
|
|
|
|
|
loop();
|