Grid pulse
A grid with a diagonal wave of light.
Customize
import {Scene, Video} from "odori";
import {GridPulse} from "../components/grid-pulse/grid-pulse";
<Video>
<Scene id="grid-pulse" duration="6s">
<GridPulse
variant="dots"
speed={1}
intensity={0.8}
/>
</Scene>
</Video>Installation
pnpm odori add component grid-pulsenpx odori add component grid-pulsebunx odori add component grid-pulseEdit the source in videos/components/grid-pulse/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/grid-pulse/grid-pulse.tsx
import {Fill, useBrand, useDesignScale, useFrame, useVideo} from "odori";
/**
* A quiet grid with one soft wave of emphasis crossing it diagonally.
*
* A static grid is furniture; a grid where every cell pulses is noise. One
* travelling wave is the amount of motion that reads as alive without asking
* for attention: each cell brightens as the wave passes its diagonal and
* settles back, and because the wave repeats along the diagonal there is
* always exactly one crest somewhere in frame.
*
* The lines variant swaps dots for hairlines and pulses whole rows and
* columns, which suits denser layouts where dots would fight with type.
*/
export type GridPulseProps = {
/** Dots at the intersections, or the lines between them. */
variant?: "dots" | "lines";
/** Cell spacing, in design pixels. */
gap?: number;
/** Grid colour. Defaults to the brand's border. */
color?: string;
/** How fast the wave travels the diagonal. */
speed?: number;
/** How far a cell brightens above the resting grid. */
intensity?: number;
};
export const GridPulse = ({variant = "dots", gap = 72, color, speed = 1, intensity = 0.8}: GridPulseProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {fps, width, height} = useVideo();
const ink = color ?? brand.colors.muted;
const spacing = gap * scale;
const columns = Math.ceil(width / spacing) + 1;
const rows = Math.ceil(height / spacing) + 1;
// The wave's position along the diagonal, in cells. Cosine of this minus a
// cell's own diagonal is that cell's moment in the pulse.
const travel = (frame / fps) * speed * 2.5;
const pulseAt = (diagonal: number) => Math.pow(0.5 + 0.5 * Math.cos((diagonal - travel) * 0.9), 6);
const resting = 0.14;
return (
<Fill style={{background: brand.colors.background}}>
<svg aria-hidden height={height} width={width} style={{display: "block"}}>
{variant === "dots"
? Array.from({length: columns * rows}, (_, index) => {
const column = index % columns;
const row = Math.floor(index / columns);
const pulse = pulseAt(column + row);
return (
<circle
key={index}
cx={column * spacing}
cy={row * spacing}
fill={ink}
opacity={resting + intensity * 0.5 * pulse}
r={(2.5 + 1.5 * pulse * intensity) * scale}
/>
);
})
: [
...Array.from({length: columns}, (_, column) => (
<line
key={`column-${column}`}
opacity={resting + intensity * 0.4 * pulseAt(column)}
stroke={ink}
strokeWidth={scale}
x1={column * spacing}
x2={column * spacing}
y1={0}
y2={height}
/>
)),
...Array.from({length: rows}, (_, row) => (
<line
key={`row-${row}`}
opacity={resting + intensity * 0.4 * pulseAt(row)}
stroke={ink}
strokeWidth={scale}
x1={0}
x2={width}
y1={row * spacing}
y2={row * spacing}
/>
)),
]}
</svg>
</Fill>
);
};
videos/components/grid-pulse/grid-pulse.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {GridPulse} from "./grid-pulse";
export default defineComponentPreview({
title: "Grid pulse",
category: "Backgrounds",
description: "A grid with a diagonal wave of light.",
component: GridPulse,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
variant: {type: "select", defaultValue: "dots", options: ["dots", "lines"]},
speed: {type: "number", defaultValue: 1, min: 0, max: 3, step: 0.1},
intensity: {type: "number", defaultValue: 0.8, min: 0, max: 1, step: 0.05},
},
examples: [
{name: "Brand", props: {}},
{name: "Lines", props: {variant: "lines", gap: 96}},
{name: "Fine", props: {gap: 48, intensity: 0.3, speed: 0.7}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {GridPulse} from "../components/grid-pulse/grid-pulse";
<Video>
<Scene id="grid-pulse" duration="6s">
<GridPulse
variant="dots"
speed={1}
intensity={0.8}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Backgrounds
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 30 frames · 1s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- set speed to 0 and the wave freezes into a still emphasis gradient across the grid
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
variantDots at the intersections, or the lines between them. | "dots" | "lines" | "dots" | — |
gapCell spacing, in design pixels. | number | 72 | — |
colorGrid colour. Defaults to the brand's border. | string | — | — |
speedHow fast the wave travels the diagonal. | number | 1 | — |
intensityHow far a cell brightens above the resting grid. | number | 0.8 | — |
Related components
All componentsAurora veilFlowing curtains of colored light.Bead fieldA fixed grid of dots that swell as two drifting ripples cross them.Deep swellOcean waves beneath a sunlit horizon.Dither fieldA moving gradient with pixelated shading.Dust motesSoft particles drifting across the frame.Gel washBlurred bands of color over a dark background.