orbit-studio / src /composables /useViewerClock.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
3.66 kB
// 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<typeof useViewerClock>;