Skip to main content
P5.js is a creative coding library that makes drawing and animation accessible. Helios enables precise control over P5.js sketches for video export.

Basic setup

P5.js has a built-in draw loop that must be disabled. Use noLoop() and drive rendering from Helios.
import { Helios } from '@helios-project/core';
import p5 from 'p5';

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

const sketch = (p: p5) => {
  p.setup = () => {
    p.createCanvas(window.innerWidth, window.innerHeight);
    p.noLoop(); // CRITICAL: Stop P5's internal loop
  };

  p.draw = () => {
    // CRITICAL: Use Helios time, not P5 time
    const time = helios.currentTime.value;
    const progress = (time % 10) / 10;

    p.background(20);
    p.fill(255, 0, 0);
    p.noStroke();

    // Example animation logic
    const x = p.width * progress;
    const y = p.height / 2;
    p.ellipse(x, y, 100);
  };

  p.windowResized = () => {
    p.resizeCanvas(window.innerWidth, window.innerHeight);
  };
};

const container = document.getElementById('p5-container');
if (container) {
  const mySketch = new p5(sketch, container);

  // Bind to document timeline for Renderer
  helios.bindToDocumentTimeline();

  // Drive P5 from Helios
  helios.subscribe(() => {
    // p.redraw() executes draw() once
    mySketch.redraw();
  });

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

Critical patterns

Disable P5.js draw loop

P5.js automatically calls draw() at 60fps. You must disable this:
p.setup = () => {
  p.createCanvas(800, 600);
  p.noLoop(); // Stop automatic drawing
};
Then trigger draws manually from Helios:
helios.subscribe(() => {
  mySketch.redraw(); // Call draw() once per Helios frame
});

Use Helios time, not P5 time

P5.js provides frameCount and millis() for animation. Don’t use them with Helios:
p.draw = () => {
  // DON'T USE P5 TIME
  // const time = p.millis() / 1000;
  // const frame = p.frameCount;
  
  // USE HELIOS TIME INSTEAD
  const time = helios.currentTime.value;
  const frame = helios.getState().currentFrame;
  
  // Now animations are deterministic
  const x = p.width * (time / 10);
  p.ellipse(x, p.height / 2, 50);
};

Access Helios in draw function

Create the Helios instance in the outer scope to access it inside P5.js functions:
const helios = new Helios({ fps: 30, duration: 10 });

const sketch = (p: p5) => {
  p.draw = () => {
    const time = helios.currentTime.value; // Accessible here
    // ... draw logic
  };
};

Handle window resize

Update canvas size on window resize:
p.windowResized = () => {
  p.resizeCanvas(window.innerWidth, window.innerHeight);
};

Complete example with animation

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

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

const sketch = (p: p5) => {
  let particles: { x: number; y: number; speed: number }[] = [];

  p.setup = () => {
    p.createCanvas(window.innerWidth, window.innerHeight);
    p.noLoop();
    
    // Initialize particles
    for (let i = 0; i < 50; i++) {
      particles.push({
        x: p.random(p.width),
        y: p.random(p.height),
        speed: p.random(0.5, 2)
      });
    }
  };

  p.draw = () => {
    const time = helios.currentTime.value;
    
    p.background(20, 20, 30);
    p.noStroke();
    
    // Draw animated particles
    particles.forEach((particle, i) => {
      const hue = (i * 10 + time * 50) % 360;
      p.colorMode(p.HSB);
      p.fill(hue, 70, 90);
      
      const x = (particle.x + time * particle.speed * 50) % p.width;
      const y = particle.y + p.sin(time + i) * 50;
      const size = 20 + p.sin(time * 2 + i) * 10;
      
      p.ellipse(x, y, size);
    });
  };

  p.windowResized = () => {
    p.resizeCanvas(window.innerWidth, window.innerHeight);
  };
};

const container = document.getElementById('p5-container');
if (container) {
  const mySketch = new p5(sketch, container);
  
  helios.bindToDocumentTimeline();
  
  helios.subscribe(() => {
    mySketch.redraw();
  });
  
  (window as any).helios = helios;
}

Performance considerations

Canvas size and resolution

P5.js renders at the canvas resolution. Larger canvases are slower:
// 1080p - good performance
p.createCanvas(1920, 1080);

// 4K - slower, more memory
p.createCanvas(3840, 2160);

// Match window size for previews
p.createCanvas(window.innerWidth, window.innerHeight);

Drawing complexity

P5.js is CPU-based. Avoid excessive draw calls per frame:
// Good: reasonable particle count
for (let i = 0; i < 100; i++) {
  p.ellipse(x, y, 10);
}

// Bad: too many draw calls
for (let i = 0; i < 10000; i++) {
  p.ellipse(x, y, 10); // Will be slow
}

Background clearing

Calling background() clears the canvas. For trails, use transparency:
// Full clear each frame
p.background(0);

// Fade effect (creates trails)
p.background(0, 0, 0, 25);

Image and font loading

Preload assets in preload() to avoid loading delays:
let img: p5.Image;
let font: p5.Font;

p.preload = () => {
  img = p.loadImage('image.png');
  font = p.loadFont('font.ttf');
};

p.setup = () => {
  p.createCanvas(800, 600);
  p.noLoop();
};

p.draw = () => {
  p.image(img, 0, 0);
  p.textFont(font);
  p.text('Hello', 100, 100);
};

Shape optimization

Use built-in shapes instead of beginShape() when possible:
// Fast: built-in primitives
p.ellipse(x, y, 50);
p.rect(x, y, 100, 100);

// Slower: custom shapes
p.beginShape();
p.vertex(x1, y1);
p.vertex(x2, y2);
p.vertex(x3, y3);
p.endShape();

Frame rate considerations

P5.js is slower than WebGL-based libraries. Use lower FPS for complex sketches:
// Good for complex sketches
const helios = new Helios({ fps: 24, duration: 10 });

// May struggle with many particles
const helios = new Helios({ fps: 60, duration: 10 });

Package dependencies

{
  "dependencies": {
    "p5": "^1.9.0",
    "@helios-project/core": "latest"
  },
  "devDependencies": {
    "@types/p5": "^1.7.6",
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  }
}

Common issues

Sketch runs too fast

You forgot noLoop() in setup:
p.setup = () => {
  p.createCanvas(800, 600);
  p.noLoop(); // Required!
};

Animation not deterministic

You’re using P5.js time instead of Helios time:
// Wrong
const time = p.millis() / 1000;

// Correct
const time = helios.currentTime.value;

Random values change on replay

P5.js random() is non-deterministic. Seed it or use a deterministic pattern:
// Non-deterministic (changes each run)
const x = p.random(0, p.width);

// Deterministic (same each run)
p.randomSeed(42);
const x = p.random(0, p.width);

// Or use time-based generation
const x = p.noise(time, seed) * p.width;

Container not found

Ensure the container element exists before creating the sketch:
const container = document.getElementById('p5-container');
if (container) {
  const mySketch = new p5(sketch, container);
} else {
  console.error('Container not found');
}

Canvas appears blurry

P5.js doesn’t automatically handle device pixel ratio. Set it manually:
p.setup = () => {
  const canvas = p.createCanvas(window.innerWidth, window.innerHeight);
  canvas.elt.style.width = window.innerWidth + 'px';
  canvas.elt.style.height = window.innerHeight + 'px';
  p.pixelDensity(window.devicePixelRatio || 1);
  p.noLoop();
};

Build docs developers (and LLMs) love