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.
50 lines
1.4 KiB
TypeScript
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))
|
|
}
|