Dither videoPro
Live video, a stream or the camera dithered in a shader through the shared WebGL2 context, with every camera permission state designed and the loop paused offscreen.
On this page
ProA live clip dithered at 30 fps through the shared WebGL2 context, with method, cell size and a switch to your camera.
Installation
Usage
import { DitherVideo } from "@/components/ui/chatcn/pro/dither-video"
<DitherVideo camera label="Your camera, dithered" algorithm="bayer-8" />
<DitherVideo stream={call.localStream} label="You" pixelSize={3} levels={4} />Each frame is drawn down to one texel per dither cell (the GPU averages each cell, so nothing shimmers) and dithered in one draw. Ordered methods hold still between frames, so only real motion moves the pattern. Frames never leave the page.
Examples
States
ProThe camera prompt, waiting for permission, blocked, no camera, a missing file, paused, four tones and a negative.
Props
| Prop | Type | Notes |
|---|---|---|
src | string | A video file. Plays muted and loops. Cross-origin files need CORS headers |
stream | MediaStream | null | A call, a screen share or canvas.captureStream(). You stop its tracks |
camera | boolean | MediaTrackConstraints | Use the camera. The frame offers a button, so the prompt follows a click |
autoStart | boolean | Ask for the camera on mount, when permission was granted before |
label | string | Required. The accessible name of the picture |
mirror | boolean | Default on for the camera |
aspectRatio | string | number | Default the video's own, then 16:9 |
paused, defaultPaused, onPausedChange | Controlled or uncontrolled pause | |
pauseControl | boolean | The corner pause button. Default true |
labels | Partial<VideoEffectLabels> | Every string the frame shows, for translation |
onStatusChange | (status) => void | idle, requesting, loading, ready, denied, unavailable or error |
algorithm | "bayer-2" | "bayer-4" | "bayer-8" | "blue-noise" | Default bayer-8 |
pixelSize | number | CSS px per cell, snapped to device pixels. Default 2 |
levels | number | Tones, 2 to 8. Default 2 |
ink, paper | string | Tokens or CSS colors |
contrast, brightness, threshold, invert | Defaults 1.15, 0, 0.5, off | |
fps | number | Default 30 |
renderer | "auto" | "webgl" | "2d" | Force a path |
VideoEffectFrame and the useEffectVideo hook are exported too, for building your own video effect with the same source handling and states.
Keyboard
| Key | Action |
|---|---|
Tab | Reaches the camera button, a retry button and the pause button |
Enter, Space | Turns on the camera, retries, pauses or plays |
Accessibility
- The picture is named by your
labelthroughrole="img"; the canvas and the<video>that feeds it are hidden from screen readers. - The camera prompt, the waiting state and every error are real text with a button that fixes the problem: blocked access points to the site settings, a missing camera asks to connect one, a camera in use names the other app.
- The pause button sits in the corner of any frame that loops (WCAG 2.2.2) and is reachable by keyboard.
- Under forced colors the effect hides and the plain video shows. Under
prefers-contrast: moreink and paper snap to the text and canvas tokens.
Motion
- Frames flow only while the frame is on screen, the tab is visible and nobody paused it. A paused or offscreen file pauses its playback too, so nothing decodes in the background.
- Under reduced motion files and streams hold their first frame until the person presses play. A camera the person turned on runs: it is their own picture, not decoration.
- The first frame fades in over 200 ms; nothing else animates.