Journal

Front-end engineering ·

Porting two canvas animations to React

The jaguar artwork on this site began as two standalone HTML files. How they became one React component that stays faithful to the originals and costs nothing while it's off screen.

The starting point

Two self-contained HTML files: each embeds its painting as a base64 JPEG and animates it with about 160 lines of canvas 2D code. Both do the same things in different proportions — a slow breathing transform on the image, bands of colored light sweeping across the creature, gold orbit arcs, glowing eyes, and particles: rising motes in one, butterflies and dust shed from the flank in the other.

One component, driven by data

The port keeps every constant from the originals — feature coordinates, speeds, colors, particle counts — and moves them into data: a geometry spec per artwork (breathing, band timing, orbit layout) and a palette per theme (colors and blend modes). The draw loop is shared; a switch on the artwork decides whether motes or butterflies run.

Light that stays on the creature

The shimmer bands touch the animal and never the background. On load, the painting is drawn once into a small off-screen canvas and turned into an alpha mask from its luminance. Each frame the bands are painted into a second off-screen canvas, cut down with destination-in against that mask, and composited onto the painting with screen — or multiply on the light-theme painting, where light has to behave like ink.

Paying only while visible

  • IntersectionObserver starts the loop when the art scrolls into view and stops it when it leaves.
  • visibilitychange stops it in background tabs.
  • ResizeObserver resizes the canvas at the device pixel ratio (capped at 2) and redraws a still frame while paused.
  • Each piece exists once per theme; the hidden copy is display: none, which IntersectionObserver reports as never intersecting — so it never runs.

Reduced motion

The first version drew a single still frame whenever the operating system asked for reduced motion. On a machine with Windows animation effects turned off, the artwork never moved — which is not what the original files do. The port now animates like the originals and carries its own Pause control, so the choice sits with the visitor.

Proving it moves

A screenshot can't prove motion. The check drives headless Chrome over the DevTools protocol, reads the canvas pixels twice a second apart, and compares checksums: they must differ while running, match while paused, and differ again after Play — with prefers-reduced-motion: reduce emulated, to match the machine that exposed the bug.