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

> Explore Supersplat's event-driven timeline and animation system. Learn how its centralized Events hub synchronizes playback, keyframes, and UI across five layers for frame-accurate animation.

- Repository: [PlayCanvas/supersplat](https://github.com/playcanvas/supersplat)
- Tags: internals
- Published: 2026-05-10

---

**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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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:

```typescript
// 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:

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

```

### Listening for Frame Updates

Implement custom logic that reacts to frame changes:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/src/track-manager.ts) is automatically wrapped in snapshot/restore operations.
- **Smooth Interpolation**: `CubicSpline` in [`src/anim/spline.ts`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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.