Skip to main content
A composition in Helios is a combination of HTML/CSS/JavaScript and configuration that defines what should be rendered into a video.

Composition basics

At its core, a composition is:
  1. Configuration: Duration, FPS, dimensions, and other metadata
  2. Content: HTML, CSS, and JavaScript that renders the visual output
  3. Timeline: Optional tracks and clips for multi-layer compositions

Creating a composition

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

const helios = new Helios({
  duration: 10,      // 10 seconds
  fps: 30,           // 30 frames per second
  width: 1920,       // Optional, defaults to 1920
  height: 1080,      // Optional, defaults to 1080
  autoSyncAnimations: true  // Enable WAAPI sync
});
See packages/core/src/types.ts:11 for the full HeliosConfig interface.

Configuration options

Required properties

Duration of the composition in seconds (not frames).
const helios = new Helios({
  duration: 10,  // 10 second video
  fps: 30
});
  • Must be non-negative
  • Can be changed dynamically with setDuration()
  • Frames are calculated as duration * fps

Optional properties

interface HeliosConfig {
  width?: number;              // Default: 1920
  height?: number;             // Default: 1080
  initialFrame?: number;       // Default: 0
  loop?: boolean;              // Default: false
  playbackRange?: [number, number];  // Frame range for partial playback
  autoSyncAnimations?: boolean;      // Default: false
  inputProps?: TInputProps;    // User-defined data
  schema?: HeliosSchema;       // Validation schema for inputProps
  playbackRate?: number;       // Default: 1.0
  volume?: number;             // Default: 1.0 (0-1)
  muted?: boolean;             // Default: false
  audioTracks?: Record<string, AudioTrackState>;
  captions?: string | CaptionCue[];  // SRT string or cue array
  markers?: Marker[];          // Timeline markers
}

Input props and schemas

Compositions can accept user-defined data through inputProps. This is useful for creating reusable templates.

Basic input props

interface MyProps {
  title: string;
  author: string;
  bgColor: string;
}

const helios = new Helios<MyProps>({
  duration: 5,
  fps: 30,
  inputProps: {
    title: 'Hello World',
    author: 'Jane Doe',
    bgColor: '#ff6b6b'
  }
});

// Subscribe to changes
helios.subscribe(({ inputProps }) => {
  document.querySelector('h1').textContent = inputProps.title;
});

// Update props dynamically
helios.setInputProps({
  title: 'New Title',
  author: 'John Smith',
  bgColor: '#4ecdc4'
});

Schema validation

Define a schema to validate and provide defaults for input props:
import { HeliosSchema } from '@helios-project/core';

const schema: HeliosSchema = {
  title: {
    type: 'string',
    default: 'Untitled',
    description: 'Video title'
  },
  author: {
    type: 'string',
    default: 'Anonymous'
  },
  bgColor: {
    type: 'string',
    default: '#ffffff',
    pattern: '^#[0-9a-fA-F]{6}$'  // Hex color validation
  },
  opacity: {
    type: 'number',
    default: 1.0,
    min: 0,
    max: 1
  }
};

const helios = new Helios({
  duration: 5,
  fps: 30,
  schema,
  inputProps: {
    title: 'My Video'
    // Other props get defaults from schema
  }
});
The schema is automatically validated on construction and when calling setInputProps(). See packages/core/src/schema.ts for the full validation API.

Timeline and clips

Helios supports multi-track timelines for complex compositions.

Timeline structure

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

const timeline: HeliosTimeline = {
  tracks: [
    {
      id: 'main',
      name: 'Main Track',
      clips: [
        {
          id: 'intro',
          source: 'intro-scene',
          start: 0,      // Start at 0 seconds
          duration: 3    // 3 seconds long
        },
        {
          id: 'main',
          source: 'main-scene',
          start: 3,
          duration: 5
        }
      ]
    },
    {
      id: 'overlay',
      name: 'Graphics Overlay',
      clips: [
        {
          id: 'logo',
          source: 'logo-graphic',
          start: 1,
          duration: 6,
          props: { scale: 1.2 }  // Clip-specific data
        }
      ]
    }
  ]
};

const helios = new Helios({
  duration: 8,
  fps: 30,
  timeline
});

// Access active clips
helios.subscribe(({ activeClips, currentTime }) => {
  console.log('Active clips:', activeClips);
  // At time=2s: [intro-scene, logo-graphic]
});
See packages/core/src/types.ts:31 for timeline type definitions.

Active clip tracking

Helios automatically computes which clips are active at the current time:
helios.activeClips.value; // ReadonlySignal<HeliosClip[]>

// Subscribe to changes
helios.activeClips.subscribe(clips => {
  clips.forEach(clip => {
    console.log(`Clip ${clip.id} is active`);
  });
});
The computation happens at Helios.ts:508:
this._activeClips = computed(() => {
  const time = this._currentTime.value;
  const timeline = this._timeline.value;
  if (!timeline || !timeline.tracks) return [];

  const active: HeliosClip[] = [];
  for (const track of timeline.tracks) {
    for (const clip of track.clips) {
       const end = clip.start + clip.duration;
       if (time >= clip.start && time < end) {
         active.push(clip);
       }
    }
  }
  return active;
});

Composition file structure

For server-side rendering, compositions are typically standalone HTML files:
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>My Composition</title>
  <style>
    body {
      margin: 0;
      width: 1920px;
      height: 1080px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }

    .title {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: white;
      font-size: 72px;
      animation: fadeIn 2s ease-out forwards;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
      to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
    }
  </style>
</head>
<body>
  <h1 class="title">Hello Helios</h1>

  <script type="module">
    import { Helios } from '@helios-project/core';

    // Create global instance for renderer
    window.helios = new Helios({
      duration: 5,
      fps: 30,
      autoSyncAnimations: true
    });

    // Bind to document timeline (important for rendering)
    window.helios.bindToDocumentTimeline();
  </script>
</body>
</html>
The bindToDocumentTimeline() call is critical for server-side rendering. It tells Helios to read from document.timeline.currentTime (or __HELIOS_VIRTUAL_TIME__ in headless mode) instead of driving its own playback loop.
See Helios.ts:1085 for the timeline binding implementation.

Captions and markers

Captions

Helios supports SRT and WebVTT caption formats:
const srtString = `
1
00:00:00,000 --> 00:00:02,000
Welcome to Helios

2
00:00:02,500 --> 00:00:05,000
Programmatic video creation
`;

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

// Access active captions
helios.activeCaptions.subscribe(cues => {
  cues.forEach(cue => {
    console.log(cue.text);
  });
});
Or pass parsed cue objects:
import { CaptionCue } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  captions: [
    {
      start: 0,
      end: 2000,
      text: 'Welcome to Helios'
    },
    {
      start: 2500,
      end: 5000,
      text: 'Programmatic video creation'
    }
  ]
});
See packages/core/src/captions.ts for parsing implementation.

Markers

Markers are named points on the timeline:
import { Marker } from '@helios-project/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  markers: [
    { id: 'intro', time: 0, label: 'Introduction' },
    { id: 'main', time: 3, label: 'Main Content' },
    { id: 'outro', time: 8, label: 'Conclusion' }
  ]
});

// Seek to marker
helios.seekToMarker('main');

// Add/remove markers dynamically
helios.addMarker({ id: 'cta', time: 9.5, label: 'Call to Action' });
helios.removeMarker('intro');

Playback range

Render or preview only a portion of the composition:
const helios = new Helios({
  duration: 10,
  fps: 30,
  playbackRange: [60, 180]  // Frames 60-180 (2-6 seconds at 30fps)
});

// Or set dynamically
helios.setPlaybackRange(90, 150);
helios.clearPlaybackRange();
When a playback range is active:
  • play() starts at the range start
  • Playback stops at the range end
  • Looping wraps within the range

Composition patterns

React composition

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Helios } from '@helios-project/core';
import { HeliosProvider, useVideoFrame } from '@helios-project/react';

function MyComposition() {
  const { currentFrame, currentTime } = useVideoFrame();
  const opacity = Math.min(1, currentTime / 2);

  return (
    <div style={{ opacity }}>
      <h1>Frame {currentFrame}</h1>
    </div>
  );
}

// Global setup
const helios = new Helios({ duration: 5, fps: 30 });
window.helios = helios;

const root = createRoot(document.getElementById('root'));
root.render(
  <HeliosProvider helios={helios}>
    <MyComposition />
  </HeliosProvider>
);

helios.bindToDocumentTimeline();

Canvas composition

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

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const helios = new Helios({
  duration: 5,
  fps: 30,
  width: 1920,
  height: 1080
});

helios.subscribe(({ currentTime, width, height }) => {
  // Clear canvas
  ctx.clearRect(0, 0, width, height);

  // Draw based on time
  const x = (currentTime / 5) * width;
  ctx.fillStyle = '#ff6b6b';
  ctx.fillRect(x - 50, height / 2 - 50, 100, 100);
});

window.helios = helios;
helios.bindToDocumentTimeline();

Multi-scene composition

const scenes = {
  intro: document.querySelector('#intro'),
  main: document.querySelector('#main'),
  outro: document.querySelector('#outro')
};

const helios = new Helios({
  duration: 15,
  fps: 30,
  timeline: {
    tracks: [
      {
        id: 'scenes',
        clips: [
          { id: 'intro-clip', source: 'intro', start: 0, duration: 3 },
          { id: 'main-clip', source: 'main', start: 3, duration: 9 },
          { id: 'outro-clip', source: 'outro', start: 12, duration: 3 }
        ]
      }
    ]
  }
});

helios.subscribe(({ activeClips }) => {
  // Hide all scenes
  Object.values(scenes).forEach(el => el.style.display = 'none');

  // Show active scenes
  activeClips.forEach(clip => {
    scenes[clip.source].style.display = 'block';
  });
});

Best practices

Use absolute positioning

/* Good: Absolute positioning for predictable layout */
body {
  margin: 0;
  width: 1920px;
  height: 1080px;
  position: relative;
}

.element {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Preload assets

<!-- Preload critical assets -->
<link rel="preload" href="./logo.png" as="image">
<link rel="preload" href="./audio.mp3" as="audio">

Use CSS animations for simple motion

/* Prefer CSS over JavaScript when possible */
@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

.element {
  animation: slideIn 1s ease-out forwards;
}

Bind to document timeline for rendering

// Always call this in compositions meant for rendering
window.helios = new Helios({ /* config */ });
window.helios.bindToDocumentTimeline();
Without bindToDocumentTimeline(), the renderer won’t be able to control the composition’s timeline via CDP virtual time.

Next steps

Build docs developers (and LLMs) love