Glare sweep
A light reflection that sweeps across your content.
Customize
import {Scene, Video} from "odori";
import {GlareSweep} from "../components/glare-sweep/glare-sweep";
<Video>
<Scene id="glare-sweep" duration="3s">
<GlareSweep
at={20}
sweepFrames={26}
angle={14}
width={180}
strength={0.35}
/>
</Scene>
</Video>Installation
pnpm odori add component glare-sweepnpx odori add component glare-sweepbunx odori add component glare-sweepEdit the source in videos/components/glare-sweep/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/glare-sweep/glare-sweep.tsx
import type {ReactNode} from "react";
import {Easing, Fill, interpolate, useDesignScale, useFrame} from "odori";
export type GlareSweepProps = {
/** The card or screenshot the sweep passes over. */
children?: ReactNode;
/** Frame the sweep begins. */
at?: number;
/** How long the travel takes, edge to edge. */
sweepFrames?: number;
/** Slant of the band in degrees. */
angle?: number;
/** Band width in design pixels. */
width?: number;
/** Peak opacity of the highlight. */
strength?: number;
};
/**
* One specular sweep across a card, and only one.
*
* The looping shine is a web-page habit; in a cut it keeps pulling the eye
* back to something that already made its point. This one is scripted: the
* band crosses once between `at` and `at + sweepFrames` and never returns, so
* it lands like light catching an edge rather than like an advert. Opacity
* eases in and out at the ends of the travel, because a band that pops into
* existence at the border reads as a glitch, not a glint.
*/
export const GlareSweep = ({
children,
at = 0,
sweepFrames = 26,
angle = 14,
width = 180,
strength = 0.35,
}: GlareSweepProps) => {
const frame = useFrame();
const scale = useDesignScale();
const band = Math.max(24, width * scale);
/* From fully off one edge to fully off the other. interpolate clamps, so
after the travel the band parks off-screen and stays there. */
const travel = interpolate(frame, [at, at + sweepFrames], [-30, 130], {easing: Easing.standard});
const fade = interpolate(
frame,
[at, at + sweepFrames * 0.2, at + sweepFrames * 0.8, at + sweepFrames],
[0, 1, 1, 0],
);
return (
<Fill style={{alignItems: "center", justifyContent: "center"}}>
<div style={{maxHeight: "100%", maxWidth: "100%", position: "relative"}}>
{children}
<div aria-hidden style={{inset: 0, overflow: "hidden", pointerEvents: "none", position: "absolute"}}>
<div
style={{
background: `linear-gradient(90deg, transparent, rgba(255, 255, 255, ${strength}), transparent)`,
bottom: "-25%",
left: `calc(${travel}% - ${band / 2}px)`,
opacity: fade,
position: "absolute",
top: "-25%",
transform: `skewX(${-angle}deg)`,
width: band,
}}
/>
</div>
</div>
</Fill>
);
};
videos/components/glare-sweep/glare-sweep.preview.tsx
import {useBrand} from "odori";
import {defineComponentPreview} from "odori/preview";
import {GlareSweep} from "./glare-sweep";
const Card = () => {
const brand = useBrand();
return (
<div
style={{
alignItems: "center",
background: brand.colors.surface,
border: `2px solid ${brand.colors.border}`,
borderRadius: 24,
color: brand.colors.foreground,
display: "flex",
fontFamily: brand.typography.sans,
fontSize: 64,
fontWeight: 600,
height: 480,
justifyContent: "center",
letterSpacing: "-0.03em",
overflow: "hidden",
width: 960,
}}
>
v2.0 — out today
</div>
);
};
export default defineComponentPreview({
title: "Glare sweep",
category: "Media/Treatments",
description: "A light reflection that sweeps across your content.",
component: GlareSweep,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
at: {type: "number", defaultValue: 20, min: 0, max: 80},
sweepFrames: {type: "number", defaultValue: 26, min: 8, max: 90},
angle: {type: "number", defaultValue: 14, min: -45, max: 45, step: 1},
width: {type: "number", defaultValue: 180, min: 40, max: 600, step: 10},
strength: {type: "number", defaultValue: 0.35, min: 0.05, max: 1, step: 0.05},
},
examples: [
{name: "Default", props: {children: <Card />, at: 20}},
{name: "Wide and soft", props: {children: <Card />, at: 20, width: 420, strength: 0.2, sweepFrames: 40}},
{name: "Hard glint", props: {children: <Card />, at: 30, width: 80, strength: 0.6, angle: 24, sweepFrames: 14}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {GlareSweep} from "../components/glare-sweep/glare-sweep";
<Video>
<Scene id="glare-sweep" duration="3s">
<GlareSweep
at={20}
sweepFrames={26}
angle={14}
width={180}
strength={0.35}
/>
</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
- 90 frames · 3s
- Minimum duration
- 40 frames · 1.33s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- set strength to 0 and the sweep never appears
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
childrenThe card or screenshot the sweep passes over. | ReactNode | — | — |
atFrame the sweep begins. | number | 0 | — |
sweepFramesHow long the travel takes, edge to edge. | number | 26 | — |
angleSlant of the band in degrees. | number | 14 | — |
widthBand width in design pixels. | number | 180 | — |
strengthPeak opacity of the highlight. | number | 0.35 | — |
Related components
All componentsB-roll windowSupporting footage framed beside the main narrative.Canvas 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.Halftone printText revealed as a pattern of printed dots.