Underwater Ripple
Hold a scene under moving water — layered refraction that closes on a whole number of cycles
This filter re-reads the real pixels of the scene it wraps 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, 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 fallback is what keeps your composition renderable if that happens.
Installation
$ pnpm dlx shadcn@latest add @remocn/underwater-rippleUsage
UnderwaterRipple is a wrapper. Put a scene inside it and every pixel is read
through a moving water surface: the picture bends along the slope of the field,
and where the water bends hardest the colour channels separate a little, the way
light does through a real surface.
import { UnderwaterRipple } from "@/components/remocn/underwater-ripple";
export const MyScene = () => (
<UnderwaterRipple>
<Scene />
</UnderwaterRipple>
);The loop closes by construction
speed is not a rate, it is a count: how many complete cycles the water goes
through over the composition. Keep it a whole number and the last frame hands
back to the first with nothing to hide — put the section on repeat and there is
no seam to find.
That is also why the filter reads durationInFrames rather than seconds. Inside
a Sequence it picks up that sequence's length, so a ten-second ambient hold and
a two-second beat each close on their own.
A fractional speed still renders, it simply will not loop.
What to reach for it for
Surface out of the water
The arrival. The scene starts deep under a heavy ripple and settles into stillness over a second and a half — motion that resolves rather than motion that merely stops.
import { interpolate, useCurrentFrame } from "remotion";
import { UnderwaterRipple } from "@/components/remocn/underwater-ripple";
export const Opening = () => {
const frame = useCurrentFrame();
const amplitude = interpolate(frame, [0, 46], [34, 0], {
extrapolateRight: "clamp",
});
return (
<UnderwaterRipple amplitude={amplitude} speed={3}>
<Scene />
</UnderwaterRipple>
);
};The preview sinks back at the end so that it loops; a real opening would settle
at 0 and stay there. At amplitude={0} the scene passes through untouched, so
there is nothing to tear down afterwards.
Water behind, type in front
The more useful shape in practice. Wrap only the backdrop and leave the type outside the wrapper: the water moves, the words stay perfectly crisp. Refraction is the one thing in this pack you almost never want on your own copy.
import { AbsoluteFill } from "remotion";
import { UnderwaterRipple } from "@/components/remocn/underwater-ripple";
export const Scene = () => (
<AbsoluteFill>
<UnderwaterRipple amplitude={14} scale={0.8} speed={2}>
<PoolBackdrop />
</UnderwaterRipple>
<AbsoluteFill>{/* headline stays sharp out here */}</AbsoluteFill>
</AbsoluteFill>
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
amplitude | number | 7 | How far the water pushes the picture, in composition units. Read every frame, so interpolating it is how a scene surfaces. At 0 the scene passes through untouched. |
scale | number | 1 | Size of the wave structure. Low values give long swells, high values a tight chop. Changing it does not change how far pixels move — that stays with amplitude. |
speed | number | 2 | Complete cycles across the composition, not a rate. Whole numbers loop seamlessly; higher values are faster water on the same length of scene. |
dispersion | number | 1 | How far red and blue separate where the water bends hardest. Scales with the local slope, so flat water stays clean and the fringing appears only in the troughs and crests. |
children | React.ReactNode | — | The scene the filter wraps and re-reads every frame. |
Notes
The field is a closed-form sum of three travelling waves whose temporal phases are whole multiples of the base cycle, and the displacement is its exact analytic gradient rather than a sampled difference. Nothing is simulated forward from the previous frame, so any frame can be rendered in isolation and parallel workers cannot drift apart.
It never sweeps. The whole surface moves at once, so there is no front crossing the frame and no direction to the effect — which is what lets it sit on a scene for a minute without becoming an event.
The filter expects a full-bleed scene. It reads pixels from a displaced coordinate, so where the scene is transparent it has nothing to bend and the untouched original shows through underneath — put a background inside the wrapper rather than behind it.
In browsers without html-in-canvas the scene renders untouched. CSS cannot read a pixel from anywhere but where it is, and a wobbling blur would be a different effect wearing this one's name.