character-rise
Characters rise with a crisp per-letter stagger.
import {Scene, Video} from "odori";
import {CharacterRise} from "../components/character-rise/character-rise";
<Video>
<Scene id="character-rise" duration="4s">
<CharacterRise
text="Momentum"
detail="Motion with a purpose."
stagger={2}
/>
</Scene>
</Video>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/character-risenpx odori add @odori/character-risebunx odori add @odori/character-riseSource is copied into videos/components/character-rise/ and belongs to your repository. odori diff compares it with upstream later; odori update applies the change you accept. It has no registry dependencies.
Copy and paste the following code into your project.
videos/components/character-rise/character-rise.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type CharacterRiseProps = {
text: string;
detail?: string;
/** Frames between one character and the next. */
stagger?: number;
};
/**
* Characters rise out of a clipped baseline one after another. Spaces hold
* their slot so the line never reflows while it assembles.
*/
export const CharacterRise = ({text, detail, stagger = 2}: CharacterRiseProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const characters = [...text];
const settled = 8 + characters.length * stagger + 22;
return (
<Fill
style={{
alignItems: "center",
gap: 30 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
<div style={{display: "flex", overflow: "hidden", paddingBottom: 14 * scale}}>
{characters.map((character, index) => {
const delay = 8 + index * stagger;
const progress = interpolate(frame, [delay, delay + 22], [0, 1], {easing: Easing.standard});
return (
<span
key={`${character}-${index}`}
style={{
display: "inline-block",
fontSize: 132 * scale,
fontWeight: 600,
letterSpacing: "-0.04em",
lineHeight: 1.02,
opacity: progress,
transform: `translateY(${(1 - progress) * 84 * scale}px)`,
whiteSpace: "pre",
}}
>
{character}
</span>
);
})}
</div>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
opacity: interpolate(frame, [settled, settled + 18], [0, 1], {easing: Easing.standard}),
}}
>
{detail}
</div>
) : null}
</Fill>
);
};
videos/components/character-rise/character-rise.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {CharacterRise} from "./character-rise";
export default defineComponentPreview({
title: "Character rise",
category: "Typography",
description: "Characters rise with a crisp per-letter stagger.",
component: CharacterRise,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
text: {type: "text", defaultValue: "Momentum", maxLength: 22},
detail: {type: "text", defaultValue: "Motion with a purpose."},
stagger: {type: "number", defaultValue: 2, min: 1, max: 8},
},
examples: [
{name: "Default", props: {text: "Momentum", detail: "Motion with a purpose."}},
{name: "Slow", props: {text: "Deliberate", stagger: 5}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {CharacterRise} from "../components/character-rise/character-rise";
<Video>
<Scene id="character-rise" duration="4s">
<CharacterRise
text="Momentum"
detail="Motion with a purpose."
stagger={2}
/>
</Scene>
</Video>Nothing else to install.
The component imports only from odori, so there are no dependencies to add and no import paths to rewrite.
Timing contract
Every registry component declares how it behaves in time.odori test fails a cut that violates these.
- Family
- Typography
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 45 frames · 1.5s
- Entrance
- 34 frames
- Exit
- 10 frames
- Reduced motion
- the word fades without per-letter motion
- Requires
- sans font
- Content limits
- text ≤ 22
Props
| Prop | Type | Default | Required |
|---|---|---|---|
text | string | — | Yes |
detail | string | — | — |
staggerFrames between one character and the next. | number | 2 | — |