I have made a wrapper in which I have animated the same effect as Apple on their Airpods Pro page. It's basically a video, when I scroll the video plays bit by bit. The video's position is fixed so the text nicely scrolls over it. However, the text is only visible when between the offset of a specific division (text-display).
That part works fine. Now I want that when the user has scrolled to the end of the video, and thus the animation is finished, that the video-effect-wrapper goes from a fixed position to a relative position. So that website will scroll it's content normally after the video-animation.
JSFIDDLE CODE + DEMO
This is an example of what I already tried:
//If video-animation ended: Make position of video-wrapper relative to continue scrolling
if ($(window).scrollTop() >= $("#video-effect-wrapper").height()) {
$(video).css("position", "relative");
$("#video-effect-wrapper .text").css("display", "none");
}
This kind of works... But is everything except smooth. And it also needs to be possible to reverse scroll the webpage backwards.
Problems I encountered when trying to fix this problem:
An element with position: fixed; is positioned relative to the viewport, which means it always stays in the same place even if the page is scrolled. The top, right, bottom, and left properties are used to position the element.
To position an element "fixed" relative to a parent element, you want position:absolute on the child element, and any position mode other than the default or static on your parent element. This will position childDiv element 50 pixels left and 20 pixels down relative to parentDiv's position.
Set everything up as you would if you want to position: absolute inside a position: relative container, and then create a new fixed position div inside the div with position: absolute , but do not set its top and left properties. It will then be fixed wherever you want it, relative to the container.
The scrollTop() method sets or returns the vertical scrollbar position for the selected elements. Tip: When the scrollbar is on the top, the position is 0. When used to return the position: This method returns the vertical position of the scrollbar for the FIRST matched element.
When doing some reverse engineering on the Airpods Pro page, we notice that the animation doesn't use a video
, but a canvas
. The implementation is as follows:
HTMLImageElement
scroll
DOM event and request an animation frame corresponding to the nearest image, with requestAnimationFrame
ctx.drawImage
(ctx
being the 2d
context of the canvas
element)The requestAnimationFrame
function should help you achieve a smoother effect as the frames will be deferred and synchronized with the "frames per second" rate of the target screen.
For more information on how to properly display a frame on a scroll event, you can read this: https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event
That being said, concerning your main problem, I have a working solution that consists in:
video
element. Its purpose is to avoid the video to overlap the rest of the HTML when set to absolute
positionscroll
event callback, when the placeholder reaches the top of the viewport, set the video's position to absolute
, and the right top
valueThe idea is that the video always remains out of the flow, and takes place over the placeholder at the right moment when scrolling to the bottom.
Here is the JavaScript:
//Get video element
let video = $("#video-effect-wrapper video").get(0);
video.pause();
let topOffset;
$(window).resize(onResize);
function computeVideoSizeAndPosition() {
const { width, height } = video.getBoundingClientRect();
const videoPlaceholder = $("#video-placeholder");
videoPlaceholder.css("width", width);
videoPlaceholder.css("height", height);
topOffset = videoPlaceholder.position().top;
}
function updateVideoPosition() {
if ($(window).scrollTop() >= topOffset) {
$(video).css("position", "absolute");
$(video).css("left", "0px");
$(video).css("top", topOffset);
} else {
$(video).css("position", "fixed");
$(video).css("left", "0px");
$(video).css("top", "0px");
}
}
function onResize() {
computeVideoSizeAndPosition();
updateVideoPosition();
}
onResize();
//Initialize video effect wrapper
$(document).ready(function () {
//If .first text-element is set, place it in bottom of
//text-display
if ($("#video-effect-wrapper .text.first").length) {
//Get text-display position properties
let textDisplay = $("#video-effect-wrapper #text-display");
let textDisplayPosition = textDisplay.offset().top;
let textDisplayHeight = textDisplay.height();
let textDisplayBottom = textDisplayPosition + textDisplayHeight;
//Get .text.first positions
let firstText = $("#video-effect-wrapper .text.first");
let firstTextHeight = firstText.height();
let startPositionOfFirstText = textDisplayBottom - firstTextHeight + 50;
//Set start position of .text.first
firstText.css("margin-top", startPositionOfFirstText);
}
});
//Code to launch video-effect when user scrolls
$(document).scroll(function () {
//Calculate amount of pixels there is scrolled in the video-effect-wrapper
let n = $(window).scrollTop() - $("#video-effect-wrapper").offset().top + 408;
n = n < 0 ? 0 : n;
//If .text.first is set, we need to calculate one less text-box
let x = $("#video-effect-wrapper .text.first").length == 0 ? 0 : 1;
//Calculate how many percent of the video-effect-wrapper is currenlty scrolled
let percentage = n / ($(".text").eq(1).outerHeight(true) * ($("#video-effect-wrapper .text").length - x)) * 100;
//console.log(percentage);
//console.log(percentage);
//Get duration of video
let duration = video.duration;
//Calculate to which second in video we need to go
let skipTo = duration / 100 * percentage;
//console.log(skipTo);
//Skip to specified second
video.currentTime = skipTo;
//Only allow text-elements to be visible inside text-display
let textDisplay = $("#video-effect-wrapper #text-display");
let textDisplayHeight = textDisplay.height();
let textDisplayTop = textDisplay.offset().top;
let textDisplayBottom = textDisplayTop + textDisplayHeight;
$("#video-effect-wrapper .text").each(function (i) {
let text = $(this);
if (text.offset().top < textDisplayBottom && text.offset().top > textDisplayTop) {
let textProgressPoint = textDisplayTop + (textDisplayHeight / 2);
let textScrollProgressInPx = Math.abs(text.offset().top - textProgressPoint - textDisplayHeight / 2);
textScrollProgressInPx = textScrollProgressInPx <= 0 ? 0 : textScrollProgressInPx;
let textScrollProgressInPerc = textScrollProgressInPx / (textDisplayHeight / 2) * 100;
//console.log(textScrollProgressInPerc);
if (text.hasClass("first"))
textScrollProgressInPerc = 100;
text.css("opacity", textScrollProgressInPerc / 100);
} else {
text.css("transition", "0.5s ease");
text.css("opacity", "0");
}
});
updateVideoPosition();
});
Here is the HTML:
<div id="video-effect-wrapper">
<video muted autoplay>
<source src="https://ndvibes.com/test/video/video.mp4" type="video/mp4" id="video">
</video>
<div id="text-display"/>
<div class="text first">
Scroll down to test this little demo
</div>
<div class="text">
Still a lot to improve
</div>
<div class="text">
So please help me
</div>
<div class="text">
Thanks! :D
</div>
</div>
<div id="video-placeholder">
</div>
<div id="other-parts-of-website">
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
</div>
You can try here: https://jsfiddle.net/crkj1m0v/3/
If you want the video to lock back in place as you scroll back up, you'll need to mark the place where you switch from fixed
to relative
.
//Get video element
let video = $("#video-effect-wrapper video").get(0);
video.pause();
let videoLocked = true;
let lockPoint = -1;
const vidHeight = 408;
//Initialize video effect wrapper
$(document).ready(function() {
const videoHeight = $("#video-effect-wrapper").height();
//If .first text-element is set, place it in bottom of
//text-display
if ($("#video-effect-wrapper .text.first").length) {
//Get text-display position properties
let textDisplay = $("#video-effect-wrapper #text-display");
let textDisplayPosition = textDisplay.offset().top;
let textDisplayHeight = textDisplay.height();
let textDisplayBottom = textDisplayPosition + textDisplayHeight;
//Get .text.first positions
let firstText = $("#video-effect-wrapper .text.first");
let firstTextHeight = firstText.height();
let startPositionOfFirstText = textDisplayBottom - firstTextHeight + 50;
//Set start position of .text.first
firstText.css("margin-top", startPositionOfFirstText);
}
//Code to launch video-effect when user scrolls
$(document).scroll(function() {
//Calculate amount of pixels there is scrolled in the video-effect-wrapper
let n = $(window).scrollTop() - $("#video-effect-wrapper").offset().top + vidHeight;
n = n < 0 ? 0 : n;
// console.log('n: ' + n);
//If .text.first is set, we need to calculate one less text-box
let x = $("#video-effect-wrapper .text.first").length == 0 ? 0 : 1;
//Calculate how many percent of the video-effect-wrapper is currenlty scrolled
let percentage = n / ($(".text").eq(1).outerHeight(true) * ($("#video-effect-wrapper .text").length - x)) * 100;
//console.log(percentage);
//Get duration of video
let duration = video.duration;
//Calculate to which second in video we need to go
let skipTo = duration / 100 * percentage;
//console.log(skipTo);
//Skip to specified second
video.currentTime = skipTo;
//Only allow text-elements to be visible inside text-display
let textDisplay = $("#video-effect-wrapper #text-display");
let textDisplayHeight = textDisplay.height();
let textDisplayTop = textDisplay.offset().top;
let textDisplayBottom = textDisplayTop + textDisplayHeight;
$("#video-effect-wrapper .text").each(function(i) {
let text = $(this);
if (text.offset().top < textDisplayBottom && text.offset().top > textDisplayTop) {
let textProgressPoint = textDisplayTop + (textDisplayHeight / 2);
let textScrollProgressInPx = Math.abs(text.offset().top - textProgressPoint - textDisplayHeight / 2);
textScrollProgressInPx = textScrollProgressInPx <= 0 ? 0 : textScrollProgressInPx;
let textScrollProgressInPerc = textScrollProgressInPx / (textDisplayHeight / 2) * 100;
//console.log(textScrollProgressInPerc);
if (text.hasClass("first"))
textScrollProgressInPerc = 100;
text.css("opacity", textScrollProgressInPerc / 100);
} else {
text.css("transition", "0.5s ease");
text.css("opacity", "0");
}
});
//If video-animation ended: Make position of video-wrapper relative to continue scrolling
if (videoLocked) {
if ($(window).scrollTop() >= videoHeight) {
$('video').css("position", "relative");
videoLocked = false;
lockPoint = $(window).scrollTop() - 10;
// I gave it an extra 10px to avoid flickering between locked and unlocked.
}
} else if ($(window).scrollTop() < lockPoint) {
$('video').css("position", "fixed");
videoLocked = true;
}
});
});
body {
margin: 0;
padding: 0;
background-color: green;
}
#video-effect-wrapper {
height: auto;
width: 100%;
}
#video-effect-wrapper video {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: -2;
object-fit: cover;
}
#video-effect-wrapper::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
background: #000000;
background: linear-gradient(to top, #434343, #000000);
opacity: 0.4;
z-index: -1;
}
#video-effect-wrapper .text {
color: #FFFFFF;
font-weight: bold;
font-size: 3em;
width: 100%;
margin-top: 50vh;
font-family: Arial, sans-serif;
text-align: center;
opacity: 0;
/*
background-color: blue;
*/
}
#video-effect-wrapper .text.first {
margin-top: 50vh;
opacity: 1;
}
#video-effect-wrapper .text:last-child {
/*margin-bottom: 100vh;*/
margin-bottom: 50vh;
}
#video-effect-wrapper #text-display {
display: block;
width: 100%;
height: 225px;
position: fixed;
top: 50%;
transform: translate(0, -50%);
z-index: -1;
/*
background-color: red;
*/
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="video-effect-wrapper">
<video muted autoplay>
<source src="https://ndvibes.com/test/video/video.mp4" type="video/mp4" id="video">
</video>
<div id="text-display"></div>
<div class="text first">
Scroll down to test this little demo
</div>
<div class="text">
Still a lot to improve
</div>
<div class="text">
So please help me
</div>
<div class="text">
Thanks! :D
</div>
</div>
<div id="other-parts-of-website">
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
<p>
Normal scroll behaviour wanted.
</p>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With