Skip to main content
This example demonstrates how to create animations using the HTML5 Canvas API with Helios. Perfect for custom graphics, data visualizations, and high-performance animations.

Overview

The canvas animation example shows how to:
  • Set up a full-screen canvas element
  • Respond to Helios state changes
  • Draw frame-synchronized graphics
  • Handle canvas resizing

Setup

1

Create the HTML canvas

Start with an HTML file containing a full-screen canvas element:
<!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>
2

Initialize canvas and Helios

Set up the canvas context and initialize Helios:
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}`);
};

// Resize immediately and on window resize
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

const duration = 5; // seconds
const fps = 30;

// Initialize Helios
const helios = new Helios({
  duration,
  fps
});

// Bind to document.timeline so the Renderer can drive us
helios.bindToDocumentTimeline();
3

Create the draw function

Implement your drawing logic based on the current frame:
function draw(currentFrame: number) {
  const time = currentFrame / fps * 1000; // in ms
  const progress = (time % (duration * 1000)) / (duration * 1000);

  const { width, height } = canvas;

  // Clear canvas
  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();
}
4

Subscribe to state changes

Connect your draw function to Helios state updates:
// Subscribe to Helios state changes
helios.subscribe((state: HeliosState) => {
  draw(state.currentFrame);
});

// Make helios available globally for debugging
declare global {
  interface Window {
    helios: Helios;
  }
}
window.helios = helios;

How it works

State subscription

Helios uses a subscription model to notify your code when the timeline state changes. Each time Helios updates (during playback or seeking), your draw function is called with the current frame number.
helios.subscribe((state: HeliosState) => {
  // state.currentFrame - the current frame number
  // state.isPlaying - whether playback is active
  // state.progress - normalized progress (0 to 1)
  draw(state.currentFrame);
});

Frame-based rendering

Canvas animations in Helios are frame-based, not time-based. This ensures:
  • Deterministic rendering - same frame always produces same output
  • Perfect seeking - any frame can be rendered independently
  • Export reliability - rendered videos are frame-perfect

Canvas resizing

Proper canvas sizing is critical for quality output. The example handles window resize events and updates the canvas dimensions accordingly:
const resizeCanvas = () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
};

window.addEventListener('resize', resizeCanvas);

Key concepts

  • Subscribe pattern: Listen to state changes instead of using requestAnimationFrame
  • Frame numbers: Use currentFrame to calculate animation progress
  • Deterministic drawing: Same frame should always produce identical output
  • Canvas context: Store and reuse the 2D rendering context

Performance tips

  1. Clear efficiently: Only clear the portions of canvas that change
  2. Batch operations: Group similar drawing operations together
  3. Save/restore context: Use ctx.save() and ctx.restore() for transforms
  4. Avoid allocations: Reuse objects instead of creating new ones each frame

Complete example

View the complete source code in the simple-canvas-animation directory.

Next steps

Build docs developers (and LLMs) love