Janky parallax text with GSAP - javascript

I've tried to make a text with parallax and failed. Code seems innocuous and doesn't seem to be doing anything wrong, yet the scroll look&feel is quite wrong.
Markup is like this:
<!-- content above -->
<section class="section-parallax">
<div class="container text-center">
<div class="flex-row-columns">
<div class="flex-row">
<h2 class="heading flex-8">
<span class="heading-sub">Some Header</span>
<span class="heading-bottom">getting also long<sup>
</h2>
</div>
</div>
</div>
</section>
<!-- content below -->
Styles like this:
.container {
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
padding-left: 15px;
padding-right: 15px;
width: 1400px;
}
.text-center {
text-align: center;
}
.section-parallax {
background: black;
overflow: hidden;
&,
& .container,
& .flex-row {
min-height: 545px;
}
& .container {
transform: translate3d(0, -100%, 0);
}
}
.flex-row-columns {
display: flex;
flex-direction: column;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
}
.flex-row {
align-items: center;
display: flex;
justify-content: center;
margin-left: -15px;
margin-right: -15px;
}
.flex-8 {
flex-basis: 66.66666666666667%;
padding-left: 15px;
padding-right: 15px;
}
.heading {
color: white;
font-size: 54px;
letter-spacing: .66px;
line-height: 1.273em;
}
.heading-sub {
display: block;
margin-bottom: 20px;
}
And finally the JS I've used is like this:
class ParallaxSection {
constructor() {
this.el = document.querySelector('.section-parallax');
this.els = {
container: this.el.querySelector('.container')
};
this.calcBounds();
window.addEventListener('scroll', this.onScroll.bind(this));
window.addEventListener('resize', () => {
this.calcBounds();
this.onScroll();
});
}
calcBounds() {
if (this.tween) {
this.tween.kill();
this.els.container.removeAttribute('style');
}
const rect = this.el.getBoundingClientRect();
const scrollY = ParallaxSection.getScroll();
this.start = (rect.top + scrollY) - (window.innerHeight * 0.75);
this.end = this.start + this.el.offsetHeight + window.innerHeight;
this.tween = TweenLite.fromTo(this.els.container, 1, {
css: {
force3D: true,
y: -this.el.offsetHeight
}
}, {
paused: true,
css: {
force3D: true,
y: this.end - this.start - this.el.offsetHeight
},
ease: Linear.easeNone
});
}
onScroll() {
const scroll = ParallaxSection.getScroll();
if (scroll >= this.start && scroll <= this.end) {
const diff = this.end - this.start;
const offset = scroll - this.start;
const perc = offset / diff;
this.tween.progress(perc);
}
}
static getScroll() {
return window.pageYOffset || document.documentElement.scrollTop;
}
}
const p = new ParallaxSection();
Now, the odd thing is that while trying to discover the issue I put this into a Pen so I could try to see where it failed and in the pen seemed alright. That led me to remove all the elements on my page and replicate the pen exactly, turns out that, for some unknown reason the effect is perfect on codepen and fails out of it.
I've downloaded the whole HTML that codepen generates and it suffers the same experience.
Pen is here to be seen.
What's wrong with it?

I think the problems comes from trying to execute the logic on every scrolled pixel. Maybe you can fix that by using:
passive event listeners
debouncing the event handler on scroll

Related

Can I use requestAnimationFrame to smooth out scroll behaviour?

I have a small scroll effect which simulate that a logo will disappear if a lower div will scroll over it.
Currently I'm checking if two divs are intersecting. If this is true, then the height of the div of the logo will decrease with the scroll position of the div beneath.
Unfortunately, my demo is not foolproof and some fragments of the logo are still visible.
Is there a way to do this jank-free? Maybe with requestAnimationFrame?
function elementsOverlap(el1, el2) {
const domRect1 = el1.getBoundingClientRect();
const domRect2 = el2.getBoundingClientRect();
return !(
domRect1.top > domRect2.bottom ||
domRect1.right < domRect2.left ||
domRect1.bottom < domRect2.top ||
domRect1.left > domRect2.right
);
}
const el1 = document.querySelector(".logo");
const el2 = document.querySelector(".clickblocks");
let scrollPositionEl2;
let heightDifference;
const logoHeight = el1.offsetHeight;
document.addEventListener("DOMContentLoaded", () => {
var scrollDirectionDown;
scrollDirectionDown = true;
window.addEventListener("scroll", () => {
if (this.oldScroll > this.scrollY) {
scrollDirectionDown = false;
} else {
scrollDirectionDown = true;
}
this.oldScroll = this.scrollY;
// test
if (scrollDirectionDown) {
if (elementsOverlap(el1, el2) === true) {
scrollPositionEl2 = el2.getBoundingClientRect().top;
heightDifference = logoHeight - scrollPositionEl2 + 100;
//console.log(logoHeight - heightDifference);
el1.style.height = `${logoHeight - heightDifference}px`;
}
} else {
//scrolling up
scrollPositionEl2 = el2.getBoundingClientRect().top - 100;
el1.style.height = `${scrollPositionEl2}px`;
//console.log(logoHeight);
}
});
});
#import url("https://fonts.googleapis.com/css2?family=Inter:wght#900&display=swap");
.wrapper {
max-width: 100vw;
margin: 0 auto;
background-image: url("https://picsum.photos/1920/1080");
background-size: cover;
background-attachment: fixed;
height: 1200px;
position: relative;
&::after {
content: "";
position: absolute;
background: rgba(0, 0, 0, 0.3);
width: 100%;
height: 100%;
inset: 0;
}
}
body {
margin: 0;
}
main {
width: 100%;
height: 100vh;
position: relative;
z-index: 1;
}
.clickblocks {
width: 100%;
height: 200px;
display: grid;
grid-template-columns: repeat(12, (minmax(0, 1fr)));
}
.clickblock {
transition: all ease-in-out 0.2s;
backdrop-filter: blur(0px);
border: 1px solid #fff;
height: 100%;
grid-column: span 6 / span 6;
font-size: 54px;
font-weight: 700;
padding: 24px;
font-family: "Inter", sans-serif;
color: white;
text-transform: uppercase;
&:hover {
backdrop-filter: blur(10px);
}
}
.logo {
background: url("https://svgshare.com/i/ivR.svg");
width: 100%;
background-repeat: no-repeat;
background-position: top;
position: fixed;
top: 100px;
}
.logo-wrapper {
position: relative;
}
<div class="wrapper">
<main>
<div class="logo-wrapper" style="height: 390px">
<div class="logo" style="height: 300px">
</div>
</div>
<div class="clickblocks">
<div class="clickblock">
Some Content
</div>
</div>
</main>
</div>
Few things here to optimize your performance.
getBoundingClientRect() is a rather expensive calculation. If there are NO other options it's fine.
The Intersection Observer API is a lot more performant, and you can set the root element on the API. Then observe the element that is moving. This should be able to telly you if their are colliding.
Whenever you do scroll based logic, you should really try and throttle the logic so that the scroll any fires ever 16.6ms. That will reduce the number of times the calculations are made, and speed things up on the FE.
Learn how to use Google Chrome's performance tab. It can be overwhelming at first, but it gives you the ability to drill into the exact piece of code that's slowing your site down.
Learn about JS's event loop, and what's really going on under the hood. This video by Jake Archibald really help me understand it.
Hope this helped, sorry that I didn't give you an actual solution.

Move DIV using CSS Grid

I created a CSS Grid layout with global variables:
#app{
--width-l: 0.5fr;
--width-c: 0.5fr;
}
So I have a bar in the middle of my screen. On the other hand, in JavaScript I have two events that observe if the mouse is pressed (mousedown) and in motion (mousemove) that move the div. It has a problem, the movement is above the mouse position and at the height of the #app. So it works in parts, when I'm near the top, the div#bar doesn't go up anymore, and near the bottom, the same thing happens but at a greater distance.
I'm looking for a solution to make the transition smoothly, using the grid positions.
This is the code I created to try:
var split = document.querySelector(".split");
var app = document.querySelector("#app");
var position = app.getBoundingClientRect();
var isMouseMove = false;
split.addEventListener("mousedown", (e) => {
isMouseMove = true;
this.addEventListener("mouseup", (e) => {
isMouseMove = false;
});
});
split.addEventListener("mousemove", (e) => {
if (isMouseMove) {
let fullSize = app.offsetHeight;
let average = (100 * (e.y - position.top)) / fullSize;
let up = (average / 100).toFixed(4);
let down = (1 - average / 100).toFixed(4);
app.style.setProperty("--width-l", `${up}fr`);
app.style.setProperty("--width-c", `${down}fr`);
}
});
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
header {
background: aquamarine;
height: 50px;
}
footer {
background: aqua;
height: 50px;
}
#app {
--width-l: 0.5fr;
--width-c: 0.5fr;
height: calc(100vh - 100px);
display: grid;
grid-template: "aside up" var(--width-l) "aside split" 50px "aside down" var(
--width-c
) / 100px auto;
}
#app .aside {
grid-area: aside;
background: blue;
}
#app .up {
grid-area: up;
background: yellow;
resize: horizontal;
}
#app .split {
grid-area: split;
background: floralwhite;
display: flex;
justify-content: center;
align-items: center;
font-weight: 900;
color: tomato;
user-select: none;
}
#app .down {
grid-area: down;
background: green;
}
#app .split:active {
cursor: move;
}
<header></header>
<div id="app">
<div class="aside"></div>
<div class="up"></div>
<div class="split"> CLICK AND MOVE </div>
<div class="down"></div>
</div>
<footer></footer>

I'm trying to make my navbar retract upwards when I scroll down and then appear again when I scroll up, but it doesn't do anything

I'm very new to writing code but I've seen this done the same way, I don't get why it does nothing. The issue is likely somewhere in the CSS part.
JavaScript
var navbar = document.getElementsByClassName("navbar");
var lastY = window.pageYOffset;
window.onscroll = function()
{
var currY = window.pageYOffset;
if (lastY > currY)
{
document.getElementsByClassName("navbar").style.top = "0";
}
else
{
document.getElementsByClassName("navbar").style.top = "-50px";
}
lastY = currY;
}
HTML- I'm not exactly sure why I have to put all the links in their separate navbar-items class divs, but if I don't do this they start overlapping the header (i want the navbar to have that name on the left and the links on the right) and also make the other contents of the page after the navbar vanish.
<div class="navbar">
<h1>Квартална кръчма Тримата глупаци</h1>
<div class="navbar-items">
<div class="navbar-items">Home</div>
<div class="navbar-items"><a style="color:red;" href="Menu.html">Menu</a></div>
<div class="navbar-items">About us</div>
<div class="navbar-items">Contact us</div>
</div>
</div>
CSS
.navbar {
overflow: hidden;
position: fixed;
display: flex;
align-items: center;
justify-content: space-between;
background:#505d61;
z-index: 99;
padding: 10px;
height: 60px;
top: 0;
width: 100%;
box-shadow: 0 0 10px black;
}
.navbar-items {
overflow: hidden;
float:left;
display:block;
margin-left: 40px;
margin-right: 5px;
gap: 80px;
font-size: 21px;
font-weight: 550;
}
.navbar a{
color:black;
text-decoration: none;
}
.navbar a:hover{
color: white;
}
As the comment from CBroe points to, you issue is that you are expecting an element back from document.getElementsByClassName("navbar") but you are actually getting back an array. To access the element you need to pull it out of the array, you can do this like document.getElementsByClassName("navbar")[0].
So updating your code to
var lastY = window.pageYOffset;
window.onscroll = function(){
var currY = window.pageYOffset;
if (lastY > currY){
document.getElementsByClassName("navbar")[0].style.top = "0";
}
else{
document.getElementsByClassName("navbar")[0].style.top = "-50px";
}
lastY = currY;
}
Should fix the issue.

How to rotate and scale cards while swiping

As I'm trying to make a carousel that rotates I wonder how to move the cards rotated and it scales from a minimum of 0.8(left and right cards) and a maximum scale of 1(center card) when the user keeps swiping
Scale:
left = 0.8
center = 1
right = 0.8
I'm trying to solve on how to rotate them using transform and z-index properties. The cards will also rotate however I'm still trying to make a formula on how to make a function that makes the cards rotate
Any alternative solutions are accepted The animation is
similar to this carousel from codepen however it doesn't swipe Carousel Rotate
const CONTAINER_FLEX = document.querySelector('.container-flex');
const items = document.querySelectorAll('.item');
let touchStartX = 0;
let touchMoveX = 0;
let count = 0;
let current_translate = 0;
let previous_translate = 0;
CONTAINER_FLEX.addEventListener('touchstart', (event) => {
touchStartX = event.touches[0].pageX;
});
CONTAINER_FLEX.addEventListener('touchmove', (event) => {
touchMoveX = event.touches[0].pageX;
current_translate = previous_translate + (touchMoveX - touchStartX);
console.log(current_translate);
items[1].style.transform = `translateX(${current_translate}px)`;
});
CONTAINER_FLEX.addEventListener('touchend', () => {
current_translate = touchMoveX - touchStartX;
previous_translate = current_translate;
});
*,
::before,
::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
background-color: #131b24;
}
.main-container {
padding: 30px 0;
height: 300px;
width: 900px;
border-top: 1px solid #444;
border-bottom: 1px solid #444;
overflow: hidden;
background-color: white;
}
.container-flex {
height: 100%;
display: flex;
transition: transform 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.item {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
min-width: 300px;
max-width: 300px;
}
.item h1 {
font-size: 40px;
color: white;
}
/* ITEMS */
.item-1 {
background-color: #2c3e50;
transform: translateX(100px);
z-index: 1;
}
.item-2 {
background-color: #3498db;
z-index: 2;
}
.item-3 {
background-color: #1abc9c;
transform: translateX(-100px);
z-index: 1;
}
<div class="main-container">
<div class="container-flex" id="container-flex">
<div class="item item-1">
<h1>1</h1>
</div>
<div class="item item-2">
<h1>2</h1>
</div>
<div class="item item-3">
<h1>3</h1>
</div>
</div>
</div>
Here is a working example https://jsfiddle.net/4ue5sgm9/3/
I wonder how to move the cards when it goes from 0 to 200 however
Have all your cards in an array.
var cards = [ thing1, thing2, thing3];
Use % - modulus operator, it's the secret for cycling back to the beginning
cardIndex = (cardIndex + 1) % cards.length
Copied from the chat. I made it verbose for clarity
scrollLeft() {
nextIndex = items.indexOf(displayed[2])
nextIndex = ++nextIndex % items.length-1
displayed = items[nextIndex]
nextIndex = ++nextIndex % items.length-1;
displayed.push( items[nextIndex] );
nextIndex = ++nextIndex % items.length-1;
displayed.push( items[nextIndex] )
}
P.S. Write and iterator for items array. An iterator stops after the last item. An iterator is why "for (x in thisArray)" works. BUT write the next() function to return "% this.length-1" instead => now you have a never-ending looper. All that code just above goes away.

Scroll Points / overflow-y breaking javascript function with window.pageYOffset

I have a page where I was using JS - specifically window.pageYOffset - and HTML data to change the inner HTML of the h1 footer, use l1 links to scroll the page to each section, and to add classes to each li when I reached the top of each section.work-page.
However, after I implemented CSS scroll points and added the div.container over the scrollable sections my javascript stopped working. Specifically when I set the overflow-y: scroll.
Basically when I made the div.container overflow-y: scroll; the doWork function stopped working and I can't figure out why.
^^^^ div.container in CSS
const doWork = function () {
const p01Tag = document.getElementById("p01")
const p02Tag = document.getElementById("p02")
const p03Tag = document.getElementById("p03")
const p04Tag = document.getElementById("p04")
const container = document.querySelector("div.container")
const sections = document.querySelectorAll("section.work-page")
const clientTag = document.querySelector("h2.about")
document.addEventListener("scroll", function () {
const pixels = window.pageYOffset
console.log(pixels)
sections.forEach(section => {
if(section.offsetTop - 400 <= pixels) {
clientTag.innerHTML = section.getAttribute("data-client")
if (section.hasAttribute("data-seen-1")) {
p01Tag.classList.add("move")
} else {
p01Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-2")) {
p02Tag.classList.add("move")
} else {
p02Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-3")) {
p03Tag.classList.add("move")
} else {
p03Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-4")) {
p04Tag.classList.add("move")
} else {
p04Tag.classList.remove("move")
}
}
})
})
// scrolling between projects ============================
function smoothScroll(target, duration) {
const targetTag = document.querySelector(target);
let targetPosition = targetTag.getBoundingClientRect().top;
const startPosition = window.pageYOffset;
let startTime = null;
function animation(currentTime) {
if(startTime === null ) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const run = ease(timeElapsed, startPosition, targetPosition, duration);
window.scrollTo(0,run);
if (timeElapsed < duration) requestAnimationFrame(animation)
}
function ease(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
requestAnimationFrame(animation)
}
p01Tag.addEventListener("click", function() {
smoothScroll('section.fn-up', 800)
})
p02Tag.addEventListener("click", function() {
smoothScroll('section.cameron', 800)
})
p03Tag.addEventListener("click", function() {
smoothScroll('section.truax', 800)
})
p04Tag.addEventListener("click", function() {
smoothScroll('section.romero', 800)
})
}
doWork()
const doInfo = function () {
const toggleTag = document.querySelector("a.contact")
const sectionTag = document.querySelector("section.info-page")
toggleTag.addEventListener("click", function () {
sectionTag.classList.toggle("open")
if (sectionTag.classList.contains("open")) {
toggleTag.innerHTML = "Close"
} else {
toggleTag.innerHTML = "Info"
}
})
}
doInfo()
html {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*, *:before, *:after {
-webkit-box-sizing: inherit;
-moz-box-sizing: inherit;
box-sizing: inherit;
}
body {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
background-color: #050505;
color: #ffffff;
line-height: 1.1;
}
header {
width: 100%;
z-index: 3;
position: fixed;
top: 0;
left: 0;
padding-top: 40px;
padding-left: 40px;
padding-right: 40px;
}
.contact {
float: right;
}
ul {
font-family: 'IBM Plex Mono', Arial;
font-size: 14px;
}
p {
margin-bottom: 50px;
}
/* Info page -------------------- */
section.info-page {
z-index: 2;
position: fixed;
top: -100vh;
left: 0;
display: flex;
margin-top: 100px;
margin-left: 40px;
margin-right: 40px;
width: 100vw;
min-height: 100vh;
max-width: 100vw;
transition: top 0.5s;
}
section.info-page.open {
top: 0;
}
/* Work page ------------------------*/
div.container {
top: 0;
left: 0;
max-width: 100vw;
max-height: 100vh;
/* WHEN WE ADD THIS OVERFLOW SETTING IN ORDER TO GET THE CSS SCROLL SNAP POINTS TO WORK IT BREAKS THE JAVASCRIPT */
/* overflow-y: scroll; */
scroll-snap-type: y mandatory;
position: relative;
z-index: 1;
}
div.work-info {
width: 13vw;
top: 0;
left: 0;
height: 100vh;
position: fixed;
z-index: 2;
padding-right: 80px;
display: flex;
align-items: center;
margin-left: 40px;
}
div.work-info li {
padding-bottom: 30px;
transition: transform 0.3s;
}
div.work-info li.move {
transform: translateX(15px);
}
footer {
width: 100%;
z-index: 1;
position: fixed;
bottom: 0;
left: 0;
padding-left: 40px;
padding-right: 40px;
padding-bottom: 40px;
color: #979797;
}
section.work-page {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
position: relative;
}
section.work-page img {
max-width: 60vw;
}
<body>
<!-- hidden modal that runs off of the info.js script -->
<section class="info-page">
<h1>
Hello
</h1>
</section>
<header>
<a class="contact" href="#">Info</a>
</header>
<!-- objects that get new classes with javascript on pageYOffset -->
<div class="work-info">
<ul>
<li id="p01" data-number="FN-UP Magazine">01</li>
<li id="p02" data-number="Cameron Tidball-Sciullo">02</li>
<li id="p03" data-number="Jacob Truax">03</li>
<li id="p04" data-number="Alexander Romero">04</li>
</ul>
</div>
<!-- scollable sections using the scroll points and triggering the pageYOffset -->
<div class="container">
<section class="work-page fn-up" data-client="FN-UP Magazine" data-seen-1="yes">
<div class="content">
<img src="lib/fn-up.png">
</div>
</section>
<section class="work-page cameron" data-client="Cameron Tidball-Sciullo" data-seen-2="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
<section class="work-page truax" data-client="Jacob Truax" data-seen-3="yes">
<div class="content">
<img src="lib/old.png">
</div>
</section>
<section class="work-page romero" data-client="Alexander Romero" data-seen-4="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
</div>
<footer class="footer">
<h2 class="about">FN-UP Magazine</h2>
</footer>
</body>
You have added a event listener to the page's Document object.
document.addEventListener("scroll", function () {
Then you calculate the number of pixels the document is currently scrolled along the vertical axis using window.pageYOffset.
const pixels = window.pageYOffset
When you set the CSS attribute overflow-y to scroll in the div.container element, new scrollbars appears on the window. According to MDN:
scroll
Content is clipped if necessary to fit the padding box. Browsers display scrollbars whether or not any content is actually clipped. (This prevents scrollbars from appearing or disappearing when the content changes.) Printers may still print overflowing content.
From that moment on, you are not scrolling the document, you are scrolling div.container. That won't trigger you scroll event.
You need to bound the event to the div element:
const container = document.querySelector("div.container")
container.addEventListener("scroll", function () {
And, instead of calculating how much document has scrolled, get the scrollTop property of the div.container:
const pixels = container.scrollTop
You need to make the same changes in whatever part of the code that involves the above calculations. In smoothScroll():
// const startPosition = window.pageYOffset;
const startPosition = container.scrollTop;
// window.scrollTo(0,run);
container.scrollTo(0,run);

Categories

Resources