138 lines
3.3 KiB
JavaScript
138 lines
3.3 KiB
JavaScript
import { ref, computed } from 'vue'
|
|
|
|
const TICK_MS = 100
|
|
|
|
export function useReplayControls() {
|
|
const isPaused = ref(true)
|
|
const speed = ref(60)
|
|
const scenarioStartMs = ref(0)
|
|
const scenarioEndMs = ref(0)
|
|
const currentMs = ref(0)
|
|
let timer = null
|
|
|
|
const scenarioDurationMinutes = computed(() => {
|
|
const span = scenarioEndMs.value - scenarioStartMs.value
|
|
return span > 0 ? span / 60_000 : 0
|
|
})
|
|
|
|
const currentOffsetMinutes = computed(() => {
|
|
const span = currentMs.value - scenarioStartMs.value
|
|
return span > 0 ? span / 60_000 : 0
|
|
})
|
|
|
|
const progress = computed(() => {
|
|
const span = scenarioEndMs.value - scenarioStartMs.value
|
|
if (span <= 0) return 0
|
|
return Math.min(100, ((currentMs.value - scenarioStartMs.value) / span) * 100)
|
|
})
|
|
|
|
const formattedTime = computed(() => {
|
|
const mins = currentOffsetMinutes.value
|
|
const h = Math.floor(mins / 60)
|
|
const m = Math.floor(mins % 60)
|
|
return `${h}:${String(m).padStart(2, '0')}`
|
|
})
|
|
|
|
function setScenarioBounds(startMs, endMs) {
|
|
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return
|
|
scenarioStartMs.value = startMs
|
|
scenarioEndMs.value = endMs
|
|
if (currentMs.value < startMs || currentMs.value > endMs) {
|
|
currentMs.value = endMs
|
|
}
|
|
}
|
|
|
|
function syncToEnd() {
|
|
if (scenarioEndMs.value > scenarioStartMs.value) {
|
|
currentMs.value = scenarioEndMs.value
|
|
}
|
|
}
|
|
|
|
function pause() {
|
|
isPaused.value = true
|
|
stopTick()
|
|
}
|
|
|
|
function resume() {
|
|
if (scenarioEndMs.value <= scenarioStartMs.value) return
|
|
if (currentMs.value >= scenarioEndMs.value) {
|
|
currentMs.value = scenarioStartMs.value
|
|
}
|
|
isPaused.value = false
|
|
startTick()
|
|
}
|
|
|
|
function setSpeed(s) {
|
|
speed.value = s
|
|
if (s === 0) {
|
|
currentMs.value = scenarioEndMs.value
|
|
pause()
|
|
}
|
|
}
|
|
|
|
function jumpToOffset(minutes) {
|
|
if (scenarioEndMs.value <= scenarioStartMs.value) return
|
|
const target = scenarioStartMs.value + minutes * 60_000
|
|
currentMs.value = Math.min(Math.max(target, scenarioStartMs.value), scenarioEndMs.value)
|
|
}
|
|
|
|
function jumpToTimestamp(iso) {
|
|
if (!iso || scenarioEndMs.value <= scenarioStartMs.value) return
|
|
const ms = new Date(iso).getTime()
|
|
currentMs.value = Math.min(Math.max(ms, scenarioStartMs.value), scenarioEndMs.value)
|
|
}
|
|
|
|
function isAtOrBefore(iso) {
|
|
if (!iso) return true
|
|
return new Date(iso).getTime() <= currentMs.value
|
|
}
|
|
|
|
function tick() {
|
|
if (isPaused.value || scenarioEndMs.value <= scenarioStartMs.value) return
|
|
|
|
if (speed.value === 0) {
|
|
currentMs.value = scenarioEndMs.value
|
|
pause()
|
|
return
|
|
}
|
|
|
|
currentMs.value = Math.min(currentMs.value + TICK_MS * speed.value, scenarioEndMs.value)
|
|
if (currentMs.value >= scenarioEndMs.value) pause()
|
|
}
|
|
|
|
function startTick() {
|
|
stopTick()
|
|
timer = setInterval(tick, TICK_MS)
|
|
}
|
|
|
|
function stopTick() {
|
|
if (timer) clearInterval(timer)
|
|
timer = null
|
|
}
|
|
|
|
function stopPlayback() {
|
|
stopTick()
|
|
}
|
|
|
|
return {
|
|
isPaused,
|
|
speed,
|
|
scenarioStartMs,
|
|
scenarioEndMs,
|
|
currentMs,
|
|
scenarioDurationMinutes,
|
|
currentOffsetMinutes,
|
|
progress,
|
|
formattedTime,
|
|
setScenarioBounds,
|
|
syncToEnd,
|
|
pause,
|
|
resume,
|
|
setSpeed,
|
|
jumpToOffset,
|
|
jumpToTimestamp,
|
|
isAtOrBefore,
|
|
stopPlayback,
|
|
}
|
|
}
|