Product launch
The full launch story: headline, source proof, and a closing call to action, scored under one bed.
16:9 · every frame here is the frame odori export writes.
The files
A block is a directory, not a bundle. Pick a file to read the source it ships with.
videos/components/code-proof/end-card/stage/title-reveal/launch/
videos/launch/video.tsx
import {Audio, Scene, Video, defineVideoMetadata} from "odori";
import {CodeProof} from "../components/code-proof/code-proof";
import {EndCard} from "../components/end-card/end-card";
import {Stage} from "../components/stage/stage";
import {TitleReveal} from "../components/title-reveal/title-reveal";
import {productLayout} from "../layout";
import {launchInput} from "./schema";
const SOURCE = `export const metadata = defineVideoMetadata({
id: "launch",
title: "Product launch",
duration: "12s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene id="opening" duration="4s">
<TitleReveal title="Author the story." />
</Scene>
</Video>
);
}`;
export const metadata = defineVideoMetadata({
id: "launch",
title: "Odori launch",
description: "A concise introduction to the odori authoring model.",
duration: "12s",
layout: productLayout,
schema: launchInput,
tags: ["launch", "framework"],
thumbnailFrame: 45,
});
export default function LaunchVideo({
headline,
callToAction,
}: {
headline: string;
callToAction: string;
}) {
return (
<Video>
{/* One bed under the whole cut, ducked so scene sounds stay audible. */}
<Audio src="bed.main" gain={0.6} fadeIn="1s" fadeOut="1.5s" duckUnder />
<Scene id="opening" duration="4s" name="Opening">
<Stage>
<TitleReveal title={headline} detail="An independent React video framework." />
</Stage>
</Scene>
<Scene id="proof" duration="5s" name="Proof">
<Stage grid={false}>
<CodeProof
code={SOURCE}
language="tsx"
title="videos/launch/video.tsx"
caption="Static metadata for discovery. Ordinary JSX for the timeline."
/>
</Stage>
</Scene>
<Scene id="resolution" duration="3s" name="Resolution">
<Audio src="ui.confirm" from="0.2s" duration="1s" gain={0.7} />
<Stage>
<EndCard
title={"One definition.\nEvery render path."}
detail="Preview needs no encode. Export freezes the approved cut."
callToAction={callToAction}
/>
</Stage>
</Scene>
</Video>
);
}
videos/launch/schema.ts
import {defineInputSchema} from "odori";
export const launchInput = defineInputSchema({
headline: {type: "text", defaultValue: "Author the story.\nKeep the source.", maxLength: 64, multiline: true},
callToAction: {type: "text", defaultValue: "pnpm odori dev", maxLength: 28},
});
videos/layout.tsx
import {defineBrand, defineVideoLayout} from "odori";
import {uiReveal} from "./components/ui-reveal/ui-reveal";
import {uiKey} from "./components/ui-key/ui-key";
/**
* Brand is policy: tokens, fonts, motion, and audio defaults that every video
* inherits. Font files are served from public/, so preview and render resolve
* the same faces.
*/
export const odoriBrand = defineBrand({
name: "odori",
colors: {
background: "#000000",
surface: "#0a0a0a",
foreground: "#ededed",
muted: "#a1a1a1",
accent: "#ffffff",
border: "#1f1f1f",
},
typography: {
sans: '"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
mono: '"Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace',
},
fonts: [
{family: "Geist Sans", url: "/fonts/Geist-Variable.woff2", weight: "100 900"},
{family: "Geist Mono", url: "/fonts/GeistMono-Variable.woff2", weight: "100 900"},
],
motion: {standard: [0.16, 1, 0.3, 1], staggerFrames: 3},
audio: {
// Named so a video asks for a role and the brand decides the track.
cues: {
"bed.main": "/audio/bed-energetic.m4a",
"bed.calm": "/audio/bed-soft-pulse.m4a",
"bed.warm": "/audio/bed-warm-minimal.m4a",
"ui.confirm": "/audio/ui-reveal.m4a",
// A generated cue: source in this repository, rendered identically for
// preview and for the encode.
"ui.key": uiKey(),
"ui.reveal": uiReveal(),
},
targetLufs: -14,
},
});
export const productLayout = defineVideoLayout({
format: {width: 1920, height: 1080, fps: 30},
brand: odoriBrand,
safeArea: {x: 96, y: 72},
audio: {palette: "product-cinematic", targetLufs: -14},
});
videos/components/stage/stage.tsx
import type {CSSProperties, ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type StageProps = {
children?: ReactNode;
/** A hairline grid keeps large empty frames from feeling flat. */
grid?: boolean;
/** A slow radial glow behind the content. */
glow?: boolean;
align?: "center" | "start";
style?: CSSProperties;
};
/**
* The shared backdrop. Every scene sits on the same surface so cuts feel like
* one film rather than a stack of unrelated slides.
*/
/** Relative luminance, so the backdrop adapts to a light or dark brand. */
const isLight = (hex: string) => {
const value = hex.replace("#", "");
const full = value.length === 3 ? value.split("").map((part) => part + part).join("") : value;
const [red, green, blue] = [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16) / 255);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue > 0.5;
};
export const Stage = ({children, grid = true, glow = true, align = "center", style}: StageProps) => {
const frame = useFrame();
const brand = useBrand();
const {width, height} = useVideo();
const scale = useDesignScale();
const drift = interpolate(frame, [0, 240], [0, 1], {extrapolateRight: "extend"});
const cell = 80 * scale;
const light = isLight(brand.colors.background);
const glowColor = light ? "rgba(0,0,0,0.05)" : "rgba(255,255,255,0.07)";
const vignette = light ? "rgba(0,0,0,0.06)" : "rgba(0,0,0,0.55)";
return (
<Fill style={{background: brand.colors.background, ...style}}>
{grid ? (
<Fill
style={{
backgroundImage: `linear-gradient(to right, ${brand.colors.border} 1px, transparent 1px), linear-gradient(to bottom, ${brand.colors.border} 1px, transparent 1px)`,
backgroundSize: `${cell}px ${cell}px`,
maskImage: `radial-gradient(ellipse ${width * 0.55}px ${height * 0.5}px at 50% 45%, rgba(0,0,0,0.65), transparent 78%)`,
WebkitMaskImage: `radial-gradient(ellipse ${width * 0.55}px ${height * 0.5}px at 50% 45%, rgba(0,0,0,0.65), transparent 78%)`,
opacity: interpolate(frame, [0, 24], [0, 0.7], {easing: Easing.standard}),
transform: `translateY(${drift * -14 * scale}px)`,
}}
/>
) : null}
{glow ? (
<Fill
style={{
backgroundImage: `radial-gradient(ellipse ${width * 0.45}px ${height * 0.42}px at 50% ${
38 + drift * 3
}%, ${glowColor}, transparent 70%)`,
}}
/>
) : null}
<Fill
style={{
alignItems: "center",
justifyContent: align === "center" ? "center" : "flex-start",
}}
>
{children}
</Fill>
<Fill
style={{
boxShadow: `inset 0 0 ${240 * scale}px ${vignette}`,
pointerEvents: "none",
}}
/>
</Fill>
);
};
videos/components/title-reveal/title-reveal.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type TitleRevealProps = {
title: string;
detail?: string;
align?: "left" | "center";
};
/**
* A masked, word-staggered headline. Each word rises out of a clipped line
* box, so the reveal reads as typography rather than a fade.
*/
export const TitleReveal = ({title, detail, align = "center"}: TitleRevealProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const lines = title.split("\n");
let wordIndex = 0;
return (
<Fill
style={{
alignItems: align === "center" ? "center" : "flex-start",
gap: 34 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
textAlign: align,
}}
>
<div style={{display: "grid", gap: 6 * scale, maxWidth: 1560 * scale}}>
{lines.map((line, lineNumber) => (
<div
key={`${line}-${lineNumber}`}
style={{
display: "flex",
flexWrap: "wrap",
gap: `0 ${26 * scale}px`,
justifyContent: align === "center" ? "center" : "flex-start",
overflow: "hidden",
paddingBottom: 12 * scale,
}}
>
{line.split(/\s+/).map((word) => {
const delay = 4 + wordIndex * brand.motion.staggerFrames;
wordIndex += 1;
const progress = interpolate(frame, [delay, delay + 24], [0, 1], {easing: Easing.standard});
return (
<span
key={`${word}-${wordIndex}`}
style={{
display: "inline-block",
fontSize: 116 * scale,
fontWeight: 560,
letterSpacing: "-0.045em",
lineHeight: 1.04,
opacity: progress,
transform: `translateY(${(1 - progress) * 78 * scale}px)`,
}}
>
{word}
</span>
);
})}
</div>
))}
</div>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 36 * scale,
letterSpacing: "-0.015em",
maxWidth: 1100 * scale,
opacity: interpolate(frame, [16, 34], [0, 1], {easing: Easing.standard}),
transform: `translateY(${interpolate(frame, [16, 34], [14 * scale, 0], {easing: Easing.standard})}px)`,
}}
>
{detail}
</div>
) : null}
</Fill>
);
};
videos/components/code-proof/code-proof.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type CodeProofProps = {
code: string;
language?: string;
title?: string;
/** One-based line numbers to emphasize. Other lines dim. */
focus?: number[];
caption?: string;
};
type Token = {pattern: RegExp; role: "comment" | "keyword" | "string" | "number" | "type" | "punctuation"};
const TOKENS: Token[] = [
{pattern: /(\/\/[^\n]*)/g, role: "comment"},
{pattern: /("[^"]*"|'[^']*'|`[^`]*`)/g, role: "string"},
{
pattern: /\b(import|export|from|const|let|var|return|function|default|await|async|type|interface|new|class)\b/g,
role: "keyword",
},
{pattern: /\b([A-Z][A-Za-z0-9]*)\b/g, role: "type"},
{pattern: /\b(\d+(?:\.\d+)?)\b/g, role: "number"},
];
const PALETTE: Record<Token["role"], string> = {
comment: "#6f6f6f",
keyword: "#c586ff",
string: "#7ee787",
number: "#ffc86b",
type: "#79c0ff",
punctuation: "#8b8b8b",
};
const highlight = (line: string) => {
const marks: Array<{start: number; end: number; color: string}> = [];
for (const token of TOKENS) {
for (const match of line.matchAll(token.pattern)) {
const start = match.index ?? 0;
const end = start + match[0].length;
if (marks.some((mark) => start < mark.end && end > mark.start)) continue;
marks.push({start, end, color: PALETTE[token.role]});
}
}
marks.sort((left, right) => left.start - right.start);
const parts: Array<{text: string; color?: string}> = [];
let cursor = 0;
for (const mark of marks) {
if (mark.start > cursor) parts.push({text: line.slice(cursor, mark.start)});
parts.push({text: line.slice(mark.start, mark.end), color: mark.color});
cursor = mark.end;
}
if (cursor < line.length) parts.push({text: line.slice(cursor)});
return parts;
};
/**
* A source excerpt in a product-grade window. Lines resolve from a small blur
* so the eye lands on the code rather than on the motion.
*/
export const CodeProof = ({code, language = "tsx", title, focus = [], caption}: CodeProofProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const lines = code.replace(/\n+$/, "").split("\n");
const enter = interpolate(frame, [0, 22], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", gap: 30 * scale, justifyContent: "center", padding: 110 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 16 * scale,
boxShadow: `0 ${30 * scale}px ${90 * scale}px rgba(0,0,0,0.6)`,
maxWidth: 1440 * scale,
opacity: enter,
overflow: "hidden",
transform: `translateY(${(1 - enter) * 28 * scale}px)`,
width: "100%",
}}
>
<div
style={{
alignItems: "center",
background: "rgba(255,255,255,0.02)",
borderBottom: `1px solid ${brand.colors.border}`,
display: "flex",
gap: 14 * scale,
padding: `${18 * scale}px ${24 * scale}px`,
}}
>
<div style={{display: "flex", gap: 9 * scale}}>
{["#2a2a2a", "#2a2a2a", "#2a2a2a"].map((color, index) => (
<span key={index} style={{background: color, borderRadius: 999, height: 12 * scale, width: 12 * scale}} />
))}
</div>
{title ? (
<span
style={{
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 21 * scale,
letterSpacing: "-0.01em",
marginLeft: 8 * scale,
}}
>
{title}
</span>
) : null}
<span
style={{
border: `1px solid ${brand.colors.border}`,
borderRadius: 6 * scale,
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 21 * scale,
marginLeft: "auto",
padding: `${4 * scale}px ${12 * scale}px`,
textTransform: "uppercase",
}}
>
{language}
</span>
</div>
<pre
style={{
color: brand.colors.foreground,
fontFamily: brand.typography.mono,
fontSize: 28 * scale,
lineHeight: 1.65,
margin: 0,
padding: `${30 * scale}px ${28 * scale}px`,
}}
>
{lines.map((line, index) => {
const delay = 10 + index * 3;
const appear = interpolate(frame, [delay, delay + 14], [0, 1], {easing: Easing.standard});
const focused = focus.length === 0 || focus.includes(index + 1);
return (
<div
key={`${line}-${index}`}
style={{
display: "flex",
filter: `blur(${(1 - appear) * 6}px)`,
gap: 24 * scale,
opacity: appear * (focused ? 1 : 0.32),
transform: `translateY(${(1 - appear) * 8 * scale}px)`,
}}
>
<span style={{color: "#3a3a3a", minWidth: 32 * scale, textAlign: "right", userSelect: "none"}}>
{index + 1}
</span>
<span style={{whiteSpace: "pre"}}>
{highlight(line).map((part, partIndex) => (
<span key={partIndex} style={{color: part.color}}>
{part.text}
</span>
))}
</span>
</div>
);
})}
</pre>
</div>
{caption ? (
<div
style={{
color: brand.colors.muted,
fontSize: 30 * scale,
letterSpacing: "-0.01em",
opacity: interpolate(frame, [24, 40], [0, 1], {easing: Easing.standard}),
}}
>
{caption}
</div>
) : null}
</Fill>
);
};
videos/components/end-card/end-card.tsx
import {Fill, Easing, interpolate, spring, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type EndCardProps = {
title: string;
detail?: string;
callToAction?: string;
};
/** The closing lockup: the statement, and one command to run. */
export const EndCard = ({title, detail, callToAction}: EndCardProps) => {
const frame = useFrame();
const brand = useBrand();
const {fps} = useVideo();
const scale = useDesignScale();
const rise = spring({frame, fps, from: 0.96, to: 1, damping: 20});
const line = interpolate(frame, [8, 34], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", gap: 30 * scale, justifyContent: "center", padding: 110 * scale}}>
<div
style={{
fontSize: 108 * scale,
fontWeight: 560,
letterSpacing: "-0.05em",
lineHeight: 1.02,
maxWidth: 1400 * scale,
opacity: interpolate(frame, [0, 18], [0, 1], {easing: Easing.standard}),
textAlign: "center",
transform: `scale(${rise})`,
whiteSpace: "pre-line",
}}
>
{title}
</div>
<div
style={{
background: `linear-gradient(90deg, transparent, ${brand.colors.border}, transparent)`,
height: 1,
opacity: line,
width: `${line * 46}%`,
}}
/>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
letterSpacing: "-0.015em",
opacity: interpolate(frame, [14, 30], [0, 1], {easing: Easing.standard}),
textAlign: "center",
}}
>
{detail}
</div>
) : null}
{callToAction ? (
<div
style={{
alignItems: "center",
background: "rgba(255,255,255,0.04)",
border: `1px solid ${brand.colors.border}`,
borderRadius: 10 * scale,
color: brand.colors.foreground,
display: "flex",
fontFamily: brand.typography.mono,
fontSize: 28 * scale,
gap: 14 * scale,
marginTop: 8 * scale,
opacity: interpolate(frame, [22, 38], [0, 1], {easing: Easing.standard}),
padding: `${16 * scale}px ${28 * scale}px`,
transform: `translateY(${interpolate(frame, [22, 38], [12 * scale, 0], {easing: Easing.standard})}px)`,
}}
>
<span style={{color: brand.colors.muted}}>{"$"}</span>
{callToAction}
</div>
) : null}
</Fill>
);
};
Remix it
Scaffold a project, install the components this block uses, then paste the entry into videos/launch/video.tsx. Every file above is yours to edit from that point on.
pnpm create odori@latest my-videopnpm odori add @odori/stage @odori/title-reveal @odori/code-proof @odori/end-card