Pixel wipe
A cover of solid blocks clearing one by one, a wipe that travels but crackles.
Customize
import {Scene, Video} from "odori";
import {PixelWipe} from "../components/pixel-wipe/pixel-wipe";
<Video>
<Scene id="pixel-wipe" duration="2s">
<PixelWipe
at={0}
durationInFrames={30}
block={64}
direction="right"
/>
</Scene>
</Video>Installation
pnpm odori add component pixel-wipenpx odori add component pixel-wipebunx odori add component pixel-wipeEdit the source in videos/components/pixel-wipe/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/pixel-wipe/pixel-wipe.tsx
import type {ReactNode} from "react";
import {Fill, random, useBrand, useDesignScale, useFrame, useVideo} from "odori";
export type PixelWipeProps = {
/** What is revealed as the blocks clear. */
children?: ReactNode;
/** Frame the wipe begins. */
at?: number;
/** How long the last block takes to clear. */
durationInFrames?: number;
/** Block size in design pixels. Capped so the cover stays cheap. */
block?: number;
/** Colour of the cover. Defaults to the brand background. */
color?: string;
/** Which way the wipe travels. */
direction?: "right" | "left" | "down" | "up";
};
/**
* A scene arriving through a seeded block dissolve.
*
* A cover of solid blocks sits over the content and each block disappears on
* its own frame. The order is a diagonal position plus seeded noise, so the
* wipe clearly travels — the eye can follow it — but the edge crackles instead
* of drawing a straight line, which is what separates this from a plain wipe
* with a jagged costume. The same seeds fire in every render worker, so the
* crackle is identical in the preview and the export.
*/
export const PixelWipe = ({
children,
at = 0,
durationInFrames = 30,
block = 64,
color,
direction = "right",
}: PixelWipeProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {width, height} = useVideo();
const plate = color ?? brand.colors.background;
/* The grid, capped: a tiny block size on a large canvas would mean thousands
of divs, and past a few hundred the crackle reads the same anyway. */
let side = Math.max(8, block * scale);
if ((width / side) * (height / side) > 600) side = Math.sqrt((width * height) / 600);
const cols = Math.ceil(width / side);
const rows = Math.ceil(height / side);
const done = frame >= at + durationInFrames;
const blocks: ReactNode[] = [];
if (!done) {
for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
/* Where this block sits along the travel, 0 first to clear. The cross
axis leans in at half weight, which slants the front into a diagonal
rather than a flat bar. */
const main =
direction === "right"
? col / Math.max(1, cols - 1)
: direction === "left"
? 1 - col / Math.max(1, cols - 1)
: direction === "down"
? row / Math.max(1, rows - 1)
: 1 - row / Math.max(1, rows - 1);
const cross =
direction === "right" || direction === "left"
? row / Math.max(1, rows - 1)
: col / Math.max(1, cols - 1);
const along = (main + cross * 0.5) / 1.5;
/* Two thirds order, one third crackle. Each block clears exactly once,
on a frame that is a pure function of its position and seed. */
const jitter = random(["pixel-wipe", row, col]);
const clearFrame = at + (along * 0.66 + jitter * 0.34) * durationInFrames;
if (frame >= clearFrame) continue;
blocks.push(
<div
key={`${row}-${col}`}
style={{
background: plate,
height: Math.ceil(side) + 1,
left: col * side,
position: "absolute",
top: row * side,
width: Math.ceil(side) + 1,
}}
/>,
);
}
}
}
return (
<Fill>
{children}
{blocks.length > 0 ? <Fill aria-hidden>{blocks}</Fill> : null}
</Fill>
);
};
videos/components/pixel-wipe/pixel-wipe.preview.tsx
import {useBrand} from "odori";
import {defineComponentPreview} from "odori/preview";
import {PixelWipe} from "./pixel-wipe";
const Behind = () => {
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: 96,
inset: 0,
justifyContent: "center",
letterSpacing: "-0.04em",
position: "absolute",
}}
>
Block by block.
</div>
);
};
export default defineComponentPreview({
title: "Pixel wipe",
category: "Motion/Transitions",
description: "A cover of solid blocks clearing one by one, a wipe that travels but crackles.",
component: PixelWipe,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
at: {type: "number", defaultValue: 0, min: 0, max: 60},
durationInFrames: {type: "number", defaultValue: 30, min: 8, max: 90},
block: {type: "number", defaultValue: 64, min: 24, max: 240, step: 8},
direction: {type: "select", defaultValue: "right", options: ["right", "left", "down", "up"]},
},
examples: [
{name: "Default", props: {children: <Behind />}},
{name: "Coarse, downward", props: {children: <Behind />, block: 160, direction: "down"}},
{name: "Fine and slow", props: {children: <Behind />, block: 32, durationInFrames: 60}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {PixelWipe} from "../components/pixel-wipe/pixel-wipe";
<Video>
<Scene id="pixel-wipe" duration="2s">
<PixelWipe
at={0}
durationInFrames={30}
block={64}
direction="right"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Motion
- Recommended duration
- 60 frames · 2s
- Minimum duration
- 30 frames · 1s
- Entrance
- 30 frames
- Exit
- 0 frames
- Reduced motion
- the cover is gone from the first frame
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
childrenWhat is revealed as the blocks clear. | ReactNode | — | — |
atFrame the wipe begins. | number | 0 | — |
durationInFramesHow long the last block takes to clear. | number | 30 | — |
blockBlock size in design pixels. Capped so the cover stays cheap. | number | 64 | — |
colorColour of the cover. Defaults to the brand background. | string | — | — |
directionWhich way the wipe travels. | "right" | "left" | "down" | "up" | "right" | — |
Related components
All componentsCamera stageAn animated camera that moves through your scene.Cell revealTiles slide into place to reveal your content.Charged borderA moving glow around a rounded frame.Dissolve revealA noise threshold sweeps across the frame, so content arrives as a torn edge.Gradient fieldA looping gradient in your brand colors.Halftone revealContent revealed through expanding dots.