153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
import type { MouseEvent } from 'react';
|
||
|
||
export interface TimelineControlProps {
|
||
currentTime: number;
|
||
maxTime: number;
|
||
isPlaying: boolean;
|
||
playbackSpeed: number;
|
||
onTimeChange: (time: number) => void;
|
||
onPlayPause: () => void;
|
||
onSpeedChange: (speed: number) => void;
|
||
simulationStartTime?: string;
|
||
stepSize?: number;
|
||
tickInterval?: number;
|
||
majorTickEvery?: number;
|
||
timeUnitLabel?: string;
|
||
formatOffset?: (t: number) => string;
|
||
formatAbsolute?: (t: number, base: Date) => string;
|
||
showSpeedToggle?: boolean;
|
||
}
|
||
|
||
export function TimelineControl({
|
||
currentTime,
|
||
maxTime,
|
||
isPlaying,
|
||
playbackSpeed,
|
||
onTimeChange,
|
||
onPlayPause,
|
||
onSpeedChange,
|
||
simulationStartTime,
|
||
stepSize = 6,
|
||
tickInterval = 6,
|
||
majorTickEvery = 12,
|
||
timeUnitLabel = 'h',
|
||
formatOffset,
|
||
formatAbsolute,
|
||
showSpeedToggle = true,
|
||
}: TimelineControlProps) {
|
||
const progressPercent = maxTime > 0 ? (currentTime / maxTime) * 100 : 0;
|
||
|
||
const handleRewind = () => onTimeChange(Math.max(0, currentTime - stepSize));
|
||
const handleForward = () => onTimeChange(Math.min(maxTime, currentTime + stepSize));
|
||
const handleStart = () => onTimeChange(0);
|
||
const handleEnd = () => onTimeChange(maxTime);
|
||
|
||
const toggleSpeed = () => {
|
||
const speeds = [1, 2, 4];
|
||
const currentIndex = speeds.indexOf(playbackSpeed);
|
||
onSpeedChange(speeds[(currentIndex + 1) % speeds.length]);
|
||
};
|
||
|
||
const handleTimelineClick = (e: MouseEvent<HTMLDivElement>) => {
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
const percent = (e.clientX - rect.left) / rect.width;
|
||
onTimeChange(Math.max(0, Math.min(maxTime, Math.round(percent * maxTime))));
|
||
};
|
||
|
||
const timeLabels: number[] = [];
|
||
for (let t = 0; t <= maxTime; t += tickInterval) {
|
||
timeLabels.push(t);
|
||
}
|
||
|
||
const defaultOffset = (t: number) => `+${t.toFixed(0)}${timeUnitLabel}`;
|
||
const defaultAbsolute = (t: number, base: Date) => {
|
||
const d = new Date(base.getTime() + t * 3600 * 1000);
|
||
return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} KST`;
|
||
};
|
||
|
||
const offsetStr = (formatOffset ?? defaultOffset)(currentTime);
|
||
const baseDate = simulationStartTime ? new Date(simulationStartTime) : new Date();
|
||
const absoluteStr = (formatAbsolute ?? defaultAbsolute)(currentTime, baseDate);
|
||
|
||
return (
|
||
<div className="tlb">
|
||
<div className="tlc">
|
||
<div className="tb" onClick={handleStart}>
|
||
⏮
|
||
</div>
|
||
<div className="tb" onClick={handleRewind}>
|
||
◀
|
||
</div>
|
||
<div className={`tb ${isPlaying ? 'on' : ''}`} onClick={onPlayPause}>
|
||
{isPlaying ? '⏸' : '▶'}
|
||
</div>
|
||
<div className="tb" onClick={handleForward}>
|
||
▶▶
|
||
</div>
|
||
<div className="tb" onClick={handleEnd}>
|
||
⏭
|
||
</div>
|
||
{showSpeedToggle && (
|
||
<>
|
||
<div className="w-2" />
|
||
<div className="tb" onClick={toggleSpeed}>
|
||
{playbackSpeed}×
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="tlt">
|
||
<div className="tlls">
|
||
{timeLabels.map((t) => (
|
||
<span
|
||
key={t}
|
||
className={`tll ${Math.abs(currentTime - t) < 1 ? 'on' : ''}`}
|
||
style={{ left: `${(t / maxTime) * 100}%` }}
|
||
>
|
||
{t}
|
||
{timeUnitLabel}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="tlsw" onClick={handleTimelineClick}>
|
||
<div className="tlr">
|
||
<div className="tlp" style={{ width: `${progressPercent}%` }} />
|
||
{timeLabels.map((t) => (
|
||
<div
|
||
key={`marker-${t}`}
|
||
className={`tlm ${t % majorTickEvery === 0 ? 'mj' : ''}`}
|
||
style={{ left: `${(t / maxTime) * 100}%` }}
|
||
/>
|
||
))}
|
||
</div>
|
||
<div className="tlth" style={{ left: `${progressPercent}%` }} />
|
||
</div>
|
||
</div>
|
||
<div className="tli">
|
||
<div className="tlct">
|
||
{offsetStr} — {absoluteStr}
|
||
</div>
|
||
<div className="tlss">
|
||
<div className="tls">
|
||
<span className="tlsl">진행률</span>
|
||
<span className="tlsv">{progressPercent.toFixed(0)}%</span>
|
||
</div>
|
||
{showSpeedToggle && (
|
||
<div className="tls">
|
||
<span className="tlsl">속도</span>
|
||
<span className="tlsv">{playbackSpeed}×</span>
|
||
</div>
|
||
)}
|
||
<div className="tls">
|
||
<span className="tlsl">시간</span>
|
||
<span className="tlsv">
|
||
{currentTime.toFixed(0)}/{maxTime}
|
||
{timeUnitLabel}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|