Skip to main content
Drivers are the abstraction layer that connects Helios’s timeline to the rendering environment. They control how time advances and how animations synchronize.

Driver architecture

The TimeDriver interface

All drivers implement the TimeDriver interface:
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>;
}
See packages/core/src/drivers/TimeDriver.ts:15 for the full interface definition.

Driver selection

Helios automatically selects a driver based on configuration:
const helios = new Helios({
  duration: 10,
  fps: 30,
  autoSyncAnimations: true  // Uses DomDriver
});
Driver selection logic at Helios.ts:542:
if (options.driver) {
  this.driver = options.driver;  // Explicit driver
} else if (this.autoSyncAnimations) {
  this.driver = new DomDriver();  // Auto-sync enabled
} else {
  this.driver = new NoopDriver();  // No-op driver
}

Built-in drivers

NoopDriver

The simplest driver—does nothing. Used when you don’t need animation synchronization.
import { NoopDriver } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new NoopDriver()
});
Use cases:
  • Compositions with no CSS/WAAPI animations
  • Manual animation control only
  • Headless calculations without rendering
Implementation at packages/core/src/drivers/NoopDriver.ts:3:
export class NoopDriver implements TimeDriver {
  init(scope: unknown) {}
  update(timeInMs: number, options?: {...}) {}
  waitUntilStable(): Promise<void> {
    return Promise.resolve();
  }
}

DomDriver

Synchronizes CSS animations, WAAPI timelines, and media elements in the browser.
import { DomDriver } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new DomDriver(),
  animationScope: document  // Optional: scope to search for animations
});
Features:
  • WAAPI animation seeking
  • Media element (<video>, <audio>) synchronization
  • Shadow DOM support
  • Audio track discovery and mixing
  • Stability checks (fonts, images, media)
Implementation details: See packages/core/src/drivers/DomDriver.ts:11 for the full implementation.
// Simplified from DomDriver.ts:372
private syncWaapiAnimations(timeInMs: number) {
  let anims: Animation[] = [];

  // Discover animations in all scopes (document + shadow roots)
  for (const scope of this.scopes) {
    if (typeof (scope as any).getAnimations === 'function') {
      const scopeAnims = (scope as any).getAnimations({ subtree: true });
      scopeAnims.forEach((a: Animation) => anims.push(a));
    }
  }

  // Seek all animations to target time
  anims.forEach((anim: Animation) => {
    anim.currentTime = timeInMs;
    if (anim.playState !== 'paused') {
      anim.pause();
    }
  });
}

CdpTimeDriver (Renderer)

Uses Chrome DevTools Protocol to virtualize time for deterministic server-side rendering.
import { CdpTimeDriver } from '@helios-project/renderer';

const driver = new CdpTimeDriver(30000); // 30s timeout
await driver.init(page);
await driver.prepare(page);
await driver.setTime(page, 1.5); // Advance to 1.5 seconds
How it works:
  1. Pause virtual time: Freeze the browser’s internal clock
  2. Sync media elements: Manually seek <video>/<audio> to target time
  3. Advance virtual time: Resume clock for exact frame duration
  4. Wait for stability: Check for async operations to complete
See packages/renderer/src/drivers/CdpTimeDriver.ts:6 for implementation.
// From CdpTimeDriver.ts:19
async prepare(page: Page): Promise<void> {
  this.client = await page.context().newCDPSession(page);
  
  // Initialize virtual time to deterministic epoch
  const INITIAL_VIRTUAL_TIME = 1704067200; // 2024-01-01T00:00:00Z
  await this.client.send('Emulation.setVirtualTimePolicy', {
    policy: 'pause',
    initialVirtualTime: INITIAL_VIRTUAL_TIME
  });

  // Override performance.now() to match virtual time
  await page.evaluate((epoch) => {
    window.performance.now = () => Date.now() - epoch;
  }, INITIAL_VIRTUAL_TIME * 1000);

  this.currentTime = 0;
}
CDP virtual time is deterministic because the browser’s event loop is paused until the time budget expires. This means requestAnimationFrame callbacks and CSS animations all update to the exact target time before rendering continues.

Media element synchronization

Data attributes

Control media playback with custom attributes:
<audio
  data-helios-track-id="bgm"
  data-helios-offset="2"
  data-helios-seek="5"
  data-helios-fade-in="1"
  data-helios-fade-out="2"
  data-helios-fade-easing="easeOut.cubic"
  src="background.mp3"
></audio>
Attributes:
  • data-helios-track-id: Unique identifier for track control
  • data-helios-offset: When the track starts in composition time (seconds)
  • data-helios-seek: In-point in the media file (seconds)
  • data-helios-fade-in: Fade-in duration (seconds)
  • data-helios-fade-out: Fade-out duration (seconds)
  • data-helios-fade-easing: Easing function for fades

Audio track control

const helios = new Helios({
  duration: 10,
  fps: 30,
  autoSyncAnimations: true,
  volume: 0.8,  // Master volume
  audioTracks: {
    'bgm': { volume: 0.5, muted: false },
    'sfx': { volume: 1.0, muted: false }
  }
});

// Change track volume
helios.setAudioTrackVolume('bgm', 0.3);

// Mute track
helios.setAudioTrackMuted('sfx', true);

// Master controls
helios.setAudioVolume(0.5);
helios.setAudioMuted(true);
Final volume calculation:
finalVolume = baseVolume * masterVolume * trackVolume * fadeInMultiplier * fadeOutMultiplier;
See DomDriver.ts:461 for fade and volume logic.

Audio track discovery

DomDriver automatically discovers audio tracks:
helios.availableAudioTracks.subscribe(tracks => {
  console.log('Discovered tracks:', tracks);
  // [
  //   {
  //     id: 'bgm',
  //     src: 'background.mp3',
  //     startTime: 2,
  //     duration: 120,
  //     fadeInDuration: 1,
  //     fadeOutDuration: 2,
  //     fadeEasing: 'easeOut.cubic'
  //   }
  // ]
});

Custom drivers

Create a custom driver to integrate with non-standard time sources.

Example: External clock driver

import { TimeDriver } from '@helios-project/core';

export class ExternalClockDriver implements TimeDriver {
  private externalClock: ExternalTimeSource;

  init(scope: unknown) {
    this.externalClock = new ExternalTimeSource();
  }

  update(timeInMs: number, options: {...}) {
    // Sync external clock to Helios time
    this.externalClock.setTime(timeInMs);
    
    // Update dependent systems
    if (options.isPlaying) {
      this.externalClock.play(options.playbackRate);
    } else {
      this.externalClock.pause();
    }
  }

  async waitUntilStable(): Promise<void> {
    // Wait for external system to stabilize
    await this.externalClock.waitForSync();
  }

  dispose() {
    this.externalClock.disconnect();
  }
}

// Use custom driver
const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new ExternalClockDriver()
});

Example: MIDI timecode driver

import { TimeDriver } from '@helios-project/core';

export class MidiTimecodeDriver implements TimeDriver {
  private midiAccess: any;

  async init(scope: unknown) {
    this.midiAccess = await navigator.requestMIDIAccess();
    // Listen for MIDI timecode messages
  }

  update(timeInMs: number, options: {...}) {
    // Send MIDI timecode to hardware
    const timecode = this.convertToMTC(timeInMs);
    this.sendMTC(timecode);
  }

  async waitUntilStable(): Promise<void> {
    // Wait for MIDI buffer to flush
    return new Promise(resolve => setTimeout(resolve, 10));
  }

  private convertToMTC(ms: number) {
    // Convert milliseconds to MIDI Timecode format
    // ...
  }

  private sendMTC(timecode: any) {
    // Send via MIDI output
    // ...
  }
}

Stability patterns

Custom stability checks

Register async operations that should block rendering:
helios.registerStabilityCheck(async () => {
  // Wait for Three.js scene to load
  await scene.loadAssets();
});

helios.registerStabilityCheck(async () => {
  // Wait for custom data fetch
  await fetch('/api/data').then(r => r.json());
});

// Both checks run in parallel during waitUntilStable()
await helios.waitUntilStable();
See Helios.ts:876 for the stability check API.

Virtual time synchronization

When using bindToDocumentTimeline(), Helios polls __HELIOS_VIRTUAL_TIME__:
// Set by renderer (CdpTimeDriver)
window.__HELIOS_VIRTUAL_TIME__ = 1500; // 1.5 seconds

// Helios automatically syncs
helios.currentTime.value; // 1.5
Stability waiting ensures Helios catches up to virtual time:
// From Helios.ts:952
const virtualTimePromise = new Promise<void>((resolve) => {
  const checkSync = () => {
    const virtualTime = window.__HELIOS_VIRTUAL_TIME__;
    const targetFrame = (virtualTime / 1000) * this._fps.value;
    
    if (Math.abs(targetFrame - this._currentFrame.peek()) <= 0.01) {
      resolve(); // Synced within 0.01 frame tolerance
    } else {
      requestAnimationFrame(checkSync); // Keep polling
    }
  };
  checkSync();
});

Tickers

Drivers use tickers to manage the playback loop. Tickers are separate from drivers to allow different timing strategies.

RafTicker (default)

Uses requestAnimationFrame for smooth browser playback:
import { RafTicker } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  ticker: new RafTicker()
});

TimeoutTicker

Uses setTimeout for Node.js or non-browser environments:
import { TimeoutTicker } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  ticker: new TimeoutTicker()
});

ManualTicker

Manually advance time for testing:
import { ManualTicker } from '@helios-project/core';

const ticker = new ManualTicker();
const helios = new Helios({
  duration: 10,
  fps: 30,
  ticker
});

helios.play();

// Manually tick
ticker.tick(33.33); // Advance by one frame (at 30fps)
ticker.tick(33.33);
See packages/core/src/drivers/ for all ticker implementations.

Best practices

Choose the right driver

  • NoopDriver: Manual control, no animations
  • DomDriver: CSS/WAAPI animations, media elements
  • CdpTimeDriver: Server-side rendering only

Scope animations carefully

// Narrow scope for better performance
const helios = new Helios({
  duration: 10,
  fps: 30,
  autoSyncAnimations: true,
  animationScope: document.querySelector('#composition')
});

Preload media elements

<audio preload="auto" src="audio.mp3"></audio>
This ensures media is ready before rendering starts.

Handle stability timeouts

For long-running async operations, increase the timeout:
const driver = new CdpTimeDriver(60000); // 60 second timeout
Or split into smaller stability checks:
helios.registerStabilityCheck(async () => {
  await loadCriticalAssets();
});

helios.registerStabilityCheck(async () => {
  await loadSecondaryAssets();
});

Next steps

Build docs developers (and LLMs) love