brand-resolve
The closing sting: a fifth, a root, and a sub under the landing.
import {Scene, Video} from "odori";
import {BrandResolve} from "../components/brand-resolve/brand-resolve";
<Video>
<Scene id="brand-resolve" duration="1.2s">
<BrandResolve />
</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/brand-resolvenpx odori add @odori/brand-resolvebunx odori add @odori/brand-resolveSource is copied into videos/components/brand-resolve/ 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/brand-resolve/brand-resolve.tsx
import {
Fill,
SAMPLE_RATE,
cueSamples,
defineCue,
mix,
normalize,
shape,
sine,
useBrand,
useFrame,
useDesignScale,
useVideo,
type CueDefinition,
} from "odori";
export type BrandResolveOptions = {
/** The note the sting lands on, in hertz. */
root?: number;
/** Sub weight under the landing, 0 to 1. */
weight?: number;
peak?: number;
};
/**
* The closing sting: a fifth above the root arriving first, the root landing
* under it, and a sub giving the landing weight. Thirty six frames, so an end
* card can breathe before the cut.
*/
export const brandResolve = ({root = 196, weight = 0.5, peak = 0.8}: BrandResolveOptions = {}): CueDefinition =>
defineCue({
name: "brand.resolve",
durationInFrames: 36,
params: {root, weight, peak},
render: ({samples}) =>
normalize(
mix(
shape(sine(samples, root * 1.5), {attack: 0.01, decay: 0.3, sustain: 0.18, release: 0.5}),
shape(sine(samples, root), {attack: 0.04, decay: 0.35, sustain: 0.4, release: 0.6}),
shape(sine(samples, root / 2), {attack: 0.02, decay: 0.45, sustain: weight * 0.6, release: 0.5}),
),
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 BrandResolveWave = ({root, weight, peak, label = "brand.resolve"}: BrandResolveOptions & {label?: string}) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {fps, durationInFrames} = useVideo();
const cue = brandResolve({root, weight, 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/brand-resolve/brand-resolve.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {BrandResolveWave} from "./brand-resolve";
export default defineComponentPreview({
title: "Brand resolve",
category: "Sound",
description: "The closing sting: a fifth, a root, and a sub under the landing.",
component: BrandResolveWave,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
root: {type: "number", defaultValue: 196, min: 80, max: 400},
weight: {type: "number", defaultValue: 0.5, min: 0, max: 1, step: 0.05},
},
examples: [
{name: "Default", props: {}},
{name: "Lighter", props: {root: 262, weight: 0.2}},
],
});
Use it in a scene.
import {Scene, Video} from "odori";
import {BrandResolve} from "../components/brand-resolve/brand-resolve";
<Video>
<Scene id="brand-resolve" duration="1.2s">
<BrandResolve />
</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
- brand.resolve
- Aspect ratios
- 16:9 · 9:16 · 1:1
- Recommended duration
- 36 frames · 1.2s
- Minimum duration
- 36 frames · 1.2s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none