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.
This example demonstrates how to structure animations for distributed rendering, where multiple workers render different frame ranges independently and outputs are stitched together.
Overview
The distributed rendering example shows:
- Stateless frame rendering
- Deterministic composition structure
- Frame-independent state calculation
- Optimizations for parallel execution
Complete implementation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Composition</title>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: #111;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="composition-canvas"></canvas>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
import { Helios, HeliosState } from '@helios-project/core';
const canvas = document.getElementById('composition-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
console.log(`Canvas resized to ${canvas.width}x${canvas.height}`);
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
const duration = 5; // seconds
const fps = 30;
const helios = new Helios({
duration,
fps
});
helios.bindToDocumentTimeline();
function draw(currentFrame: number) {
const time = currentFrame / fps * 1000; // in ms
const progress = (time % (duration * 1000)) / (duration * 1000);
const { width, height } = canvas;
// Clear
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, width, height);
const x = progress * width;
const y = height / 2;
const radius = 50;
// Draw moving circle
ctx.fillStyle = 'royalblue';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// Draw rotating square
const squareSize = 100;
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate(progress * Math.PI * 2);
ctx.fillStyle = 'tomato';
ctx.fillRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize);
ctx.restore();
}
helios.subscribe((state: HeliosState) => {
draw(state.currentFrame);
});
declare global {
interface Window {
helios: Helios;
}
}
window.helios = helios;
Key patterns for distributed rendering
Stateless frame calculation
Each frame must be renderable independently without relying on previous frames:
// Good - stateless, frame-independent
function draw(currentFrame: number) {
const progress = currentFrame / totalFrames;
const x = progress * width;
// All state derived from currentFrame
}
// Bad - stateful, depends on previous frames
let x = 0;
function draw() {
x += velocity; // Requires previous frame's x value
}
Deterministic calculations
All calculations must be deterministic given only the frame number:
function draw(currentFrame: number) {
// Frame-based time (deterministic)
const time = currentFrame / fps;
// Deterministic progress calculation
const progress = (time % duration) / duration;
// All positions derived from frame number
const x = progress * width;
const rotation = progress * Math.PI * 2;
}
Avoid temporal dependencies
Never store state between frames that affects rendering:
// Good - no temporal dependencies
helios.subscribe((state) => {
const angle = (state.currentFrame / 30) * Math.PI * 2;
draw(angle);
});
// Bad - state accumulates across frames
let angle = 0;
helios.subscribe(() => {
angle += 0.1; // Non-deterministic across frame ranges
draw(angle);
});
Frame-derived randomness
Use deterministic random functions seeded by frame number:
import { random } from '@helios-project/core';
function draw(currentFrame: number) {
// Same frame always produces same random values
for (let i = 0; i < 100; i++) {
const seed = currentFrame * 1000 + i;
const x = random(seed) * width;
const y = random(seed + 10000) * height;
// Draw element at (x, y)
}
}
Distributed rendering architecture
Worker assignment
Divide frame ranges among workers:
const totalFrames = duration * fps;
const workerCount = 4;
const framesPerWorker = Math.ceil(totalFrames / workerCount);
const assignments = Array.from({ length: workerCount }, (_, i) => ({
workerId: i,
startFrame: i * framesPerWorker,
endFrame: Math.min((i + 1) * framesPerWorker, totalFrames)
}));
// Worker 0: frames 0-37
// Worker 1: frames 38-75
// Worker 2: frames 76-112
// Worker 3: frames 113-150
Frame seeking
Jump directly to any frame without replaying previous frames:
// In distributed worker
function renderFrameRange(startFrame: number, endFrame: number) {
for (let frame = startFrame; frame < endFrame; frame++) {
// Seek directly to frame (no replay needed)
helios.currentFrame.value = frame;
// Render frame
draw(frame);
// Capture output
captureFrame(frame);
}
}
Output stitching
Combine worker outputs without re-encoding:
# Concatenate video segments using ffmpeg
ffmpeg -f concat -safe 0 -i segments.txt -c copy output.mp4
# segments.txt:
# file 'segment_0.mp4'
# file 'segment_1.mp4'
# file 'segment_2.mp4'
# file 'segment_3.mp4'
Optimize initialization
Minimize per-frame initialization by separating setup from rendering:
// One-time setup
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const helios = new Helios({ duration: 10, fps: 30 });
// Pre-calculate constant values
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Fast per-frame rendering
function draw(frame: number) {
const angle = (frame / 30) * Math.PI * 2;
// Use pre-calculated constants
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ... render frame
}
Cache expensive calculations
Store results that don’t change between frames:
// Pre-calculate path data
const pathPoints = generateComplexPath();
// Reuse in each frame
function draw(frame: number) {
const progress = frame / totalFrames;
const index = Math.floor(progress * pathPoints.length);
const point = pathPoints[index];
// Render at point
}
Use typed arrays
Typed arrays are faster for numeric data:
// Pre-allocate typed arrays
const positions = new Float32Array(PARTICLE_COUNT * 2);
const colors = new Uint8ClampedArray(PARTICLE_COUNT * 4);
function draw(frame: number) {
for (let i = 0; i < PARTICLE_COUNT; i++) {
// Update positions
positions[i * 2] = calculateX(frame, i);
positions[i * 2 + 1] = calculateY(frame, i);
}
// Render from typed arrays
}
Minimize garbage collection
Reuse objects instead of creating new ones each frame:
// Good - reuse object
const point = { x: 0, y: 0 };
function draw(frame: number) {
point.x = calculateX(frame);
point.y = calculateY(frame);
render(point);
}
// Bad - creates garbage
function draw(frame: number) {
const point = { x: calculateX(frame), y: calculateY(frame) };
render(point);
}
Advanced techniques
Chunk-based rendering
Render in chunks to balance load and enable checkpointing:
const CHUNK_SIZE = 30; // 1 second at 30fps
async function renderChunk(startFrame: number) {
const endFrame = Math.min(startFrame + CHUNK_SIZE, totalFrames);
const frames: Blob[] = [];
for (let frame = startFrame; frame < endFrame; frame++) {
helios.currentFrame.value = frame;
const blob = await captureFrame();
frames.push(blob);
}
// Save chunk
await saveChunk(startFrame, frames);
// Report progress
console.log(`Rendered frames ${startFrame}-${endFrame}`);
}
Dynamic work distribution
Assign work based on worker speed:
class WorkQueue {
private queue: number[] = [];
private completed = 0;
constructor(totalFrames: number, chunkSize: number) {
for (let i = 0; i < totalFrames; i += chunkSize) {
this.queue.push(i);
}
}
getNextChunk(): number | null {
return this.queue.shift() ?? null;
}
markComplete(startFrame: number) {
this.completed++;
console.log(`Progress: ${this.completed}/${this.queue.length}`);
}
}
// Workers request chunks dynamically
const queue = new WorkQueue(150, 30);
while (true) {
const chunk = queue.getNextChunk();
if (!chunk) break;
await renderChunk(chunk);
queue.markComplete(chunk);
}
Fault tolerance
Implement retry logic and checkpointing:
async function renderWithRetry(frame: number, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
helios.currentFrame.value = frame;
const result = await captureFrame();
await saveFrame(frame, result);
return;
} catch (error) {
console.warn(`Frame ${frame} failed (attempt ${attempt + 1}/${maxRetries})`);
if (attempt === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
Parallel composition
Render multiple compositions simultaneously:
async function renderParallel(compositions: string[], frameRange: [number, number]) {
const results = await Promise.all(
compositions.map(async (comp) => {
const iframe = createIsolatedFrame(comp);
return await renderRange(iframe, ...frameRange);
})
);
return results;
}