From 96ce11a8c568477349baf95e545949a1971c1a93 Mon Sep 17 00:00:00 2001 From: Jay Singh Date: Sun, 7 Jun 2026 01:15:06 +0530 Subject: [PATCH 1/2] fix: add fixed timestep accumulator to prevent energy drift in BouncingBall --- simulations/BouncingBall.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/simulations/BouncingBall.jsx b/simulations/BouncingBall.jsx index 051a21cf..bb74dab1 100644 --- a/simulations/BouncingBall.jsx +++ b/simulations/BouncingBall.jsx @@ -55,6 +55,7 @@ export default function BouncingBall() { const dragControllerRef = useRef(null); const maxHeightRef = useRef(0); const fallStartTimeRef = useRef(0); + const accumulatorRef = useRef(0); // Sim info const { simData, updateSimInfo } = useSimInfo({ @@ -145,8 +146,17 @@ export default function BouncingBall() { p.draw = () => { if (!bodyRef.current || !trailLayer) return; - const dt = computeDelta(p); - if (dt === 0) return; + const rawDt = computeDelta(p); + if (rawDt === 0) return; + + // Fixed timestep accumulator — prevents energy drift from variable frame rate + const FIXED_DT = 1 / 120; // 120Hz physics steps + accumulatorRef.current += rawDt; + // Cap accumulator to prevent spiral of death + if (accumulatorRef.current > 0.1) accumulatorRef.current = 0.1; + const dt = FIXED_DT; + if (accumulatorRef.current < FIXED_DT) return; + accumulatorRef.current -= FIXED_DT; const { size, gravity, trailEnabled, ballColor, mass, restitution } = inputsRef.current; From ede209f05a63a0cf6212a160a10b82caf36f1de2 Mon Sep 17 00:00:00 2001 From: Jay Singh Date: Sun, 7 Jun 2026 15:02:56 +0530 Subject: [PATCH 2/2] fix: centralize fixed timestep accumulator in Time.js to prevent energy drift across all simulations --- app/(core)/constants/Time.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/(core)/constants/Time.js b/app/(core)/constants/Time.js index c4ba3f37..56b9c37f 100644 --- a/app/(core)/constants/Time.js +++ b/app/(core)/constants/Time.js @@ -3,6 +3,8 @@ let timeScale = 1; let paused = false; let manualStepDelta = 0; let simulationInstances = new Map(); // Mappa per tenere traccia di ogni istanza +let accumulators = new Map(); // Fixed timestep accumulators per instance +const FIXED_DT = 1 / 120; // 120Hz fixed physics timestep /** * Returns dt in seconds, limited to a max step for stability. @@ -26,16 +28,25 @@ export function computeDelta(p) { } let lastInstanceMillis = simulationInstances.get(instanceId); - let dt = (now - lastInstanceMillis) / 1000; // seconds + let rawDt = (now - lastInstanceMillis) / 1000; // seconds // Aggiorna lastMillis per questa istanza simulationInstances.set(instanceId, now); // limit burst (e.g. when tab regains focus) const maxStep = 1 / 30; // ~33ms - if (dt > maxStep) dt = maxStep; + if (rawDt > maxStep) rawDt = maxStep; - return dt * timeScale; + rawDt *= timeScale; + + // Fixed timestep accumulator — prevents energy drift from variable frame rate + const acc = (accumulators.get(instanceId) || 0) + rawDt; + if (acc < FIXED_DT) { + accumulators.set(instanceId, acc); + return 0; + } + accumulators.set(instanceId, acc - FIXED_DT); + return FIXED_DT; } export function setTimeScale(scale) { @@ -47,6 +58,7 @@ export function togglePause() { // Quando mettiamo in pausa, resettiamo i lastMillis per evitare salti al resume if (paused) { simulationInstances.clear(); + accumulators.clear(); } } @@ -54,6 +66,7 @@ export function setPause(value) { paused = value; if (paused) { simulationInstances.clear(); + accumulators.clear(); } } @@ -66,6 +79,7 @@ export function stepSimulation(delta) { export function resetTime() { paused = false; simulationInstances.clear(); // Pulisci tutte le istanze + accumulators.clear(); } export function isPaused() {