Recreating Apples Scroll Driven Device Showcases
This article demonstrates how to animate a Rotato-rendered mockup in the browser using GSAP.js. By exporting a transparent video, extracting it into image frames, and rendering those frames to a canvas, we can achieve smooth, scroll-controlled animations with precise timing and high visual fidelity. The example uses GSAP’s ScrollTrigger, but the same technique applies to any interaction-driven animation.
Step 1: Video Rendering & Frame Extraction
First, render a video of the device you wish to animate using video editing software or a 3D rendering tool, Rotato is the one i've been playing with lately, it makes animating a mockup dead simple. Export the video from your tool of choice with a transparent background.
Once the video is rendered, it’s converted into a sequence of individual image frames. This approach gives frame-accurate control over the animation and avoids the limitations of video playback when syncing animation progress to scroll position. I use FFmpeg to extract individual frames from the video.
The following command converts the video into a numbered PNG sequence, making it easy to reference and load each frame programmatically.
ffmpeg -i input_video.mov -vf "fps=1" -vsync vfr -c:v png output_folder/%04d.png
Step 2: Markup & JavaScript
The animation is rendered using an HTML <canvas>, which allows us to efficiently draw and swap frames as the user scrolls. The canvas is wrapped in a container that fills the viewport and can be pinned during scroll to create a controlled animation segment.
In the JavaScript code, we load the extracted images into an array and use GSAP to animate them on scroll. We set up a scrollTrigger to control the animation based on the user's scroll position.
.canvas-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
<div class="canvas-container">
<canvas id="canvas" />
</div>
In JavaScript, the canvas is initialised and sized to match the exported image dimensions. Matching the canvas size to the source frames ensures the images render at full resolution without scaling artifacts.
Each frame in the sequence is then loaded into memory ahead of time. Preloading prevents visible frame popping during scroll and ensures smooth playback.
GSAP is used to animate a simple frame counter object. As the frame value updates, the corresponding image is drawn to the canvas. ScrollTrigger ties this animation directly to the user’s scroll position, allowing precise control over progress, pinning, and scrub behavior.
The render function clears the canvas and draws the current frame. This runs on every animation update, effectively turning scroll progress into frame-by-frame playback.
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
//set the canvas dimensions to match your images
canvas.width = 1152;
canvas.height = 2048;
const frameCount = 203;
const currentFrame = (index) =>
//where sequence is the target directory
`sequence/${(index + 1).toString().padStart(4, "0")}.png`;
const images = [];
const mockup = {
frame: 0,
};
for (let i = 0; i < frameCount; i++) {
const img = new Image();
img.src = currentFrame(i);
console.log(currentFrame(i));
images.push(img);
}
gsap.to(mockup, {
frame: frameCount - 1,
snap: "frame",
ease: "none",
scrollTrigger: {
trigger: ".canvas-container",
start: "top top",
end: "+=3500",
markers: false,
pin: true,
scrub: 0.5,
},
// use animation onUpdate
onUpdate: render,
});
images[0].onload = render;
//render to the canvas
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(images[mockup.frame], 0, 0);
}
This setup results in a high-quality, scroll-driven mockup animation with full control over timing, easing, and interaction, while remaining performant and framework-agnostic.

