DEV Community

Cover image for Scroll-Driven Image Sequence Animation
Barigbue Nbira
Barigbue Nbira

Posted on • Edited on

Scroll-Driven Image Sequence Animation

I came across this interesting animation on the polaroid's i2 camera website recently.

A GIF image of the Polaroid I-2 camera website

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). This animation can be seen on the Apple, Samsung, and lots of creative websites out there.

Here's the same animation for the Apple airpod.

This technique adds visual interest and interactivity to your website. It can tell a story, showcase product(s), or simply enhance the visual appeal of your website

Prerequisites

  • Basic understanding of React and JavaScript.
  • A sequence of images (use online tools to convert a short video into individual frames).
  • Optimized images in WebP format (convert the image sequence to WebP for faster loading).

Getting Started

JSX Markup and Styling

First, create the component that will serve as a container for our animation:




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 element that serves as a wrapper for our animation.
  • A element where our images will 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.

Styling with SCSS

N/B: Don't forget to do your 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 this, we will use the useEffect hook to wrap our animation code, and get the canvas element and its 2D rendering context.



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 would be rendered on the canvas in sequence, based on the user's scroll position.

Name the images in in a sequence that matches their intended order, for example, image001.jpg, image002.jpg, image003.jpg, and so on. Don’t worry about this, the tool you used in creating the image sequence will handle this automatically.

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

This is my result.

sequence scrolling  animation

Conclusion

That's it folks, thank you for getting to this point. You can go crazy with this by implementing smooth scrolling using the Lenis smooth scroll library, or add text at different scroll positions using GSAP animation library. Until next time, Happy coding!

Top comments (0)