System preferences

Back

SECTOR 02.1Transmission open

Scroll-based 60 fps animations

Every scroll animation you have admired this year is a flipbook. The scrollbar is the thumb.

John Barnes Linnett patented that in 1868 as the kineograph. Apple's AirPods Pro page ships it today at 2560x1440, and someone rebuilt it in a pen. Every frame already drawn, every frame already there, and the reader can stop on frame 41 and stare at it.

The obvious way to build it is an <img />, a scroll listener, and some offset arithmetic. Wrong, and clunky. Linnett's toy works for three reasons, and each one maps to a rule:

  1. canvas, never img. The canvas repaints far faster than the image tag, which was built for static views. This is the paper stock. Turning a page has to cost nothing.

  2. Preload every asset. Mandatory. Otherwise the browser fetches each frame the moment it is needed and the thing stutters. A flipbook whose pages arrive one at a time is not a flipbook. It is a slideshow.

  3. Never hijack scroll. Fix the canvas to a tall container and read the scroll position instead. The thumb belongs to the reader. Take it away and every user feels the theft, even when they cannot name it.

And the price, honestly:

  1. You are fetching hundreds of images. The compressed gif above is 5MB and looks like a fax. The 174 source frames behind the demo are 8MB, at full 2560x1440 jpeg. Sixty percent more bytes, an order of magnitude better picture. This is video minus the codec, so check the connection before committing a user to the download.

  2. You need the photos first. A high-quality sequence is a prerequisite, not a detail, and no amount of requestAnimationFrame rescues a bad one.

Apply all five and you get this.

The markup

CODE // TRANSMISSION15 LINES
<!-- Preload -->
<head>
  <!-- Repeat for as many frames as needed -->
  <link
    rel="preload"
    as="image"
    type="image/jpeg"
    href="/asset/frame/001.jpeg"
  />
</head>

<!-- Markup -->
<div class="container">
  <canvas></canvas>
</div>

The render

That is real code from this site. It drives the demo above.

CODE // TRANSMISSION16 LINES
const context = canvas.getContext('2d')
const image = new Image()
const frameMax = 174

$container.addEventListener('scroll', () => {
  const scrollTop = $container.scrollTop
  const maxScrollTop = $container.scrollHeight - window.innerHeight
  const scrollFraction = scrollTop / maxScrollTop
  const current = Math.floor(scrollFraction * frameMax)
  const frame = Math.min(frameMax - 1, current)

  requestAnimationFrame(() => {
    image.src = createFrameRoute(frame + 1)
    context.drawImage(image, 0, 0)
  })
})

requestAnimationFrame is the whole trick on the JavaScript side. It hands the paint to the browser's own frame budget, so the sequence stays smooth while the main thread is busy with something else.

The stick

CODE // TRANSMISSION13 LINES
.container {
  /* The long-height container */
  height: 600vh;
  position: relative;
}

canvas {
  max-height: 100vh;
  max-width: 100vw;
  /* Keep the canvas in front of the container while scrolling */
  position: sticky;
  top: 0;
}

600vh is the length of the flipbook. Shorten it and the animation runs fast and cheap. Lengthen it and every frame gets room to land. That number is the only pacing control you have, and tuning it is the entire feel of the thing.

Linnett gave the reader the thumb in 1868. Do not take it back.