Skip to content
Sign in
EffectsDither revealMenu

Dither revealPro

The signature chatcn generation reveal: the image arrives through an ordered dither, a 1 bit threshold sweep out of the paper, then 4 and 8 tones, then full tone resolving into the color photo.

ProA portrait arriving through the dither reveal, driven by a scripted diffusion job or a timer.

Installation

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

Usage

tsx
import { DitherReveal } from "@/components/ui/chatcn/pro/dither-reveal"

<DitherReveal src={job.previewUrl} alt={job.prompt} progress={job.progress} aspectRatio="4:5" />

Feed progress from your job events (diffusion steps, bytes, percent). The reveal eases toward each report, never runs backwards, never sits at 99 and finishes when progress reaches 1. Without progress it plays on a timer once the image has loaded. To run a new job on the same frame, give the reveal a new key.

The tone steps are held, not blended, so each one reads as a step. Light areas take whichever of ink and paper is lighter, so the picture keeps its values in both themes. It draws through the shared effect context, so a feed of reveals never runs out of WebGL contexts, and falls back to canvas 2D.

Every reveal is built on two shared pieces you can use for your own: useReveal from @/hooks/use-chatcn-pro-reveal (the clock: progress driver or timer, offscreen pause, reduced motion) and RevealFrame from @/lib/chatcn-pro-reveal (the image, live region and error state).

Examples

Every reveal

All ten reveals on one timer. Replay all to compare them, or to record a clip.

ProAll ten reveals side by side on one timer, with one replay for all.

States

Waiting for the image, running at 40%, running with no numbers, held by revealed={false}, revealed, a missing file, a timer and right to left.

ProWaiting for the image, running at 40%, running with no numbers, held, revealed, a missing file, a timer and held at 70%.

Props

PropTypeNotes
src, altstringalt is required: the accessible name, announced once the image is in
progressnumber | nullReal job progress, 0 to 1. null means running with no numbers. Leave it out for timer mode
revealedbooleanTimer mode: false holds the first frame, true plays once the image loads. Default true
durationnumberTimer length in ms. Default 1800
onRevealed() => voidOnce per image, when the plain image shows
onStatusChange(status: "loading" | "ready" | "revealing" | "revealed" | "error") => void
aspectRatio, width, heightSet the frame before the image loads, so nothing shifts
paperstringThe ground under the effect. Token or CSS color. Default var(--chatcn-bg)
announcementstring | falseWhat the live region says once revealed. Default Image ready: <alt>
errorText, retryLabelCopy for the error state and its reload button
pixelSizenumberCSS px per dither cell, snapped to whole device pixels. Default 2
inkstringMarks in the mono stages. Default var(--chatcn-fg)
contrastnumberDefault 1.1
maxPixelsnumberDefault 1.5 MP. Lower it for many small tiles
renderer"auto" | "webgl" | "2d"Force a path

Keyboard

KeyAction
TabReaches the Reload image button in the error state. The reveal itself takes no focus

Accessibility

  • The effect layer is aria-hidden. The real <img> with your alt is always there, and the frame is aria-busy until the reveal ends.
  • A polite live region says Image ready: <alt> once, when the plain image shows. Progress ticks are never announced.
  • Under forced-colors: active the effect hides and the plain image shows with system colors.
  • A broken file shows what happened and a Reload image button that retries the download.

Motion

  • Four stages over 1.8 s: the 1 bit sweep (32%), 4 tones (14%), 8 tones (14%), then the color resolve (40%). Steps are held; only the resolve blends.
  • Under prefers-contrast: more the mono stages stay at 1 bit in pure ink and paper.
  • Under reduced motion one still frame holds while the job runs, then the image crossfades in over 200 ms. Nothing moves.
  • The timer only runs on screen in a visible tab; offscreen it waits where it was.