I have a set of 100vh
sections with child
images and text. I'd like to apply a scroll transform
to the text to create a parallax effect that moves the text over the image. I've gotten it to work for a single element using vanilla javascript however, I can't quite get it right to apply the same effect to multiple sections with the same class
. As it stands now, the effect is applied to all the divs
, regardless of if their parent section
is in the viewport.
Can you help me edit the javascript such that the child div with the class
scroll
only transforms if its parent section
is in the viewport?
Thanks,
window.addEventListener('scroll', function(e) {
var section = document.querySelectorAll('.section');
var length = section.length
for (var i = 0; i < length; i++) {
var bounding = section[i].getBoundingClientRect();
if (
bounding.top >=0 || bounding.bottom >=0
) {
const target = section[i].querySelector('.scroll');
var rate = window.pageYOffset * -0.5;
target.style.transform = 'translate3d(0px, '+rate+'px, 0px)'
}
}
});
/*!
Theme Name: Frozen Land
Author: Will Caulfield
Author URI: http://caulfield.co/
Description: Description
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: LICENSE
Text Domain: frozenland.co
*/
body {
font-family: 'Lato', sans-serif;
}
p, i {
color: #9c5f89;
}
h1 {
color: white;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 800;
font-size: calc(70px + 0.3vw);
margin: 10px 0px 25px 0px;
}
h3 {
color: white;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 800;
font-size: calc(30px + 0.3vw);
margin: 10px 0px 25px 0px;
}
main {
background: -webkit-gradient(linear, left top, left bottom, from(#f9c4cc), to(#f27aaa));
background: linear-gradient(#f9c4cc, #f27aaa);
}
footer {
background-color: #c9db79;
height: 500px;
}
section.local {
height: 300px;
}
section.hero {
height: 100vh;
}
@-webkit-keyframes floatIce {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-15px);
transform: translatey(-15px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatIce {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-15px);
transform: translatey(-15px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@-webkit-keyframes floatText {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-10px);
transform: translatey(-10px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatText {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-10px);
transform: translatey(-10px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@-webkit-keyframes floatLand {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-5px);
transform: translatey(-5px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatLand {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-5px);
transform: translatey(-5px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
.hero img {
margin-top: 100px;
margin-bottom: 100px;
width: 500px;
}
div.hero {
position: relative;
top: -50px;
}
.hero-ice {
width: 200px;
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatIce 6s ease-in-out infinite;
animation: floatIce 6s ease-in-out infinite;
}
.hero-text {
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatText 6s ease-in-out infinite;
animation: floatText 6s ease-in-out infinite;
}
.hero-land {
width: 250px;
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatText 6s ease-in-out infinite;
animation: floatText 6s ease-in-out infinite;
}
.candy-float {
width: 50px;
}
section.cone {
height: 100vh;
}
.cone img {
width: 300px;
}
nav img {
width: 300px;
margin-top: 15px;
}
section.froyo {
height: 100vh;
margin-top: 150px;
}
.froyo img {
width: 400px;
}
.froyo div {
position: absolute;
}
section.toppings {
height: 100vh;
margin-top: 150px;
}
.toppings img {
width: 400px;
}
.toppings div {
position: absolute;
}
/*# sourceMappingURL=style.css.map */
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- Custom CSS -->
<link rel="stylesheet" href="css/style.css">
<!-- Googl Fonts -->
<link href="https://fonts.googleapis.com/css?family=Lato:400,900&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.0/css/all.css" integrity="sha384-REHJTs1r2ErKBuJB0fCK99gCYsVjwxHrSU0N7I1zl9vZbggVJXRMsv/sLlOAGb4M" crossorigin="anonymous">
<!-- Scripts -->
<title>Hello, world!</title>
</head>
<body>
<main>
<section class="hero section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img class="scroll" data-rate="-0.5" src="img/logo.png" />
</section>
<section class="cone section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/cone.png"/>
<div class="scroll" data-rate="-0.5">
<h3>delicious</h3>
<h1>ice cream</h1>
</div>
</section>
<section class="froyo section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/froyo.png"/>
<div class="scroll" data-rate="-0.5">
<h3>Frozen</h3>
<h1>Yogurt</h1>
</div>
</section>
<section class="toppings section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/toppings.png" />
<div class="scroll">
<h3>and lots of</h3>
<h1>toppings!</h1>
</div>
</section>
</main>
<footer class="container-fluid">
<div class="row">
<div class="col">
</div>
<div class="col text-center pt-5">
<p>10911 Lindbrook Drive<br/>Los Angeles, CA 90024</p>
<p>(310) 824-8191</p>
<p>© 2020</p>
</div>
<div class="col">
</div>
</div>
</footer>
<script>
// window.addEventListener('scroll', function(e) {
// var section = document.querySelector('.section');
// var bounding = section.getBoundingClientRect();
// if (
// bounding.top >=0 || bounding.bottom >=0
// ) {
// const target = section.querySelector('.scroll');
// var rate = window.pageYOffset * -0.5;
// target.style.transform = 'translate3d(0px, '+rate+'px, 0px)'
// } else {
// console.log("Not in Viewport!");
// }
// });
// window.addEventListener('scroll', function(e) {
// const target = document.querySelectorAll('.scroll');
// //var scrolled = window.pageYOffset;
// //var rate = scrolled * -0.5;
// //target.style.transform = 'translate3d(0px, '+rate+'px, 0px)'
// var index = 0, length = target.length;
// for (index; index < length; index++) {
// var pos = window.pageYOffset * target[index].dataset.rate;
// target[index].style.transform = 'translate3d(0px, '+pos+'px, 0px)';
// };
// });
</script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<!-- Other Scripts -->
<script src="/js/main.js"></script>
</body>
</html>
Use the IntersectionObserver API
to observer if elements are in view, or not. This api monitors elements that you specify and triggers a callback whenever an observed element enters or leaves the viewport. This way you can create a list of elements that are in the viewport.
To help with that create a Set
. Sets take any kind of type of value and stores it in a list, like a supercharged array, but only with unique values. So no element will be in there twice.
In the intersectionCallback
check if an element leaves the viewport and add it to the set. If it leaves the screen, remove it from the set.
Then instead of looping over each .section
loop over the set with the sections that are currently in the view. The contents of this set changes constantly depending on where the scroll position is currently.
In the scroll
event callback calculate the position of the section relative to the top of the screen. You'll need to offset each transformation to the section to get the correct position of your parallax elements.
I hope this helps you out.
Let me know if anything here is unclear or if I didn't help you in any way.
Cheers!
I felt that the performance was a little janky so I looked into the code for improvements. The first one was adding a passive listener
to the scroll
event listener which tells the listener not to wait for event.preventDefault()
. Because it will not wait it will cycle faster to the next time the scroll
event is fired.
Then there are the calculations. Each time the scroll
event is fired the offset is calculated. But the offset does not change when scrolling, so it would make sense to calculate it first and use it later when needed. The same goes for selecting the target
element, that one also stays the same.
So I've added two Map
objects that hold a connection between the section and their offset + targets. So now the scroll function will get the previously calculated offset and target based on the current section
it is looping over.
All theses additions will significantly improve the scroll performance and make your parallax effects feel smooth. I know it is a lot of code and I don't expect that everything makes sense right away, but if you want I could try to explain what everything does and why/how you use it.
const inViewSections = new Set();
const offsetMap = new Map();
const targetMap = new Map();
function calculateOffsetsOfSections(sections, map) {
const bodyTop = document.body.getBoundingClientRect().top;
sections.forEach(function(section) {
const sectionTop = section.getBoundingClientRect().top;
const offset = sectionTop - bodyTop;
map.set(section, offset);
});
}
function mapTargetsOfSections(sections, map) {
sections.forEach(function(section) {
const target = section.querySelector('.scroll');
if (target !== null) {
map.set(section, target);
}
});
}
function observeSections(sections, observer) {
sections.forEach(function(section) {
observer.observe(section);
});
}
function intersectionCallback(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting === true) {
inViewSections.add(entry.target);
} else {
inViewSections.delete(entry.target);
}
});
}
const observer = new IntersectionObserver(intersectionCallback, {
root: null,
rootMargin: '0px',
threshold: [0]
});
let sections = document.querySelectorAll('.section');
calculateOffsetsOfSections(sections, offsetMap);
mapTargetsOfSections(sections, targetMap);
observeSections(sections, observer);
window.addEventListener('resize', function(e) {
calculateOffsetsOfSections(sections, offsetMap);
});
window.addEventListener('scroll', function(e) {
inViewSections.forEach(function(section) {
if (offsetMap.has(section) && targetMap.has(section)) {
const target = targetMap.get(section);
const offset = offsetMap.get(section);
var rate = Math.round((window.pageYOffset - offset) * -0.5);
target.style.transform = 'translate3d(0px, ' + rate + 'px, 0px)';
}
});
}, {passive: true});
/*!
Theme Name: Frozen Land
Author: Will Caulfield
Author URI: http://caulfield.co/
Description: Description
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: LICENSE
Text Domain: frozenland.co
*/
body {
font-family: 'Lato', sans-serif;
}
p,
i {
color: #9c5f89;
}
h1 {
color: white;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 800;
font-size: calc(70px + 0.3vw);
margin: 10px 0px 25px 0px;
}
h3 {
color: white;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 800;
font-size: calc(30px + 0.3vw);
margin: 10px 0px 25px 0px;
}
main {
background: -webkit-gradient(linear, left top, left bottom, from(#f9c4cc), to(#f27aaa));
background: linear-gradient(#f9c4cc, #f27aaa);
}
footer {
background-color: #c9db79;
height: 500px;
}
section.local {
height: 300px;
}
section.hero {
height: 100vh;
}
@-webkit-keyframes floatIce {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-15px);
transform: translatey(-15px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatIce {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-15px);
transform: translatey(-15px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@-webkit-keyframes floatText {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-10px);
transform: translatey(-10px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatText {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-10px);
transform: translatey(-10px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@-webkit-keyframes floatLand {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-5px);
transform: translatey(-5px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
@keyframes floatLand {
0% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
50% {
-webkit-transform: translatey(-5px);
transform: translatey(-5px);
}
100% {
-webkit-transform: translatey(0px);
transform: translatey(0px);
}
}
.hero img {
margin-top: 100px;
margin-bottom: 100px;
width: 500px;
}
div.hero {
position: relative;
top: -50px;
}
.hero-ice {
width: 200px;
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatIce 6s ease-in-out infinite;
animation: floatIce 6s ease-in-out infinite;
}
.hero-text {
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatText 6s ease-in-out infinite;
animation: floatText 6s ease-in-out infinite;
}
.hero-land {
width: 250px;
-webkit-transform: translatey(0px);
transform: translatey(0px);
-webkit-animation: floatText 6s ease-in-out infinite;
animation: floatText 6s ease-in-out infinite;
}
.candy-float {
width: 50px;
}
section.cone {
height: 100vh;
}
.cone img {
width: 300px;
}
nav img {
width: 300px;
margin-top: 15px;
}
section.froyo {
height: 100vh;
margin-top: 150px;
}
.froyo img {
width: 400px;
}
.froyo div {
position: absolute;
}
section.toppings {
height: 100vh;
margin-top: 150px;
}
.toppings img {
width: 400px;
}
.toppings div {
position: absolute;
}
/*# sourceMappingURL=style.css.map */
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- Custom CSS -->
<link rel="stylesheet" href="css/style.css">
<!-- Googl Fonts -->
<link href="https://fonts.googleapis.com/css?family=Lato:400,900&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.0/css/all.css" integrity="sha384-REHJTs1r2ErKBuJB0fCK99gCYsVjwxHrSU0N7I1zl9vZbggVJXRMsv/sLlOAGb4M" crossorigin="anonymous">
<!-- Scripts -->
<title>Hello, world!</title>
</head>
<body>
<main>
<section class="hero section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img class="scroll" data-rate="-0.5" src="img/logo.png" />
</section>
<section class="cone section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/cone.png" />
<div class="scroll" data-rate="-0.5">
<h3>delicious</h3>
<h1>ice cream</h1>
</div>
</section>
<section class="froyo section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/froyo.png" />
<div class="scroll" data-rate="-0.5">
<h3>Frozen</h3>
<h1>Yogurt</h1>
</div>
</section>
<section class="toppings section text-center d-flex flex-column justify-content-center align-items-center position-relative">
<img src="img/toppings.png" />
<div class="scroll">
<h3>and lots of</h3>
<h1>toppings!</h1>
</div>
</section>
</main>
<footer class="container-fluid">
<div class="row">
<div class="col">
</div>
<div class="col text-center pt-5">
<p>10911 Lindbrook Drive<br/>Los Angeles, CA 90024</p>
<p>(310) 824-8191</p>
<p>© 2020</p>
</div>
<div class="col">
</div>
</div>
</footer>
<script>
// window.addEventListener('scroll', function(e) {
// var section = document.querySelector('.section');
// var bounding = section.getBoundingClientRect();
// if (
// bounding.top >=0 || bounding.bottom >=0
// ) {
// const target = section.querySelector('.scroll');
// var rate = window.pageYOffset * -0.5;
// target.style.transform = 'translate3d(0px, '+rate+'px, 0px)'
// } else {
// console.log("Not in Viewport!");
// }
// });
// window.addEventListener('scroll', function(e) {
// const target = document.querySelectorAll('.scroll');
// //var scrolled = window.pageYOffset;
// //var rate = scrolled * -0.5;
// //target.style.transform = 'translate3d(0px, '+rate+'px, 0px)'
// var index = 0, length = target.length;
// for (index; index < length; index++) {
// var pos = window.pageYOffset * target[index].dataset.rate;
// target[index].style.transform = 'translate3d(0px, '+pos+'px, 0px)';
// };
// });
</script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<!-- Other Scripts -->
<script src="/js/main.js"></script>
</body>
</html>