How the Timeline and Animation System in Supersplat Works: Event-Driven Architecture Explained

The timeline and animation system in Supersplat uses a centralized Events hub to synchronize playback state, keyframe data, and UI updates across five distinct architectural layers, enabling frame-accurate camera animation with full undo support and cubic spline interpolation.

The timeline and animation system in Supersplat enables the creation of smooth camera animations in Gaussian Splatting workflows through a modular, event-driven architecture. Implemented in the playcanvas/supersplat repository, this system decouples state management from rendering by routing all operations through a central Events hub. This design allows developers to programmatically control animation playback, modify keyframes, and extend functionality without modifying core UI components.

Architecture Overview: Five Decoupled Layers

According to the playcanvas/supersplat source code, the animation architecture separates concerns into five distinct layers that communicate exclusively through the global Events object:

  • Global Timeline (src/timeline.ts): Maintains the current frame, total frames, frame rate, smoothness settings, and playback state. Fires change events when any value updates.
  • Track Management (src/track-manager.ts): Resolves the active AnimTrack (currently the camera track) and exposes keyframe operations (add, remove, move, copy) as events. Wraps each operation in an undoable edit.
  • UI & Interaction (src/ui/timeline-panel.ts): Renders the visual timeline, scrubbing cursor, and draggable key handles. Translates mouse and keyboard actions into the corresponding events.
  • Track Implementation (src/anim-track.ts): Defines the AnimTrack interface contract that concrete tracks must implement, including mutation methods and snapshot/restore capabilities for undo.
  • Spline Evaluation (src/anim/spline.ts): Provides cubic-spline utilities that tracks use to interpolate smooth values between discrete keyframes.

Global Timeline State and Playback Loop

The global timeline state is initialized via registerTimelineEvents(events) in src/timeline.ts. This function registers five core state variables—frames, frameRate, smoothness, frame, and playing—each exposed through a getter (events.function('timeline.xxx')) and a setter (events.on('timeline.setXxx')).

When a setter executes, it updates the internal value and immediately fires a corresponding change event (e.g., events.fire('timeline.frames', frames)). Playback is driven by the application's global update event loop. When playing is true, the handler increments time, computes the new frame via setFrame(Math.floor(time)), and emits timeline.time for UI components that require raw time values (see lines 87‑96 in src/timeline.ts).

Keyboard shortcuts such as timeline.togglePlay, timeline.prevFrame, and timeline.nextKey are wired directly to these setters, ensuring UI logic remains decoupled from state management.

Track Management and Undo Operations

The src/track-manager.ts layer manages the active animation track, currently hardcoded to the camera via events.invoke('camera.animTrack'). All track-level mutations are exposed as events: track.addKey, track.removeKey, track.moveKey, and track.copyKey.

Each event handler wraps the operation in an undoable edit (AnimTrackEditOp) by snapshotting the track before execution and restoring on undo. The UI queries available keys through events.function('track.keys'), which returns the raw array from the active track's implementation.

The AnimTrack Interface Contract

AnimTrack in src/anim-track.ts is a pure interface defining the contract for any animatable entity. Concrete implementations must provide:

  • keys: number[]: Storage for keyframe indices.
  • Mutation methods: addKey(frame), removeKey(frame), moveKey(from, to), and copyKey(from, to), each returning boolean success.
  • Undo support: snapshot() returns a serializable state, while restore(s) reinstates that state.

Camera tracks implement these methods and internally use spline utilities to evaluate interpolated camera transforms at any frame.

Smooth Interpolation with Cubic Splines

Tracks requiring smooth motion utilize the CubicSpline class in src/anim/spline.ts. The static calcKnots method computes tangent information from raw keyframe values, while evaluate(t) returns interpolated results for a given time parameter.

For seamless looping animations, the utility provides fromPointsLooping, which duplicates the first and last points to ensure continuous tangent calculations. This enables the camera track to animate endlessly without visible seams between the final and initial keyframes.

UI Rendering and Event Binding

The TimelinePanel class in src/ui/timeline-panel.ts constructs the visual timeline interface. It instantiates a Ticks sub-component responsible for drawing frame labels, the playhead cursor, and draggable key markers.

On each rebuild—triggered by canvas resize, frame changes, or key list updates—the panel queries events.invoke('timeline.frames') and events.invoke('track.keys') to synchronize the display. User interactions such as click-dragging a key or scrubbing the background fire the appropriate events (track.moveKey, timeline.setFrame), keeping the UI layer thin and declarative.

Practical Code Examples

Adding a Keyframe Programmatically

To insert a key at the current timeline position from external code:

// Assuming `events` is the central Events instance
events.fire('track.addKey');  // Automatically uses the current timeline frame

Controlling Playback and Frame Rate

Configure animation settings and start playback:

events.fire('timeline.setFrameRate', 24);
events.fire('timeline.setPlaying', true);

Listening for Frame Updates

Implement custom logic that reacts to frame changes:

events.on('timeline.frame', (frame: number) => {
    console.log('Now on frame', frame);
    // Evaluate custom tracks or trigger external events here
});

Implementing a Custom AnimTrack

Create a simple track for custom objects by implementing the interface:

class SimpleTrack implements AnimTrack {
    keys: number[] = [];

    addKey(frame: number): boolean {
        if (!this.keys.includes(frame)) { 
            this.keys.push(frame); 
            return true; 
        }
        return false;
    }

    removeKey(frame: number): boolean {
        const i = this.keys.indexOf(frame);
        if (i >= 0) { 
            this.keys.splice(i, 1); 
            return true; 
        }
        return false;
    }

    moveKey(from: number, to: number): boolean {
        const i = this.keys.indexOf(from);
        if (i >= 0 && !this.keys.includes(to)) {
            this.keys[i] = to;
            return true;
        }
        return false;
    }

    copyKey(from: number, to: number): boolean {
        if (this.keys.includes(from) && !this.keys.includes(to)) {
            this.keys.push(to);
            return true;
        }
        return false;
    }

    clear() { this.keys = []; }

    snapshot() { return { keys: [...this.keys] }; }

    restore(s: any) { this.keys = [...s.keys]; }
}

Expose the track to the system via the events hub:

const myTrack = new SimpleTrack();
events.function('myObject.animTrack', () => myTrack);

Once registered, the existing timeline UI will automatically display its keyframes and respond to standard timeline operations.

Summary

  • Centralized Event Hub: All timeline and animation state flows through the Events object in src/timeline.ts, decoupling UI from logic.
  • Layered Architecture: The system separates global state, track management, spline math, and rendering into distinct, testable modules.
  • Interface-Based Tracks: The AnimTrack contract in src/anim-track.ts allows easy extension for new animatable elements beyond the camera.
  • Built-in Undo: Every keyframe mutation in src/track-manager.ts is automatically wrapped in snapshot/restore operations.
  • Smooth Interpolation: CubicSpline in src/anim/spline.ts provides production-ready easing with optional looping support.

Frequently Asked Questions

How does Supersplat handle animation playback timing?

Playback timing is managed in src/timeline.ts by a handler registered to the global update event. When playing is true, this handler increments an internal time accumulator, calculates the corresponding frame number, and fires timeline.frame events. The system respects the configured frameRate to determine how much time advances per application frame, ensuring consistent playback speed regardless of render performance.

What is the role of the Events hub in the animation system?

The Events hub acts as the sole communication channel between the timeline, track managers, and UI components. State changes are propagated via events.fire(), while queries use events.function() or events.invoke(). This pattern eliminates direct dependencies between modules, allowing external scripts to control animation or developers to replace UI components without touching core animation logic.

How can I add custom animation tracks to Supersplat?

Implement the AnimTrack interface from src/anim-track.ts with methods for addKey, removeKey, moveKey, copyKey, and the snapshot/restore pair for undo support. Register your instance by exposing it through events.function('yourId.animTrack', () => trackInstance). The existing TimelinePanel will automatically render your track's keyframes and route user interactions to your implementation.

Does Supersplat support looping animations?

Yes, the spline system supports seamless loops. In src/anim/spline.ts, the CubicSpline.fromPointsLooping static method duplicates the first and last control points when calculating knots. This ensures continuous tangents at the loop boundary, allowing tracks like the camera animation to play indefinitely without visible discontinuities between the final and initial keyframes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →