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 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 03:16:26 +01:00
parent 2a58ad372e
commit f0f3516efa

View File

@@ -1,20 +1,31 @@
// Clock simulation engine // Clock simulation engine — uses real timestamps for consistent timing
import { state } from './state.js'; import { state } from './state.js';
import { evaluateAll } from './gates.js'; import { evaluateAll } from './gates.js';
import { forceRecordSample } from './waveform.js'; import { forceRecordSample } from './waveform.js';
export function simTick() { let lastTickTime = 0;
// Toggle all CLOCK gates let animFrameId = null;
state.gates.forEach(g => {
if (g.type === 'CLOCK') { function simLoop(now) {
g.value = g.value ? 0 : 1; 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() { export function startSim() {
@@ -29,18 +40,18 @@ export function startSim() {
state.waveformVisible = true; state.waveformVisible = true;
document.getElementById('waveform-panel').classList.add('visible'); document.getElementById('waveform-panel').classList.add('visible');
document.getElementById('sim-btn').classList.add('active'); document.getElementById('sim-btn').classList.add('active');
// Trigger resize via event so renderer picks it up
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
state.simInterval = setInterval(simTick, state.simSpeed); lastTickTime = performance.now();
animFrameId = requestAnimationFrame(simLoop);
updateSimUI(); updateSimUI();
} }
export function stopSim() { export function stopSim() {
state.simRunning = false; state.simRunning = false;
if (state.simInterval) clearInterval(state.simInterval); if (animFrameId) cancelAnimationFrame(animFrameId);
state.simInterval = null; animFrameId = null;
updateSimUI(); updateSimUI();
} }
@@ -57,10 +68,8 @@ export function updateSimUI() {
} }
export function adjustSpeed(delta) { export function adjustSpeed(delta) {
const wasRunning = state.simRunning;
state.simSpeed = Math.max(50, Math.min(2000, state.simSpeed + delta)); state.simSpeed = Math.max(50, Math.min(2000, state.simSpeed + delta));
if (state.simRunning) { // No need to restart — the loop reads simSpeed dynamically
clearInterval(state.simInterval);
state.simInterval = setInterval(simTick, state.simSpeed);
}
updateSimUI(); updateSimUI();
} }