From f0f3516efa51c65ccd8d7575fb0d3b59112bb750 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 03:16:26 +0100 Subject: [PATCH] fix: use requestAnimationFrame + real timestamps for clock simulation Replace setInterval with requestAnimationFrame loop that tracks elapsed time via performance.now(). Clock ticks now fire based on real time rather than assuming perfect interval spacing, so user interactions (toggling inputs, dragging gates) no longer cause the clock to stutter or pause. Co-Authored-By: Claude Opus 4.6 --- js/simulation.js | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/js/simulation.js b/js/simulation.js index 463a5af..abaeffb 100644 --- a/js/simulation.js +++ b/js/simulation.js @@ -1,20 +1,31 @@ -// Clock simulation engine +// Clock simulation engine — uses real timestamps for consistent timing import { state } from './state.js'; import { evaluateAll } from './gates.js'; import { forceRecordSample } from './waveform.js'; -export function simTick() { - // Toggle all CLOCK gates - state.gates.forEach(g => { - if (g.type === 'CLOCK') { - g.value = g.value ? 0 : 1; +let lastTickTime = 0; +let animFrameId = null; + +function simLoop(now) { + if (!state.simRunning) return; + + // Fire ticks that are due based on real elapsed time + while (now - lastTickTime >= state.simSpeed) { + lastTickTime += state.simSpeed; + + // Toggle all CLOCK gates + state.gates.forEach(g => { + if (g.type === 'CLOCK') { + g.value = g.value ? 0 : 1; + } + }); + evaluateAll(); + if (state.recording && state.waveformVisible) { + forceRecordSample(); } - }); - evaluateAll(); - // Force record even if evaluateAll didn't detect change - if (state.recording && state.waveformVisible) { - forceRecordSample(); } + + animFrameId = requestAnimationFrame(simLoop); } export function startSim() { @@ -29,18 +40,18 @@ export function startSim() { state.waveformVisible = true; document.getElementById('waveform-panel').classList.add('visible'); document.getElementById('sim-btn').classList.add('active'); - // Trigger resize via event so renderer picks it up window.dispatchEvent(new Event('resize')); } - state.simInterval = setInterval(simTick, state.simSpeed); + lastTickTime = performance.now(); + animFrameId = requestAnimationFrame(simLoop); updateSimUI(); } export function stopSim() { state.simRunning = false; - if (state.simInterval) clearInterval(state.simInterval); - state.simInterval = null; + if (animFrameId) cancelAnimationFrame(animFrameId); + animFrameId = null; updateSimUI(); } @@ -57,10 +68,8 @@ export function updateSimUI() { } export function adjustSpeed(delta) { + const wasRunning = state.simRunning; state.simSpeed = Math.max(50, Math.min(2000, state.simSpeed + delta)); - if (state.simRunning) { - clearInterval(state.simInterval); - state.simInterval = setInterval(simTick, state.simSpeed); - } + // No need to restart — the loop reads simSpeed dynamically updateSimUI(); }