B-roll window
Supporting footage framed beside the main narrative.
Customize
import {Scene, Video} from "odori";
import {BRollWindow} from "../components/b-roll-window/b-roll-window";
<Video>
<Scene id="b-roll-window" duration="6s">
<BRollWindow
corner="bottom-right"
at={24}
until={140}
label="odori export launch"
/>
</Scene>
</Video>Installation
pnpm odori add component b-roll-windownpx odori add component b-roll-windowbunx odori add component b-roll-windowEdit the source in videos/components/b-roll-window/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/b-roll-window/b-roll-window.tsx
import type {ReactNode} from "react";
import {Easing, Fill, interpolate, placeholderImage, useBrand, useDesignScale, useFrame} from "odori";
export type BRollWindowProps = {
/** The main narrative, filling the frame. */
children?: ReactNode;
/** Supporting footage. A string is an image URL. */
media?: string | ReactNode;
/** Which corner the window sits in. */
corner?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
/** Frame it enters and frame it leaves. */
at?: number;
until?: number;
label?: string;
};
/** Which two edges each corner hugs, so the offsets below stay declarative. */
const CORNERS: Record<string, {top?: boolean; bottom?: boolean; left?: boolean; right?: boolean}> = {
"bottom-right": {bottom: true, right: true},
"bottom-left": {bottom: true, left: true},
"top-right": {top: true, right: true},
"top-left": {top: true, left: true},
};
/**
* Supporting footage beside the main narrative, in and out on cue.
*
* The window enters from the edge it sits against, which is what makes it read
* as arriving from off-screen rather than fading up out of nowhere — and it
* leaves the same way, so it never has to be cut around.
*/
export const BRollWindow = ({
children,
media,
corner = "bottom-right",
at = 24,
until = 140,
label,
}: BRollWindowProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const place = CORNERS[corner];
const fromRight = corner.endsWith("right");
const enter = interpolate(frame, [at, at + 14], [0, 1], {easing: Easing.standard});
const leave = interpolate(frame, [until, until + 14], [1, 0], {easing: Easing.standard});
const visible = Math.min(enter, leave);
const source = media ?? placeholderImage({label: "b-roll", seed: "broll", width: 800, height: 500});
return (
<Fill>
{children}
{visible > 0.001 ? (
<div
style={{
border: `1px solid ${brand.colors.border}`,
borderRadius: 16 * scale,
bottom: place.bottom ? 64 * scale : undefined,
boxShadow: `0 ${24 * scale}px ${60 * scale}px ${brand.colors.shadow}`,
height: 340 * scale,
left: place.left ? 64 * scale : undefined,
opacity: visible,
overflow: "hidden",
position: "absolute",
right: place.right ? 64 * scale : undefined,
top: place.top ? 64 * scale : undefined,
transform: `translateX(${(1 - visible) * (fromRight ? 60 : -60) * scale}px)`,
width: 560 * scale,
}}
>
{typeof source === "string" ? (
<img src={source} alt="" style={{height: "100%", objectFit: "cover", width: "100%"}} />
) : (
source
)}
{label ? (
<span
style={{
background: "rgba(0,0,0,0.6)",
bottom: 0,
color: brand.colors.foreground,
fontFamily: brand.typography.mono,
fontSize: 20 * scale,
left: 0,
padding: `${10 * scale}px ${16 * scale}px`,
position: "absolute",
right: 0,
}}
>
{label}
</span>
) : null}
</div>
) : null}
</Fill>
);
};
videos/components/b-roll-window/b-roll-window.preview.tsx
import {useBrand} from "odori";
import {defineComponentPreview} from "odori/preview";
import {BRollWindow} from "./b-roll-window";
const Main = () => {
const brand = useBrand();
return (
<div
style={{
alignItems: "center",
background: brand.colors.background,
color: brand.colors.foreground,
display: "flex",
fontFamily: "ui-sans-serif, system-ui",
fontSize: 82,
inset: 0,
justifyContent: "center",
letterSpacing: "-0.04em",
position: "absolute",
}}
>
The main narrative
</div>
);
};
export default defineComponentPreview({
title: "B-roll window",
category: "Media/Footage",
description: "Supporting footage framed beside the main narrative.",
component: BRollWindow,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
corner: {
type: "select",
defaultValue: "bottom-right",
options: ["bottom-right", "bottom-left", "top-right", "top-left"],
},
at: {type: "number", defaultValue: 24, min: 0, max: 90},
until: {type: "number", defaultValue: 140, min: 30, max: 200},
label: {type: "text", defaultValue: "odori export launch"},
},
examples: [
{name: "Default", props: {children: <Main />}},
{name: "Top left", props: {children: <Main />, corner: "top-left"}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {BRollWindow} from "../components/b-roll-window/b-roll-window";
<Video>
<Scene id="b-roll-window" duration="6s">
<BRollWindow
corner="bottom-right"
at={24}
until={140}
label="odori export launch"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Media
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 45 frames · 1.5s
- Entrance
- 14 frames
- Exit
- 14 frames
- Reduced motion
- the window is present without sliding in
- Requires
- mono font
- Content limits
- label ≤ 48
Props
| Prop | Type | Default | Required |
|---|---|---|---|
childrenThe main narrative, filling the frame. | ReactNode | — | — |
mediaSupporting footage. A string is an image URL. | string | ReactNode | — | — |
cornerWhich corner the window sits in. | "bottom-right" | "bottom-left" | "top-right" | "top-left" | "bottom-right" | — |
atFrame it enters and frame it leaves. | number | 24 | — |
until | number | 140 | — |
label | string | — | — |
Related components
All componentsCanvas magnifierA magnifying lens over your content.Canvas stageFrame-driven canvas drawing with export parity.CarouselA paced media sequence with stable aspect handling.CRT terminalA retro terminal with curved glass and glowing text.Glare sweepA light reflection that sweeps across your content.Halftone printText revealed as a pattern of printed dots.