03 / 10
Pixel Reveal
- TypeScript
- Next.js
- Canvas 2D
- sharp

Every product image on Dr.Martens arrives as a coarse mosaic and resolves to sharp, refining outward from a focal point computed from the picture itself. Built at Build in Amsterdam.
It is a branding device, not a loading indicator. It plays on a schedule rather than on network conditions, so it looks the same on a fast connection as on a slow one.
Constraints
At this traffic the effect mattered less than what it was not allowed to cost: no extra CDN
requests, next/image untouched, no fidelity loss against the prototype, and no trace at all on a
slow device, with reduced motion, in forced colours or without JavaScript.
Most approaches fail the first rule. CSS has no downsampling, SVG filters cannot refine per
region, stacked low-resolution variants are extra requests, and WebGL refuses cross-origin
textures without CORS. The prototype’s crossOrigin plus getImageData works, but the attribute
changes the cache key, so every returning visitor would download every image again.
The canvas
Drawing a cross-origin image into a 2D canvas without CORS is allowed. It taints the canvas, which forbids reading pixels back but not showing them, and nothing here reads pixels: each cell’s level is arithmetic, and drawing is clip plus draw. So there is no attribute, no cache change and no extra request.
The real image renders normally and is never hidden. A canvas over it at mosaic resolution, about
128×160 and 80 KB, is scaled up with image-rendering: pixelated. On the last tick every cell
turns transparent and the canvas is removed.
The image is sampled once into a pyramid of four or five tiny canvases, 8 to 128 pixels wide. Each cell’s level comes from its distance to the focal point plus seeded noise sampled at coarse steps, so refinement advances in chunky patches rather than single cells. Nearest neighbour only, with no easing or fades. Pace, seed and coarseness change on every play.
The focal point is the first of: a point set in the CMS, a contrast centroid computed on the server from a 32-pixel thumbnail, the same algorithm run in the browser for same-origin images, or a hash of the source.
A build takes 1.6 to 3.2 ms and a tick 0.1 to 0.3 ms at the median. A playing image holds about 300 KB of buffers for a second, and at rest there are no canvases at all.
Doing no harm
Low core count, low memory, save-data or a 2G connection prevent a play. A circuit breaker aborts
one after two consecutive ticks over 24 ms, and two strikes end it for the session. The real
<img> and its alt text stay in place throughout, and reduced motion and forced colours are
handled in CSS, so those visitors see the image at first paint.
The trade-off is deliberate: every first view of an image reads as pixelated for about 0.8 seconds. It covers the LCP element without delaying it. In testing, the covered hero image was still the LCP entry in every run.