Confirmation
Two rising notes for a confirmation.
Customize
import {Scene, Video} from "odori";
import {Success} from "../components/success/success";
<Video>
<Scene id="success" duration="0.5s">
<Success />
</Scene>
</Video>Installation
pnpm odori add audio successnpx odori add audio successbunx odori add audio successEdit the source in videos/components/success/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/success/success.tsx
import {defineCue, gain, mix, normalize, note, sequence, shape, sine, type CueDefinition} from "odori";
export type SuccessOptions = {
/** The note the confirmation starts from. */
root?: string;
/** Peak level before the mix normalizes the whole track. */
peak?: number;
};
/**
* Two notes rising a fifth: the shape every interface has agreed means "that
* worked". Half a second, so it fits inside the frame where the check mark
* appears.
*
* Rising is the entire message. The same two notes in the other order are
* `error`, which is why both are written from one interval.
*/
export const success = ({root = "e5", peak = 0.6}: SuccessOptions = {}): CueDefinition => {
const samples = Math.round(48000 * 0.5);
const base = note(root);
return defineCue({
name: "ui.success",
durationInFrames: 15,
params: {root, peak},
render: () =>
normalize(
sequence(
[base, base * 2 ** (7 / 12)].map((frequency, index) => ({
at: index,
play: (length: number) =>
mix(
shape(sine(length, frequency), {attack: 0.004, decay: 0.24, sustain: 0.06, release: 0.12}),
gain(shape(sine(length, frequency * 2), {attack: 0.004, decay: 0.12, sustain: 0}), 0.3),
),
gain: index === 0 ? 0.7 : 0.85,
})),
{bpm: 150, division: 16, samples},
),
peak,
),
});
};
videos/components/success/success.preview.tsx
import {CueWave, defineComponentPreview} from "odori/preview";
import {success, type SuccessOptions} from "./success";
const Wave = ({root, peak}: SuccessOptions) => <CueWave cue={success({root, peak})} />;
export default defineComponentPreview({
title: "Confirmation",
category: "Sound/Interface",
description: "Two rising notes for a confirmation.",
component: Wave,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
root: {type: "select", defaultValue: "e5", options: ["c5", "d5", "e5", "g5"]},
peak: {type: "number", defaultValue: 0.6, min: 0.1, max: 1, step: 0.05},
},
examples: [{name: "Default", props: {}}, {name: "Lower", props: {root: "c5"}}],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Success} from "../components/success/success";
<Video>
<Scene id="success" duration="0.5s">
<Success />
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Sound
- Brand cue
- ui.success
- Recommended duration
- 15 frames · 0.5s
- Minimum duration
- 15 frames · 0.5s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none