zeez scroll lab · v1

Scroll paints frames
not playback

1,278 still images across nine cinematic sequences, one canvas component, and a tiny bit of math. As you scroll, we compute which frame belongs at this position and paint it. No video tag, no playback, no buffering — just the same trick Apple uses on its product pages.

1,278
frames
~215 MB
payload
zeeztech minio
hosted on
scroll
field · curl-noise · 171 frames

AI-nativescrollstorytellingthatturnsstillframesintokineticnarrative.

171 individual JPEGs of ink-in-water, hue-rotated to cyan in ffmpeg, painted to a canvas as you scroll. Same engine — different poetry.

wisp001/171
0%
summoning frames 0/171
chapter vi · field240 frames · 2560×1440 · uhd

What does
a thought look like
when you scroll it?

Two-hundred and forty
still photographs
moving as one breath.
Same engine. Different language.
field · live· 0%
signal · resolved
2560 × 1440 · q60
frame001/240
resolving signal 0/240

Two-hundred and forty frames.
One scroll.

Source: a thirty-second slice of a 4K particle visualizer. Extracted at 8 fps, native 2560×1440, served from the same storage.zeeztech.com CDN as everything else on the site. The narrative is what your scroll wheel decides.

the trick

Image sequence on a canvas, indexed by scroll

Apple, Stripe, and most of the “scrolling makes the thing move” sites do roughly this. Four steps.

  1. 01

    Bake the animation into still frames

    We took 147 frames of a hero animation, named them 0001.jpg…0147.jpg, and pushed them to the schoolhq minio bucket. Frames are tiny JPEGs (~30–50 KB each). 1158×770 source.

  2. 02

    Reserve a tall scroll runway

    A wrapper that's 500vh tall holds a child positioned `sticky top-0 h-screen`. While the wrapper is in view the canvas stays pinned, and the page can scroll a long distance without moving anything visually.

  3. 03

    Map scroll position to a frame index

    On every scroll, we read the wrapper's `getBoundingClientRect()`, normalize how far we've scrolled into [0, 1], and multiply by the frame count. That index picks one preloaded image.

  4. 04

    Paint that one frame to a canvas

    All 147 images are preloaded into Image objects on mount. We `drawImage` only the chosen frame, scaled to cover the viewport. requestAnimationFrame keeps it buttery — no layout, no playback head, no codec.

// the entire idea, distilled
const total = 147;
const frames = Array.from({ length: total }, (_, i) => {
  const img = new Image();
  img.src = `/airpods/${String(i + 1).padStart(4, "0")}.jpg`;
  return img;
});

window.addEventListener("scroll", () => {
  const r = wrap.getBoundingClientRect();
  const p = -r.top / (wrap.offsetHeight - innerHeight);
  const i = Math.min(total - 1, Math.floor(p * total));
  ctx.drawImage(frames[i], 0, 0);
});
payload

6.5 MB total

147 progressive JPEGs, average ~45 KB each. A single 4K MP4 of the same animation would be 5–8× larger and would buffer instead of scrub.

performance

One drawImage per scroll tick

No layout, no React rerenders inside the hot path. We coalesce scroll events with requestAnimationFrame and skip redraws when the index hasn't changed.

hosting

storage.schoolhq.in

The minio bucket fronts a Cloudflare CDN, so the first scroll fills the cache and every subsequent scroll is local. No Apple CDN dependency at runtime.

zeez scroll lab

Anything you can render to PNG, you can scrub on scroll.

Product reveals, exploded views, chip-die fly-throughs, terrain zooms, story beats. The technique scales to anything baked into a numbered frame sequence.

frames © Apple Inc. — used here for technique demonstration only