---
name: pixelated-reveal
description: Use when you want "Retro, techy, playful" - Transitions from pixelated blocks into a clean image.
---

# Pixelated Reveal

> **Category:** Image / Media  -  **Personality:** Retro, techy, playful
>
> **Best use:** Gaming, AI image tools, portfolios
>
> **Ratings:** Professional 3/5 - Casual 4/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 3/5

## What it is

Pixelated Reveal resolves an image from a handful of chunky colour blocks into the full-resolution picture — the way an AI image generator (or an old dial-up JPEG) appears to "render in." It runs entirely on a `<canvas>`: the source image is drawn into a tiny buffer and scaled back up with image smoothing turned off, then the buffer is grown over time so the blocks shrink down to single pixels. It reads retro, techy and a little playful, which makes it ideal for game UIs, AI image tools and portfolio reveals.

## Dependencies / CDN

None - vanilla JS (Canvas 2D API). The demo loads two display fonts; optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
```

## HTML

A framed canvas plus a replay button. The image is **not** an `<img>` — it's loaded in JS and painted onto the canvas:

```html
<div class="pr-cwrap" id="wrap">
  <canvas id="px" role="img" aria-label="AI-generated render: Alpine Valley"></canvas>
</div>
<button id="gen" type="button">Generate</button>
```

## CSS

The canvas just needs to fill a fixed-ratio frame. `image-rendering:pixelated` keeps the blocks crisp if the backing store and the displayed size ever differ:

```css
.pr-cwrap{position:relative; aspect-ratio:16/10; border-radius:14px;
  overflow:hidden; background:#060410}
.pr-cwrap canvas{position:absolute; inset:0; width:100%; height:100%;
  image-rendering:pixelated}
```

## JavaScript

The whole effect is the two `drawImage()` calls in `drawBlock()`; everything else just animates the block size from big to `1`:

```js
var canvas = document.getElementById('px'),
    wrap   = document.getElementById('wrap'),
    ctx    = canvas.getContext('2d'),
    reduce = matchMedia('(prefers-reduced-motion: reduce)').matches,
    dpr    = Math.min(window.devicePixelRatio || 1, 2),
    raf = 0, img = new Image();

function sizeCanvas(){               // backing store = displayed size x dpr (no double resample)
  var r = wrap.getBoundingClientRect();
  canvas.width  = Math.round(r.width  * dpr);
  canvas.height = Math.round(r.height * dpr);
}
function cover(){                    // cover-fit crop so the image fills the frame, no stretch
  var cw = canvas.width, ch = canvas.height, ir = img.width/img.height, cr = cw/ch;
  if (ir > cr){ var sh = img.height, sw = sh*cr; return [(img.width-sw)/2, 0, sw, sh]; }
  var sw2 = img.width, sh2 = sw2/cr; return [0, (img.height-sh2)/2, sw2, sh2];
}
function drawBlock(block){
  var cw = canvas.width, ch = canvas.height, c = cover(),
      w = Math.max(1, Math.ceil(cw/block)),   // tiny buffer size for this block size
      h = Math.max(1, Math.ceil(ch/block));
  ctx.imageSmoothingEnabled = false;          // nearest-neighbour => hard square pixels
  ctx.drawImage(img, c[0], c[1], c[2], c[3], 0, 0, w, h); // 1) shrink into top-left buffer
  ctx.drawImage(canvas, 0, 0, w, h, 0, 0, cw, ch);        // 2) blow that buffer back up
}
function reveal(){
  cancelAnimationFrame(raf); sizeCanvas();
  var maxBlock = Math.max(6, Math.round(canvas.width / 6)); // ~6 chunky blocks across to start
  if (reduce){ drawBlock(1); return; }                      // reduced motion => sharp at once
  var dur = 2600, t0 = performance.now(), last = -1;
  (function frame(now){
    var t = Math.min(1, (now - t0) / dur),
        e = t*t*(3 - 2*t),                                  // smoothstep ease
        block = Math.max(1, Math.round(Math.pow(maxBlock, 1 - e))); // geometric decay -> 1px
    if (block !== last){ drawBlock(block); last = block; }  // only repaint when the block changes
    if (t < 1) raf = requestAnimationFrame(frame);
  })(performance.now());
}
img.onload = reveal;                                        // auto-play once it loads
img.src = '/front-end-design-effects-catalog/assets/img/landscape-mountain.jpg';
document.getElementById('gen').addEventListener('click', reveal); // replay / "regenerate"
```

## How it works

- **Downscale then upscale with smoothing off.** Drawing the image into a `w x h` buffer throws away detail; drawing that buffer back across the full canvas with `imageSmoothingEnabled = false` uses nearest-neighbour sampling, so each source texel becomes a hard square — the pixel blocks.
- **Block size drives everything.** `buffer = ceil(canvasSize / block)`. A big `block` -> a tiny buffer -> giant blocks. `block = 1` -> buffer equals the canvas -> the full sharp image.
- **Geometric decay (`maxBlock^(1-e)`)** spends equal time at each "resolution doubling" (48 -> 24 -> 12 -> 6 -> 3 -> 1), which reads as a satisfying, computational resolve rather than a linear blur.
- **`dpr`-matched backing store** means the canvas's pixels map 1:1 to the screen, so the final frame is crisp on retina *and* the blocks stay hard-edged — no second, blurry browser resample.
- **Self-copy is the trick.** The second `drawImage` reads the canvas back onto itself; the small buffer lives in the top-left corner and gets stretched over the whole surface every frame.

## Customization

- **Block count / chunkiness:** change the `/ 6` in `maxBlock` (smaller divisor = bigger starting blocks).
- **Speed & feel:** tune `dur`, or swap the `smoothstep` for linear/ease-out. Drop the `Math.pow` for a linear `maxBlock*(1-e)` if you want even, mechanical steps.
- **Trigger:** auto-play on load (shown), on a `Replay`/`Generate` button (shown), on `IntersectionObserver` (reveal when scrolled into view), or on hover.
- **Theme:** layer scanlines, a moving scan-line, a CRT vignette or a colour tint over the canvas (all pure CSS overlays) to push the retro/HUD vibe — that's what the live demo's "pixelforge" console does.
- **Reverse it:** run the loop from `1` up to `maxBlock` to *pixelate out* (good for transitions between images).

## Accessibility & performance

- **Reduced motion:** check `matchMedia('(prefers-reduced-motion: reduce)')` in JS and paint the sharp image once (`drawBlock(1)`) instead of running the loop — CSS alone can't stop a `requestAnimationFrame` animation.
- **Semantics:** give the canvas `role="img"` and an `aria-label` describing the picture, since the pixels are invisible to assistive tech.
- **Cost is low:** two `drawImage` calls per frame, and we skip the repaint entirely when the integer block size hasn't changed between frames.
- **Same-origin images.** Painting a cross-origin image without proper CORS headers *taints* the canvas; the draw still works here, but you lose `getImageData`/export. Keep assets same-origin (or send `Access-Control-Allow-Origin` + set `img.crossOrigin`).

## Gotchas

- **`imageSmoothingEnabled` resets to `true` whenever you assign `canvas.width`/`height`.** Set it inside the draw (as above), not once at startup, or your "pixels" come out blurry after any resize.
- **Match the backing store to the displayed size x dpr.** If the canvas's intrinsic `width`/`height` don't match its CSS box, the browser does a *second* resample on top of yours and the blocks soften — `image-rendering:pixelated` is the safety net.
- **`Math.ceil` the buffer size,** not floor: `floor` can drop the last partial row/column and leave a thin gap on the right/bottom edge.
- **Wait for `img.onload`** before the first draw — drawing a not-yet-decoded image paints nothing.
- **Size from a real layout box.** Read `getBoundingClientRect()` after the element is in the DOM and has a height (here an `aspect-ratio` frame guarantees one) or the canvas comes out 0x0.
