Grid Wave
Send a wave across the frame that raises the picture into a grid of solid blocks and sets them back down carrying the next scene
This transition post-processes the real pixels of both scenes through the experimental html-in-canvas browser API. The preview above shows the true effect in Chrome 149+ with chrome://flags/#canvas-draw-element enabled (nested canvases need 152.0.7944.0+). Every other browser — Safari, Firefox, Chrome without the flag — falls back to a CSS approximation of the same cut, so the page and your video still play.
Rendering is supported through the CLI, Studio, Lambda and SSR with --gl=angle, or --gl=swangle on a machine without a GPU. Chrome may change or discontinue the API; the CSS fallback is what keeps your composition renderable if that happens.
Installation
$ pnpm dlx shadcn@latest add @remocn/grid-waveUsage
gridWave() is a Remotion transition presentation. Pass it to a
TransitionSeries.Transition between two sequences. A wave travels across the
frame; wherever it passes, the picture divides into cells and each one rises off
the surface as a solid block — a lit top face carrying its piece of the picture,
a shaded side below it where the block has come away, and a highlight around the
top edge. The block sets back down as the wave leaves, carrying the next scene.
Each block rises from its own footprint. The frame as a whole never moves: only the ring of blocks under the wave is off the surface at any moment, so the picture stays anchored while the wave travels through it.
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { gridWave } from "@/components/remocn/grid-wave";
export const MyVideo = () => (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
timing={linearTiming({ durationInFrames: 36 })}
presentation={gridWave()}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>
);direction decides how the wave travels. "ripple" spreads outward from the
centre like a drop landing on the surface; an axis sweeps it straight across.
Why this one needs the canvas
A grid-shaped mask over a crossfade is something CSS can do. A raised solid is not. Each block is projected rather than masked:
- The top face is re-projected. It is offset upward, leaned slightly away from the frame's centre for perspective, and magnified — so the pixel being written samples the scene somewhere else entirely, and the amount depends on how high that particular block has risen.
- The side face is extruded. The strip of footprint the block has vacated samples the top face's own bottom edge and repeats it downward under shading, which is what gives the block visible thickness instead of reading as a floating rectangle.
- Blocks occlude each other. A pixel is resolved against the neighbouring blocks and the highest one wins, so a raised block correctly covers the gap and the shoulder of the one behind it.
All three are reads at coordinates other than the pixel being written, which is exactly what a mask or a blend mode cannot express.
The entrance mirrors the exit
There is no separate treatment for the incoming scene, because it is the same block. A block goes up carrying the outgoing scene and comes down carrying the incoming one, and the hand-over sits at the crest — the moment the block is highest, largest and most lit, and the eye is least on its contents. Play the transition backwards and it is the same wave carrying the other scene, exactly.
Both ends are exact. The wave starts and finishes outside the frame, and every lift is scaled by the wave's own pulse, so at progress 0 nothing is raised and the outgoing frame is untouched; at progress 1 the same is true of the incoming one. Nothing fades at any point.
Determinism
The wave position and every block's lift, lean, magnification, shading and hand-over are closed-form functions of the transition's progress and the block's own coordinate — no simulation, no wall-clock time, nothing carried between frames. The same frame renders identically in every chunk of a parallel render.
Fallback
Where html-in-canvas is unavailable the incoming scene resolves through a short scale-and-fade settle. It keeps the timing; the tiles need the outgoing scene's pixels, so none of the props apply there.
Props
gridWave(props) accepts:
| Prop | Type | Default | Description |
|---|---|---|---|
tileSize | number | 90 | Size of one cell in pixels. Large cells read as panels, small ones as a fine mosaic. |
waveWidth | number | 0.16 | Thickness of the wave as a fraction of its travel. This is the pacing knob: a narrow wave is a sharp crest that snaps past each cell, a wide one has half the frame divided at once. |
lift | number | 2 | How high a block rises at the crest, which also sets how deep its side face reads. 0 keeps the frame flat, so the wave degrades to a grid-shaped wipe. |
gap | number | 0.12 | Space between blocks as a fraction of one cell. Some gap is what lets a raised block's side face be seen against its neighbours; 0 closes the grid into a continuous surface and the depth stops reading. |
tint | string | "#8fb4ff" | Colour that cells blend toward at the crest, as a hex string. Set it to a brand accent to carry colour through the cut. |
direction | "ripple" | "left" | "right" | "up" | "down" | "ripple" | How the wave travels. "ripple" spreads outward from the centre; an axis sweeps it straight across from that edge. |