ui-reveal
A short upward sweep that settles, for an entrance.
import {Scene, Video} from "odori";
import {UiReveal} from "../components/ui-reveal/ui-reveal";
<Video>
<Scene id="ui-reveal" duration="0.6s">
<UiReveal />
</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/ui-revealnpx odori add @odori/ui-revealbunx odori add @odori/ui-revealSource is copied into videos/components/ui-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/ui-reveal/ui-reveal.tsx
import {
Fill,
SAMPLE_RATE,
cueSamples,
defineCue,
lowPass,
mix,
noise,
normalize,
shape,
sweep,
useBrand,
useFrame,
useDesignScale,
useVideo,
type CueDefinition,
} from "odori";
export type UiRevealOptions = {
/** Where the sweep starts, in hertz. */
from?: number;
/** Where it lands. The distance is what makes it feel like an arrival. */
to?: number;
peak?: number;
};
/**
* A reveal: a short sweep upward that settles on a tone, with a breath of
* filtered noise behind it. Eighteen frames, matched to a typical entrance.
*/
export const uiReveal = ({from = 220, to = 660, peak = 0.75}: UiRevealOptions = {}): CueDefinition =>
defineCue({
name: "ui.reveal",
durationInFrames: 18,
params: {from, to, peak},
render: ({samples}) =>
normalize(
mix(
shape(sweep(samples, from, to), {attack: 0.02, decay: 0.22, sustain: 0.3, release: 0.2}),
shape(lowPass(noise(samples, 11), 2400), {attack: 0.05, decay: 0.25, sustain: 0.08, release: 0.25}),
),
peak,
),
});
/**
* The cue drawn as a waveform, with a playhead crossing it in time. Rendering
* the score is deterministic, so this is a truthful picture of the sound rather
* than a decorative squiggle.
*/
export const UiRevealWave = ({from, to, peak, label = "ui.reveal"}: UiRevealOptions & {label?: string}) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {fps, durationInFrames} = useVideo();
const cue = uiReveal({from, to, peak});
const signal = cue.render({samples: cueSamples(cue, fps), sampleRate: SAMPLE_RATE});
const channel = signal.channels[0];
const columns = 120;
const step = Math.max(1, Math.floor(channel.length / columns));
const peaks = Array.from({length: columns}, (_, index) => {
let highest = 0;
for (let sample = index * step; sample < Math.min(channel.length, (index + 1) * step); sample += 1) {
highest = Math.max(highest, Math.abs(channel[sample]));
}
return highest;
});
const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);
return (
<Fill style={{alignItems: "center", gap: 34 * scale, justifyContent: "center", padding: 120 * scale}}>
<div style={{alignItems: "center", display: "flex", gap: 4 * scale, height: 260 * scale}}>
{peaks.map((value, index) => (
<span
key={index}
style={{
background: index / columns <= progress ? brand.colors.accent : brand.colors.border,
borderRadius: 999,
height: Math.max(2 * scale, value * 260 * scale),
width: 6 * scale,
}}
/>
))}
</div>
<div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>
{label} · {cue.durationInFrames} frames
</div>
</Fill>
);
};
videos/components/ui-reveal/ui-reveal.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {UiRevealWave} from "./ui-reveal";
export default defineComponentPreview({
title: "Reveal",
category: "Sound",
description: "A short upward sweep that settles, for an entrance.",
component: UiRevealWave,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
from: {type: "number", defaultValue: 220, min: 80, max: 800},
to: {type: "number", defaultValue: 660, min: 200, max: 2000},
},
examples: [
{name: "Default", props: {}},
{name: "Wider", props: {from: 140, to: 1200}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {UiReveal} from "../components/ui-reveal/ui-reveal";
<Video>
<Scene id="ui-reveal" duration="0.6s">
<UiReveal />
</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
- Sound
- Brand cue
- ui.reveal
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 18 frames · 0.6s
- Minimum duration
- 18 frames · 0.6s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none