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:
| Property | Type | Description |
|---|---|---|
ctx.frame | number | Current frame (0, 1, 2, ...) |
ctx.sceneTimeSeconds | number | Time in seconds (0.0, 0.033, 0.066, ...) |
ctx.sceneProgress | number | Progress 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 valueto— Ending valueprogress— 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
| 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 |
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 oscillationdamping(default: 10) — lower = more bouncymass(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 1each— 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.0Multi-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
- How It Works — Understand the rendering pipeline
- CLI Reference — Commands for rendering to MP4
- Player — Embed videos in web apps