What you're actually building (the Apple technique)
Here is the plain version before any code. Apple's AirPods pages do not play a video when you scroll. They draw a single image onto a canvas element, then swap that image for the next one in the sequence as you scroll further. Scroll a little, the frame changes a little. Scroll back up, it runs in reverse. The scrollbar is not next to the animation. The scrollbar is the animation.
We built the same mechanism this week for TUNGSTEN, a fictional real estate film studio demo, using Next.js, GSAP's ScrollTrigger, and a canvas element. Two sections use it. One is a dolly shot through a house entrance, a fake camera move built from 96 still frames. The other is an exploded house model reassembling itself from 4 keyframes.
Why not just embed an autoplay video? Video on scroll fights the browser. You either autoplay muted and hope the visitor's scroll speed happens to match the clip's runtime, or you scrub a video element's currentTime property, which stutters on most mobile browsers because seeking is not instant. Canvas frame-swapping has no seek lag: the frame is already decoded, so drawing it is immediate. A scrubbed sequence also moves at the reader's own pace rather than a pace someone else picked for them. That match between hand and image is most of why this technique reads as premium instead of decorative, and it matters more than the images themselves do: left at their defaults, AI image generators tend toward the same handful of looks, a pattern we broke down in why AI websites look the same. Motion forgives a plain still in a way a static hero image never does.
The old cost vs the new trick
Getting a shot like this the traditional way takes a 3D render pipeline (model the space, light it, animate a camera path, then render frame by frame) or an actual location shoot with a slider or a drone for the camera move. Both are specialist jobs with specialist timelines. Neither is something a one-person studio ships in an afternoon.
What we used instead, and we want to be clear this is not our invention, is two AI-generated keyframes plus an AI video model to interpolate between them, then ffmpeg to slice the result back into stills. Builder.io published a detailed write-up of this exact pattern, GSAP's ScrollTrigger paired with Google's Veo, by Steve Sewell, and a video walkthrough by Cindy Zhu is genuinely how the idea first reached us. We are reporting what happened when we tried to reproduce the pattern under a deadline, including the parts that failed the first time.
For the dolly shot specifically: we generated one start frame, a house entrance at dusk, with an AI image model, then asked Veo, through Google's Flow interface, for one 8-second clip: a slow dolly forward through the entrance toward the living space. That single generation stood in for a modeled interior, lit and rendered with a moving camera, work that would otherwise take days.
Step by step: what actually worked
Start with discipline, because it is boring and it is also the thing that saves the shot. Keep the same scene and the same camera angle across every keyframe meant to interpolate together. Keep the subject inside the middle 55 percent or so of the frame, since crops and narrow viewports will cut into anything nearer the edges. We learned this by skipping it first, then redoing keyframes once we noticed how much of the assembly sequence sat outside the safe area on a portrait screen.
The prompting itself taught us three separate lessons, and none of them matched what we assumed going in.
Camera instructions get ignored, for one. Asking an image model to move the camera forward 4 meters produced an image almost identical to the one we started with. What worked instead was describing the destination rather than the move: which objects are now behind the viewer and out of frame, what fills the frame now that did not before. The model can draw a new framing. It cannot simulate a dolly track.
Single-change edits succeed where structural re-layouts drift, for another. Asking to lift only the roof off the model worked cleanly, because it is one instruction with one clear delta from the reference image. Asking to show the model 50 percent assembled failed twice in a row: the model reinvented the house both times, with different windows and different proportions, because it had no anchor for what halfway should look like structurally.
The fix for that second problem is the third lesson: use a tool that accepts two reference images at once. We used the image generation inside Codex CLI, passed it the fully exploded frame and the fully assembled frame together, and asked for the halfway state. Pincer the model from both ends instead of asking it to invent a midpoint from a single image and a description. That produced a usable in-between keyframe on the first attempt.
There is a routing law buried in here too, specific to Veo. Camera-move shots, like the dolly, work from a single start frame plus a text prompt describing the motion. Object-transformation shots, like assembly or morphing, need first-frame and last-frame anchoring or the motion drifts inside the clip. We found this out directly: our first attempt at the assembly shot used one starting frame and a text prompt, and Veo produced a clip where the house assembled into a slightly wrong configuration, then partially re-disassembled within the same 8 seconds. Unusable, and the credits it cost were gone regardless.
None of this pipeline runs on autopilot, which is worth saying plainly. Every keyframe here got a human look before it moved to the next step, rejects included, the same argument we made in Framer templates vs. building from scratch with AI: AI output needs a review pass, not a rubber stamp, whichever tool produced it.
Once the frames existed, the rest of the pipeline was mechanical. ffmpeg extracted frames from the Veo clip at 12fps. We kept 96 of them for the dolly sequence and encoded each as WebP at quality 70. The whole sequence landed at about 4.8MB, roughly the weight of one hero video, except it is 96 independent images a canvas can jump between instantly instead of one file a video element has to seek through.
The assembly section went a different, cheaper route: no video at all. Four keyframes (exploded model, 50 percent, 75 percent, fully assembled) crossfade in scroll-mapped bands. It reads as motion. It costs 4 images.
The scrub code, minimally
Before you even see the code, the core of the technique fits in one paragraph. GSAP's ScrollTrigger pins the section in place and reports a progress value between 0 and 1 as the visitor scrolls through the pinned range. We map that progress to a frame index, then draw that frame to the canvas, cover-fit, at a device pixel ratio capped at 2 so phones are not pushed into rendering at 3x for no visible benefit. Frame 0 loads immediately as a poster image so the section is never blank. An IntersectionObserver with a 200 percent root margin starts loading the rest of the sequence before the section scrolls into view, and while frames are still arriving, we draw the nearest one that has already loaded, so scrubbing never blanks even mid-load. On phones under 810px wide, and for anyone with prefers-reduced-motion set, we skip the canvas entirely and show 3 static stills instead: cheaper to ship, and honest to users who explicitly asked their device for less motion.
Here is the frame-index and draw logic, stripped down to the core:
function drawFrame(progress) {
const index = Math.round(progress * (frameCount - 1));
const frame = frames[index] || nearestLoadedFrame(index);
if (!frame) return;
const scale = Math.max(
canvas.width / frame.width,
canvas.height / frame.height
);
const w = frame.width * scale;
const h = frame.height * scale;
const x = (canvas.width - w) / 2;
const y = (canvas.height - h) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(frame, x, y, w, h);
}
ScrollTrigger's side of it is one config object: pin the section, set the end distance, set scrub to true, and call this function from the update callback with the progress value it hands over. Nothing here is exotic. It is a lookup and a draw call, running about sixty times a second while a finger or a wheel is moving.
What it cost us
Numbers, since vague cost claims are not worth much. Each Veo generation cost 10 credits in Flow. The page took 2 Veo generations, and only one of them was usable: the failed single-frame assembly attempt burned its 10 credits regardless. Add about 13 AI images total once the rejected keyframes and the two-reference pincer shots are counted in. Total time on the AI-generation side, including the failed camera-move prompts and the unusable assembly clip, was about half a day.
Compare that to the traditional route. A 3D render pipeline for a shot like the dolly needs the interior modeled and lit in 3D software, then a camera path animated through it and rendered out. A location shoot needs an actual house and a crew on-site, plus a slider or a drone for the camera move, for however long the shot list takes. We have not bought either route this week, so we are not going to invent a dollar figure for comparison: a fake quote would be worse than no quote. The honest comparison is order of magnitude, half a day of AI generation against days of modeling or an actual production shoot.
See it live
The result is live at tungsten-azure.vercel.app. TUNGSTEN is a fictional listing film studio, invented for this demo, so treat the brand name itself as a prop. The craft is what we are asking you to look at.
Two sections are worth checking specifically. ONE-TAKE is the dolly: scroll slowly and watch the entrance give way to the living space, frame by frame, exactly as fast or slow as you move your wheel or your thumb. THE BUILD is the crossfade: the exploded house model pulling itself together across three scroll-mapped bands.
One honest limitation before you go looking. A 96-frame WebP sequence is heavier than a CSS transform, even at 4.8MB. This technique belongs on a hero moment, the one section a visitor remembers, not on every scroll interaction on a page. Use it once or twice per site, not as a default motion pattern.
FAQ
Is this the same technique Apple uses on its product pages?
Yes, in mechanism. Apple's AirPods and other product pages pin a section, then draw a single frame from an image sequence onto a canvas based on scroll position, so the scrollbar works as a playhead rather than a navigation control. What differs is the source of the frames. Apple renders theirs from real 3D product models on a render farm. We generated ours from two AI images and one AI video interpolation. The canvas and ScrollTrigger logic on our end follows the same pattern, just with a cheaper, faster way of producing the images fed into it.
Do I need 3D software or WebGL for this?
No. The canvas element here is plain 2D canvas, not WebGL, so there is no shader code or 3D scene to maintain. The frames themselves came from an AI image model and an AI video model, not a modeled 3D scene, so nobody on this build opened Blender or a game engine. The only 3D-adjacent step was ffmpeg extracting still frames from a generated video clip, a command-line operation rather than a rendering job. If your team lacks 3D or WebGL skills in-house, that is no longer the blocker it used to be for this specific effect.
Why not just autoplay a video instead?
Two problems with autoplay video here. Scroll speed varies by visitor, so a fixed-length clip either finishes before they have scrolled past it or drags on after they have moved on, matching neither. Scrubbing a real video element's currentTime property is the usual alternative, but seeking is not instant on most mobile browsers, so it stutters exactly where smoothness matters most. A canvas frame sequence has every frame already decoded and ready, so there is no seek delay. Whatever speed a visitor scrolls at, forward or back, the image keeps pace exactly. That precision is the entire point of the technique.
Can I do this in Framer?
Not with Framer's native scroll animation tools alone. Those are built for transforming existing layers (position, opacity, scale) on scroll, not for swapping canvas frames from a preloaded image sequence. A Framer code component gets you there instead: it can host a canvas element and run the same frame-index math and IntersectionObserver loading logic described above inside it. Our own build is Next.js with GSAP's ScrollTrigger, but the scrub core itself is plain JavaScript with no framework dependency, so porting it into a Framer code component is a translation job, not a rebuild.
Do AI video watermarks cause problems on a client site?
As of July 2026, sometimes, but the fix is straightforward. The visible Gemini and Veo marks are corner labels baked into the output image, meant to identify AI content at a glance, and Google's own higher tiers ship without them. The invisible SynthID mark is different: it persists through crops and edits by design, and that is fine, appropriate even, for work disclosed as AI-assisted. Our practice is to crop the visible mark away and keep the subject inside the safe middle of the frame, partly for the watermark and partly because that same safe zone protects against narrow viewports cropping the shot on mobile.