Skip to main content
assetsforwordpress

Mosaic Image Reveal

Mosaic Image Reveal breaks your image into a grid of tiles that animate into place, scaling, fading, and optionally rotating in 3D, powered by GSAP 3 and ScrollTrigger.

Everything is painted on a single <canvas>, so it stays feather-light on the DOM: no extra DIVs, no per-tile elements, just one node covering the image. The original <img> remains untouched in your markup (hidden only during the reveal), keeping the page fully semantic and SEO-friendly.

The canvas mirrors the image’s rendered box at any size, so the mosaic re-tessellates on resize instead of cropping. And it respects accessibility: with prefers-reduced-motion: reduce, no canvas is created and no animation plays, the plain image simply displays as-is.

A detailed illustration of a large futuristic robot with traditional Chinese elements, surrounded by people in traditional attire and modern technology.
A detailed illustration of large, futuristic mecha robots with soldiers standing in front, showcasing advanced technology and cyberpunk aesthetics.
A detailed illustration of a futuristic robot with a cyclist, blending technology and urban life, with Asian-inspired design elements.
A detailed illustration of a futuristic robot with a cyclist, blending technology and urban life, with Asian-inspired design elements.

Instructions

Open the JS script in the Supa Code Block element and edit the options at the end of the script. Each option is explained below:

Grid layout

cols — default: 8 The number of tiles the image is divided into horizontally. The whole grid is cols × rows tiles, so with the default you get 40 tiles. Values are rounded, and anything below 1 is clamped to 1. Higher numbers = smaller tiles = more detailed mosaic, but also more drawing work per frame.

rows — default: 5 Same idea, but vertically — how many tiles stack from top to bottom.

Reveal order

order — default: "center" Controls which tile starts animating first — i.e., the direction the reveal sweeps across the image. Passed straight to GSAP's stagger.from option. Accepted values:

  • "center" — tiles in the middle of the image animate first, spreading outward (ripple from the middle).
  • "edges" — the opposite: border tiles go first, converging toward the center.
  • "random" — tiles fire in a random order, giving a scattered/dissolve feel.
  • "start" — begins at the first tile (top-left) and proceeds in reading order, left-to-right, top-to-bottom.
  • "end" — begins at the last tile (bottom-right) and runs in reverse reading order.

The actual spacing between tiles is controlled by stagger.

Transform start values (each animates to identity)

scaleFrom — default: 0.4 Each tile's starting size, as a multiplier. The tile animates from this value to 1 (full size). Range 0–2:

  • 0 — tiles start as invisible dots growing out.
  • 0.4 (default) — tiles start at 40% size and scale up.
  • 1 — no scaling at all; tiles are full-size from the start.
  • > 1 — tiles start oversized and shrink into place.

Note: a value ≤ 1 (with no rotation) also enables a performance optimization — tiles never paint outside their own grid cell, so only changed cells get repainted.

opacityFrom — default: 0 Each tile's starting transparency, animating to fully opaque (1). Range 0–1. 0 means tiles fade in from nothing; 1 means they're fully visible the whole time (no fade).

rotateXFrom — default: 0 Starting rotation around the X axis, in degrees, animating to 0. Positive values tip the image so the bottom swings toward the viewer. Crucially, the pivot is the center of the whole image, not each tile — so the entire mosaic rotates as one coherent 3D plane while the tiles individually fly in. Any non-zero value here (or below) switches tiles to the more expensive 3D mesh rendering path.

rotateYFrom — default: 0 Starting rotation around the Y axis, in degrees, animating to 0. Positive values turn the image so the right side swings away from the viewer. Same center-of-image pivot behavior as rotateXFrom.

perspective — default: 900 The camera distance (in px) used for the 3D projection, only when a rotation is set. Smaller values exaggerate the 3D effect (closer camera, more distortion); larger values flatten it toward a subtle tilt. Internally clamped to a minimum of 50 so the projection can't blow up. Ignored entirely if both rotation values are 0.

Timing

duration — default: 0.5 How long each individual tile's tween takes, in seconds. This is per tile, not the whole animation — total time depends on this plus the stagger.

stagger — default: 0.025 The delay, in seconds, between consecutive tiles starting their animation. With 40 tiles and the default, the last tile begins a full second after the first. Set to 0 and all tiles animate simultaneously.

ease — default: "power3.out" Any GSAP easing name. "power3.out" starts fast and decelerates smoothly into the final position — a natural "settle" feel. You could swap in "power4.out", "elastic.out", "back.out(1.7)", "none" (linear), etc.

Triggers

trigger — default: "click" How the reveal gets started. Two modes:

  • "click" — the canvas is made clickable (cursor becomes pointer, and it's given role="button" + tabindex="0" so it's keyboard-operable via Enter/Space). Each click replays the animation.
  • "viewport" — uses the ScrollTrigger plugin to fire the animation automatically when the container scrolls into view, using the start, end, once, scrub, and scroller options below. If the ScrollTrigger plugin isn't loaded, it prints a warning and falls back to click mode.

start — default: "top 75%" (viewport mode only) A ScrollTrigger start position. "top 75%" means: trigger when the top of the container reaches 75% of the way down the viewport — i.e., shortly after the image becomes visible while scrolling.

end — default: "bottom center" (viewport mode only) A ScrollTrigger end position. Mostly relevant for scrub mode (below), where it defines the scroll distance over which the animation progress is mapped. "bottom center" means the animation completes when the bottom of the container reaches the vertical center of the viewport.

once — default: true (viewport mode, non-scrub only) When true, the ScrollTrigger fires a single time and then kills itself — the reveal plays once and stays done. When false, the trigger re-fires every time the element re-enters the start position (replaying the animation). Irrelevant in scrub mode, since scrubbing is continuous.

scrub — default: false (viewport mode only) Turns the animation into one that's driven by the scrollbar rather than a timer. Accepts:

  • false — normal time-based playback when the trigger fires.
  • true — tile progress is directly tied to scroll position; scroll down = tiles assemble, scroll up = they disassemble.
  • A number (e.g. 0.5) — scrubbing with "catch-up": the animation follows the scroll but takes that many seconds to smooth out, giving a heavier, lagging feel.

Bonus behavior: in scrub mode, calling play() smooth-scrolls the scroller back to just before the trigger start, rewinding the reveal.

scroller — default: null (viewport mode only) By default ScrollTrigger watches the window/page scroll. If your page scrolls inside a custom container (e.g., an overflow-div or a smooth-scroll library wrapper), pass that element here so trigger positions are computed against the right scroller.

hideImage — default: true While the reveal is active, the original <img> is hidden (visibility: hidden) so you see the canvas tiles instead — without this, the full image would show through and the effect would be pointless. The previous visibility value is remembered and restored by destroy(). Set to false if you want the underlying image visible behind/under the tiles for some reason.

Accessibility

respectReducedMotion — default: true Honors the visitor's OS-level "reduce motion" preference (prefers-reduced-motion: reduce). When enabled and the visitor has requested reduced motion, the factory becomes a complete no-op: no canvas is created, no tween scheduled, and the plain <img> is simply displayed as-is. The returned stub still has the same shape (play(), destroy(), etc.) so your calling code doesn't need special guards — but reducedMotion will be true. Set to false only if you're certain you want to force animation on everyone.