Camera Lens
Put a scene behind real glass — resolution falling toward the corners, blooming highlights, edge fringing
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/camera-lensUsage
CameraLens is a wrapper. Put a scene inside it and it comes back as something
that was photographed: sharp in the middle and softening toward the corners,
highlights blooming out through the glass, colour fringing where the channels
stop agreeing at the edges, and light falling off gently at the rim.
import { CameraLens } from "@/components/remocn/camera-lens";
export const MyScene = () => (
<CameraLens>
<Scene />
</CameraLens>
);Why this is not a curved screen
The obvious way to build a lens is to bend the frame, but bending is what a tube
does — reach for that and you get crt-screen with the scanlines missing. What
separates glass from a display is light, not geometry.
A CRT is uniformly sharp across its whole surface and puts a mask over it. A lens
is sharp in one place and loses resolution everywhere else, and it scatters
bright light into the area around it. Those two things are what the defaults here
are made of; distortion exists but starts near zero, because modern glass barely
bends anything and leaning on it is exactly what makes a lens read as a monitor.
What to reach for it for
The pass that stops it looking rendered
The defaults are quiet on purpose — quiet enough to leave on for a whole demo. The preview ramps the whole lens on and back off, because the honest way to judge it is to see what disappears when you turn it off rather than what appears when you turn it on.
<CameraLens>
<Screencast />
</CameraLens>Hand it a camera move
The lens is optics, not motion — it never moves on its own. Put a slow push inside it and the two together read as a shot: the scene travels, and the travelling content passes through stationary glass, so different parts of the picture take their turn in the soft corners.
import { CameraLens } from "@/components/remocn/camera-lens";
import { Drift } from "@/components/remocn/drift";
export const MyScene = () => (
<CameraLens>
<Drift grow={0.06}>
<Scene />
</Drift>
</CameraLens>
);Order matters. Drift goes inside so the scene moves behind stationary glass. Put
the lens inside the drift instead and you would be scaling the optics themselves,
which is a lens breathing rather than a camera pushing in.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
softness | number | 0.5 | How much resolution the corners lose. The blur radius grows with distance from centre, so the middle of the frame stays fully sharp — this is the single strongest signal that a picture came through glass. |
bloom | number | 0.5 | How far bright areas scatter into what surrounds them. Only the highlights bloom, so dark UI stays clean and a white panel glows at its edges. |
aberration | number | 0.5 | How far the colour channels stop agreeing. Scales with radius, so the centre stays neutral and the fringing lives at the edges where a real lens puts it. |
vignette | number | 0.5 | Light falloff toward the rim, on the cos⁴ curve real optics follow. Smooth rather than drawn, so it reads as the lens rather than as a dark ring. |
distortion | number | 0.05 | Barrel bend. Near zero by default because modern glass barely bends anything, and because leaning on it is what makes a lens start reading as a CRT. Raise it for a wide-angle look or during a punch. |
children | React.ReactNode | — | The scene the filter wraps and re-reads every frame. |
Notes
Nothing animates. The lens is fixed optics re-reading the scene every frame, so a still scene is pixel-identical frame to frame and any movement belongs to what you put inside the wrapper.
This is the heaviest shader in the pack so far — the corner blur and the bloom
are both multi-tap gathers, and the aberration runs the blur three times. It is
comfortable on a GPU; on a software renderer (--gl=swangle) expect it to be
noticeably slower than the rest of the category.
The filter expects a full-bleed scene. It reads pixels from displaced coordinates, so where the scene is transparent it has nothing to gather — put a background inside the wrapper rather than behind it.
In browsers without html-in-canvas the scene keeps a bloom approximation and the vignette but not the per-pixel corner softness. CSS has one blur radius for a whole element, and a radius that varies across the frame is precisely the part that cannot be faked.