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
| Option | Default | Description |
|---|---|---|
y, x | 20, 0 | Translate offset at entrance |
scale | 1 | Scale at entrance (e.g. 0.5 for pop-in) |
rotate | 0 | Rotation at entrance |
easing | "easeOutCubic" | Entrance easing curve |
at | 0 | Stagger offset (0-1) inside the phase |
duration | 1 | Fraction of phase the motion spans |
exit | true | Set 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
| Family | In | Out | In-Out |
|---|---|---|---|
| Quad | easeInQuad | easeOutQuad | easeInOutQuad |
| Cubic | easeInCubic | easeOutCubic | easeInOutCubic |
| Quart | easeInQuart | easeOutQuart | easeInOutQuart |
| Quint | easeInQuint | easeOutQuint | easeInOutQuint |
| Sine | easeInSine | easeOutSine | easeInOutSine |
| Expo | easeInExpo | easeOutExpo | easeInOutExpo |
| Circ | easeInCirc | easeOutCirc | easeInOutCirc |
| Elastic | easeInElastic | easeOutElastic | easeInOutElastic |
| Back | easeInBack | easeOutBack | easeInOutBack |
| Bounce | easeInBounce | easeOutBounce | easeInOutBounce |
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 oscillationdamping(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
- Timing With Score And Cues — Score phases, cue timestamps, and how to combine them
- How It Works — Understand the rendering pipeline
- CLI Reference — Commands for rendering to MP4
- Player — Embed videos in web apps