Skip to main content
Pixi.js is a fast 2D rendering engine built on WebGL. Helios provides deterministic frame control for exporting Pixi.js animations to video.

Basic setup

Pixi.js requires disabling its internal ticker and using Helios for frame updates.
import { Helios } from '@helios-project/core';
import { Application, Graphics } from 'pixi.js';

async function init() {
  // Initialize Pixi
  const app = new Application();
  await app.init({
    resizeTo: window,
    backgroundColor: 0x111111,
  });

  // Append canvas to DOM
  document.getElementById('app')!.appendChild(app.canvas);

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

  // Create a rotating rectangle
  const graphics = new Graphics();
  graphics.rect(0, 0, 100, 100).fill({ color: 0xff4444 });
  graphics.pivot.set(50, 50);
  graphics.position.set(app.screen.width / 2, app.screen.height / 2);

  app.stage.addChild(graphics);

  // Handle Resize
  window.addEventListener('resize', () => {
    graphics.position.set(app.screen.width / 2, app.screen.height / 2);
  });

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

  // Sync with Helios
  helios.subscribe((state) => {
    const time = state.currentFrame / helios.fps;
    graphics.rotation = time * Math.PI; // Rotate 180 degrees per second
  });

  // Expose helios for the Renderer/Bridge
  (window as any).helios = helios;
}

init();

React integration

For React applications, manage Pixi.js lifecycle in a useEffect hook.
import React, { useRef, useEffect } from 'react';
import { Helios } from '@helios-project/core';
import { Application, Graphics } from 'pixi.js';

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

helios.bindToDocumentTimeline();

if (typeof window !== 'undefined') {
  window.helios = helios;
}

export default function App() {
  const containerRef = useRef(null);

  useEffect(() => {
    let app = null;
    let unsubscribe = null;
    let mounted = true;

    const init = async () => {
      const pixiApp = new Application();

      await pixiApp.init({
        resizeTo: window,
        backgroundColor: 0x111111,
        antialias: true
      });

      if (!mounted) {
        pixiApp.destroy({ removeView: true, children: true });
        return;
      }

      app = pixiApp;

      if (containerRef.current) {
        containerRef.current.appendChild(app.canvas);
      }

      // Create a simple rotating rectangle
      const rect = new Graphics();
      rect.rect(-50, -50, 100, 100);
      rect.fill(0x61dafb);

      rect.x = app.screen.width / 2;
      rect.y = app.screen.height / 2;

      app.stage.addChild(rect);

      // Sync with Helios
      unsubscribe = helios.subscribe((state) => {
        const time = state.currentTime;
        // Rotate based on time
        rect.rotation = time * Math.PI;

        // Keep centered on resize
        rect.x = app.screen.width / 2;
        rect.y = app.screen.height / 2;
      });
    };

    init();

    return () => {
      mounted = false;
      if (unsubscribe) unsubscribe();
      if (app) {
        app.destroy({ removeView: true, children: true });
      }
    };
  }, []);

  return (
    <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
  );
}

Custom React hook

Create a reusable hook to sync with Helios frames:
import { useState, useEffect } from 'react';

export function useVideoFrame(helios) {
  const [frame, setFrame] = useState(helios.getState().currentFrame);

  useEffect(() => {
    const update = (state) => setFrame(state.currentFrame);
    return helios.subscribe(update);
  }, [helios]);

  return frame;
}
Usage:
import { useVideoFrame } from './hooks/useVideoFrame';

function MyComponent() {
  const frame = useVideoFrame(helios);
  
  // Use frame in your rendering logic
}

Critical patterns

Disable Pixi.js ticker

Pixi.js has an internal ticker for animations. Don’t use it with Helios:
// DON'T DO THIS
app.ticker.add(() => {
  // This creates non-deterministic rendering
});

// DO THIS INSTEAD
helios.subscribe((state) => {
  const time = state.currentFrame / helios.fps;
  // Update your graphics based on Helios time
});

Use Helios time for animations

Calculate all animations from state.currentFrame or state.currentTime:
helios.subscribe((state) => {
  const time = state.currentFrame / helios.fps;
  
  // Position animation
  sprite.x = Math.sin(time * 2) * 100 + app.screen.width / 2;
  
  // Rotation animation
  sprite.rotation = time * Math.PI;
  
  // Alpha animation
  sprite.alpha = (Math.sin(time) + 1) / 2;
});

Async initialization

Pixi.js v8+ requires async initialization. Always await app.init():
const app = new Application();
await app.init({
  resizeTo: window,
  backgroundColor: 0x111111,
});

Performance considerations

Sprite batching

Pixi.js automatically batches sprites with the same texture. Keep sprites using the same texture together:
// Good: sprites batched together
const texture = await Assets.load('sprite.png');
for (let i = 0; i < 1000; i++) {
  const sprite = new Sprite(texture);
  app.stage.addChild(sprite);
}

// Bad: forces multiple draw calls
for (let i = 0; i < 1000; i++) {
  const texture = await Assets.load('sprite.png');
  const sprite = new Sprite(texture);
  app.stage.addChild(sprite);
}

Graphics objects

Reuse Graphics objects instead of recreating them each frame:
// Good: create once, update properties
const graphics = new Graphics();
app.stage.addChild(graphics);

helios.subscribe((state) => {
  const time = state.currentTime;
  graphics.rotation = time;
});

// Bad: recreates every frame
helios.subscribe((state) => {
  const graphics = new Graphics(); // Memory leak!
  graphics.rect(0, 0, 100, 100);
  app.stage.addChild(graphics);
});

Texture management

Preload textures and dispose of them when done:
import { Assets } from 'pixi.js';

// Preload textures
const textures = await Assets.load([
  'sprite1.png',
  'sprite2.png',
  'background.png'
]);

// Use textures
const sprite = new Sprite(textures['sprite1.png']);

// Clean up when unmounting
useEffect(() => {
  return () => {
    Assets.unload('sprite1.png');
  };
}, []);

Canvas resolution

Set resolution based on your export target:
const app = new Application();
await app.init({
  width: 1920,
  height: 1080,
  resolution: window.devicePixelRatio || 1,
  autoDensity: true,
});
Higher resolutions increase memory usage and render time.

Filters and effects

Filters are GPU-intensive. Use sparingly:
import { BlurFilter } from 'pixi.js';

const blur = new BlurFilter();
sprite.filters = [blur];

// Limit filter updates
helios.subscribe((state) => {
  if (state.currentFrame % 5 === 0) {
    blur.blur = Math.sin(state.currentTime) * 10;
  }
});

Package dependencies

{
  "dependencies": {
    "pixi.js": "^8.0.0",
    "@helios-project/core": "latest"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  }
}

Common issues

Canvas not appearing

Ensure you await Pixi.js initialization and append the canvas:
const app = new Application();
await app.init({ resizeTo: window });
document.getElementById('app')!.appendChild(app.canvas);

Animation stuttering

Verify Helios is properly bound and you’re not mixing ticker with Helios:
// Don't use both!
helios.bindToDocumentTimeline(); // Use this
// app.ticker.add(...); // Not this

Memory leaks in React

Always destroy the Pixi.js app on unmount:
useEffect(() => {
  const init = async () => {
    const app = new Application();
    await app.init();
    // ... setup
  };
  
  init();
  
  return () => {
    app?.destroy({ removeView: true, children: true });
  };
}, []);

Blurry graphics

Set autoDensity and match device pixel ratio:
await app.init({
  resolution: window.devicePixelRatio || 1,
  autoDensity: true,
});

Build docs developers (and LLMs) love