Skip to main content
This example demonstrates how to create procedurally generated animations that are fully deterministic and reproducible using Helios utility functions.

Overview

The procedural generation example shows:
  • Deterministic random number generation
  • Frame-based procedural animations
  • Color interpolation over time
  • Grid-based generative patterns

Complete implementation

composition.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Procedural Generation Example</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        background-color: black;
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <script type="module" src="./src/main.ts"></script>
  </body>
</html>
main.ts
import { Helios, random, interpolateColors } from '@helios-project/core';

// Init Helios
const helios = new Helios({ fps: 30, duration: 10 });
helios.bindToDocumentTimeline();

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

// Grid Config
const COLS = 10;
const ROWS = 10;

// Resize handler
function resize() {
    if (!canvas) return;
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    helios.setSize(canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

helios.subscribe((state) => {
  const frame = state.currentFrame;
  const { width, height } = canvas;

  // Background Color Cycle
  const bg = interpolateColors(
    frame,
    [0, 300],
    ['#1a1a2e', '#16213e'],
    { extrapolateRight: 'clamp' }
  );
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, width, height);

  const cellW = width / COLS;
  const cellH = height / ROWS;

  // Draw Grid
  for (let i = 0; i < COLS * ROWS; i++) {
     const col = i % COLS;
     const row = Math.floor(i / COLS);

     // Deterministic properties based on index
     const seed = i;
     const baseSize = random(seed) * (Math.min(cellW, cellH) * 0.5);
     const speed = random(seed + 1000) * 0.2;
     const offset = random(seed + 2000) * Math.PI * 2;
     const colorSeed = random(seed + 3000);

     // Animate scale using sine wave based on frame
     const currentScale = 0.5 + 0.5 * Math.sin(frame * speed + offset);

     const x = col * cellW + cellW/2;
     const y = row * cellH + cellH/2;

     // Color based on position or random
     ctx.fillStyle = colorSeed > 0.5 ? 'tomato' : 'teal';

     // Draw
     ctx.beginPath();
     ctx.arc(x, y, baseSize * currentScale, 0, Math.PI * 2);
     ctx.fill();
  }
});

// Expose helios for verification
(window as any).helios = helios;

Key patterns

Deterministic random generation

The random() function from Helios core provides deterministic pseudo-random numbers based on a seed:
import { random } from '@helios-project/core';

// Same seed always returns same value
const value1 = random(42);  // Always returns same number
const value2 = random(42);  // Identical to value1

// Different seeds return different values
const value3 = random(43);  // Different from value1
This ensures that procedural animations are reproducible and render identically every time.

Seed-based property generation

Generate unique but deterministic properties for each element:
for (let i = 0; i < COLS * ROWS; i++) {
    const seed = i;
    const baseSize = random(seed) * (Math.min(cellW, cellH) * 0.5);
    const speed = random(seed + 1000) * 0.2;
    const offset = random(seed + 2000) * Math.PI * 2;
    const colorSeed = random(seed + 3000);
}
By using different seed offsets (1000, 2000, 3000), each property gets independent randomness while remaining deterministic.

Color interpolation

Use interpolateColors() for smooth color transitions:
import { interpolateColors } from '@helios-project/core';

const bg = interpolateColors(
    frame,
    [0, 300],
    ['#1a1a2e', '#16213e'],
    { extrapolateRight: 'clamp' }
);
This interpolates between colors based on frame number with clamping at the end.

Frame-based animation

Combine random seeds with frame-based timing for dynamic animations:
const speed = random(seed + 1000) * 0.2;
const offset = random(seed + 2000) * Math.PI * 2;
const currentScale = 0.5 + 0.5 * Math.sin(frame * speed + offset);
Each element animates at its own deterministic speed and phase offset.

Performance tips

Cache random values

For large grids, cache random values instead of recalculating:
// Pre-generate random properties
const elements = Array.from({ length: COLS * ROWS }, (_, i) => ({
    seed: i,
    baseSize: random(i) * maxSize,
    speed: random(i + 1000) * 0.2,
    offset: random(i + 2000) * Math.PI * 2,
    color: random(i + 3000) > 0.5 ? 'tomato' : 'teal'
}));

// Use cached values in draw loop
helios.subscribe((state) => {
    elements.forEach(elem => {
        const scale = 0.5 + 0.5 * Math.sin(state.currentFrame * elem.speed + elem.offset);
        // Draw with elem.baseSize * scale
    });
});

Optimize grid rendering

Skip rendering elements outside the viewport:
for (let i = 0; i < COLS * ROWS; i++) {
    const x = col * cellW + cellW/2;
    const y = row * cellH + cellH/2;
    
    // Skip if outside viewport
    if (x < -margin || x > width + margin || 
        y < -margin || y > height + margin) {
        continue;
    }
    
    // Render element
}

Use integer calculations

Where possible, use integer math for better performance:
// Good - integer operations
const col = i % COLS;
const row = Math.floor(i / COLS);

// Avoid - floating point when unnecessary
const col = Math.floor(i / COLS) * COLS;

Batch canvas operations

Group similar drawing operations to reduce state changes:
// Draw all circles of one color
ctx.fillStyle = 'tomato';
elementsA.forEach(elem => {
    ctx.beginPath();
    ctx.arc(elem.x, elem.y, elem.radius, 0, Math.PI * 2);
    ctx.fill();
});

// Then draw all circles of another color
ctx.fillStyle = 'teal';
elementsB.forEach(elem => {
    ctx.beginPath();
    ctx.arc(elem.x, elem.y, elem.radius, 0, Math.PI * 2);
    ctx.fill();
});

Advanced techniques

Noise-based generation

Implement Perlin or simplex noise for organic patterns:
import { createNoise2D } from 'simplex-noise';
import { random } from '@helios-project/core';

// Create seeded noise function
const noise2D = createNoise2D(() => random(12345));

helios.subscribe((state) => {
    for (let x = 0; x < width; x += 10) {
        for (let y = 0; y < height; y += 10) {
            const noiseValue = noise2D(x * 0.01, y * 0.01 + state.currentTime * 0.1);
            const brightness = (noiseValue + 1) * 127.5;
            ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;
            ctx.fillRect(x, y, 10, 10);
        }
    }
});

Particle systems

Create deterministic particle systems:
const PARTICLE_COUNT = 1000;
const particles = Array.from({ length: PARTICLE_COUNT }, (_, i) => ({
    x: random(i) * width,
    y: random(i + 10000) * height,
    vx: (random(i + 20000) - 0.5) * 2,
    vy: (random(i + 30000) - 0.5) * 2,
    radius: random(i + 40000) * 5 + 2,
    color: `hsl(${random(i + 50000) * 360}, 70%, 60%)`
}));

helios.subscribe((state) => {
    const dt = 1 / state.fps;
    
    particles.forEach(p => {
        // Update position based on velocity and time
        p.x += p.vx * dt * state.currentFrame;
        p.y += p.vy * dt * state.currentFrame;
        
        // Wrap around screen
        p.x = ((p.x % width) + width) % width;
        p.y = ((p.y % height) + height) % height;
        
        // Draw
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
        ctx.fill();
    });
});

L-System generation

Generate fractal patterns using L-Systems:
function generateLSystem(axiom: string, rules: Record<string, string>, iterations: number): string {
    let result = axiom;
    for (let i = 0; i < iterations; i++) {
        result = result.split('').map(char => rules[char] || char).join('');
    }
    return result;
}

const rules = { 'F': 'F+F-F-F+F' };
const pattern = generateLSystem('F', rules, 3);

// Render L-System with turtle graphics
function drawLSystem(instructions: string, frame: number) {
    let x = width / 2, y = height / 2;
    let angle = 0;
    const step = 10;
    const angleStep = Math.PI / 2;
    
    // Animate drawing based on frame
    const progress = Math.min(frame / 300, 1);
    const visibleLength = Math.floor(instructions.length * progress);
    
    for (let i = 0; i < visibleLength; i++) {
        const cmd = instructions[i];
        if (cmd === 'F') {
            const newX = x + Math.cos(angle) * step;
            const newY = y + Math.sin(angle) * step;
            ctx.beginPath();
            ctx.moveTo(x, y);
            ctx.lineTo(newX, newY);
            ctx.stroke();
            x = newX;
            y = newY;
        } else if (cmd === '+') {
            angle += angleStep;
        } else if (cmd === '-') {
            angle -= angleStep;
        }
    }
}

Multi-layer composition

Combine multiple procedural layers:
helios.subscribe((state) => {
    const frame = state.currentFrame;
    
    // Layer 1: Background gradient
    const gradient = ctx.createLinearGradient(0, 0, width, height);
    gradient.addColorStop(0, interpolateColors(frame, [0, 300], ['#000428', '#004e92']));
    gradient.addColorStop(1, interpolateColors(frame, [0, 300], ['#004e92', '#000428']));
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, width, height);
    
    // Layer 2: Animated grid
    drawProceduralGrid(frame);
    
    // Layer 3: Particle overlay
    drawParticles(frame);
    
    // Layer 4: Noise texture
    drawNoiseOverlay(frame, 0.1);
});

Build docs developers (and LLMs) love