This demo applies the Paper Shaders ImageDithering effect to a live video using WebGL2.
Run a local server:
npx serve .
# or
python3 -m http.server 8000Open http://localhost:3000 (or the port shown).
The ShaderMount class from @paper-design/shaders only supports HTMLImageElement or string URLs as texture sources. It doesn't handle HTMLVideoElement natively. To use video, we must use WebGL2 directly and manage the texture ourselves.
WebGL can sample directly from an HTMLVideoElement. The key is calling gl.texImage2D() with the video element every frame to upload the current video frame to the GPU:
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);This is a CPU→GPU transfer, but browsers heavily optimize this path for video.
The imageDitheringFragmentShader expects specific types:
| Uniform | Type | Notes |
|---|---|---|
u_resolution |
vec2 |
Canvas width/height |
u_pixelRatio |
float |
window.devicePixelRatio |
u_originX, u_originY |
float |
Origin point (0.5 = center) |
u_worldWidth, u_worldHeight |
float |
World dimensions |
u_fit |
float |
1 = contain, 2 = cover |
u_scale |
float |
Scale factor |
u_rotation |
float |
Rotation in radians |
u_offsetX, u_offsetY |
float |
Position offset |
u_image |
sampler2D |
Set via uniform1i with texture unit |
u_imageAspectRatio |
float |
Video width / height |
u_colorFront |
vec4 |
Primary dither color (RGBA 0-1) |
u_colorBack |
vec4 |
Background color (RGBA 0-1) |
u_colorHighlight |
vec4 |
Highlight color (RGBA 0-1) |
u_type |
float |
1=random, 2=2x2, 3=4x4, 4=8x8 |
u_pxSize |
float |
Dither pixel size |
u_colorSteps |
float |
Number of color steps |
u_originalColors |
bool |
Use original image colors |
u_inverted |
bool |
Invert the effect |
This approach is reasonably efficient. Main costs:
texImage2Dper frame - Unavoidable for video, but well-optimized by browsers- Fragment shader - Runs per-pixel
- Lower canvas resolution - e.g., 320×180 instead of 640×360
- Increase
u_pxSize- Larger dither pixels = fewer calculations - Use
requestVideoFrameCallback()- Only render when video has a new frame - Skip frames - Render every 2nd or 3rd frame if needed
├── index.html # Entry point
├── main.js # WebGL setup and render loop
├── styles.css # Styling
└── README.md # This file