Building a Scrollytelling Hero with Canvas Frame Scrubbing
August 2026 · 10 min read
The scrollytelling hero animation on this portfolio replaces a traditional <video> tag with something more interactive and performant: a sequence of 75 WebP images rendered on an HTML5 Canvas, scrubbed by the user's scroll position. Here's how it works.
Why Not Just Use a Video?
Video tags are simple but have critical limitations for scroll-linked interaction: no frame-precise control (you're at the mercy of the browser's buffering), codec compatibility issues across browsers, and inability to pause at exact positions. Canvas gives us pixel-perfect frame control synced to scroll position.
The Image Sequence Pipeline
The animation was originally a 3-second cinematic AI-generated video. I converted it to WebP format using ezgif at 15fps, then split it into 75 individual frames. Each frame is approximately 8KB — totaling ~600KB for the entire sequence. Compare this to the original 15MB video file: a 96% size reduction with better control.
The RAF Lerp Loop
The rendering engine uses requestAnimationFrame with linear interpolation (lerp). When the user scrolls, we calculate a target frame index based on scroll progress (0-1 mapped to frame 0-74). The lerp function smoothly interpolates between the current frame and target frame with a factor of 0.15.
// Don't render on scroll events — render on every frame
const render = () => {
currentFrame += (targetFrame - currentFrame) * 0.15;
const idx = Math.round(currentFrame);
if (frames[idx]) {
ctx.drawImage(frames[idx], 0, 0, canvas.width, canvas.height);
}
requestAnimationFrame(render);
};
// Scroll handler only updates the target
const onScroll = () => {
const progress = -rect.top / (rect.height - window.innerHeight);
targetFrame = progress * 74;
};
The key insight: decouple input from rendering. The scroll handler just updates the target; the RAF loop handles all rendering. This prevents frame drops even during fast or janky scrolling.
Progressive Preloading
All 75 frames are preloaded via JavaScript Image objects before the canvas renders anything. We track load progress and show a loading indicator. Frames are rendered as soon as they load (not waiting for all 75), so the user sees the animation progressively appear.
The Vignette Overlay
A critical detail: the canvas sits behind the content, and a CSS vignette overlay blends the animation edges into the page background color (#07090e). This gradient mask ensures seamless transitions between the animated hero and the content sections below.