identity-reveal
Product mark and name entrance with visual restraint.
import {Scene, Video} from "odori";
import {IdentityReveal} from "../components/identity-reveal/identity-reveal";
<Video>
<Scene id="identity-reveal" duration="3s">
<IdentityReveal
wordmark="odori"
detail="An independent React video framework."
/>
</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/identity-revealnpx odori add @odori/identity-revealbunx odori add @odori/identity-revealSource is copied into videos/components/identity-reveal/ 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/identity-reveal/identity-reveal.tsx
import type {ReactNode} from "react";
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type IdentityRevealProps = {
/** Defaults to the brand name, so a project never hardcodes its own mark. */
wordmark?: string;
/**
* Your own mark, as JSX. Inline SVG scales with the design, takes brand
* colors, and can animate per frame, which a file can never do.
*/
mark?: ReactNode;
/** A key in `brand.logos`, or a URL. Used when no `mark` is given. */
logo?: string;
detail?: string;
};
/**
* The product mark and name entering together: the mark settles first, the
* wordmark follows out of a clipped box, and a rule draws under both. Restraint
* is the point, so nothing spins or bounces.
*/
export const IdentityReveal = ({wordmark, mark: markNode, logo, detail}: IdentityRevealProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const name = wordmark ?? brand.name;
const source = logo ? brand.logos[logo] ?? logo : undefined;
const mark = interpolate(frame, [0, 24], [0, 1], {easing: Easing.standard});
const word = interpolate(frame, [12, 38], [0, 1], {easing: Easing.standard});
const rule = interpolate(frame, [28, 54], [0, 1], {easing: Easing.standard});
const note = interpolate(frame, [38, 60], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", gap: 26 * scale, justifyContent: "center"}}>
<div style={{alignItems: "center", display: "flex", gap: 26 * scale}}>
{markNode ? (
<div
style={{
alignItems: "center",
display: "flex",
height: 88 * scale,
opacity: mark,
transform: `scale(${0.92 + mark * 0.08})`,
}}
>
{markNode}
</div>
) : source ? (
<img alt="" src={source} style={{height: 88 * scale, opacity: mark, transform: `scale(${0.92 + mark * 0.08})`}} />
) : (
<span
style={{
background: brand.colors.accent,
borderRadius: 18 * scale,
display: "block",
height: 72 * scale,
opacity: mark,
transform: `scale(${0.9 + mark * 0.1})`,
width: 72 * scale,
}}
/>
)}
<div style={{overflow: "hidden", paddingBottom: 8 * scale}}>
<span
style={{
display: "inline-block",
fontSize: 96 * scale,
fontWeight: 600,
letterSpacing: "-0.05em",
opacity: word,
transform: `translateY(${(1 - word) * 66 * scale}px)`,
}}
>
{name}
</span>
</div>
</div>
<div style={{background: brand.colors.border, height: Math.max(1, 1 * scale), transform: `scaleX(${rule})`, width: 420 * scale}} />
{detail ? <div style={{color: brand.colors.muted, fontSize: 32 * scale, opacity: note}}>{detail}</div> : null}
</Fill>
);
};
videos/components/identity-reveal/identity-reveal.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {IdentityReveal} from "./identity-reveal";
export default defineComponentPreview({
title: "Identity reveal",
category: "Foundation",
description: "Product mark and name entrance with visual restraint.",
component: IdentityReveal,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
wordmark: {type: "text", defaultValue: "odori", maxLength: 24},
detail: {type: "text", defaultValue: "An independent React video framework."},
},
examples: [
{name: "Mark and name", props: {}},
{name: "Name only", props: {wordmark: "odori", detail: ""}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {IdentityReveal} from "../components/identity-reveal/identity-reveal";
<Video>
<Scene id="identity-reveal" duration="3s">
<IdentityReveal
wordmark="odori"
detail="An independent React video framework."
/>
</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
- Foundation
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 90 frames · 3s
- Minimum duration
- 60 frames · 2s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- mark and name fade in together
- Requires
- sans font
- Content limits
- wordmark ≤ 24, detail ≤ 72
Props
| Prop | Type | Default | Required |
|---|---|---|---|
wordmarkDefaults to the brand name, so a project never hardcodes its own mark. | string | — | — |
markYour own mark, as JSX. Inline SVG scales with the design, takes brand colors, and can animate per frame, which a file can never do. | ReactNode | — | — |
logoA key in `brand.logos`, or a URL. Used when no `mark` is given. | string | — | — |
detail | string | — | — |