mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
The main timeline is the only time control; the Weather panel shows its frame time
The Weather panel kept a second time transport of its own: a slider, step buttons, Latest and Play, backed by a replay in the weather clock that stepped the observed frames every two seconds and went round from the last to the first. The weather clock also attached itself to the world clock as a driver, so the timeline's Play became that replay whenever weather was on. The panel now shows the frame time only: the newest frame and its age while live, the timeline time alone at a past time. The weather clock no longer has setTarget, step, latest, play, pause or togglePlay, its state no longer has playing, and it no longer drives the world clock. It still follows the world time, registers each product's frames as marks the timeline's arrow keys step to, holds its products, and gates the world's continuous play at each next frame until it loads. weatherClock.world names the world clock it follows. A weather row's play, latest and step params move the world clock as the timeline does: play toggles continuous play (from the start of the covered time when live), latest goes live, and step goes to the previous or next mark. The rail timeline component, which only the panel used, is removed, along with its unused messages in every locale.
This commit is contained in:
1 parent
bce737bf32
commit
3cfd414e70
35 files changed
+328
-1033
No files matched your search
+22
-146
@@ -59,12 +59,15 @@ export const PLAY_WAIT_MS = 8_000;
|
||||
// costs play nothing.
|
||||
|
||||
let clocks = 0;
|
||||
// How many weather clocks follow each world clock.
|
||||
const followers = new WeakMap();
|
||||
|
||||
/**
|
||||
* Transient observed time. Products must honor the signal before installing a
|
||||
* frame. Its time is the world clock's: latest while the world is live,
|
||||
* history at the world's time otherwise. Replay steps the observed frames and
|
||||
* moves the world clock as it goes, as the world clock's driver.
|
||||
* history at the world's time otherwise. It never moves the world clock
|
||||
* itself; the world's own controls (its timeline) move time, and while the
|
||||
* world plays it stops at each next frame until that frame loads.
|
||||
*/
|
||||
export function createWeatherClock({
|
||||
now = Date.now,
|
||||
@@ -76,17 +79,10 @@ export function createWeatherClock({
|
||||
const listeners = new Set();
|
||||
let mode = 'latest';
|
||||
let target = null;
|
||||
let playing = false;
|
||||
// The world time last applied; while this clock moves the world, the
|
||||
// transition its move started.
|
||||
// The world time last applied.
|
||||
let seen = 'latest';
|
||||
let moving = false;
|
||||
let moved = null;
|
||||
const releases = new Map();
|
||||
// Attached at the end, once the API exists.
|
||||
let driver = null;
|
||||
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}`;
|
||||
@@ -147,10 +143,9 @@ export function createWeatherClock({
|
||||
const getState = () => ({
|
||||
mode,
|
||||
target,
|
||||
playing,
|
||||
// Frames come on one after another: this clock replays them, or the world
|
||||
// plays on through them, so the next is worth loading ahead.
|
||||
advancing: playing || worldPlaysOn(),
|
||||
// Frames come on one after another as the world plays on through them,
|
||||
// so the next is worth loading ahead.
|
||||
advancing: worldPlaysOn(),
|
||||
timeline: getTimeline(),
|
||||
products: [...registry.values()].map(({ product }) => ({
|
||||
id: product.id,
|
||||
@@ -162,60 +157,12 @@ export function createWeatherClock({
|
||||
const notify = () => {
|
||||
if (!destroyed) for (const listener of listeners) listener(getState());
|
||||
};
|
||||
const cancelTimer = () => {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
const canPlay = () => getTimeline().length >= 2;
|
||||
const stopPlaying = () => {
|
||||
if (!playing) return;
|
||||
playing = false;
|
||||
driver?.driven(false);
|
||||
};
|
||||
// Move the world clock; the change comes back through followWorld. Returns
|
||||
// the transition the move started, if it started one.
|
||||
const move = (fn) => {
|
||||
const outer = [moving, moved];
|
||||
moving = true;
|
||||
moved = null;
|
||||
try {
|
||||
fn();
|
||||
return moved;
|
||||
} finally {
|
||||
[moving, moved] = outer;
|
||||
}
|
||||
};
|
||||
const selections = () =>
|
||||
[...registry.values()].map(({ product }) => selectedFor(product)).join();
|
||||
let applied = null;
|
||||
function schedule() {
|
||||
cancelTimer();
|
||||
if (!playing || destroyed || pending) return;
|
||||
if (!canPlay()) {
|
||||
stopPlaying();
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
const owner = generation;
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
if (destroyed || !playing || owner !== generation) return;
|
||||
const timeline = getTimeline();
|
||||
if (timeline.length < 2) {
|
||||
api.pause();
|
||||
return;
|
||||
}
|
||||
const next = timeline.find(
|
||||
(time) => Date.parse(time) > Date.parse(target),
|
||||
);
|
||||
void api.setTarget(next ?? timeline[0]);
|
||||
}, 2000);
|
||||
}
|
||||
function transition() {
|
||||
if (destroyed) return Promise.resolve(false);
|
||||
cancelTimer();
|
||||
const owner = ++generation;
|
||||
if (!canPlay()) stopPlaying();
|
||||
// A scrub drops what loads and moves on; play goes on as this loads.
|
||||
applied = selections();
|
||||
deferred = false;
|
||||
@@ -235,15 +182,12 @@ export function createWeatherClock({
|
||||
if (destroyed || owner !== generation) return false;
|
||||
stopWaiting();
|
||||
pending = null;
|
||||
if (!canPlay()) stopPlaying();
|
||||
notify();
|
||||
schedule();
|
||||
// Play held at the next frame for this one goes on to it.
|
||||
if (deferred) void transition();
|
||||
return true;
|
||||
});
|
||||
pending = result;
|
||||
if (moving) moved ??= result;
|
||||
notify();
|
||||
return result;
|
||||
}
|
||||
@@ -294,81 +238,17 @@ export function createWeatherClock({
|
||||
},
|
||||
getTimeline,
|
||||
selectFor,
|
||||
setTarget(time) {
|
||||
if (destroyed) return Promise.resolve(false);
|
||||
if (!Number.isFinite(Date.parse(time)))
|
||||
throw new TypeError('Weather target must be a UTC time');
|
||||
const normalized = new Date(time).toISOString();
|
||||
if (mode === 'history' && target === normalized)
|
||||
return pending ?? Promise.resolve(true);
|
||||
return (
|
||||
move(() => world.seek(Date.parse(normalized))) ??
|
||||
pending ??
|
||||
Promise.resolve(true)
|
||||
);
|
||||
},
|
||||
step(direction) {
|
||||
if (destroyed || ![-1, 1].includes(direction))
|
||||
return Promise.resolve(false);
|
||||
api.pause();
|
||||
const timeline = getTimeline();
|
||||
if (!timeline.length) return Promise.resolve(false);
|
||||
const at = Date.parse(target ?? timeline.at(-1));
|
||||
const next =
|
||||
direction < 0
|
||||
? (timeline.findLast((time) => Date.parse(time) < at) ?? timeline[0])
|
||||
: (timeline.find((time) => Date.parse(time) > at) ?? timeline.at(-1));
|
||||
return api.setTarget(next);
|
||||
},
|
||||
latest() {
|
||||
if (destroyed) return Promise.resolve(false);
|
||||
stopPlaying();
|
||||
if (mode === 'latest') return transition();
|
||||
return move(() => world.goLive()) ?? pending ?? Promise.resolve(true);
|
||||
},
|
||||
play() {
|
||||
if (destroyed || playing || !canPlay())
|
||||
return pending ?? Promise.resolve(false);
|
||||
playing = true;
|
||||
if (mode === 'latest') {
|
||||
const timeline = getTimeline();
|
||||
const start =
|
||||
timeline.findLast((time) => Date.parse(time) <= now()) ?? timeline[0];
|
||||
// Playing and at the first frame in one move of the world.
|
||||
return (
|
||||
move(() => driver.driven(true, Date.parse(start))) ??
|
||||
pending ??
|
||||
Promise.resolve(true)
|
||||
);
|
||||
}
|
||||
driver.driven(true);
|
||||
notify();
|
||||
schedule();
|
||||
return pending ?? Promise.resolve(true);
|
||||
},
|
||||
pause() {
|
||||
cancelTimer();
|
||||
if (!playing) {
|
||||
// The world may be playing on its own; a pause or step here holds it.
|
||||
if (world.isPlaying()) world.pause();
|
||||
return;
|
||||
}
|
||||
stopPlaying();
|
||||
notify();
|
||||
},
|
||||
togglePlay() {
|
||||
return playing ? api.pause() : api.play();
|
||||
},
|
||||
/**
|
||||
* The world clock this clock follows. Time moves there, through the same
|
||||
* seek, play and live the world's timeline uses, never through this clock.
|
||||
*/
|
||||
world,
|
||||
refresh() {
|
||||
if (destroyed) return Promise.resolve(false);
|
||||
// New frames, or products added or dropped, and with them the next.
|
||||
world.coverageChanged();
|
||||
gated = undefined;
|
||||
gateNext(world.getState());
|
||||
if (!canPlay()) {
|
||||
stopPlaying();
|
||||
cancelTimer();
|
||||
}
|
||||
if (mode === 'history') return transition();
|
||||
notify();
|
||||
return Promise.resolve(true);
|
||||
@@ -380,15 +260,15 @@ export function createWeatherClock({
|
||||
},
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
stopPlaying();
|
||||
destroyed = true;
|
||||
++generation;
|
||||
stopWaiting();
|
||||
world.gate(holdId, null);
|
||||
cancelTimer();
|
||||
unfollowWorld();
|
||||
// The last weather clock leaves the world live, as a new one expects.
|
||||
if (driver.detach() === 0) world.goLive();
|
||||
const left = followers.get(world) - 1;
|
||||
followers.set(world, left);
|
||||
if (left === 0) world.goLive();
|
||||
for (const release of releases.values()) release();
|
||||
releases.clear();
|
||||
for (const entry of registry.values()) entry.controller?.abort();
|
||||
@@ -397,9 +277,8 @@ export function createWeatherClock({
|
||||
pending = null;
|
||||
},
|
||||
};
|
||||
// Follow the world clock. A move of this clock's own always applies; one
|
||||
// from elsewhere (a host's seek, continuous play) applies only when it
|
||||
// changes the mode or a product's frame.
|
||||
// Follow the world clock. A move (a seek, play going on) applies only when
|
||||
// it changes the mode or a product's frame.
|
||||
function followWorld(state) {
|
||||
if (destroyed) return;
|
||||
gateNext(state);
|
||||
@@ -420,7 +299,7 @@ export function createWeatherClock({
|
||||
const modeChanged = nextMode !== mode;
|
||||
mode = nextMode;
|
||||
target = nextTarget;
|
||||
if (!moving && !modeChanged && selections() === applied) {
|
||||
if (!modeChanged && selections() === applied) {
|
||||
// Back at the frame that loads, play need not hold for it.
|
||||
if (deferred) {
|
||||
deferred = false;
|
||||
@@ -433,7 +312,7 @@ export function createWeatherClock({
|
||||
}
|
||||
// Playing on into the next frame while the one before it loads, the
|
||||
// world holds here until that one shows.
|
||||
if (!moving && !modeChanged && pending && playsOn) {
|
||||
if (!modeChanged && pending && playsOn) {
|
||||
deferred = true;
|
||||
startWaiting();
|
||||
notify();
|
||||
@@ -456,10 +335,7 @@ export function createWeatherClock({
|
||||
world.gate(holdId, next);
|
||||
}
|
||||
const unfollowWorld = world.subscribe(followWorld);
|
||||
driver = world.attachDriver({
|
||||
play: () => (canPlay() ? void api.play() : false),
|
||||
pause: () => api.pause(),
|
||||
});
|
||||
followers.set(world, (followers.get(world) ?? 0) + 1);
|
||||
followWorld(world.getState());
|
||||
return api;
|
||||
}
|
||||
Reference in new issue
Block a user