kinetic-center
A centered statement assembled from timed word groups.
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>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/kinetic-centernpx odori add @odori/kinetic-centerbunx odori add @odori/kinetic-centerSource is copied into videos/components/kinetic-center/ 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/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",
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}},
],
});
Use it in 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>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
- 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 | — | — |