Docs

Animation Basics

In traditional video editors, you set keyframes and the software interpolates between them. In SuperImg, you write the interpolation logic directly.

The Mental Model

Instead of thinking "animate opacity from 0 to 1 over 1 second," think: "What is the opacity right now?"

render(ctx) {
  const { sceneTimeSeconds: time, std } = ctx;
 
  // If we're in the first second, fade in
  // Otherwise, stay fully visible
  const opacity = time < 1
    ? std.tween(0, 1, time / 1)
    : 1;
 
  return `<div style="opacity: ${opacity}">Hello</div>`;
}

Your render function is called 30 times per second (or whatever your fps is). Each call, you calculate what values should be at that exact moment.

Key Timing Properties

The ctx object gives you timing information:

PropertyTypeDescription
ctx.framenumberCurrent frame (0, 1, 2, ...)
ctx.sceneTimeSecondsnumberTime in seconds (0.0, 0.033, 0.066, ...)
ctx.sceneProgressnumberProgress from 0 to 1 over the scene

Use sceneProgress when you want animations relative to the scene duration. Use sceneTimeSeconds when you want precise timing in seconds.

The Tween Function

std.tween smoothly interpolates between two values:

std.tween(from, to, progress, easing?)
  • from — Starting value
  • to — Ending value
  • progress — Current progress (0 to 1)
  • easing — Optional easing function name
// Fade from 0 to 1 over the scene duration
const opacity = std.tween(0, 1, ctx.sceneProgress);
 
// Move from x=0 to x=500
const x = std.tween(0, 500, ctx.sceneProgress);

Easing Functions

Easing controls the feel of motion. Without easing (linear), animations feel robotic.

// Linear - constant speed (robotic)
std.tween(0, 100, progress, "linear");
 
// Ease out - starts fast, slows down (natural deceleration)
std.tween(0, 100, progress, "easeOutCubic");
 
// Ease in - starts slow, speeds up
std.tween(0, 100, progress, "easeInCubic");
 
// Ease in-out - slow start and end
std.tween(0, 100, progress, "easeInOutQuad");
 
// Bounce - playful, bouncy feel
std.tween(0, 100, progress, "easeOutBounce");
 
// Elastic - overshoot and settle
std.tween(0, 100, progress, "easeOutElastic");
 
// Back - slight overshoot
std.tween(0, 100, progress, "easeOutBack");

Available Easings

FamilyInOutIn-Out
QuadeaseInQuadeaseOutQuadeaseInOutQuad
CubiceaseInCubiceaseOutCubiceaseInOutCubic
QuarteaseInQuarteaseOutQuarteaseInOutQuart
QuinteaseInQuinteaseOutQuinteaseInOutQuint
SineeaseInSineeaseOutSineeaseInOutSine
ExpoeaseInExpoeaseOutExpoeaseInOutExpo
CirceaseInCirceaseOutCirceaseInOutCirc
ElasticeaseInElasticeaseOutElasticeaseInOutElastic
BackeaseInBackeaseOutBackeaseInOutBack
BounceeaseInBounceeaseOutBounceeaseInOutBounce

Rule of thumb: Use easeOut variants for most UI animations. They feel responsive because they start fast.

Common Patterns

Fade In

const opacity = std.tween(0, 1, ctx.sceneProgress, "easeOutCubic");
return `<div style="opacity: ${opacity}">Hello</div>`;

Slide In From Left

const x = std.tween(-100, 0, ctx.sceneProgress, "easeOutCubic");
return `<div style="transform: translateX(${x}px)">Hello</div>`;

Scale Up

const scale = std.tween(0.5, 1, ctx.sceneProgress, "easeOutBack");
return `<div style="transform: scale(${scale})">Hello</div>`;

Rotate In

const rotation = std.tween(-90, 0, ctx.sceneProgress, "easeOutCubic");
return `<div style="transform: rotate(${rotation}deg)">Hello</div>`;

Combined Transform

const opacity = std.tween(0, 1, ctx.sceneProgress);
const y = std.tween(20, 0, ctx.sceneProgress, "easeOutCubic");
const scale = std.tween(0.95, 1, ctx.sceneProgress, "easeOutCubic");
 
return `
  <div style="
    opacity: ${opacity};
    transform: translateY(${y}px) scale(${scale});
  ">
    Hello
  </div>
`;

Spring Physics

For organic, bouncy motion, use spring physics instead of easing curves. Springs overshoot their target and settle naturally.

// Spring curve: 0→overshoot→1
const val = std.spring(progress);
const val = std.spring(progress, { stiffness: 200, damping: 8 });
 
// Interpolate between values with spring physics
const scale = std.springTween(0.8, 1, ctx.sceneProgress, { stiffness: 200, damping: 8 });
 
// Create reusable spring easing for std.tween()
const bounce = std.createSpring({ stiffness: 150, damping: 12 });
const y = std.tween(40, 0, ctx.sceneProgress, bounce);

Config options:

  • stiffness (default: 100) — higher = faster oscillation
  • damping (default: 10) — lower = more bouncy
  • mass (default: 1) — higher = slower, more momentum

When to use spring vs easing: Use easeOutCubic for subtle UI motion. Use spring when you want visible overshoot — card entrances, number counters, playful interactions.

Staggered Animations

Use std.stagger() to cascade animations across multiple elements:

render(ctx) {
  const { sceneProgress, std } = ctx;
  const items = ["First", "Second", "Third"];
 
  const staggered = std.stagger(items, sceneProgress, {
    duration: 0.3,
    easing: "easeOutCubic",
  });
 
  return staggered.map(({ item, progress }) => {
    const opacity = std.tween(0, 1, progress);
    const y = std.tween(20, 0, progress);
    return `<div style="opacity: ${opacity}; transform: translateY(${y}px)">${item}</div>`;
  }).join("");
}

Options:

  • duration — each item's animation window as a fraction of 1
  • each — delay between item starts (alternative to duration)
  • from — stagger direction: "start", "end", "center", "edges"
  • easing — per-item easing

The count-based overload returns plain progress values:

const progresses = std.stagger(3, sceneProgress, { duration: 0.4 });
// progresses[0] = 0.8, progresses[1] = 0.4, progresses[2] = 0.0

Multi-Keyframe Interpolation

std.interpolate() maps a value through multiple keyframe ranges — like Remotion's interpolate():

// Fade in (0-20%), hold (20-80%), fade out (80-100%)
const opacity = std.interpolate(ctx.sceneProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]);
 
// Multi-stop position
const x = std.interpolate(ctx.sceneProgress, [0, 0.5, 1], [0, 500, 300]);
 
// With per-segment easing
const y = std.interpolate(ctx.sceneProgress, [0, 0.5, 1], [0, 200, 0], {
  easing: "easeInOutCubic",
});

For color transitions through multiple stops:

const bg = std.interpolateColor(ctx.sceneProgress,
  [0, 0.3, 0.7, 1],
  ["#0f172a", "#1e1b4b", "#1e1b4b", "#0f172a"]
);

Sequenced Animations

Use std.phases() to split progress into named phases:

render(ctx) {
  const { sceneProgress, std } = ctx;
 
  const { enter, hold, exit } = std.phases(sceneProgress, { enter: 1, hold: 2, exit: 1 });
 
  const titleOpacity = std.tween(0, 1, enter.progress, "easeOutCubic");
  const exitOpacity = std.tween(1, 0, exit.progress, "easeInCubic");
 
  return `<h1 style="opacity: ${titleOpacity * exitOpacity}">Title</h1>`;
}

Each phase has { progress, active, done, start, end }. Weights are proportional — { enter: 1, hold: 2, exit: 1 } gives enter 25%, hold 50%, exit 25%.

Looping Animations

For animations that repeat:

render(ctx) {
  const { sceneTimeSeconds: time, std } = ctx;
 
  // Pulse every 2 seconds
  const cycleProgress = (time % 2) / 2;  // 0 to 1, repeating
  const scale = std.tween(1, 1.1, cycleProgress, "easeInOutSine");
 
  return `<div style="transform: scale(${scale})">Pulse</div>`;
}

Math Helpers

The standard library includes useful math functions:

// Clamp a value to a range
std.math.clamp(value, min, max);
 
// Map a value from one range to another
std.math.map(value, inMin, inMax, outMin, outMax);
 
// Map and clamp combined
std.math.mapClamp(value, inMin, inMax, outMin, outMax);

Example:

// Animate only during seconds 1-3 of the video
const animationProgress = std.math.mapClamp(time, 1, 3, 0, 1);
const x = std.tween(0, 100, animationProgress);

Next Steps