ui-key
A typed key: filtered noise with a short tone under it.
import {Scene, Video} from "odori";
import {UiKey} from "../components/ui-key/ui-key";
<Video>
<Scene id="ui-key" duration="0.2s">
<UiKey />
</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-keynpx odori add @odori/ui-keybunx odori add @odori/ui-keySource is copied into videos/components/ui-key/ 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-key/ui-key.tsx
import {
Fill,
SAMPLE_RATE,
cueSamples,
defineCue,
mix,
noise,
normalize,
shape,
sine,
useBrand,
useFrame,
useDesignScale,
useVideo,
type CueDefinition,
} from "odori";
export type UiKeyOptions = {
/** How much body the click keeps. Lower is duller, higher is sharper. */
tone?: number;
/** Peak level before the mix normalizes the whole track. */
peak?: number;
};
/**
* A key press: filtered noise for the click, a short sine underneath for
* weight. Six frames, so it lands inside a single typed character.
*
* A sound as source. Change the numbers, hear it in the next preview frame,
* and the encoder mixes the samples you approved.
*/
export const uiKey = ({tone = 320, peak = 0.7}: UiKeyOptions = {}): CueDefinition =>
defineCue({
name: "ui.key",
durationInFrames: 6,
params: {tone, peak},
render: ({samples}) =>
normalize(
mix(
shape(noise(samples, 7), {attack: 0.001, decay: 0.05, sustain: 0, release: 0.02}),
shape(sine(samples, tone), {attack: 0.001, decay: 0.035, sustain: 0, release: 0.02}),
),
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 UiKeyWave = ({tone, peak, label = "ui.key"}: UiKeyOptions & {label?: string}) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {fps, durationInFrames} = useVideo();
const cue = uiKey({tone, 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-key/ui-key.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {UiKeyWave} from "./ui-key";
export default defineComponentPreview({
title: "Key click",
category: "Sound",
description: "A typed key: filtered noise with a short tone under it.",
component: UiKeyWave,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
tone: {type: "number", defaultValue: 320, min: 80, max: 900},
peak: {type: "number", defaultValue: 0.7, min: 0.1, max: 1, step: 0.05},
},
examples: [
{name: "Default", props: {}},
{name: "Softer", props: {tone: 180, peak: 0.5}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {UiKey} from "../components/ui-key/ui-key";
<Video>
<Scene id="ui-key" duration="0.2s">
<UiKey />
</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.key
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 6 frames · 0.2s
- Minimum duration
- 6 frames · 0.2s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none