Compositions and frames
A composition is a function of the frame: what that means, the core APIs (Composition, Sequence, interpolate, spring), and the rules that keep every render the same.
A composition is a React component that raea calls once for every frame. Give it frame N and it draws frame N. It doesn't remember the frame before, and it doesn't wait for anything. That one rule is why raea can render any frame on its own, play the same file live, and promise that the same code always renders the same pixels.
The smallest composition
import { AbsoluteFill, Composition, registerRoot, useCurrentFrame } from "raea";
function Counter() {
const frame = useCurrentFrame();
return <AbsoluteFill style={{ background: "#000", color: "#fff", fontSize: 200, justifyContent: "center", alignItems: "center" }}>{frame}</AbsoluteFill>;
}
registerRoot(() => <Composition id="counter" component={Counter} width={1080} height={1080} fps={30} durationInFrames={60} />);registerRootnames what the file contains. A file can register several compositions;raea render FILE --composition IDpicks one.Compositionsets the size in pixels, the frame rate and the length in frames.useCurrentFrame()is the frame being drawn. Everything that moves is computed from it.AbsoluteFillis adivthat fills its parent, laid out as a flex column.
Layout and styling are the web's: div, span and img elements, inline style objects or Tailwind classes in
className, flexbox and grid. raea draws them on the GPU itself; there is no browser.
Moving things: interpolate and spring
interpolate maps a range of frames onto a range of values:
const x = interpolate(frame, [0, 30], [-400, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });Frames 0 to 30 move x from -400 to 0. Without the clamp options the line keeps going past its ends, so at frame 60
x would be 400. Clamp every interpolate unless you want that. Add easing: Easing.out(Easing.cubic) to ease it.
spring gives the motion of a physical spring, from 0 to 1, and settles by itself:
// check: frames 0,12,40
import { AbsoluteFill, Composition, registerRoot, spring, useCurrentFrame, useVideoConfig } from "raea";
const WORDS = ["Make", "what", "you", "imagine."];
// Each word springs up into place, four frames after the one before it.
function Title() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ background: "#0f1115", justifyContent: "center", alignItems: "center", flexDirection: "row", gap: 28 }}>
{WORDS.map((word, i) => {
const p = spring({ frame, fps, delay: i * 4, config: { damping: 14, stiffness: 160 } });
return (
<span key={word} style={{ color: "#ffffff", fontSize: 110, fontWeight: 700, opacity: p, transform: `translateY(${(1 - p) * 80}px)` }}>
{word}
</span>
);
})}
</AbsoluteFill>
);
}
registerRoot(() => <Composition id="title" component={Title} width={1920} height={1080} fps={30} durationInFrames={60} />);spring({ frame, fps }) is 0 at frame 0 and settles at 1. delay starts it later; config sets its mass,
damping and stiffness (defaults 1, 10 and 100: lower damping bounces more). from and to change the range, and
durationInFrames stretches it to settle in exactly that many frames.
Placing things in time: Sequence and Series
Sequence shows its children from one frame for a number of frames, and restarts their clock:
<Sequence from={30} durationInFrames={60}>
<Card text="Hello" />
</Sequence>Inside, useCurrentFrame() is 0 at composition frame 30. A component written to animate from frame 0 works anywhere
you place it. Series places Sequences one after another without adding up the frames yourself.
The rules
- Everything that moves reads the frame.
useState,useEffect, refs and timers don't animate a render. A value that changes over time is a function ofuseCurrentFrame(). - Random is seeded per frame.
Math.random()returns new numbers every frame, so something placed with it jumps. Seed what should hold still from an index, or decide it once in a game'sstep. - Clamp interpolations. An unclamped value runs off and can turn into NaN, which fails the render and names the line.
- Cue on words or events, not on frame numbers you guessed.
word("phrase")anduseGameEventsstay right when the cut or the rules change. A typed frame number drifts. - Media comes from files.
<Video>,<Audio>and<Img>take a path (usestaticFile("name")for the project'smedia/folder) or anhttps://URL, which raea fetches once into its cache.
raea guide motion has the full list of core rules with the reasoning behind each.
Where to go next
- Rendering and checking: stills, exports and the checks.
- Captions and cues on words: timing to speech.
- The SDK reference: every import.