webdevpuneet.com

Canvas Pixelate Image Reveal — Free Scroll/Hover Sharpen Effect

Canvas Pixelate Image Reveal · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<section class="pxr-wrap">
  <span class="pxr-tag">canvas 2d · intersectionobserver</span>
  <h1>Pixelate reveal</h1>
  <p>Scroll the card into view, or hover it, to sharpen from a tiny mosaic to full detail.</p>

  <div class="pxr-grid">
    <div class="pxr-card" data-seed="1">
      <canvas class="pxr-canvas"></canvas>
      <span class="pxr-label">Aurora</span>
    </div>
    <div class="pxr-card" data-seed="2">
      <canvas class="pxr-canvas"></canvas>
      <span class="pxr-label">Ember</span>
    </div>
    <div class="pxr-card" data-seed="3">
      <canvas class="pxr-canvas"></canvas>
      <span class="pxr-label">Tidepool</span>
    </div>
  </div>
</section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Nearest-neighbor upscale
imageSmoothingEnabled = false produces crisp pixel blocks.
Fresh downsample per frame
Always samples the untouched full-detail source.
Eased resolution steps
A spring-like tween, not a hard resolution jump.
Dual trigger, one code path
Scroll-into-view and hover call the same reveal function.
Once-per-card auto reveal
A WeakSet ensures the scroll trigger fires only once.
Re-pixelates on mouse leave
Hover is fully reversible, not one-directional.
Procedural source art
No external image needed — gradient plus soft blooms.
Touch-friendly
touchstart also triggers the reveal on mobile.

About this UI Snippet

Canvas Pixelate Image Reveal — From Mosaic to Full Detail

Screenshot of the Canvas Pixelate Image Reveal snippet rendered live

The pixelate reveal is the "developing photograph" effect: a card starts as a chunky, low-resolution mosaic and sharpens smoothly into full detail as it scrolls into view or is hovered. The whole trick lives in two canvas properties — drawing at a tiny resolution, then reading that tiny canvas back at full size with imageSmoothingEnabled = false.

Why downscale-then-upscale produces pixel blocks

drawAtResolution(cells) first draws the full-detail source image onto a tiny offscreen canvas only cells pixels wide — at cells = 6, an entire 320px-wide card is represented by six columns of pixels. That tiny canvas is then drawn back onto the visible canvas at full size. With ctx.imageSmoothingEnabled = false on the destination context, the browser's own scaling algorithm switches from bilinear interpolation to nearest-neighbor, so each of those six source pixels becomes one crisp, hard-edged block rather than a blurry smear. Toggling that single boolean is the entire difference between a "pixelated" look and a "blurry" one.

A source that's never redrawn small

The full-detail image is painted once, at native resolution, onto its own offscreen src canvas via paintSource() (a gradient plus a few soft circular blooms, since there's no external image to load). Every reveal frame downsamples fresh from that same source — it's never progressively degraded by repeatedly scaling an already-scaled canvas, which is what keeps the "full detail" end state actually sharp instead of soft after a few reveal cycles.

Easing resolution instead of jumping to it

Rather than snapping straight from 6 cells to 96, animate() eases current toward a target cell count with current += (target - current) * 0.12 every frame — the same simple spring-like approach used for numeric tweens throughout this library. Because cells is rounded before each redraw, the visible result is a smooth, discrete step-up in resolution rather than a single hard cut, which reads as "sharpening" instead of "swapping images."

Two triggers, one animation function

Both card.addEventListener('mouseenter', reveal) and an IntersectionObserver calling the same reveal() on first scroll-into-view drive the identical animation path — there's no duplicated logic for "reveal by hover" versus "reveal by scroll." The observer uses a WeakSet to reveal each card only once on its first appearance, while hover continues to work afterward via mouseleave re-pixelating and mouseenter re-sharpening.

Extending it

Swap paintSource for a real ctx.drawImage(yourImg, 0, 0, W, H) once an image has loaded; tie target's cell count to scroll progress instead of a boolean for a scrubbable reveal; or pair the technique with image blur-up for a hybrid loading placeholder, or scroll reveal grid for the surrounding entrance choreography.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThree cards render heavily pixelated on load.
  2. 2
    Scroll them into viewEach sharpens once automatically via IntersectionObserver.
  3. 3
    Hover a cardIt sharpens further; move away and it re-pixelates.
  4. 4
    Watch the easingResolution steps up smoothly, not in one jump.
  5. 5
    Swap the sourceReplace paintSource with ctx.drawImage(yourImg, 0, 0, W, H).
  6. 6
    Tune the rangeChange MIN_CELLS and MAX_CELLS for a coarser or finer effect.

Real-world uses

Common Use Cases

Portfolio and gallery grids
Pair with a scroll reveal grid so images sharpen from chunky blocks as each row scrolls into view.
Product image reveals
Sharpen a product shot on scroll, upscaling a tiny canvas with imageSmoothingEnabled = false for crisp, deliberate pixel blocks.
Stylised loading placeholders
Provide a bolder alternative to an image blur-up, stepping resolution up with an eased spring-like tween.
Project cards with intrigue
Add mystery before a case study opens, resampling the untouched full-detail source on every frame for a clean result.
Developing-photo hover effects
Sharpen on hover for a tactile editorial layout, as scroll and hover both call the same reveal function.
Related: Canvas Snow Overlay
See the Canvas Snow Overlay for a related animations pattern worth pairing with this one.
Related: Status Icon Morph — Spinner to Check/Cross
See the Status Icon Morph — Spinner to Check/Cross for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The source image is first drawn onto a tiny offscreen canvas only a handful of pixels wide, then that tiny canvas is drawn back onto the visible canvas at full size with the destination context's imageSmoothingEnabled set to false. That single property switches the browser's scaling from smooth bilinear interpolation to nearest-neighbor, so each source pixel becomes one crisp, hard-edged block instead of a blurry gradient — which is the difference between "pixelated" and simply "blurry."

Keeping a single untouched, full-detail source canvas means every reveal frame downsamples fresh from that original rather than repeatedly scaling an already-scaled canvas. If the code instead progressively resized the same visible canvas in place, quality would degrade with each cycle and the "full detail" end state would end up soft after a few reveals; sampling from a pristine source avoids that entirely.

animate() moves a current cell-count value toward a target value by a fixed fraction of the remaining distance each frame (current += (target - current) * 0.12), the same simple spring-like tween used elsewhere in this library. Because that value is rounded and redrawn every frame, the card visibly steps up through several intermediate pixelation levels, which reads as "sharpening" rather than a single abrupt image swap.

Both triggers call the exact same reveal() function, which just changes the target cell count and kicks off the shared animate() loop if it is not already running. The IntersectionObserver additionally tracks which cards have already been revealed in a WeakSet so the scroll trigger only fires once per card, while mouseenter and mouseleave continue to call reveal() and hide() freely afterward for a fully reversible hover interaction.

Yes — replace the body of paintSource with sctx.drawImage(yourLoadedImageElement, 0, 0, W, H) once the image has finished loading (listen for its load event, or use an already-decoded Image object). Everything downstream, including the downscale/upscale pixelation and the scroll/hover triggers, works unchanged because it only ever reads pixels from the src offscreen canvas.