Files
meadows/src/composables/usePlayback.ts
Julien Calixte 69435b1315 feat(editor): animate the canvas in step with the simulation
A simulation store owns one run and one playhead, so the chart and the canvas
read off the same instant. While the panel is open, Stocks show a live value and
a fill gauge, Flows and Converters their current value; a play/pause + scrubber
drives the playhead (a wall-clock-paced clock in usePlayback), and a marker
tracks it on the chart.
2026-06-20 14:35:45 +02:00

50 lines
1.4 KiB
TypeScript

/**
* Playback clock — the wall-clock that advances the simulation playhead while
* `playing`. It lives in a composable (mounted once, in the Editor) because it
* needs `requestAnimationFrame` and component teardown; the simulation store
* stays pure state + actions.
*
* A run plays over a fixed wall-clock window regardless of how many samples it
* has, so a 40-step model and a 480-step one feel the same speed. Elapsed time is
* accumulated and drained in whole frames, so a slow tab catches up rather than
* drifting.
*/
import { onBeforeUnmount, watch } from "vue"
import { useSimulationStore } from "@/store/simulation"
/** Target wall-clock duration for a whole run, in milliseconds. */
const RUN_DURATION_MS = 6000
export function usePlayback(): void {
const sim = useSimulationStore()
let raf = 0
let last = 0
let carry = 0
function frame(now: number): void {
if (!sim.playing) return
carry += now - last
last = now
const msPerFrame = Math.max(16, RUN_DURATION_MS / Math.max(1, sim.frameCount))
while (carry >= msPerFrame && sim.playing) {
carry -= msPerFrame
sim.tick()
}
raf = requestAnimationFrame(frame)
}
watch(
() => sim.playing,
(playing) => {
cancelAnimationFrame(raf)
if (playing) {
last = performance.now()
carry = 0
raf = requestAnimationFrame(frame)
}
},
)
onBeforeUnmount(() => cancelAnimationFrame(raf))
}