> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/BintzGavin/helios/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom drivers

> Creating custom TimeDriver implementations for specialized rendering workflows

## Overview

Helios uses a driver-based architecture to synchronize animations across different rendering contexts. The `TimeDriver` interface allows you to create custom implementations for specialized workflows beyond DOM and canvas.

## TimeDriver interface

All drivers must implement the `TimeDriver` interface:

```typescript theme={null}
export interface TimeDriver {
  init(scope: unknown): void;
  update(timeInMs: number, options?: {
    isPlaying: boolean;
    playbackRate: number;
    volume?: number;
    muted?: boolean;
    audioTracks?: Record<string, { volume: number; muted: boolean }>;
  }): void;
  waitUntilStable(): Promise<void>;
  dispose?(): void;
  subscribeToMetadata?(callback: (meta: DriverMetadata) => void): () => void;
  getAudioContext?(): Promise<unknown>;
  getAudioSourceNode?(trackId: string): Promise<unknown>;
}
```

### Required methods

**`init(scope: unknown)`**

Called once when the Helios instance is created. Use this to set up your rendering context.

```typescript theme={null}
init(scope: unknown) {
  this.scope = scope as MyCustomContext;
  // Initialize resources, scan for elements, etc.
}
```

**`update(timeInMs: number, options)`**

Called every frame to synchronize your rendering context to the given time.

```typescript theme={null}
update(timeInMs: number, options) {
  const { isPlaying, playbackRate, volume, muted } = options;
  // Update animations, audio, etc. to match timeInMs
}
```

**`waitUntilStable()`**

Return a promise that resolves when all assets are loaded and ready to render.

```typescript theme={null}
async waitUntilStable() {
  // Wait for fonts, images, models, etc.
  await Promise.all(this.pendingAssets);
}
```

### Optional methods

**`dispose()`** - Clean up resources when the instance is destroyed

**`subscribeToMetadata(callback)`** - Notify Helios about discovered audio tracks

**`getAudioContext()`** - Return Web Audio API context for mixing

**`getAudioSourceNode(trackId)`** - Return audio source node for a specific track

## Built-in drivers

### DomDriver

The default driver for HTML/CSS animations. It:

* Scans for animations using `document.getAnimations()`
* Seeks animations via `animation.currentTime`
* Discovers shadow DOM boundaries
* Syncs `<audio>` and `<video>` elements
* Tracks audio metadata with `data-helios-track-id`

### NoopDriver

A no-op driver that does nothing. Useful for:

* Pure canvas compositions with manual timing
* Testing
* Headless environments without DOM

```typescript theme={null}
import { Helios, NoopDriver } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new NoopDriver()
});
```

## Creating a custom driver

### Example: Three.js driver

Here's a custom driver for synchronizing Three.js animations:

```typescript theme={null}
import { TimeDriver } from '@helios-project/core';
import * as THREE from 'three';

interface ThreeScope {
  scene: THREE.Scene;
  mixer?: THREE.AnimationMixer;
}

export class ThreeDriver implements TimeDriver {
  private scope: ThreeScope | null = null;
  private mixer: THREE.AnimationMixer | null = null;

  init(scope: unknown) {
    this.scope = scope as ThreeScope;
    
    // Create animation mixer if scene is provided
    if (this.scope.scene) {
      this.mixer = new THREE.AnimationMixer(this.scope.scene);
    } else if (this.scope.mixer) {
      this.mixer = this.scope.mixer;
    }
  }

  update(timeInMs: number) {
    if (!this.mixer) return;
    
    // Convert to seconds and set mixer time
    const timeInSeconds = timeInMs / 1000;
    this.mixer.setTime(timeInSeconds);
  }

  async waitUntilStable() {
    // Wait for GLTF models, textures, etc.
    if (!this.scope?.scene) return;
    
    const promises: Promise<void>[] = [];
    
    this.scope.scene.traverse((obj) => {
      if (obj instanceof THREE.Mesh) {
        const material = obj.material as THREE.MeshStandardMaterial;
        if (material.map && !material.map.image) {
          promises.push(
            new Promise((resolve) => {
              material.map!.onLoad = () => resolve();
            })
          );
        }
      }
    });
    
    await Promise.all(promises);
  }

  dispose() {
    this.mixer = null;
    this.scope = null;
  }
}
```

### Using the custom driver

```typescript theme={null}
import { Helios } from '@helios-project/core';
import { ThreeDriver } from './ThreeDriver';
import * as THREE from 'three';

const scene = new THREE.Scene();
const mixer = new THREE.AnimationMixer(scene);

// Load animations and add to mixer
const clip = THREE.AnimationClip.CreateFromMorphTargetSequence(/* ... */);
const action = mixer.clipAction(clip);
action.play();

const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new ThreeDriver(),
  animationScope: { scene, mixer }
});

helios.subscribe((state) => {
  renderer.render(scene, camera);
});
```

## Custom tickers

Drivers control **where** animations happen. Tickers control **when** frames are rendered during preview.

### Ticker interface

```typescript theme={null}
export interface Ticker {
  start(callback: TickCallback): void;
  stop(): void;
}

type TickCallback = (deltaTime: number) => void;
```

### Built-in tickers

**`RafTicker`** - Uses `requestAnimationFrame` (default for browser)

**`TimeoutTicker`** - Uses `setTimeout` for Node.js environments

**`ManualTicker`** - Allows manual frame stepping for tests

### Example: Fixed timestep ticker

```typescript theme={null}
import { Ticker, TickCallback } from '@helios-project/core';

export class FixedTicker implements Ticker {
  private interval: NodeJS.Timeout | null = null;
  private callback: TickCallback | null = null;
  private fps: number;

  constructor(fps: number = 60) {
    this.fps = fps;
  }

  start(cb: TickCallback) {
    this.callback = cb;
    const frameTime = 1000 / this.fps;
    
    this.interval = setInterval(() => {
      if (this.callback) {
        this.callback(frameTime);
      }
    }, frameTime);
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
    this.callback = null;
  }
}
```

## Audio track discovery

Drivers can notify Helios about audio tracks for mixing and export.

```typescript theme={null}
import { TimeDriver, DriverMetadata, AudioTrackMetadata } from '@helios-project/core';

export class CustomAudioDriver implements TimeDriver {
  private tracks = new Map<string, AudioTrackMetadata>();
  private subscribers = new Set<(meta: DriverMetadata) => void>();

  init(scope: unknown) {
    // Scan for audio sources
    this.discoverAudioTracks();
  }

  private discoverAudioTracks() {
    // Example: Scan custom audio elements
    const audioElements = document.querySelectorAll('[data-audio-track]');
    
    audioElements.forEach((el) => {
      const id = el.getAttribute('data-audio-track')!;
      const src = el.getAttribute('src') || '';
      const offset = parseFloat(el.getAttribute('data-offset') || '0');
      
      this.tracks.set(id, {
        id,
        src,
        startTime: offset,
        duration: 0, // Will be updated when metadata loads
        fadeInDuration: 0,
        fadeOutDuration: 0
      });
    });
    
    this.notifySubscribers();
  }

  subscribeToMetadata(callback: (meta: DriverMetadata) => void) {
    this.subscribers.add(callback);
    
    // Immediately notify with current state
    callback({ audioTracks: Array.from(this.tracks.values()) });
    
    return () => this.subscribers.delete(callback);
  }

  private notifySubscribers() {
    const metadata = { audioTracks: Array.from(this.tracks.values()) };
    this.subscribers.forEach(cb => cb(metadata));
  }

  update(timeInMs: number, options) {
    // Sync audio playback
  }

  async waitUntilStable() {
    // Wait for audio to load
  }
}
```

<Warning>
  Custom drivers must be deterministic. The same `timeInMs` should always produce identical visual output, regardless of how many times `update()` is called or in what order.
</Warning>

## Best practices

**Use existing drivers when possible** - `DomDriver` handles most HTML/CSS/SVG use cases

**Make drivers stateless** - Don't accumulate state across `update()` calls

**Handle disposal properly** - Clean up event listeners, contexts, and resources

**Test with manual ticker** - Use `ManualTicker` to verify frame-perfect accuracy

**Document scope requirements** - Make it clear what `animationScope` should contain

## Next steps

* Learn about [canvas and WebGL optimization](/advanced/canvas-webgl)
* Explore [performance tuning](/advanced/performance)
* Debug compositions with [diagnostics](/advanced/debugging)
