Skip to main content
Helios drives the browser’s native animation engine rather than reimplementing animation in JavaScript. This means your existing CSS animations, WAAPI timelines, and animation libraries work out of the box.

CSS animations

Standard CSS @keyframes animations work natively with Helios when autoSyncAnimations is enabled.

Basic CSS animation

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.title {
  animation: fadeIn 2s ease-out forwards;
}
import { Helios } from '@helios-project/core';

const helios = new Helios({
  duration: 5,
  fps: 30,
  autoSyncAnimations: true  // Enable CSS animation sync
});

window.helios = helios;
helios.bindToDocumentTimeline();
With autoSyncAnimations: true, Helios automatically:
  1. Discovers all CSS animations via document.getAnimations()
  2. Sets animation.currentTime to match the timeline
  3. Pauses animations to prevent drift
See packages/core/src/drivers/DomDriver.ts:372 for the WAAPI sync implementation.

Animation timing

CSS animation duration maps directly to composition time:
/* 3-second animation */
.element {
  animation: slideIn 3s ease-out forwards;
}
At 30fps:
  • Frame 0: Animation at 0% (start state)
  • Frame 45: Animation at 50% (1.5 seconds)
  • Frame 90: Animation at 100% (end state)

Animation delay

.element {
  animation: fadeIn 2s ease-out 1s forwards;
  /*                            ↑ 1 second delay */
}
Animation won’t start until 1 second into the composition (frame 30 at 30fps).

Multiple animations

.element {
  animation: 
    fadeIn 1s ease-out forwards,
    slideUp 1s ease-out forwards,
    rotate 2s linear infinite;
}
All animations are synchronized to the same timeline.

Web Animations API (WAAPI)

The Web Animations API provides programmatic animation control while still using the browser’s rendering engine.

Creating WAAPI animations

const element = document.querySelector('.box');

const animation = element.animate(
  [
    { transform: 'translateX(0px)', opacity: 0 },
    { transform: 'translateX(500px)', opacity: 1 }
  ],
  {
    duration: 2000,  // 2 seconds
    easing: 'ease-out',
    fill: 'forwards'
  }
);

// Helios automatically syncs this animation when autoSyncAnimations is enabled

Manual WAAPI control

Without autoSyncAnimations, you can manually control WAAPI animations:
const helios = new Helios({
  duration: 5,
  fps: 30,
  autoSyncAnimations: false
});

const animation = element.animate([...], { duration: 2000 });

helios.currentTime.subscribe(time => {
  animation.currentTime = time * 1000;  // Convert to milliseconds
  animation.pause();
});

Shadow DOM support

Helios automatically discovers animations in Shadow DOM when using DomDriver:
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        @keyframes pulse {
          0%, 100% { opacity: 1; }
          50% { opacity: 0.5; }
        }
        .inner {
          animation: pulse 2s infinite;
        }
      </style>
      <div class="inner">Pulsing element</div>
    `;
  }
}

customElements.define('my-component', MyComponent);
The DomDriver scans shadow roots automatically (see DomDriver.ts:120).

Animation libraries

Helios works with popular animation libraries that use WAAPI under the hood or provide time-based APIs.

GSAP

See examples/gsap-animation/ for a complete example.

Framer Motion

import { motion } from 'framer-motion';
import { useVideoFrame } from '@helios-project/react';

function AnimatedBox() {
  const { currentTime } = useVideoFrame();

  return (
    <motion.div
      animate={{ x: 500, opacity: 1 }}
      transition={{ duration: 2 }}
      style={{ x: 0, opacity: 0 }}
    />
  );
}
With Helios’s React adapter:
const helios = new Helios({
  duration: 5,
  fps: 30,
  autoSyncAnimations: true
});

<HeliosProvider helios={helios}>
  <AnimatedBox />
</HeliosProvider>
See examples/framer-motion-animation/ for details.

Motion One

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

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

animate('.box', { x: 500 }, { duration: 2 });
// Automatically synced via WAAPI
See examples/motion-one-animation/.

Lottie

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

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

const animation = lottie.loadAnimation({
  container: document.getElementById('lottie'),
  renderer: 'svg',
  loop: false,
  autoplay: false,
  path: 'animation.json'
});

helios.currentTime.subscribe(time => {
  const totalFrames = animation.totalFrames;
  const duration = animation.getDuration();
  const frame = (time / duration) * totalFrames;
  animation.goToAndStop(frame, true);
});
See examples/lottie-animation/.

Animation helpers

Helios provides utility functions for common animation patterns.

Interpolate

Map input values to output values with easing:
import { interpolate } from '@helios-project/core';

helios.subscribe(({ currentFrame }) => {
  // Fade in from frames 0-30
  const opacity = interpolate(
    currentFrame,
    [0, 30],
    [0, 1],
    { easing: t => t * t }  // Ease-in quadratic
  );
  
  element.style.opacity = opacity;
});
Full interpolate API at packages/core/src/animation.ts:20:
function interpolate(
  input: number,
  inputRange: number[],
  outputRange: number[],
  options?: {
    extrapolateLeft?: 'extend' | 'clamp' | 'identity',
    extrapolateRight?: 'extend' | 'clamp' | 'identity',
    easing?: (t: number) => number
  }
): number
// Linear interpolation
const x = interpolate(frame, [0, 100], [0, 500]);
// frame 0 → x = 0
// frame 50 → x = 250
// frame 100 → x = 500

Spring

Physics-based spring animations:
import { spring } from '@helios-project/core';

helios.subscribe(({ currentFrame, fps }) => {
  const y = spring({
    frame: currentFrame,
    fps,
    from: 0,
    to: 500,
    config: {
      stiffness: 100,
      damping: 10,
      mass: 1
    }
  });
  
  element.style.transform = `translateY(${y}px)`;
});
Spring configurations:
// Bouncy
{ stiffness: 200, damping: 10 }

// Gentle
{ stiffness: 50, damping: 20 }

// Quick snap
{ stiffness: 300, damping: 30 }
See animation.ts:136 for physics implementation.

Calculate spring duration

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

const frames = calculateSpringDuration(
  {
    fps: 30,
    from: 0,
    to: 500,
    config: { stiffness: 100, damping: 10 }
  },
  0.001  // Threshold for "settled"
);

console.log(`Spring settles in ${frames} frames`);

Sequencing

Coordinate animations across time.

Sequence helper

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

helios.subscribe(({ currentFrame }) => {
  // Title animation: frames 0-60
  const title = sequence({
    frame: currentFrame,
    from: 0,
    durationInFrames: 60
  });
  
  if (title.isActive) {
    const opacity = interpolate(
      title.localFrame,
      [0, 30],
      [0, 1]
    );
    titleElement.style.opacity = opacity;
  }
  
  // Subtitle animation: frames 30-90 (overlaps title)
  const subtitle = sequence({
    frame: currentFrame,
    from: 30,
    durationInFrames: 60
  });
  
  if (subtitle.isActive) {
    const y = interpolate(
      subtitle.localFrame,
      [0, 60],
      [50, 0]
    );
    subtitleElement.style.transform = `translateY(${y}px)`;
  }
});
Sequence returns:
interface SequenceResult {
  localFrame: number;      // Frame relative to start (0, 1, 2, ...)
  relativeFrame: number;   // Alias for localFrame
  progress: number;        // 0 to 1 progress through duration
  isActive: boolean;       // true if within duration
}
See packages/core/src/sequencing.ts:20 for implementation.

Series helper

Place items sequentially:
import { series } from '@helios-project/core';

const items = [
  { id: 'intro', durationInFrames: 60 },
  { id: 'main', durationInFrames: 120 },
  { id: 'outro', durationInFrames: 60 }
];

const sequenced = series(items);
// [
//   { id: 'intro', durationInFrames: 60, from: 0 },
//   { id: 'main', durationInFrames: 120, from: 60 },
//   { id: 'outro', durationInFrames: 60, from: 180 }
// ]

helios.subscribe(({ currentFrame }) => {
  sequenced.forEach(item => {
    const seq = sequence({
      frame: currentFrame,
      from: item.from,
      durationInFrames: item.durationInFrames
    });
    
    if (seq.isActive) {
      console.log(`Showing ${item.id}`);
    }
  });
});

Stagger helper

Stagger animations by a fixed interval:
import { stagger } from '@helios-project/core';

const boxes = document.querySelectorAll('.box');
const staggered = stagger(
  Array.from(boxes),
  10,  // 10 frame delay between each
  30   // Start at frame 30
);

helios.subscribe(({ currentFrame }) => {
  staggered.forEach((box, i) => {
    const seq = sequence({
      frame: currentFrame,
      from: box.from,
      durationInFrames: 30
    });
    
    if (seq.isActive) {
      const y = interpolate(seq.localFrame, [0, 30], [50, 0]);
      box.style.transform = `translateY(${y}px)`;
    }
  });
});

Canvas and procedural animation

For canvas-based compositions, you control the render loop:
import { Helios } from '@helios-project/core';

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

const helios = new Helios({
  duration: 5,
  fps: 30,
  width: 1920,
  height: 1080
});

helios.subscribe(({ currentTime, currentFrame, width, height }) => {
  // Clear canvas
  ctx.clearRect(0, 0, width, height);
  
  // Draw based on time
  const x = (currentTime / 5) * width;
  const radius = 50 + Math.sin(currentTime * Math.PI) * 20;
  
  ctx.fillStyle = '#ff6b6b';
  ctx.beginPath();
  ctx.arc(x, height / 2, radius, 0, Math.PI * 2);
  ctx.fill();
});
See examples/p5-canvas-animation/ and examples/pixi-canvas-animation/ for library integrations.

Easing functions

Helios provides common easing functions:
import { Easing } from '@helios-project/core';

// Available easings
Easing.linear
Easing.easeIn.quad
Easing.easeIn.cubic
Easing.easeOut.quad
Easing.easeOut.cubic
Easing.easeInOut.quad
Easing.easeInOut.cubic
// ... and more

// Use with interpolate
const x = interpolate(
  currentFrame,
  [0, 60],
  [0, 500],
  { easing: Easing.easeOut.cubic }
);
See packages/core/src/easing.ts for the full list.

Best practices

Prefer CSS for simple animations

/* Good: Declarative, browser-optimized */
@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

.element {
  animation: slideIn 1s ease-out forwards;
}
// Avoid: Imperative, per-frame calculation
helios.subscribe(({ currentFrame }) => {
  const progress = currentFrame / 30;
  element.style.transform = `translateX(${progress * -100}%)`;
});

Use will-change for better performance

.animated {
  will-change: transform, opacity;
}
This hints to the browser to optimize for animation.

Avoid layout thrashing

// Bad: Reading and writing in a loop
elements.forEach(el => {
  const width = el.offsetWidth;  // Read (forces layout)
  el.style.width = width + 10 + 'px';  // Write
});

// Good: Batch reads, then writes
const widths = elements.map(el => el.offsetWidth);
widths.forEach((width, i) => {
  elements[i].style.width = width + 10 + 'px';
});

Use hardware-accelerated properties

Prefer transform and opacity over layout-affecting properties:
/* Fast: GPU-accelerated */
@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

/* Slow: Triggers layout */
@keyframes slideIn {
  from { left: -100%; }
  to { left: 0; }
}

Next steps

Build docs developers (and LLMs) love