Scroll-Driven Image Sequence Animation
Tags: react, javascript, animation, css
Published: 2024-04-29 16:14:42

I came across this interesting animation on the [polaroid's i2 camera ](https://i2-camera.polaroid.com/) website recently.  

![A GIF image of the Polaroid I-2 camera website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q5twdh7sdszs8iegj7h8.gif)

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.

{% codepen https://codepen.io/j-v-w/pen/ZEbGzyv %}

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:
```jsx 

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> element that serves as a wrapper for our animation.
- A <canvas> 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.*

```scss
.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 this, we will use the `useEffect` hook to wrap our animation code, and get the canvas element and its 2D rendering context.

```jsx
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 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.

```jsx
// 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.
```javascript
// 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` 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.
```javascript
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.
```javascript
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.

```javascript
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:

```javascript
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>

  );
}

```
This is my result.

![sequence scrolling  animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/onn166jsc4ysr63xxjtk.gif)

## 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](https://lenis.studiofreight.com/) smooth scroll library, or add text at different scroll positions using [GSAP](https://greensock.com/gsap/) animation library. Until next time, Happy coding!