Kinetic center
A centered statement assembled from timed word groups.
Customize
import {Scene, Video} from "odori";
import {KineticCenter} from "../components/kinetic-center/kinetic-center";
<Video>
<Scene id="kinetic-center" duration="4s">
<KineticCenter
groups={[
"Make",
"motion",
"useful."
]}
accentIndex={2}
/>
</Scene>
</Video>Installation
pnpm odori add component kinetic-centernpx odori add component kinetic-centerbunx odori add component kinetic-centerEdit the source in videos/components/kinetic-center/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/kinetic-center/kinetic-center.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type KineticCenterProps = {
/** Word groups that assemble into one centered statement. */
groups: string[];
accentIndex?: number;
};
/**
* A centered statement assembled from timed word groups. Each group scales in
* from slightly under its final size, so the line tightens as it completes.
*/
export const KineticCenter = ({groups, accentIndex}: KineticCenterProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
return (
<Fill
style={{
alignItems: "center",
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: `${10 * scale}px ${24 * scale}px`,
justifyContent: "center",
maxWidth: 1560 * scale,
textAlign: "center",
}}
>
{groups.map((group, index) => {
const delay = 6 + index * 9;
const progress = interpolate(frame, [delay, delay + 24], [0, 1], {easing: Easing.standard});
const accent = index === accentIndex;
return (
<span
key={`${group}-${index}`}
style={{
color: accent ? brand.colors.accent : brand.colors.foreground,
display: "inline-block",
fontSize: 118 * scale,
fontWeight: accent ? 600 : 540,
letterSpacing: "-0.05em",
lineHeight: 1.04,
opacity: progress,
transform: `scale(${0.9 + progress * 0.1}) translateY(${(1 - progress) * 22 * scale}px)`,
}}
>
{group}
</span>
);
})}
</div>
</Fill>
);
};
videos/components/kinetic-center/kinetic-center.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {KineticCenter} from "./kinetic-center";
export default defineComponentPreview({
title: "Kinetic center",
category: "Typography/Titles",
description: "A centered statement assembled from timed word groups.",
component: KineticCenter,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
accentIndex: {type: "number", defaultValue: 2, min: 0, max: 5},
},
examples: [
{name: "Default", props: {groups: ["Make", "motion", "useful."], accentIndex: 2}},
{name: "Longer", props: {groups: ["Every", "frame", "is", "a", "decision."], accentIndex: 4}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {KineticCenter} from "../components/kinetic-center/kinetic-center";
<Video>
<Scene id="kinetic-center" duration="4s">
<KineticCenter
groups={[
"Make",
"motion",
"useful."
]}
accentIndex={2}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Typography
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 45 frames · 1.5s
- Entrance
- 33 frames
- Exit
- 10 frames
- Reduced motion
- groups fade without scaling
- Requires
- sans font
- Content limits
- groups ≤ 6, groupLength ≤ 16
Props
| Prop | Type | Default | Required |
|---|---|---|---|
groupsWord groups that assemble into one centered statement. | string[] | — | Yes |
accentIndex | number | — | — |
Related components
All componentsCaptionsFrame-accurate caption cues for sound-off playback.Character riseCharacters rise with a crisp per-letter stagger.Draw titleAn outline draws across the line, then fills solid.Flap boardA split-flap board clatters through to the target string.Focus inWords settle from a soft blur into a crisp line.Glitch textA title that arrives through brief seeded interference bursts.