How to create frame-by-frame moving image on scroll effect

JW
Level Up Coding
Published in
6 min readMay 4, 2020

--

A step by step guide on how to create that dynamic image background you see everywhere.

Content

  1. Introduction
  2. Result demo
  3. Prerequisite
  4. Step by step guide
  5. Next step

Introduction

Moving an image on scroll is the new parallax for the frontend. In the old days, parallax was everywhere. But it rarer for new websites. Instead, we see a lot of moving image by scrolling pattern — for example, apple new iPhone SE website:

The iPhone rotate nicely on scrolling down (and reverse when scroll up)

As you can see, the iPhone rotates frame by frame as you scroll down. In this tutorial, I will share my approach for reproducing such pattern.

Result demo

As you can see from the scroll bar, the content changes by scroll position

Codepen: https://codepen.io/josephwong2004/pen/wvKPGEO

Prerequisite

Just basic knowledge in CSS and JS

Step by step guide

Step 1: Get some images

Okay, I guess you already figured it out. The “Moving image” is actually just a bunch of images with small differences, and played frame by frame like an animation. By mapping the scroll position to a corresponding image, we get an illusion of the object in the images itself is moving or rotating.

As you can see from the demo, I get some images for vegetable falling down (20 in total).

The images I am using is from this youtube video, I don’t own the images, only using it for tutorial purpose. Source:

https://www.youtube.com/watch?v=8DsDH3JQ384

Step 2: Setup the basic

Let start building our “moving image” effect. The html is very simple:

<div class='container'>
<div class='image-container'></div>
</div>

We will put the image in the ‘image-container’ class. For CSS:

body {
margin: 0;
font-family: 'Permanent Marker', cursive;
}
.container {
position: relative;
width: 100%;
height: 1500px;
.image-container {
width: 100%;
height: 0;
padding-top: 45.347%;
position: sticky;
top: 0;
background-size: cover;
background-image: url('https://drive.google.com/uc?id=1vtaubItASKilyvb5sgQO7D7gjAQ7xo0i');
}
}

Now, most things there is pretty standard. I added a font for the overlay text later. For the image-container itself, we want it to be as large as the page width, and have a dynamic height. Unfortunately, we cannot do height: auto here, as our image-container doesn’t have any content, and the background-image doesn’t count. The container will always be 0px in height.

In order to compensate that, instead, we use a padding-top with percentage. This percentage is not random, but the image height to width ratio (height /width * 100%). With that and background-size: cover, we have container that fill up the whole page.

Step 3: Add scrolling effect

With the background image set, let’s add some JS to our code to make it “move”.

I have uploaded 20 images to google drive, and stored their links like so:

// Images asset
const fruitImages = {
1:'https://drive.google.com/uc?id=1vtaubItASKilyvb5sgQO7D7gjAQ7xo0i',
2:'https://drive.google.com/uc?id=1FJNbSIMKRPBnGPienoYK1Qf8wIwQSdpR',
3:'https://drive.google.com/uc?id=1TODQyZgnCjDX2Slr0ll8g-ymIV8Yizkh',
....... (I am not going to copy everything here)
20:'https://drive.google.com/uc?id=1D7PBddCxxb6aRk43maJ_BXgQD-PRS6R7',
}

Tips:

The default google drive share link is something like:
https://drive.google.com/open?id=1vtaubItASKilyvb5sgQO7D7gjAQ7xo0i

To use it in code, you need to replace the /open? with /uc?

Each key represent one “frame” in our scroll animation. Next, let add our scrolling function:

// Global variable to control the scrolling behavior
const step = 30; // For each 30px, change an image
function trackScrollPosition() {
const y = window.scrollY;
const label = Math.min(Math.floor(y/30) + 1, 20);
const imageToUse = fruitImages[label];
// Change the background image
$('.image-container').css('background-image', `url('${imageToUse}')`);
}$(document).ready(()=>{
$(window).scroll(()=>{
trackScrollPosition();
})
})

I am using jquery here just for convenience. The logic is very simple, we are creating a keyframe animation, but instead of time, we use pixel as our basic unit to calculate when should the next frame appear.

We are interested in the current scrollY value, which indicate how far the user has scrolled. We also create a step variable for the min and max bound of each frame to display on screen. With step set to 30, assuming the user keep scrolling, each image will display for “30px” worth of scroll time.

Our onScroll function calculate which image to use for the current scrollY position, and then set the .image-container background-image property.

Instead of time, we use pixel for changing frame

Let have a look as our result:

Pretty nice! Now let add the text as well.

Step 4: Add floating text

In addition to the moving background, we also want some text floating on top of the image to convey our message.

My approach for adding the text is also very simple (a.k.a. stupid). Since I have 20 “frames” for my image, I simply create another array to store the corresponding style of the text in each “frame”. (Using the same array would be better, but for tutorial purpose, I use a new array here)

But first thing first, let add some html and css first:

html:

<div class='container'>
<div class='image-container'></div>
<div class='text-container'>
<div class='subtitle' id='line1'>These lines float in one by one</div>
<div class='title' id='line2'>How to make</div>
<div class='title' id='line3'>Moving background</div>
<div class='subtitle' id='line4'>Disappear again when scroll top</div>
</div>
</div>

css:

.text-container {
width: 100%;
height: 100%;
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
top: 0;
color: white;
.subtitle {
opacity: 0;
font-size: 30px;
}
.title {
opacity: 0;
font-size: 80px; margin: -20px 0;
}
}

Now, let add the keyframe for the text style in an array:

const textStyle = {
1: {opacity: 0, transform: '0px'},
2: {opacity: 0, transform: '0px'},
3: {opacity: 0, transform: '0px'},
4: {opacity: 0, transform: '0px'},
5: {opacity: .25, transform: '15px'},
6: {opacity: .5, transform: '10px'},
7: {opacity: .75, transform: '5px'},
8: {opacity: 1, transform: '0px'},
... 9 - 19are the same with 8
20: {opacity: 1, transform: '0px'}
]

Each text has 5 states:

  1. Invisible, no transformation
  2. 25% visible, transform down 15px
  3. 50% visible, transform down 10px
  4. 75% visible, transform down 5px
  5. Full visible, no transformation

You can see for 1 to 4 frames, the text is invisible, and for 8–20 frames, the text is always visible. So our text stay there after scrolling certain amount.

Let modify our trackScrollPosition function to update the text style as well:

function trackScrollPosition() {
const y = window.scrollY;
const label = Math.min(Math.floor(y/30) + 1, 20);
const imageToUse = fruitImages[label];
// Change the background image
$('.image-container').css('background-image', `url('${imageToUse}')`);
// Change the text style
const textStep = 2;
const textStyleToUseLine1 = textStyle[label];
const textStyleToUseLine2 = textStyle[Math.min(Math.max(label - textStep, 1), 20)];
const textStyleToUseLine3 = textStyle[Math.min(Math.max(label - textStep * 2, 1),20)];
const textStyleToUseLine4 = textStyle[Math.min(Math.max(label - textStep * 3, 1),20)];
$('#line1').css({'opacity': textStyleToUseLine1.opacity, 'transform': `translateY(${textStyleToUseLine1.transform})`});
$('#line2').css({'opacity': textStyleToUseLine2.opacity, 'transform': `translateY(${textStyleToUseLine2.transform})`});
$('#line3').css({'opacity': textStyleToUseLine3.opacity, 'transform': `translateY(${textStyleToUseLine3.transform})`});
$('#line4').css({'opacity': textStyleToUseLine4.opacity, 'transform': `translateY(${textStyleToUseLine4.transform})`});
}

We have 4 lines of text, and we want them to display one by one. Their style is basically the same. So we simply use a textStep to add some “delay” for each line.

This bring you back to our starting demo:

And that’s it! If you like you can go further and create even more “frame”, but the concept is the same.

Next step

Obviously, the hardest thing here is getting the images you need, not the coding part. Unlike parallax, not every image work. And your result depends highly on the quality of your image.

I guess one thing to remember is the image also take time to load, in real life application, you probably want to wait for all the image to load first, or else when you scroll, the image is still loading and there will be white area below your “half-loaded” image.

And make no mistake, this tutorial is not meant to be the “best” solution for this problem, just my solution to it. If you have a better way to do so, feel free to leave a comment!

--

--