Wait for each frame while the world timeline plays, and quiet the bar

Play no longer runs ahead of the data. The world clock gains a wait, which
holds continuous play where it is while a layer loads what the time needs,
and a gate, which keeps play from passing a time a layer has yet to load.
Weather gates play at its next frame and waits there until the frame shows,
so every frame is seen however fast play runs; it waits no more than eight
seconds on a frame that never comes, and takes a wait only when its frames
change. Play pressed while a frame loads waits for it too. A scrub still
drops what loads and moves on.

The bar loses its long date and the dates at its ends: the time rides in a
bubble over the handle, which pulses while play waits, and the bar runs
left to right in every language. Play starts at real time.
This commit is contained in:
Sameh Khamis committed 2026-11-02 08:03:46 -08:00
1 parent d04e304502
commit 056b3b58c6
9 files changed
+357 -40

No files matched your search

+59 -1
View File
@@ -4,6 +4,10 @@ export const RADAR_MAX_GAP_MS = 30 * 60_000;
export const REGIONAL_INFRARED_MAX_GAP_MS = 30 * 60_000;
export const GLOBAL_INFRARED_MAX_GAP_MS = 3 * 60 * 60_000;
export const LIGHTNING_MAX_GAP_MS = 30 * 60_000;
/** The longest play waits on a frame before it goes on without it. */
export const PLAY_WAIT_MS = 8_000;
let clocks = 0;
/**
* Transient observed time. Products must honor the signal before installing a
@@ -33,6 +37,30 @@ export function createWeatherClock({
let generation = 0;
let timer = null;
let pending = null;
// This clock's name on the world, for its holds and its gate.
const holdId = `weather:${++clocks}`;
// While the world plays, the hold on it as frames for its time load, and
// the timer that lets play go on should a frame never come.
let waiting = null;
let waitTimer = null;
const stopWaiting = () => {
const release = waiting;
waiting = null;
if (waitTimer !== null) clearTimeout(waitTimer);
waitTimer = null;
release?.();
};
const startWaiting = () => {
if (waiting || destroyed || !registry.size) return;
waiting = world.wait(holdId);
waitTimer = setTimeout(() => {
waitTimer = null;
stopWaiting();
}, PLAY_WAIT_MS);
};
// Playing on its own, the world waits for frames as they load.
const worldPlaysOn = (state = world.getState()) =>
state.playing && !state.live && !state.stepping;
let destroyed = false;
const timesFor = (product) =>
[...new Set(product.getTimes())]
@@ -130,7 +158,13 @@ export function createWeatherClock({
cancelTimer();
const owner = ++generation;
if (!canPlay()) stopPlaying();
// Playing, the world waits for new frames; a later move takes over the
// wait, and a scrub (not playing) drops what loads and moves on. A refresh
// that shows the same frames does not hold play.
const framesChange = selections() !== applied;
applied = selections();
stopWaiting();
if (framesChange && worldPlaysOn()) startWaiting();
for (const entry of registry.values()) entry.controller?.abort();
// Start all products together; a rejection must not skip the settle barrier.
const tasks = [...registry.values()].map((entry) => {
@@ -144,6 +178,7 @@ export function createWeatherClock({
});
const result = Promise.allSettled(tasks).then(() => {
if (destroyed || owner !== generation) return false;
stopWaiting();
pending = null;
if (!canPlay()) stopPlaying();
notify();
@@ -269,8 +304,10 @@ export function createWeatherClock({
},
refresh() {
if (destroyed) return Promise.resolve(false);
// New frames, or products added or dropped.
// New frames, or products added or dropped, and with them the next.
world.coverageChanged();
gated = undefined;
gateNext(world.getState());
if (!canPlay()) {
stopPlaying();
cancelTimer();
@@ -289,6 +326,8 @@ export function createWeatherClock({
stopPlaying();
destroyed = true;
++generation;
stopWaiting();
world.gate(holdId, null);
cancelTimer();
unfollowWorld();
// The last weather clock leaves the world live, as a new one expects.
@@ -306,6 +345,11 @@ export function createWeatherClock({
// changes the mode or a product's frame.
function followWorld(state) {
if (destroyed) return;
gateNext(state);
// Play pressed while a frame still loads waits for it too.
const playsOn = worldPlaysOn(state);
if (playsOn && !playedOn && pending) startWaiting();
playedOn = playsOn;
const nextMode = state.live ? 'latest' : 'history';
const nextTarget = state.live
? null
@@ -319,6 +363,20 @@ export function createWeatherClock({
if (moving || modeChanged || selections() !== applied) void transition();
else notify();
}
// Playing on its own, the world stops at each next frame to load it rather
// than run past frames between its ticks.
let gated = null;
let playedOn = false;
function gateNext(state) {
const next = worldPlaysOn(state)
? (getTimeline()
.map((time) => Date.parse(time))
.find((time) => time > Math.floor(state.timeMs)) ?? null)
: null;
if (next === gated) return;
gated = next;
world.gate(holdId, next);
}
const unfollowWorld = world.subscribe(followWorld);
driver = world.attachDriver({
play: () => (canPlay() ? void api.play() : false),