P4 Infrastructure: Building an Advanced Scrolling Feature with Video
Milwaukee-based marketing and design agency, Her & Himsel, recently lead a brand and website redesign project for P4 Infrastructure, a local civil engineering firm. Spin Group took part in the web development portion of the project. P4 Infrastructure tasked the team with creating a website that showcased its innovative products and services in a fun and engaging way.
For the homepage of the website, the design team wanted to incorporate a background video that flies around a 3D rendering of Milwaukee, showing each of their products in action. The idea was to have the playback of this video tied to the visitor’s scroll position. As the visitor scrolls up and down the page, the video rewinds and fast-forwards relative to the user’s scroll position.
The scrolling feature also includes text blurbs pinned to the screen explaining P4 Infrastructure’s products and how they work at key points in the video. For example, if the video is showcasing the MonoCast product, we pin a text blurb to the screen explaining the MonoCast product for as long as the relevant scene plays.
Our first challenge was to tie the scroll position to the video playback. To accomplish this, we needed to keep track of three variables:
1) targetPos: This variable represents the timestamp value we want to move towards.
2) currentPos: This variable represents the value that is moving towards targetPos. This is the value that controls the video playback.
3) acceleration: This determines how fast currentPos will try to catch up with targetPos
Let’s take a look at how our targetPos variable is being calculated. Here’s the code:
targetPos = (window.pageYOffset/jQuery(document).height()) * vid.duration;
Let’s break this down.
The first thing to remember is that we want the runtime of the video to match up exactly with the length of the document. When we reach the end of the page, we should reach the end of the video - 1:1 correlation. To do this, we need to create a relationship between percentage of document scrolled, and percentage of video scrubbed. For example - if I’ve scrolled through 20% of the page, I want the video timestamp to also be 20% of the way through.
To do this, we first calculate the percentage of the page scrolled. This is done by finding the number of pixels you have scrolled (window.pageYOffset), and dividing that by the number of pixels in the height of the document (document.height()). Then, we multiply that value by the duration of the video (vid.duration).
For example - let’s say our video is 30 seconds long, we have a 1500px tall page, and we’ve scrolled down 300 pixels. Let’s do the math:
(300 / 1500) * 30 = 6.
300 is 20% of 1500. 6 is 20% of 30. So, we want to scrub 6 seconds into the video.
So now we’ve got our targetPos - let’s take a look at our currentPos variable. Now, you might be wondering why we need both of these values. Can’t we just set our video timestamp to targetPos and call it a day?
Yeah, that’s what I thought at first too. But remember - we want to simulate movement between the timestamps, we don’t want to just teleport between them. Think about a YouTube video - if you press pause on the video and move around to different timestamps, the video will instantly jerk from point to point, rather than smoothly transitioning.
So how do we accomplish a smooth transition? The answer lies in using our currentPos variable in conjunction with our acceleration variable to catch up to targetPos. Here’s the code:
setInterval(function(){
//Accelerate towards the target:
currentPos += (targetPos - currentPos)*acceleration;
if(currentPos != lastCurrentPos){
vid.currentTime = currentPos;
vid.pause();
}
lastCurrentPos = currentPos;
},40);
Let’s break this down. First off, notice this code is inside of an interval that runs every 40 milliseconds. This is because every 40 milliseconds, we want to calculate a value of currentPos that is a little bit closer to the targetPos. Now let’s look at the logic.
Let’s say we start at the top of the screen, and our acceleration is 0.25 - and we decide to scroll down 10% of the screen. Let’s assume that 10% of the screen is equivalent to 2 seconds of our video. Thus, our currentPos is at 0 (since we’re starting at the top of the screen), and our targetPos is at 2. Here’s how the interval will run:
FIRST RUN
currentPos starts off at 0
currentPos = 0 + (2 - 0) * 0.25 = 0.5. Now currentPos equals 0.5.
Is currentPos equal to its last value? No, the last value was 0 and now it is 0.5.
Because currentPos has changed values, we set the current time of the video to the new currentPos. Now, the video has been scrubbed 0.5 seconds.
SECOND RUN
currentPos starts off at 0.5
currentPos = 0.5 + (2 - 0.5) * 0.25 = 0.875. Now currentPos equals 0.875.
Is currentPos equal to its last value? No, the last value was 0.5 and now it is 0.875.
Because currentPos has changed values, we set the current time of the video to the new currentPos. Now, the video has been scrubbed 0.875 seconds.
THIRD RUN
currentPos starts off at 0.875
currentPos = 0.875 + (2 - 0.875) * 0.25 = 1.15625. Now currentPos equals 1.15625.
Is currentPos equal to its last value? No, the last value was 0.875 and now it is 1.15625.
Because currentPos has changed values, we set the current time of the video to the new currentPos. Now, the video has been scrubbed 1.15625 seconds.
And on and on, until we finally reach the targetPos, at which point currentPos will be equal to its last value and the video will stop scrubbing. This interval ends up creating a smooth, gradual transition between the two timestamps, decelerating the closer it gets to its target.
Phew, that was a lot of stuff! At least now we’ve got the video scrubbing smoothly. Now we’ve gotta tackle those pinned text blurbs I mentioned earlier.
To do this, we’ll go ahead and use a Javascript library called ScrollMagic. ScrollMagic allows you to create all kinds of cool scroll-based behaviors and interactions. In this case, what we want to do is quite simple. We just want to pin an element to the screen for a certain duration, and then let it scroll offscreen when that duration is up.
Once you’ve added ScrollMagic to your project, you’re going to want to create a Controller variable. This is what controls all of our ScrollMagic behavior:
// init ScrollMagic Controller
var controller = new ScrollMagic.Controller();
Now that we’ve got our Controller in place, we can start adding our pins. Each pin will be referred to as a ‘scene’. They have already been created on the page as plain HTML elements - our job is to grab each element, turn them into scenes, chain the scenes one after another, and set a specified duration (in pixels) for how long each scene should be pinned. Here’s an example of how we set up one of these scenes:
// Scene2 Handler
var scene2 = new ScrollMagic.Scene({
triggerElement: "#pinned-trigger2", // point of execution
triggerHook: 0.3,
duration: 1000 // pin the element for a total of 1000px
})
.setPin("#pinned-trigger2") // the element we want to pin
Two things are happening here. We are creating the scene and defining its parameters, and then we are setting the pin. Let’s break down each parameter:
triggerElement: This is the element that will trigger our setPin() function. In this case, it is an element with the ID of ‘pinned-trigger2’. When it enters the screen, our setPin() function will run.
triggerHook: This describes where the element has to be on the screen to trigger the function. In this case, we will not trigger our setPin() function until the element has risen at least 30% from the bottom of the screen.
duration: This determines how long the scene will run (in terms of pixels scrolled)
Once we have set up these parameters, we run the setPin() function to pin the scene we’ve just created - making sure to include the ID of the element we want to pin. This is the function that defines the type of behavior we want. There are many other functions that ScrollMagic offers (such as setTween(), which can be used for scroll-based animations), but in this case we are only concerned with setting pins.
Once we’ve finished setting up all of our scenes, we just need to add them all to the controller:
// Add Scenes to ScrollMagic Controller
controller.addScene([
scene1,
scene2,
scene3,
scene4,
scene5,
scene6,
scene7,
scene8,
scene9,
scene10,
scene11,
scene12,
]);
And we’re done! We now have our page working exactly as it should - a background video that smoothly scrubs back and forth based on your scroll position, and a set of pinned blurbs positioned at certain points on the page. Hope you got something out of this article!
Interested in seeing the final project? Take a look here.
Concept and design by Her& Himsel, 3D animation by Independent Studios US.