Skip to content
Sign in
EffectsDither videoMenu

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.

ProA live clip dithered at 30 fps through the shared WebGL2 context, with method, cell size and a switch to your camera.

Installation

$ pnpm dlx shadcn@latest add @chatcn-pro/dither-video

Usage

tsx
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

PropTypeNotes
srcstringA video file. Plays muted and loops. Cross-origin files need CORS headers
streamMediaStream | nullA call, a screen share or canvas.captureStream(). You stop its tracks
cameraboolean | MediaTrackConstraintsUse the camera. The frame offers a button, so the prompt follows a click
autoStartbooleanAsk for the camera on mount, when permission was granted before
labelstringRequired. The accessible name of the picture
mirrorbooleanDefault on for the camera
aspectRatiostring | numberDefault the video's own, then 16:9
paused, defaultPaused, onPausedChangeControlled or uncontrolled pause
pauseControlbooleanThe corner pause button. Default true
labelsPartial<VideoEffectLabels>Every string the frame shows, for translation
onStatusChange(status) => voididle, requesting, loading, ready, denied, unavailable or error
algorithm"bayer-2" | "bayer-4" | "bayer-8" | "blue-noise"Default bayer-8
pixelSizenumberCSS px per cell, snapped to device pixels. Default 2
levelsnumberTones, 2 to 8. Default 2
ink, paperstringTokens or CSS colors
contrast, brightness, threshold, invertDefaults 1.15, 0, 0.5, off
fpsnumberDefault 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

KeyAction
TabReaches the camera button, a retry button and the pause button
Enter, SpaceTurns on the camera, retries, pauses or plays

Accessibility

  • The picture is named by your label through role="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: more ink 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.