Spaces:
Running
Running
Download frontends/react/src/Timeline.ts from nhamid289/autodiff-dev: direct link, hf CLI and curl.
- Browser
- Download file 7.77 kB
-
https://huggingface.co/spaces/nhamid289/autodiff-dev/resolve/main/frontends/react/src/Timeline.ts
- Command line
-
hf download hf://spaces/nhamid289/autodiff-dev/frontends/react/src/Timeline.ts
-
curl -L -o Timeline.ts https://huggingface.co/spaces/nhamid289/autodiff-dev/resolve/main/frontends/react/src/Timeline.ts
7.77 kB
| import { flowDuration } from "./flowTiming"; | |
| import type { RunSettings, State, Step } from "./types"; | |
| // A moment to read a result once its animation has played | |
| const READING_MS = 300; | |
| // How long Play stays on each step when steps aren't animated | |
| const UNANIMATED_STEP_MS = 800; | |
| // The most a frame can move the playhead, so a stall doesn't skip ahead | |
| const MAX_FRAME_MS = 100; | |
| // A step being animated, from the numbers it starts with | |
| export interface Flow { | |
| // Tells one animation from the next | |
| id: number; | |
| step: Step; | |
| before: State; | |
| // When it starts on the timeline | |
| start: number; | |
| } | |
| export interface TimelineSnapshot { | |
| // The settings of the run on screen | |
| run: RunSettings | null; | |
| // What the graph and table show: during an animation, the step without its results yet | |
| state: State | undefined; | |
| flow: Flow | null; | |
| // The last step completed | |
| iteration: number; | |
| lastIteration: number | null; | |
| atStart: boolean; | |
| playing: boolean; | |
| // Where each step ends, starting with 0; the last is the timeline's length | |
| ticks: number[]; | |
| // Where the backward pass starts, if the run has one | |
| backwardAt: number | null; | |
| } | |
| // A run's steps laid end to end, with a playhead moving along them in milliseconds. | |
| // Steps take their animation's length and then a pause. A step's results show once its animation ends. | |
| export class Timeline { | |
| private states: State[] = []; | |
| // ends[k] is when step k finishes, including its pause, and is where stepping stops; ends[0] is 0 | |
| private ends: number[] = [0]; | |
| private animationMs: number[] = [0]; | |
| private time = 0; | |
| // Where to stop when stepping; null to run on to the end | |
| private target: number | null = null; | |
| private frame: number | null = null; | |
| private lastFrameAt: number | null = null; | |
| private playing = false; | |
| private settings: RunSettings | null = null; | |
| private snapshot: TimelineSnapshot = { run: null, state: undefined, flow: null, iteration: 0, lastIteration: null, atStart: true, playing: false, ticks: [0], backwardAt: null }; | |
| private snapshotListeners = new Set<() => void>(); | |
| private timeListeners = new Set<() => void>(); | |
| private animated: boolean; | |
| constructor(animated: boolean) { | |
| this.animated = animated; | |
| } | |
| getSnapshot = (): TimelineSnapshot => this.snapshot; | |
| getTime = (): number => this.time; | |
| subscribe = (listener: () => void): (() => void) => { | |
| this.snapshotListeners.add(listener); | |
| return () => this.snapshotListeners.delete(listener); | |
| }; | |
| // Called every frame, so only what animates should use this | |
| subscribeTime = (listener: () => void): (() => void) => { | |
| this.timeListeners.add(listener); | |
| return () => this.timeListeners.delete(listener); | |
| }; | |
| // A new run, from its state at every iteration; no states if it couldn't run | |
| load = (run: RunSettings | null, states: State[]): void => { | |
| this.stop(); | |
| this.settings = run; | |
| this.states = states; | |
| this.measure(); | |
| this.moveTo(0, true); | |
| }; | |
| setAnimated = (animated: boolean): void => { | |
| const { iteration } = this.snapshot; | |
| this.animated = animated; | |
| this.target = null; | |
| this.measure(); | |
| this.moveTo(this.ends[iteration] ?? 0, true); | |
| }; | |
| play = (): void => { | |
| this.target = null; | |
| this.playing = true; | |
| this.run(); | |
| this.publish(); | |
| }; | |
| pause = (): void => { | |
| this.stop(); | |
| this.publish(); | |
| }; | |
| // Play to the end of the step under way, or through the next one | |
| stepForward = (): void => { | |
| // A step already playing finishes straight away | |
| if (this.target !== null) { | |
| this.moveTo(this.target); | |
| } | |
| const { flow, iteration } = this.snapshot; | |
| const next = flow ? flow.id : iteration + 1; | |
| if (next >= this.ends.length) { | |
| return; | |
| } | |
| if (!this.animated) { | |
| this.stop(); | |
| this.moveTo(this.ends[next]); | |
| return; | |
| } | |
| // Skip any pause left after the step just finished, as nothing moves during it | |
| if (!flow) { | |
| this.moveTo(this.ends[iteration]); | |
| } | |
| this.target = this.ends[next]; | |
| this.playing = false; | |
| this.run(); | |
| }; | |
| // Back to the start of the step under way, or the one before | |
| stepBack = (): void => { | |
| this.stop(); | |
| const before = this.ends.filter((end) => end < this.time).length - 1; | |
| this.moveTo(this.ends[Math.max(before, 0)]); | |
| }; | |
| // Move the playhead by hand, stopping anything playing. Without animations, only steps have anything to show. | |
| seek = (time: number): void => { | |
| this.stop(); | |
| const clamped = Math.min(Math.max(time, 0), this.ends[this.ends.length - 1]); | |
| this.moveTo(this.animated ? clamped : this.nearestTick(clamped)); | |
| }; | |
| reset = (): void => { | |
| this.stop(); | |
| this.moveTo(0); | |
| }; | |
| private nearestTick(time: number): number { | |
| return this.ends.reduce((best, end) => (Math.abs(end - time) < Math.abs(best - time) ? end : best)); | |
| } | |
| private stop() { | |
| this.playing = false; | |
| this.target = null; | |
| this.lastFrameAt = null; | |
| if (this.frame !== null) { | |
| cancelAnimationFrame(this.frame); | |
| this.frame = null; | |
| } | |
| } | |
| private run() { | |
| if (this.frame === null) { | |
| this.frame = requestAnimationFrame(this.tick); | |
| } | |
| } | |
| private tick = (now: number) => { | |
| const moved = Math.min(now - (this.lastFrameAt ?? now), MAX_FRAME_MS); | |
| this.lastFrameAt = now; | |
| const stopAt = this.target ?? this.ends[this.ends.length - 1]; | |
| const time = Math.min(this.time + moved, stopAt); | |
| this.frame = null; | |
| if (time >= stopAt) { | |
| this.stop(); | |
| } else { | |
| this.run(); | |
| } | |
| this.moveTo(time); | |
| }; | |
| private measure() { | |
| this.animationMs = [0]; | |
| this.ends = [0]; | |
| for (const { step } of this.states.slice(1)) { | |
| const animation = this.animated ? flowDuration(step!) : 0; | |
| this.animationMs.push(animation); | |
| this.ends.push(this.ends[this.ends.length - 1] + animation + (this.animated ? READING_MS : UNANIMATED_STEP_MS)); | |
| } | |
| } | |
| private backwardStart(): number | null { | |
| const first = this.states.findIndex((state) => state.step?.phase === "backward"); | |
| return first < 0 ? null : this.ends[first - 1]; | |
| } | |
| private moveTo(time: number, force = false) { | |
| this.time = time; | |
| this.timeListeners.forEach((listener) => listener()); | |
| if (force) { | |
| this.snapshot = { ...this.snapshot, flow: null, iteration: -1 }; | |
| } | |
| this.publish(); | |
| } | |
| // Tell listeners only when what's shown has changed, not on every frame | |
| private publish() { | |
| // The step finished most recently, and how far into the next this is | |
| const reached = this.ends.filter((end) => end <= this.time).length - 1; | |
| const into = this.time - this.ends[reached]; | |
| const animating = into > 0 && reached + 1 < this.ends.length && into < this.animationMs[reached + 1]; | |
| const iteration = into > 0 && !animating && reached + 1 < this.ends.length ? reached + 1 : reached; | |
| const before = this.states[reached]; | |
| const playing = this.playing; | |
| const atStart = this.time === 0; | |
| const previous = this.snapshot; | |
| const flowId = animating ? reached + 1 : null; | |
| if (previous.iteration === iteration && (previous.flow?.id ?? null) === flowId && previous.playing === playing && previous.atStart === atStart) { | |
| return; | |
| } | |
| const step = animating ? this.states[reached + 1].step : undefined; | |
| this.snapshot = { | |
| run: this.settings, | |
| state: step ? { ...before, step } : this.states[iteration], | |
| flow: step ? { id: reached + 1, step, before, start: this.ends[reached] } : null, | |
| iteration, | |
| lastIteration: this.states.length > 0 ? this.states.length - 1 : null, | |
| atStart, | |
| playing, | |
| ticks: this.ends, | |
| backwardAt: this.backwardStart(), | |
| }; | |
| this.snapshotListeners.forEach((listener) => listener()); | |
| } | |
| } | |