Tape static
Falling pixels that loop across the frame.
Customize
import {Scene, Video} from "odori";
import {TapeStatic} from "../components/tape-static/tape-static";
<Video>
<Scene id="tape-static" duration="6s">
<TapeStatic
count={70}
size={10}
speed={1}
/>
</Scene>
</Video>Installation
pnpm odori add component tape-staticnpx odori add component tape-staticbunx odori add component tape-staticEdit the source in videos/components/tape-static/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/tape-static/tape-static.tsx
import {Fill, random, randomBetween, useBrand, useDesignScale, useFrame, useVideo} from "odori";
/**
* Chunky seeded pixels falling slowly, each on its own sway.
*
* The squares are deliberately square — snapped to whole pixels, no radius, no
* blur — because the charm is the crunch. A flake's depth seed drives its
* size, its opacity, and its fall rate together, so the small dim flakes are
* also the slow ones and the field reads as deep without parallax layers.
*
* The fall wraps by modular arithmetic over the frame height plus a margin:
* a flake leaves the bottom edge fully before re-entering above the top, so
* the snow never ends and never pops. Everything is a function of the frame
* and the flake's seeds, so every machine renders the same storm.
*/
export type TapeStaticProps = {
/** How many flakes. */
count?: number;
/** Side of the largest flake, in design pixels. */
size?: number;
/** Fall speed multiplier. 1 is a lazy drift. */
speed?: number;
/** Flake colour. Defaults to the brand's muted tone. */
color?: string;
/** Opacity of the nearest flake. */
opacity?: number;
};
export const TapeStatic = ({count = 70, size = 10, speed = 1, color, opacity = 0.7}: TapeStaticProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {fps, width, height} = useVideo();
const ink = color ?? brand.colors.muted;
const t = frame / fps;
return (
<Fill style={{background: brand.colors.background, overflow: "hidden"}}>
{Array.from({length: count}, (_, index) => {
const depth = randomBetween(`flake-${index}-depth`, 0.45, 1);
const side = Math.max(2, Math.round(size * depth * scale));
// The wrap span covers the frame plus one flake above and below, so
// the modulo seam sits entirely off screen.
const span = height + side * 2;
const start = random(`flake-${index}-y`) * span;
const fall = randomBetween(`flake-${index}-fall`, 28, 55) * speed * depth * scale;
const y = ((start + t * fall) % span) - side;
const homeX = random(`flake-${index}-x`) * width;
const phase = random(`flake-${index}-phase`) * Math.PI * 2;
const swayRate = randomBetween(`flake-${index}-rate`, 0.08, 0.22) * Math.PI * 2;
const swayAmp = randomBetween(`flake-${index}-sway`, 8, 26) * scale;
const x = homeX + Math.sin(t * swayRate + phase) * swayAmp;
return (
<div
key={index}
aria-hidden
style={{
background: ink,
height: side,
left: 0,
opacity: opacity * depth,
position: "absolute",
top: 0,
transform: `translate(${x}px, ${y}px)`,
width: side,
}}
/>
);
})}
</Fill>
);
};
videos/components/tape-static/tape-static.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {TapeStatic} from "./tape-static";
export default defineComponentPreview({
title: "Tape static",
category: "Backgrounds",
description: "Falling pixels that loop across the frame.",
component: TapeStatic,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
count: {type: "number", defaultValue: 70, min: 10, max: 240, step: 10},
size: {type: "number", defaultValue: 10, min: 2, max: 32, step: 1},
speed: {type: "number", defaultValue: 1, min: 0, max: 4, step: 0.25},
},
examples: [
{name: "Brand", props: {}},
{name: "Blizzard", props: {count: 200, size: 6, speed: 2.5, opacity: 0.55}},
{name: "Confetti drift", props: {color: "#ff9a3d", count: 40, size: 14, speed: 0.6}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {TapeStatic} from "../components/tape-static/tape-static";
<Video>
<Scene id="tape-static" duration="6s">
<TapeStatic
count={70}
size={10}
speed={1}
/>
</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 every flake holds its seeded position
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
countHow many flakes. | number | 70 | — |
sizeSide of the largest flake, in design pixels. | number | 10 | — |
speedFall speed multiplier. 1 is a lazy drift. | number | 1 | — |
colorFlake colour. Defaults to the brand's muted tone. | string | — | — |
opacityOpacity of the nearest flake. | number | 0.7 | — |
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.