1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { useEffect, useMemo, useRef } from 'react';
5import { Box, Typography, Slider, IconButton } from '@mui/material';
6import PlayArrowIcon from '@mui/icons-material/PlayArrow';
7import PauseIcon from '@mui/icons-material/Pause';
8import FastRewindIcon from '@mui/icons-material/FastRewind';
9import FastForwardIcon from '@mui/icons-material/FastForward';
10import { useScenarioStore } from '@/stores/scenarioStore';
12export default function Timeline() {
19 targetPlaybackDuration,
20 } = useScenarioStore();
22 const animationFrameRef = useRef(0);
23 const lastTimeRef = useRef(0);
25 // Calculate dynamic values based on simulation duration and user settings.
26 const { simulationDuration, speedFactor, sliderStep, timeStep } =
28 const duration = Math.max(
30 globalParameters.end - globalParameters.start
33 let realPlaybackDuration: number;
35 if (targetPlaybackDuration !== null && targetPlaybackDuration > 0) {
36 realPlaybackDuration = targetPlaybackDuration;
38 if (duration > 0 && duration < 5) {
39 realPlaybackDuration = 5;
41 realPlaybackDuration = duration;
46 realPlaybackDuration > 0 ? duration / realPlaybackDuration : 0;
47 const sStep = duration > 0 ? duration / 1000 : 0.01;
48 const tStep = duration > 0 ? duration / 100 : 0.01;
51 simulationDuration: duration,
57 globalParameters.start,
59 targetPlaybackDuration,
62 // Effect to stop playback when the end is reached.
64 if (isPlaying && currentTime >= globalParameters.end) {
67 }, [isPlaying, currentTime, globalParameters.end, togglePlayPause]);
70 const animate = (now: number) => {
71 const deltaTime = (now - lastTimeRef.current) / 1000;
72 lastTimeRef.current = now;
74 // Let the store handle clamping. The effect above will stop playback.
75 setCurrentTime((prevTime) => prevTime + deltaTime * speedFactor);
77 animationFrameRef.current = requestAnimationFrame(animate);
80 if (isPlaying && simulationDuration > 0) {
81 lastTimeRef.current = performance.now();
82 animationFrameRef.current = requestAnimationFrame(animate);
84 cancelAnimationFrame(animationFrameRef.current);
87 return () => cancelAnimationFrame(animationFrameRef.current);
96 const handleSliderChange = (_event: Event, newValue: number | number[]) => {
97 setCurrentTime(newValue as number);
104 alignItems: 'center',
111 <Box sx={{ flexShrink: 0 }}>
114 onClick={() => setCurrentTime((t) => t - timeStep)}
118 <IconButton onClick={togglePlayPause}>
119 {isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
123 onClick={() => setCurrentTime((t) => t + timeStep)}
130 sx={{ flexShrink: 0, width: '4em', textAlign: 'center' }}
132 {currentTime.toFixed(2)}s
136 onChange={handleSliderChange}
138 min={globalParameters.start}
139 max={globalParameters.end}
144 // Disable transitions to ensure precise, immediate tracking
145 '& .MuiSlider-thumb, & .MuiSlider-track': {
149 valueLabelDisplay="auto"
150 valueLabelFormat={(value) => `${value.toFixed(2)}s`}
154 sx={{ flexShrink: 0, width: '4em', textAlign: 'right' }}
156 {globalParameters.end.toFixed(2)}s