Skip to content本文へ移動

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>

Try it without installing.

The playground runs Celesta in your browser, export included.

Open the playground