How to create an Image Sequence
Posted on June 21, 2022
For one of my most recent projects, I was tasked with creating an image sequence effect. An Image sequence is a series of sequential still images that represent frames of a video or animation.
In my scenario, the image sequence should animate through the frames based on the end-users scroll progression.
A live example of the image sequence I created can be seen in the Hero component on this site.
Getting Started
To get started, you’re going to want to find a video or animation you can convert into png frames. There are many websites out there to help you convert the file formats, however I find ezgif pretty good.
I’ll be using this video of Drake for my example:
When you find your video, upload it to ezgif and convert it to png’s, it should give you a zip to download with each frame of your video.
Creating our App
Once you’ve found your video and created your frames, you can start to create your app. In this example, we’ll be using Next.js paired with TypeScript and Tailwind CSS.
yarn create next-app --example with-tailwindcss
Create a components directory in the root of your project, and create a component named ImageSequence.tsx , copy in the following code:
import { useState, useEffect } from "react";
import data from "../lib/data.json";
export interface ImageSequenceProps {
progress: number;
min?: number;
max?: number;
}
const ImageSequence = ({ progress, min = 0, max = 1 }: ImageSequenceProps) => {
const [frames, setFrames] = useState(data.images);
const [frame, setFrame] = useState(0);
return (
<section className="w-full h-screen">
<p>{progress}</p>
</section>
);
};
export default ImageSequence;
Set up some state to track our frames that we will from our data.json file, as well as some state to track our current frame. Don’t worry about the progress prop for now, we’ll get to that later.
Clean up the index.tsx and import the ImageSequence component you just created :
import type { NextPage } from "next";
import ImageSequence from "../components/ImageSequence";
const Home: NextPage = () => {
return (
<>
<ImageSequence />
</>
);
};
export default Home;
Next, create an images directory within the public directory of your app, then drag in the png frames you downloaded earlier. With our images in here, we’ll be able to access them on the frontend.
Create a lib folder in the root directory of your project. In this folder, create a data.json file and create an array of images with the file paths to the images we uploaded previously. My data.json file looks like this:
{
"images": [
"/images/ezgif-frame-001.png",
"/images/ezgif-frame-002.png",
"/images/ezgif-frame-003.png",
"/images/ezgif-frame-004.png",
"/images/ezgif-frame-005.png",
"/images/ezgif-frame-006.png",
"/images/ezgif-frame-007.png",
"/images/ezgif-frame-008.png",
"/images/ezgif-frame-009.png",
"/images/ezgif-frame-010.png",
"/images/ezgif-frame-011.png",
"/images/ezgif-frame-012.png",
"/images/ezgif-frame-013.png",
"/images/ezgif-frame-014.png",
"/images/ezgif-frame-015.png",
"/images/ezgif-frame-016.png",
"/images/ezgif-frame-017.png",
"/images/ezgif-frame-018.png",
"/images/ezgif-frame-019.png",
"/images/ezgif-frame-020.png",
"/images/ezgif-frame-021.png",
"/images/ezgif-frame-022.png",
"/images/ezgif-frame-023.png",
"/images/ezgif-frame-024.png",
"/images/ezgif-frame-025.png",
"/images/ezgif-frame-026.png",
"/images/ezgif-frame-027.png",
"/images/ezgif-frame-028.png",
"/images/ezgif-frame-029.png",
"/images/ezgif-frame-030.png"
]
}
We can now import this data on our index.tsx page:
import data from "../lib/data.json"
Working with react-scrollmagic
Since we want this animation to trigger on scroll, we’ll use a handy package called react-scrollmagic. ScrollMagic helps us to easily react to the user’s current scroll position. This package is great for:
- Animations based on scroll position
- Pinning elements starting at a specific scroll position
- Adding parallax effects to websites and apps
To install react-scrollmagic, run the following:
yarn add react-scrollmagic
Once installed, import the following to your index.ts file:
import { Controller, Scene } from "react-scrollmagic";
Wrap the section with the Controller and Scene components like so:
import type { NextPage } from "next";
import ImageSequence from "../components/ImageSequence";
import { Controller, Scene } from "react-scrollmagic";
import data from "../lib/data.json";
const Home: NextPage = () => {
return (
<>
<Controller>
<Scene
duration={1000}
pin={{ pushFollowers: true }}
enabled={true}
triggerHook={0}
>
{(progress: number) => (
<div>
<ImageSequence progress={progress} />
</div>
)}
</Scene>
</Controller>
</>
);
};
export default Home;
The Controller is the main wrapper for our Scene. A Controller can have one or many Scene(s). You’ll notice a few props on the Scene component:
- duration ⇒ How long we want the scene to run for
- pin ⇒ If pushFollowers is set to true , any following elements will be “pushed” down for the duration of the pin. If false , the pinned element will just scroll past them following elements will be "pushed" down for the duration of the pin, if false , the pinned element will just scroll past them
- enabled ⇒ If true, the scene is enabled and active
- triggerHook ⇒ Can be a number between 0 and 1 defining the position of the trigger Hook in relation to the viewport.
You may also be wondering what the progress callback function is doing, and why we’re passing it into our ImageSequence component — this is what actually makes the magic happen.
The fun part
Next, lets create a useEffect that will help us get and set our current frame, in your ImageSequence.tsx component, add the following code:
useEffect(() => {
const frameCount = frames.length;
const p = Math.min(1, Math.max(0, (progress - min) / (max - min)));
const currFrame = Math.min(
Math.max(Math.round(p * frameCount), 0),
frameCount - 1
);
if (frame === currFrame) return;
requestAnimationFrame(() => setFrame(currFrame));
}, [progress]);
We can now add our image frame output inside of our section like so:
<section className="w-full h-screen">
{frames.map((f: any, n: number) => (
<img
key={n}
src={f}
height="100%"
width="100%"
style={{
objectFit: "cover",
backgroundPosition: "center",
height: "100%",
width: "100%",
position: "absolute",
bottom: 0,
opacity: n === frame ? 1 : 0,
}}
/>
))}
</section>
Your complete ImageSequence.tsx file should look like the following:
import { useState, useEffect } from "react";
import data from "../lib/data.json";
export interface ImageSequenceProps {
progress: number;
min?: number;
max?: number;
}
const ImageSequence = ({ progress, min = 0, max = 1 }: ImageSequenceProps) => {
const [frames, setFrames] = useState(data.images);
const [frame, setFrame] = useState(0);
useEffect(() => {
const frameCount = frames.length;
const p = Math.min(1, Math.max(0, (progress - min) / (max - min)));
const currFrame = Math.min(
Math.max(Math.round(p * frameCount), 0),
frameCount - 1
);
if (frame === currFrame) return;
requestAnimationFrame(() => setFrame(currFrame));
}, [progress]);
return (
<section className="w-full h-screen">
{frames.map((f: any, n: number) => (
<img
key={n}
src={f}
height="100%"
width="100%"
style={{
objectFit: "cover",
backgroundPosition: "center",
height: "100%",
width: "100%",
position: "absolute",
bottom: 0,
opacity: n === frame ? 1 : 0,
}}
/>
))}
</section>
);
};
export default ImageSequence;
If everything is set up correctly, your image sequence should now flip through the frames based on your scroll position!
And there you have it, your very own image sequence effect.