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.
On this page
Installation
Usage
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.
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.
Props
| Prop | Type | Notes |
|---|---|---|
src, alt | string | alt is required: the accessible name, announced once the image is in |
progress | number | null | Real job progress, 0 to 1. null means running with no numbers. Leave it out for timer mode |
revealed | boolean | Timer mode: false holds the first frame, true plays once the image loads. Default true |
duration | number | Timer length in ms. Default 1800 |
onRevealed | () => void | Once per image, when the plain image shows |
onStatusChange | (status: "loading" | "ready" | "revealing" | "revealed" | "error") => void | |
aspectRatio, width, height | Set the frame before the image loads, so nothing shifts | |
paper | string | The ground under the effect. Token or CSS color. Default var(--chatcn-bg) |
announcement | string | false | What the live region says once revealed. Default Image ready: <alt> |
errorText, retryLabel | Copy for the error state and its reload button | |
pixelSize | number | CSS px per dither cell, snapped to whole device pixels. Default 2 |
ink | string | Marks in the mono stages. Default var(--chatcn-fg) |
contrast | number | Default 1.1 |
maxPixels | number | Default 1.5 MP. Lower it for many small tiles |
renderer | "auto" | "webgl" | "2d" | Force a path |
Keyboard
| Key | Action |
|---|---|
Tab | Reaches 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 youraltis always there, and the frame isaria-busyuntil 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: activethe 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: morethe 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.