Frames, timing & animation
Make motion a function of the frame.
A composition’s duration in seconds is durationInFrames / fps. At 30 fps, 150 frames make five seconds, with frame indices from 0 through 149. Use useCurrentFrame() inside a composition to calculate animated values.
Interpolate between values
The title example uses Math.min(frame / 30, 1) for opacity. For easing and more control, import interpolate and Easings from @celesta/react, then replace the calculation with:
const opacity = interpolate(frame, [0, 30], [0, 1], {
easing: Easings.easeOut,
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
Pass the result to <Text opacity={opacity} … />. Input ranges must be strictly increasing and have the same number of entries as the output range. Extrapolation defaults to extend; use clamp to hold the first and last values.
Easings covers the familiar easings.net curves: linear, and the easeIn/easeOut/easeInOut variants of Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, and Bounce (for example Easings.easeOutBack).
Interpolate between colors
Import interpolateColor from @celesta/react too. It takes the same input range and easing as interpolate, but maps the frame onto #RRGGBB or #RRGGBBAA colors and returns #RRGGBBAA. Use it wherever a color goes: a Rect fill, a text style’s fill, a gradient stop, or a shadow.
const sky = interpolateColor(frame, [0, 45, 90], ['#101820', '#EF7B45', '#7FC8F8']);
const title = interpolateColor(frame, [0, 20], ['#FFFFFF00', '#FFFFFF']);
<Rect width={1920} height={1080} fill={sky} />
<Text style={{ fontSize: 96, fill: { type: 'solid', color: title } }}>Morning</Text>
Unlike interpolate, extrapolation defaults to clamp, so the first and last colors hold outside the range; extend continues the end segments, and identity is not available. Colors blend in sRGB with premultiplied alpha, as gradients and CSS do: a color fading to #00000000 keeps its hue instead of darkening, and channels that extend or an overshooting easing push out of range are clamped. CSS color names, rgb(), and the short #RGB form are not accepted.
Add a little bounce
spring() simulates a damped spring: it starts at 0, settles at 1, and may overshoot on the way. It needs the frame rate, so read it from useVideoConfig():
function Badge() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, delay: 10, config: { damping: 12 } });
return (
<Rect x={960} y={540} anchorX={0.5} anchorY={0.5} scale={scale}
width={320} height={120} cornerRadius={60} fill="#78618e" />
);
}
Tune the feel with config: stiffness (default 100), damping (default 10), mass (default 1), and overshootClamping. Use from and to to map the result to another range.
Place a scene on the timeline
Import Sequence to delay or limit a group of layers. This shows the Title component from the previous example starting at frame 30 for 90 frames:
<Sequence from={30} durationInFrames={90}>
<Title />
</Sequence>
Within that sequence, useCurrentFrame() starts at 0 when the sequence begins. Child video and audio use the same local clock. Children contribute neither layers nor audio outside the sequence’s time window.
Redraw a past frame
Every frame is a function of the frame number, so <FreezeFrame frame> can draw any moment of the video again in place, for a rewind, a flashback, or a row of thumbnails. frame counts on the whole composition’s clock, even inside a Sequence:
<Group scale={0.25} x={80} y={80}>
<FreezeFrame frame={1200}>
<World />
</FreezeFrame>
</Group>
Directly inside, useCurrentFrame() returns frame, rounded to a whole frame (a Sequence within it still counts from its own start), and sequences, video, lip sync, and blinking all show that moment. A fixed frame holds still; with frame read from useCurrentFrame() in your root component, frame={frame - 90} replays 90 frames behind (three seconds at 30 fps). Frozen children are silent. A CharacterView drawn inside leaves its ref to the live view, and a Dialogue inside drives only a view inside the same FreezeFrame. Project layers (ProjectTimeline, ProjectTrack) are evaluated for the current frame only and cannot be drawn inside.
Ready-made entrances and exits
<Transition> fades, slides, or scales its children at the start (direction="in", the default) or end (direction="out") of the enclosing sequence:
<Sequence from={30} durationInFrames={120}>
<Transition type="slide" slideFrom="bottom" durationInFrames={15}
easing={Easings.easeOutCubic}>
<Transition type="fade" direction="out" durationInFrames={20}>
<Caption />
</Transition>
</Transition>
</Sequence>
Slides travel distance pixels (64 by default); scales start from scaleFrom (0.8 by default). Pass several types to combine them, such as type={['fade', 'slide']}.
Hold a scene at one frame
<FreezeFrame frame={60}> draws its children at root composition frame 60, even when wrapped in a Sequence. Child hooks, video, lip sync, and blinking follow that time; nested sequences still apply their own offsets. Audio inside FreezeFrame is muted.
<FreezeFrame frame={60}>
<AnimatedTitle />
</FreezeFrame>