Docs

Animation Basics

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

Use std.score() and its t.tween() helper when motion belongs to named scene phases like enter, hold, and exit. Use std.interpolate() when you already have a progress value and need custom ranges, multi-keyframes, loops, or non-phased math. std.tween() is not a public ctx.std API.

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.interpolate(time / 1, [0, 1], [0, 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 describe what's happening at that exact moment.

Sequenced Animations (std.score)

std.score() is the primary orchestration primitive for layouts. It breaks your scene into logical phases (e.g. intro, content, outro) and gives you motions scoped to those phases.

render(ctx) {
  const { std } = ctx;
 
  // Fractions of scene (must sum to ≤ 1)
  const t = std.score({ enter: 0.25, hold: 0.5, exit: 0.25 });
 
  const title = t.motion({ y: 20 });               // auto fade-in + fade-out
  const body  = t.motion({ at: 0.3, duration: 0.8 }); // staggered 30% into enter phase
 
  return `
    <h1 style="${title.style}">Title</h1>
    <p style="${body.style}">Body</p>
  `;
}

score() handles the exit-math automatically, eliminating the need to manually multiply by (1 - exitProgress).

Common Motion Options

OptionDefaultDescription
y, x20, 0Translate offset at entrance
scale1Scale at entrance (e.g. 0.5 for pop-in)
rotate0Rotation at entrance
easing"easeOutCubic"Entrance easing curve
at0Stagger offset (0-1) inside the phase
duration1Fraction of phase the motion spans
exittrueSet to false to hold state through exit phase

Easing Functions

Easing controls the feel of motion. Without easing (linear), animations feel robotic. You can use easing names in t.motion() or std.interpolate().

// Ease out - starts fast, slows down (natural deceleration)
t.motion({ easing: "easeOutCubic" });
 
// Bounce - playful, bouncy feel
t.motion({ easing: "easeOutBounce" });
 
// Back - slight overshoot
t.motion({ easing: "easeOutBack" });

Available Easings

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

Spring Physics

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

// Inside a score(), use spring easing as a string
const t = std.score();
const pop = t.motion({ scale: 0.5, easing: "spring(200,8)" });
 
// Standalone spring interpolation
const scale = std.spring(0.8, 1, ctx.sceneProgress, { stiffness: 200, damping: 8 });

Config options:

  • stiffness (default: 100) — higher = faster oscillation
  • damping (default: 10) — lower = more bouncy

Multi-Keyframe Interpolation

std.interpolate() maps a value through multiple ranges. Use this for loops, custom progress, or non-phased math.

// 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]);

For color transitions:

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

Staggered element collections

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.interpolate(progress, [0, 1], [0, 1]);
    const y = std.interpolate(progress, [0, 1], [20, 0]);
    return `<div style="opacity: ${opacity}; transform: translateY(${y}px)">${item}</div>`;
  }).join("");
}

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.interpolate(cycleProgress, [0, 1], [1, 1.1], "easeInOutSine");
 
  return `<div style="transform: scale(${scale})">Pulse</div>`;
}

Math Helpers

The standard library includes useful math functions for bounding and mapping values:

// Clamp a value to [0, 1]
std.clamp01(value);
 
// 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);

Next Steps