I came across an interesting animation on the polaroid’s i2 camera product page recently. This same technique have been used by Apple too.
This technique involves rendering a sequence of images on a canvas at different scroll positions. As you scroll up or down the page, different images are rendered on the canvas to create a sense of motion (it feels like playing a video on scroll).
For context, here’s an example:
See the Pen Apple AirPods Pro Animation (final demo) by Jurn (@j-v-w) on CodePen.
The first step is to create a sequence of images; I used an online tool to convert a video into a sequence of images.
To follow along and understand what is going on, you would need to have at least a basic understanding of:
- React
- JavaScript
JSX Markup
To get started, we’ll create a component called PngSequence
. This component will be responsible for rendering the images and animating them as the user scrolls.
function ScrollSequence(){ return( <section className="png__sequence"> <canvas width = {window.innerWidth} height={window.innerHeight} className = "png__sequence__canvas" id="canvas"> </canvas> </section>)
}
In this component, we have:
- A
<section>
container that will serve as a wrapper for our animation. - And a
<canvas>
element where our images would be rendered. The width and height of the canvas are set to the current width and height of the browser window usingwindow.innerWidth
andwindow.innerHeight
.
Alright, let’s get started with the styling! I used Scss, but you can use pure CSS if you are not comfortable with Scss.
N/B: Don’t forget to do your SCSS resets.
.png__sequence{ width: 100%; height: 500vh; position: relative; &__canvas{ top: 0; bottom: 0; left: 0; right: 0; max-width: 100vw; max-height: 100vh; position: sticky; z-index: 1;
}
}
The png__sequence
has a height
of 500vh
to ensure that our page has enough scroll length for the animation to work.
Creating the Canvas and Context
We want our animation to start as soon as the component mounts. To do that, we’ll use the useEffect
hook to wrap our animation code. Within the useEffect hook, we’ll get the canvas element and its 2D rendering context. Ensure to import the SCSS file that houses the styling for your component.
canvasRef
selects the canvas element.
import "./scrollSequence.scss";
import {useEffect, useRef} from "react"; function ScrollSequence(){
const canvasRef = useRef(null); useEffect(() => {
// Get the canvas element and its 2D context
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
} return (<section className="png__sequence">
<canvas ref={canvasRef} width = {window.innerWidth} height={window.innerHeight} className = "png__sequence__canvas" id="canvas"> </canvas> </section> );
}
Preparing Images
The images in our animation will be rendered on the canvas in sequence based on the user’s scroll position.
We should name the images in a sequence that matches their intended order, for example, image001.jpg, image002.jpg, image003.jpg, and so on.
Naming them this way will help us keep track of the current image being rendered on the canvas.
// number of images to be sequenced
const frameCount = 147; // Generates the filename of the image based on the current index
const currentFrame = (index) => { return `/src/assets/xioami-watch-3-hero-asset/Home_${index .toString() .padStart(3, "0")}.jpg`; };
}
The value of the frameCount
variable represents the total number of images that will be included in the PNG sequence.
Drawing Images on Canvas
The next step is to load the images and draw them on the canvas.
// Drawing the initial image on the canvas
const img = new Image(); img.src = currentFrame(0); img.onload = function () { context.drawImage(img, 0, 0, canvas.width, canvas.height); };
The drawImage
method takes 5 arguments:
- The image
- The
x
andy
axis coordinate at which to place the top-left corner of the image on the canvas -
canvas.width
specifies The width to draw the image in the destination canvas -
canvas.height
specifies The height to draw the image in the destination canvas
Preloading Images
Preloading images ensures that they are downloaded and cached by the browser, so that they are ready to be displayed when needed. This can help to ensure a smoother animation experience, as the images will not need to be loaded while the animation is running.
To preload all the images before starting the animation, we can create a new Image object for each image and set its src property to the corresponding image filename. Once the images have been created, we can start the animation.
const preloadImages = () => { Array.from({ length: frameCount }, (_, i) => { const img = new Image(); img.src = currentFrame(i); }); };
Updating Images
Update the current image, so it can be drawn on the canvas as the user scrolls.
const updateImage = (index) => { img.src = currentFrame(index); context.drawImage(img, 0, 0, canvas.width, canvas.height); };
Tracking Scroll Position
The animation is driven by the user’s scroll position. As the user scrolls down or up the page, we calculate the scroll position and map it to the appropriate frame index.
The canvas is then updated with the image corresponding to the calculated frame index, giving the illusion of movement.
window.addEventListener("scroll", () => { const html = document.documentElement; const wrap = document.querySelector(".png__sequence"); const scrollTop = html.scrollTop; const maxScrollTop = wrap.scrollHeight - window.innerHeight; const scrollFraction = scrollTop / maxScrollTop; const frameIndex = Math.min( frameCount - 1, Math.floor(scrollFraction * frameCount) ); requestAnimationFrame(() => updateImage(frameIndex + 1)); }); preloadImages();
html
is the document.documentElement object, which represents the HTML document itself.scrollTop
is the current scroll position of the HTML document.maxScrollTop
is the maximum scroll position of the.png__sequence
element.scrollFraction
is the ratio of the current scroll position to the maximum scroll position.frameIndex
is the index of the current frame, based on the scroll fraction.The requestAnimationFrame function requests a new animation frame.
The
updateImage
function updates the image on the canvas to the image corresponding to the current frame index.The
window.addEventListener
function listens for the scroll event and updates the frame index accordingly.
Putting it all together:
import { useEffect, useRef } from "react"; function scrollSequence() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); // number of images to be sequenced const frameCount = 147; // Function to generate the filename of the image based on the current index const currentFrame = (index) => { return `/src/assets/xioami-watch-3-hero-asset/Home_${index .toString() .padStart(3, "0")}.jpg`; }; // Drawing the initial images on the canvas const img = new Image(); img.src = currentFrame(0); img.onload = function () { context.drawImage(img, 0, 0, canvas.width, canvas.height); }; //preloading images const preloadImages = () => { Array.from({ length: frameCount }, (_, i) => { const img = new Image(); img.src = currentFrame(i); }); }; //update images const updateImage = (index) => { img.src = currentFrame(index); context.drawImage(img, 0, 0, canvas.width, canvas.height); }; // Tracking the user scroll position window.addEventListener("scroll", () => { const html = document.documentElement; const wrap = document.querySelector(".png__sequence"); const scrollTop = html.scrollTop; const maxScrollTop = wrap.scrollHeight - window.innerHeight; const scrollFraction = scrollTop / maxScrollTop; const frameIndex = Math.min( frameCount - 1, Math.floor(scrollFraction * frameCount) ); requestAnimationFrame(() => updateImage(frameIndex + 1)); }); preloadImages(); }, []); return (
<div className="png__sequence">
<canvas ref={canvasRef} width = {window.innerWidth} height={window.innerHeight} className = "png__sequence__canvas" id="canvas"> </canvas> </div> );
}
To ensure a smooth scroll experience, you could implement smooth scrolling using the Lenis library.
For fun, You could also add some texts at different scroll positions and animate them using the GSAP animation library.
This animation technique can add visual interest and interactivity to your website. This can be used to tell a story, showcase your products, or simply make your website more visually appealing. Happy coding!
Discover more from Coursity
Subscribe to get the latest posts sent to your email.