value-swap
Replace one label or value while preserving alignment.
import {Scene, Video} from "odori";
import {ValueSwap} from "../components/value-swap/value-swap";
<Video>
<Scene id="value-swap" duration="3s">
<ValueSwap
label="Status"
from="Draft"
to="Ready"
swapFrame={45}
/>
</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/value-swapnpx odori add @odori/value-swapbunx odori add @odori/value-swapSource is copied into videos/components/value-swap/ 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/value-swap/value-swap.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type ValueSwapProps = {
label?: string;
from: string;
to: string;
/** Frame the swap happens on. */
swapFrame?: number;
};
/**
* One value replaced by another in place. Both values occupy the same grid
* cell, so the swap cannot shift anything around it, however wide the new
* value is.
*/
export const ValueSwap = ({label, from, to, swapFrame = 45}: ValueSwapProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const out = interpolate(frame, [swapFrame - 10, swapFrame], [0, 1], {
easing: Easing.standard,
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const into = interpolate(frame, [swapFrame, swapFrame + 14], [0, 1], {
easing: Easing.standard,
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const cell = {gridColumn: 1, gridRow: 1} as const;
return (
<Fill
style={{
alignItems: "center",
gap: 24 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
{label ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
opacity: interpolate(frame, [0, 14], [0, 1], {easing: Easing.standard}),
}}
>
{label}
</div>
) : null}
<div style={{display: "grid", placeItems: "center"}}>
<span
style={{
...cell,
fontSize: 120 * scale,
fontWeight: 570,
letterSpacing: "-0.05em",
opacity: 1 - out,
transform: `translateY(${out * -34 * scale}px)`,
}}
>
{from}
</span>
<span
style={{
...cell,
color: brand.colors.accent,
fontSize: 120 * scale,
fontWeight: 600,
letterSpacing: "-0.05em",
opacity: into,
transform: `translateY(${(1 - into) * 34 * scale}px)`,
}}
>
{to}
</span>
</div>
</Fill>
);
};
videos/components/value-swap/value-swap.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {ValueSwap} from "./value-swap";
export default defineComponentPreview({
title: "Value swap",
category: "Typography",
description: "Replace one label or value while preserving alignment.",
component: ValueSwap,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
label: {type: "text", defaultValue: "Status"},
from: {type: "text", defaultValue: "Draft", maxLength: 24},
to: {type: "text", defaultValue: "Ready", maxLength: 24},
swapFrame: {type: "number", defaultValue: 45, min: 12, max: 80},
},
examples: [
{name: "Status", props: {label: "Status", from: "Draft", to: "Ready"}},
{name: "Duration", props: {label: "Render time", from: "8 min", to: "40 s"}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {ValueSwap} from "../components/value-swap/value-swap";
<Video>
<Scene id="value-swap" duration="3s">
<ValueSwap
label="Status"
from="Draft"
to="Ready"
swapFrame={45}
/>
</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
- 90 frames · 3s
- Minimum duration
- 60 frames · 2s
- Entrance
- 14 frames
- Exit
- 0 frames
- Reduced motion
- the values cross-fade without travel
- Requires
- sans font
- Content limits
- from ≤ 24, to ≤ 24
Props
| Prop | Type | Default | Required |
|---|---|---|---|
label | string | — | — |
from | string | — | Yes |
to | string | — | Yes |
swapFrameFrame the swap happens on. | number | 45 | — |