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:
- Configuration: Duration, FPS, dimensions, and other metadata
- Content: HTML, CSS, and JavaScript that renders the visual output
- 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
Frame rate in frames per second.const helios = new Helios({
duration: 10,
fps: 60 // 60fps for smooth motion
});
Common values:
24 - Cinematic
30 - Standard web video
60 - High frame rate
Can be changed with setFps() (preserves playback time).
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
}
Compositions can accept user-defined data through inputProps. This is useful for creating reusable templates.
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