Creating a PNG Sequence Animation using React and SCSS.

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>)
}
Enter fullscreen mode
Exit fullscreen mode

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 using window.innerWidth and window.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;
}
} 
Enter fullscreen mode
Exit fullscreen mode

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> );
}
Enter fullscreen mode
Exit fullscreen mode

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`; };
}
Enter fullscreen mode
Exit fullscreen mode

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); };
Enter fullscreen mode
Exit fullscreen mode

The drawImage method takes 5 arguments:

  • The image
  • The x and y 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); }); };
Enter fullscreen mode
Exit fullscreen mode

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); };
Enter fullscreen mode
Exit fullscreen mode

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();
Enter fullscreen mode
Exit fullscreen mode
  • 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> );
} 
Enter fullscreen mode
Exit fullscreen mode

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top