About
A camera, a clip or a call dithered live at 30 fps on the GPU: Bayer or blue noise, any cell size and two colors. Frames never leave the page, and the loop stops when it scrolls away.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Dither Video to your project and fits it to your theme.
Code
How Dither Video is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Toggle } from "@base-ui/react/toggle"
import { ToggleGroup } from "@base-ui/react/toggle-group"
import { DitherVideo, OrderedDither } from "@/components/ui/dither-video"
/**
* Demo footage for the video effects without a video file or an image host: an inline photo
* filmed by a virtual camera (push in, drift) under a moving key light and a passing shadow,
* drawn to a canvas and turned into a real MediaStream with `captureStream()`. The same idea as
* the kit's own demo clip, a little faster so a five second preview shows the light travel.
*/
function drawScene(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
t: number,
focus: readonly [number, number],
) {
const { width: W, height: H } = ctx.canvas
const iw = image.naturalWidth
const ih = image.naturalHeight
const zoom = 1.08 + 0.06 * Math.sin(t * 0.45)
const scale = Math.max(W / iw, H / ih) * zoom
const dx = W / 2 - focus[0] * iw * scale + Math.sin(t * 0.37) * W * 0.04
const dy = H / 2 - focus[1] * ih * scale + Math.cos(t * 0.29) * H * 0.035
ctx.globalCompositeOperation = "source-over"
ctx.drawImage(
image,
Math.min(0, Math.max(W - iw * scale, dx)),
Math.min(0, Math.max(H - ih * scale, dy)),
iw * scale,
ih * scale,
)
const lx = W * (0.5 + 0.45 * Math.sin(t * 1.1))
const light = ctx.createRadialGradient(lx, H * 0.3, 0, lx, H * 0.3, W * 0.55)
light.addColorStop(0, "rgb(255 255 255 / 0.55)")
light.addColorStop(1, "rgb(255 255 255 / 0)")
ctx.globalCompositeOperation = "soft-light"
ctx.fillStyle = light
ctx.fillRect(0, 0, W, H)
const sx = W * (1.3 - ((t * 0.22) % 1.6))
const shade = ctx.createLinearGradient(sx - W * 0.35, 0, sx + W * 0.35, 0)
shade.addColorStop(0, "rgb(0 0 0 / 0)")
shade.addColorStop(0.5, "rgb(0 0 0 / 0.28)")
shade.addColorStop(1, "rgb(0 0 0 / 0)")
ctx.globalCompositeOperation = "multiply"
ctx.fillStyle = shade
ctx.fillRect(0, 0, W, H)
ctx.globalCompositeOperation = "source-over"
}
/** A live MediaStream of `src`, or null until it loads. Stops its tracks and loop on unmount. */
function useClip(
src: string,
{ width = 960, height = 540, focus = [0.5, 0.45] as [number, number] } = {},
) {
const [stream, setStream] = React.useState<MediaStream | null>(null)
const [fx, fy] = focus
React.useEffect(() => {
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")
if (!ctx || typeof canvas.captureStream !== "function") return
const image = new Image()
image.src = src
let raf = 0
let alive = true
let live: MediaStream | null = null
const start = performance.now()
const tick = () => {
if (!alive) return
drawScene(ctx, image, (performance.now() - start) / 1000 + 2, [fx, fy])
raf = requestAnimationFrame(tick)
}
image.onload = () => {
if (!alive) return
drawScene(ctx, image, 2, [fx, fy])
live = canvas.captureStream(30)
setStream(live)
tick()
}
return () => {
alive = false
cancelAnimationFrame(raf)
if (live) for (const track of live.getTracks()) track.stop()
setStream(null)
}
}, [src, width, height, fx, fy])
return stream
}
// picsum.photos id 659, 720 x 540, inline so the 21st preview needs no image host.
const husky = {
alt: "A husky in a harness at dusk, head turned toward the light",
width: 720,
height: 540,
src: "data:image/webp;base64,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",
}
const reducedMotion = () =>
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
/** How long a choice holds a tour before it carries on. */
const RESUME_MS = 5000
/**
* Steps through `values` on a timer, so a 21st preview (and its cover video) shows every option
* without a click. Picking a value holds it, and the tour carries on from there after
* `RESUME_MS`, so a click never leaves the preview still. Reduced motion never starts it.
*/
function useCycle<T>(values: readonly T[], ms: number) {
const [index, setIndex] = React.useState(0)
const [heldAt, setHeldAt] = React.useState(0)
React.useEffect(() => {
if (reducedMotion()) return
let id = 0
const start = () => {
id = window.setInterval(() => setIndex((n) => (n + 1) % values.length), ms)
}
const wait = window.setTimeout(start, heldAt ? RESUME_MS : 0)
return () => {
window.clearTimeout(wait)
window.clearInterval(id)
}
}, [heldAt, values.length, ms])
const pick = React.useCallback(
(value: T) => {
setHeldAt(Date.now())
setIndex(Math.max(0, values.indexOf(value)))
},
[values],
)
return [values[index] as T, pick] as const
}
const METHODS = [
{ value: "bayer-8", label: "Bayer 8" },
{ value: "bayer-4", label: "Bayer 4" },
{ value: "blue-noise", label: "Blue noise" },
] as const satisfies readonly { value: OrderedDither; label: string }[]
const METHOD_VALUES = METHODS.map((m) => m.value)
const FIT_CSS = "html,body{overflow:hidden}[data-fit-small] [data-fit-hide]{display:none!important}"
/**
* 21st shows a demo in a small frame: about 674 by 421 on a component page, 358 by 224 on a
* phone and 245 by 139 on a profile card. A demo taller than its frame gets scrollbars, which
* render white on dark pages. FitFrame fills the frame exactly instead.
*
* - "scale": the content shrinks until it fits, centered. For visuals such as orbs.
* - "crop": the content stays at full size from the top and fades out at the bottom. For text
* that streams in, where a changing scale would make the page breathe.
*
* `minWidth` lays out a wide demo (a table, a two column panel) at that width and scales it down
* to the frame, instead of letting it collapse to a narrow layout. The frame centers it, so it
* shrinks evenly on both sides.
*
* Anything marked `data-fit-hide` (state buttons, the chatcn.dev line) drops out in frames
* shorter than `hideBelow`, so a profile card shows the visual alone.
*/
function FitFrame({
children,
mode = "scale",
hideBelow = 360,
minWidth,
}: {
children: React.ReactNode
mode?: "scale" | "crop"
hideBelow?: number
minWidth?: string
}) {
const content = React.useRef<HTMLDivElement>(null)
const [cropped, setCropped] = React.useState(false)
React.useLayoutEffect(() => {
const el = content.current
if (!el) return
const measure = () => {
const vw = window.innerWidth
const vh = window.innerHeight
el.toggleAttribute("data-fit-small", vh < hideBelow)
// Layout sizes ignore the transform, so measuring never feeds back.
const w = Math.max(el.offsetWidth, el.scrollWidth)
const h = Math.max(el.offsetHeight, el.scrollHeight)
const scale = mode === "scale" ? Math.min(1, vw / w, vh / h) : 1
el.style.transform = scale < 1 ? `scale(${scale})` : ""
setCropped(mode === "crop" && h > vh + 1)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(el)
// Content that changes inside a fixed width box (a headline that cycles words) never resizes
// the box itself, so changes below it measure again on the next frame.
let frame = 0
const mutations = new MutationObserver(() => {
cancelAnimationFrame(frame)
frame = requestAnimationFrame(measure)
})
mutations.observe(el, { subtree: true, childList: true, characterData: true })
window.addEventListener("resize", measure)
return () => {
observer.disconnect()
mutations.disconnect()
cancelAnimationFrame(frame)
window.removeEventListener("resize", measure)
}
}, [mode, hideBelow])
const fade = "linear-gradient(to bottom, #000 calc(100% - 40px), transparent)"
return (
<div
className="flex w-full justify-center overflow-hidden"
style={{
height: "100dvh",
alignItems: mode === "crop" ? "flex-start" : "center",
maskImage: cropped ? fade : undefined,
WebkitMaskImage: cropped ? fade : undefined,
}}
>
<style>{FIT_CSS}</style>
<div
ref={content}
className="w-full shrink-0"
style={{ transformOrigin: mode === "crop" ? "top center" : "center", minWidth }}
>
{children}
</div>
</div>
)
}
/** A labeled segmented control on Base UI ToggleGroup. One value is always selected. */
function Segmented<T extends string>({
label,
value,
onValueChange,
options,
className,
}: {
label: string
value: T
onValueChange: (value: T) => void
options: readonly { value: T; label: string }[]
className?: string
}) {
return (
<div className={cn("flex min-w-0 flex-col gap-1.5", className)}>
<span className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-subtle) font-medium">{label}</span>
<ToggleGroup
value={[value]}
onValueChange={(next) => {
const picked = next[0] as T | undefined
if (picked) onValueChange(picked)
}}
aria-label={label}
className="bg-(color:--chatcn-fill) rounded-(--chatcn-radius-md) flex h-8 w-fit max-w-full items-center gap-0.5 overflow-x-auto p-0.5 pointer-coarse:h-11"
>
{options.map((option) => (
<Toggle
key={option.value}
value={option.value}
className={cn(
"chatcn-focus rounded-(--chatcn-radius-sm) text-[0.8125rem]/[1.25rem] inline-flex h-full shrink-0 items-center px-2.5 font-medium whitespace-nowrap",
"text-(color:--chatcn-fg-subtle) ease-(--chatcn-ease-standard) transition-[color,background-color,box-shadow] duration-150",
"hover:text-(color:--chatcn-fg) data-[pressed]:bg-(color:--chatcn-card) data-[pressed]:text-(color:--chatcn-fg) data-[pressed]:shadow-(--chatcn-shadow-xs)",
)}
>
{option.label}
</Toggle>
))}
</ToggleGroup>
</div>
)
}
const CELLS = [
{ value: "2", label: "2 px" },
{ value: "3", label: "3 px" },
{ value: "4", label: "4 px" },
] as const
/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
return (
<p data-fit-hide className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
{children}{" "}
<a
href={`https://chatcn.dev/docs/components/${slug}`}
target="_blank"
rel="noopener"
className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
>
chatcn.dev
</a>
</p>
)
}
/**
* A clip dithered live at 30 fps on the GPU, switching method every second and a half: Bayer 8,
* Bayer 4, then blue noise. Point it at a camera instead and frames never leave the page.
*/
export default function DitherVideoPreview() {
const clip = useClip(husky.src, { width: 960, height: 540, focus: [0.55, 0.4] })
const [method, setMethod] = useCycle<OrderedDither>(METHOD_VALUES, 1500)
const [cell, setCell] = React.useState<"2" | "3" | "4">("3")
return (
<FitFrame>
<div className="flex w-full flex-col items-center justify-center gap-6 p-6">
<div className="flex w-full max-w-2xl flex-col gap-5">
<DitherVideo
stream={clip}
label="A husky at dusk, filmed under a moving light and dithered"
algorithm={method}
pixelSize={Number(cell)}
aspectRatio="16:9"
className="rounded-(--chatcn-radius-lg) w-full"
/>
<div data-fit-hide className="flex flex-wrap gap-x-6 gap-y-4">
<Segmented label="Method" value={method} onValueChange={setMethod} options={METHODS} />
<Segmented label="Cell" value={cell} onValueChange={setCell} options={CELLS} />
</div>
</div>
<MoreOn slug="dither-video">A camera, a clip or a call, dithered on the GPU. On</MoreOn>
</div>
</FitFrame>
)
}







