mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
308 lines
8.4 KiB
JavaScript
308 lines
8.4 KiB
JavaScript
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
|
|
/** Own playback timers and scene-clock subscriptions independently of renderer and editor state. */
|
|
export function createPlaybackClock(options) {
|
|
return new PlaybackClock(options);
|
|
}
|
|
|
|
class PlaybackClock {
|
|
constructor({
|
|
isRunning,
|
|
timingForShot,
|
|
onProgress,
|
|
now = () => Date.now(),
|
|
schedule = (callback, ms) => setInterval(callback, ms),
|
|
cancel = (timer) => clearInterval(timer),
|
|
}) {
|
|
this.isRunning = isRunning;
|
|
this.timingForShot = timingForShot;
|
|
this.onProgress = onProgress;
|
|
this.now = now;
|
|
this.schedule = schedule;
|
|
this.cancel = cancel;
|
|
this._progressTimer = null;
|
|
this._shotProgressTimer = null;
|
|
this._sceneClockTimer = null;
|
|
this._sceneClockListeners = new Set();
|
|
this._sceneClockSnapshot = null;
|
|
this._destroyed = false;
|
|
this._waitGeneration = 0;
|
|
this._shotGeneration = 0;
|
|
this._sceneGeneration = 0;
|
|
this._waiters = new Set();
|
|
}
|
|
|
|
get snapshot() {
|
|
return this._sceneClockSnapshot ? { ...this._sceneClockSnapshot } : null;
|
|
}
|
|
get activeTimers() {
|
|
return (
|
|
[
|
|
this._progressTimer,
|
|
this._shotProgressTimer,
|
|
this._sceneClockTimer,
|
|
].filter((timer) => timer !== null).length + this._waiters.size
|
|
);
|
|
}
|
|
|
|
stopShot() {
|
|
this._shotGeneration++;
|
|
this.cancel(this._shotProgressTimer);
|
|
this._shotProgressTimer = null;
|
|
}
|
|
stopSceneTimer() {
|
|
this._sceneGeneration++;
|
|
this.cancel(this._sceneClockTimer);
|
|
this._sceneClockTimer = null;
|
|
}
|
|
finish() {
|
|
this.cancel(this._progressTimer);
|
|
this._progressTimer = null;
|
|
this.stopSceneTimer();
|
|
}
|
|
stop() {
|
|
this.cancelWaits();
|
|
this.stopShot();
|
|
this.finish();
|
|
if (
|
|
!this._sceneClockSnapshot ||
|
|
this._sceneClockSnapshot.stopped ||
|
|
this._destroyed
|
|
)
|
|
return;
|
|
this._sceneClockSnapshot = {
|
|
...this._sceneClockSnapshot,
|
|
running: false,
|
|
stopped: true,
|
|
};
|
|
for (const listener of this._sceneClockListeners) {
|
|
try {
|
|
listener(this.snapshot);
|
|
} catch {
|
|
/* Observers cannot block cancellation. */
|
|
}
|
|
}
|
|
}
|
|
destroy() {
|
|
this.cancelWaits();
|
|
this._destroyed = true;
|
|
this.stopShot();
|
|
this.finish();
|
|
this._sceneClockListeners.clear();
|
|
this._sceneClockSnapshot = null;
|
|
}
|
|
|
|
cancelWaits() {
|
|
this._waitGeneration++;
|
|
for (const finish of [...this._waiters]) finish();
|
|
}
|
|
|
|
/** Own cancellable hold delays; Stop and destroy settle pending waiters immediately. */
|
|
async wait(ms, token) {
|
|
const generation = this._waitGeneration;
|
|
const endAt = this.now() + ms;
|
|
const current = () =>
|
|
!this._destroyed &&
|
|
generation === this._waitGeneration &&
|
|
!token.cancelled &&
|
|
!token.signal?.aborted;
|
|
while (current() && this.now() < endAt) {
|
|
await new Promise((resolve) => {
|
|
const finish = () => {
|
|
clearTimeout(timer);
|
|
token.signal?.removeEventListener('abort', finish);
|
|
this._waiters.delete(finish);
|
|
resolve();
|
|
};
|
|
const timer = setTimeout(finish, 70);
|
|
this._waiters.add(finish);
|
|
token.signal?.addEventListener('abort', finish, { once: true });
|
|
});
|
|
}
|
|
}
|
|
|
|
subscribe(listener) {
|
|
if (this._destroyed || typeof listener !== 'function') return () => {};
|
|
this._sceneClockListeners.add(listener);
|
|
if (this._sceneClockSnapshot) {
|
|
try {
|
|
listener({ ...this._sceneClockSnapshot });
|
|
} catch (error) {
|
|
console.warn('[Scenes] Scene clock listener failed:', error);
|
|
}
|
|
}
|
|
return () => this._sceneClockListeners.delete(listener);
|
|
}
|
|
|
|
publish(
|
|
scene,
|
|
shot,
|
|
sceneElapsedSec,
|
|
{ running = this.isRunning(), seeking = false } = {},
|
|
) {
|
|
if (this._destroyed || !scene || !shot) return;
|
|
const timing = this.timingForShot(scene, shot);
|
|
const elapsedSec = Math.max(
|
|
0,
|
|
Math.min(timing.totalSec, Number(sceneElapsedSec) || 0),
|
|
);
|
|
this._sceneClockSnapshot = {
|
|
sceneId: scene.id,
|
|
shotId: shot.id,
|
|
shotIndex: timing.shotIndex,
|
|
shotCount: scene.shots.length,
|
|
sceneElapsedSec: elapsedSec,
|
|
sceneDurationSec: timing.totalSec,
|
|
sceneProgress: timing.totalSec > 0 ? elapsedSec / timing.totalSec : 0,
|
|
running: Boolean(running),
|
|
seeking: Boolean(seeking),
|
|
};
|
|
for (const listener of this._sceneClockListeners) {
|
|
try {
|
|
listener({ ...this._sceneClockSnapshot });
|
|
} catch (error) {
|
|
console.warn('[Scenes] Scene clock listener failed:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
startRunProgress(totalSec) {
|
|
this.cancel(this._progressTimer);
|
|
if (this._destroyed) return;
|
|
const startMs = this.now();
|
|
const totalMs = Math.max(1000, totalSec * 1000);
|
|
const timer = this.schedule(() => {
|
|
if (this._destroyed || this._progressTimer !== timer || !this.isRunning())
|
|
return;
|
|
const elapsedMs = this.now() - startMs;
|
|
this.onProgress(elapsedMs / totalMs);
|
|
}, 100);
|
|
this._progressTimer = timer;
|
|
}
|
|
|
|
startShotProgress(token, seconds, from, to, sceneClock = null) {
|
|
this.stopShot();
|
|
const generation = this._shotGeneration;
|
|
if (
|
|
generation !== this._shotGeneration ||
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted ||
|
|
this.isRunning()
|
|
)
|
|
return;
|
|
this.onProgress(from);
|
|
if (generation !== this._shotGeneration || this._destroyed) return;
|
|
if (sceneClock) {
|
|
this.publish(
|
|
sceneClock.scene,
|
|
sceneClock.shot,
|
|
sceneClock.sceneElapsedFrom,
|
|
{ running: false },
|
|
);
|
|
}
|
|
if (
|
|
generation !== this._shotGeneration ||
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted ||
|
|
this.isRunning()
|
|
)
|
|
return;
|
|
if (seconds <= 0) {
|
|
this.onProgress(to);
|
|
if (generation !== this._shotGeneration || this._destroyed) return;
|
|
if (sceneClock) {
|
|
this.publish(
|
|
sceneClock.scene,
|
|
sceneClock.shot,
|
|
sceneClock.sceneElapsedTo,
|
|
{ running: false },
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
const startedAt = this.now();
|
|
const timer = this.schedule(() => {
|
|
if (this._shotProgressTimer !== timer) return;
|
|
if (
|
|
generation !== this._shotGeneration ||
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted ||
|
|
this.isRunning()
|
|
) {
|
|
this.cancel(timer);
|
|
if (this._shotProgressTimer === timer) this._shotProgressTimer = null;
|
|
return;
|
|
}
|
|
const fraction = clamp01((this.now() - startedAt) / (seconds * 1000));
|
|
this.onProgress(from + (to - from) * fraction);
|
|
if (generation !== this._shotGeneration || this._destroyed) return;
|
|
if (sceneClock) {
|
|
this.publish(
|
|
sceneClock.scene,
|
|
sceneClock.shot,
|
|
sceneClock.sceneElapsedFrom +
|
|
(sceneClock.sceneElapsedTo - sceneClock.sceneElapsedFrom) *
|
|
fraction,
|
|
{ running: false },
|
|
);
|
|
}
|
|
if (fraction >= 1) {
|
|
this.cancel(timer);
|
|
if (this._shotProgressTimer === timer) this._shotProgressTimer = null;
|
|
}
|
|
}, 100);
|
|
timer.unref?.();
|
|
this._shotProgressTimer = timer;
|
|
}
|
|
|
|
startScene(scene, shot, token) {
|
|
this.stopSceneTimer();
|
|
const generation = this._sceneGeneration;
|
|
const timing = this.timingForShot(scene, shot);
|
|
if (
|
|
generation !== this._sceneGeneration ||
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted
|
|
)
|
|
return timing;
|
|
const startedAt = this.now();
|
|
this.publish(scene, shot, timing.startElapsedSec, {
|
|
running: true,
|
|
});
|
|
if (
|
|
generation !== this._sceneGeneration ||
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted
|
|
)
|
|
return timing;
|
|
const timer = this.schedule(() => {
|
|
if (this._sceneClockTimer !== timer) return;
|
|
if (
|
|
this._destroyed ||
|
|
token.cancelled ||
|
|
token.signal?.aborted ||
|
|
!this.isRunning()
|
|
) {
|
|
this.cancel(timer);
|
|
if (this._sceneClockTimer === timer) this._sceneClockTimer = null;
|
|
return;
|
|
}
|
|
const shotElapsedSec = Math.min(
|
|
timing.durationSec,
|
|
(this.now() - startedAt) / 1000,
|
|
);
|
|
this.publish(scene, shot, timing.startElapsedSec + shotElapsedSec, {
|
|
running: true,
|
|
});
|
|
}, 50);
|
|
timer.unref?.();
|
|
this._sceneClockTimer = timer;
|
|
return timing;
|
|
}
|
|
}
|