// One reactive view of the viewer's clock, and the only place the deck writes it. // Scrubbing writes the clock directly and records the moment on release: pinning per // pointer event goes through `CesiumController.setTime`, which rebuilds the window // under the thumb. import { JulianDate } from "@cesium/engine"; import { computed, onUnmounted, ref } from "vue"; import { rungFor, LADDER } from "../modules/util/clockDeck"; import { useCesiumStore } from "../stores/cesium"; import { useController } from "./useController"; // Every write here re-renders the deck, and the tick listener runs at render rate. const READOUT_MS = 100; /** The granularity the url records anyway. See CONTEXT.md, live vs pinned time. */ const PRESENT_TOLERANCE_MS = 60_000; export function useViewerClock() { const cc = useController(); const cesiumStore = useCesiumStore(); const { clock, clockViewModel } = cc.viewer; const now = ref(JulianDate.toDate(clock.currentTime)); const playing = ref(clock.shouldAnimate); const multiplier = ref(clock.multiplier); // Wall-clock time, sampled at the same throttle, so "how far from the present is // this" is answerable without a second timer. const systemNow = ref(Date.now()); // While a finger is down the clock is written, not read, or the thumb fights the tick. let scrubbing = false; let lastRead = 0; const removeTickListener = clock.onTick.addEventListener(() => { playing.value = clock.shouldAnimate; multiplier.value = clock.multiplier; const elapsed = performance.now(); if (elapsed - lastRead < READOUT_MS) { return; } lastRead = elapsed; systemNow.value = Date.now(); if (scrubbing) { return; } now.value = JulianDate.toDate(clock.currentTime); }); onUnmounted(removeTickListener); /** Not the same as pinned: 60× leaves the present in a second, with nothing touched. */ const offPresent = computed(() => Math.abs(now.value.getTime() - systemNow.value) > PRESENT_TOLERANCE_MS); const rung = computed(() => rungFor(multiplier.value)); function setPlaying(value: boolean): void { clockViewModel.shouldAnimate = value; playing.value = value; cc.viewer.scene.requestRender(); } const togglePlaying = (): void => setPlaying(!playing.value); function setMultiplier(value: number): void { clockViewModel.multiplier = value; multiplier.value = value; cc.viewer.scene.requestRender(); } /** Set the rate from a rung. */ const setRung = (index: number): void => setMultiplier(LADDER[index]!); function beginScrub(): void { scrubbing = true; } function scrubTo(date: Date): void { clock.currentTime = JulianDate.fromDate(date); now.value = date; cc.viewer.scene.requestRender(); } /** Records the moment, which pins the clock and puts `?time=` in the url. */ function endScrub(): void { scrubbing = false; cesiumStore.setTime(now.value.toISOString()); } /** Let go without recording: the clock never moved. */ function cancelScrub(): void { scrubbing = false; } /** Back to the present and to live, which also rebuilds the window to −12 h / +7 d. */ function goLive(): void { cc.setTime(new Date()); cesiumStore.setTime(null); systemNow.value = Date.now(); now.value = JulianDate.toDate(clock.currentTime); cc.viewer.scene.requestRender(); } return { now, playing, multiplier, rung, offPresent, togglePlaying, setMultiplier, setRung, beginScrub, scrubTo, endScrub, cancelScrub, goLive, }; } export type ViewerClock = ReturnType;