Javascript animation trigger only when reaching a certain part of webpage - javascript

I want a counter animation which is triggered only when webpage reaches that certain part. For example, the js file would be this. I want the count to start only when the page reaches that certain section.
const counters = document.querySelectorAll('.counter');
const speed = 200; // The lower the slower
counters.forEach(counter => {
const updateCount = () => {
const target = +counter.getAttribute('data-target');
const count = +counter.innerText;
// Lower inc to slow and higher to slow
const inc = target / speed;
// console.log(inc);
// console.log(count);
// Check if target is reached
if (count < target) {
// Add inc to count and output in counter
counter.innerText = count + inc;
// Call function every ms
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
};
updateCount();
});

Yo can easily do it using Jquery
$(document).ready(function() {
$(window).scroll(function() {
if ($(document).scrollTop() > 50) {
$("div").css("background-color", "#111111");
} else {
$("div").css("background-color", "transparent");
}
});
});
div {
height: 120vh;
transition-duration: 0.5s;
}
<div>
Scroll to Change Background
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>

You can use Intersection Observer to do that
const $observeSection = document.querySelector('#second');
const options = {
root: null,
rootMargin: '0px',
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > options.threshold) {
$observeSection.classList.add('yellow');
} else {
$observeSection.classList.remove('yellow');
}
});
}, options);
observer.observe($observeSection);
main {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
main section {
width: 100vw;
height: 100vh;
}
#first {
background: red;
}
#second {
background: blue;
}
#third {
background: green;
}
.yellow {
background: yellow!important;
}
<main>
<section id="first"></section>
<section id="second"></section>
<section id="third"></section>
</main>
In this example, I observe the second section, and when the scroll come to the middle of the section (threshold: 0.5), I add a class to change the color of this section.
Be careful if you need to handle legacies browsers as you can see here :
https://caniuse.com/#feat=intersectionobserver

You don't need jquery to achieve this.
Here is a VanillaJS solution:
window.onscroll = (e) => {
e.stopPropagation();
window.pageYOffset > 50 && console.log("do smh");
}

Related

Why does window.requestAnimationFrame not keeping the last transition?

i'm trying to build my own simple fullpage.js or something similar, just so I can learn more about how to use requestAnimationFrame function, however I am stuck into this problem of transform3d value resetting, which behaves like this
You can see in the gif that every time I switch slides, the animation starts always at the first slide
Here is the implementation of the slider
html
<div class="scroller">
<div class="scroller-content">
<section class="scroller-section s1">SLIDE 1</section>
<section class="scroller-section s2">SLIDE 2</section>
<section class="scroller-section s3">SLIDE 3</section>
<section class="scroller-section s4">SLIDE 4</section>
</div>
</div>
css
.scroller {
width: 100vw;
height: 100vh;
overflow-y: hidden;
}
.scroller-section {
width: 100vw;
height: 100vh;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
font-size: 25rem;
}
.scroller-section.s1 {
background-color: #535bf2;
}
.scroller-section.s2 {
background-color: #242424;
}
.scroller-section.s3 {
background-color: #213547;
}
.scroller-section.s4 {
background-color: #747bff;
}
const scrollerContent = document.querySelector(".scroller-content")
const offsets = [0, -100, -200, -300]
let index = 0
window.addEventListener("wheel", (e) => {
if (e.deltaY > 0) {
index = shift(index + 1) // go next
const offset = offsets[index]
createAnimationFrames((delta) => {
scrollerContent.style.transform = `translate3d(0, ${delta * offset}vh,0)`
}, 1000)
} else if (e.deltaY < 0) {
index = shift(index - 1) // go prev
const offset = offsets[index]
createAnimationFrames((delta) => {
scrollerContent.style.transform = `translate3d(0, ${delta * offset}vh,0)`
}, 1000)
}
})
Here is the implementation of createAnimationFrames
const createAnimationFrames = (callback: (delta: number) => void, duration: number) => {
let start = new Date();
let raf = requestAnimationFrame;
const animate = () => {
let now = new Date();
const delta = Math.min((now.getTime() - start.getTime()) / duration, 1);
callback(delta);
if (delta < 1) {
raf(animate);
}
};
raf(animate);
};
Basically it runs the callback every frame request and the callback takes delta as a reference for the progress of the animation.
The thing is that it works normally by just having a simple reassign to the transform attribute and not using the requestAnimationFrame. Any solutions for this?

How to make horizontal scroll smoother?

I added this code to my WordPress based website to make its front page horizontal. But it's not smooth and I cannot add scroll snap or anchor points. Can you help me about these? My website is https://kozb.art
<script type="text/javascript">
function replaceVerticalScrollByHorizontal( event ) {
if ( event.deltaY !== 0 ) {
window.scroll(window.scrollX + event.deltaY * 2, window.scrollY );
event.preventDefault();
}
}
const mediaQuery = window.matchMedia( '(min-width: 770px)' );
if ( mediaQuery.matches ) {
window.addEventListener( 'wheel', replaceVerticalScrollByHorizontal );
}
</script>
Edit: Here's my CSS code to make front page horizontal:
.elementor-section-wrap{
display: inline-flex;
}
.elementor-section{
width:100vw;
}
body{
overflow-y: hidden;
overscroll-behavior-y: none;
}
#media (max-width:768px){
.elementor-section-wrap{
display: block;
}
body{
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior-x: none;
}
}
You need animation knowledge to move the horizontal scroll smoothly. Let's start with a horizontal scrolling environment.
// index.html
...
<head>
<link href="index.css" rel="stylesheet">
</head>
<body>
<div id="screen">
<div id="content1"></div><div id="content2"></div><div id="content3"></div><div id="content4"></div>
</div>
<script src="./main.js"></script>
</body>
...
/* index.css */
body {
margin: 0;
width: 100vw;
height: 100vh;
overflow-y: hidden;
}
#screen {
white-space: nowrap;
height: 100%;
}
#screen > div {
width: 100vw;
height: 100vh;
display: inline-block;
}
#screen > div:nth-child(1) {
background: aqua;
}
#screen > div:nth-child(2) {
background: blueviolet
}
#screen > div:nth-child(3) {
background: chocolate
}
#screen > div:nth-child(4) {
background: darkolivegreen;
}
A web page has now been created that has four sections and occupies one screen size per section. For smooth, snap horizontal scroll applications, let's think step by step about what we need to make code for animation.
To implement Snap, you need to know what value scroll X should move to. In the current layout, the value is the offsetLeft value of the section element. And the section size changes depending on the browser size. So the code to get the offsetLeft of the section can be created as follows:
let sectionAnchorPointer = [];
const resizeHandler = () => {
const content1 = document.getElementById('content1');
const content2 = document.getElementById('content2');
const content3 = document.getElementById('content3');
const content4 = document.getElementById('content4');
sectionAnchorPointer = [content1.offsetLeft, content2.offsetLeft, content3.offsetLeft, content4.offsetLeft];
};
addEventListener('resize', resizeHandler);
addEventListener('DOMContentLoaded', () => {
resizeHandler();
});
In order to store the section offsetLeft value from the beginning, a function was executed to update the section offsetLeft when DOMContentLoaded occurred. If you want to make it more efficient, please apply debounce to the resize event handler.
Next, when the wheel occurs, find the section to move. In order to find the section to be moved, it is necessary to determine where the section is located and then perform the calculation according to the direction of the wheel. The code is as follows:
let nextSectionIndex = 0;
const getCurrentSectionIndex = () => sectionAnchorPointer.findIndex((leftValue, i, array) => {
const scrollX = Math.ceil(window.scrollX); // Fixed a bug where scrollX was decimalized
const rightValue = array[i + 1] ?? Infinity;
return leftValue <= scrollX && scrollX < rightValue;
});
window.addEventListener('wheel', ({ deltaY }) => {
const currentSectionIndex = getCurrentSectionIndex();
const add = Math.abs(deltaY) / deltaY;
nextSectionIndex = currentSectionIndex + add;
nextSectionIndex = Math.min(sectionAnchorPointer.length - 1, Math.max(0, nextSectionIndex)); // To avoid pointing to a section index that does not exist
console.log(sectionAnchorPointer[nextSectionIndex]);
});
To save the scroll position when accessing the page, Call the function when a DOMContentLoaded event occurs.
...
addEventListener('DOMContentLoaded', () => {
resizeHandler();
nextSectionIndex = getCurrentSectionIndex();
});
...
Next, apply the animation so that it slowly changes to the offsetLeft value of the section that needs to move the current scrollX value. For ease of understanding, let's make it a linear animation without acceleration. The code is as follows:
const SCROLL_SPEED = 70; // It can be changed for speed control.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// linear animtion
if (Math.abs(window.scrollX - nextScrollX) > SCROLL_SPEED) {
const val = -Math.abs(window.scrollX - nextScrollX) / (window.scrollX - nextScrollX);
window.scroll(window.scrollX + val * SCROLL_SPEED, window.scrollY);
} else {
window.scroll(nextScrollX, window.scrollY);
}
requestAnimationFrame(scroll);
});
To apply dynamic animation by adding acceleration, add the following code instead of the code above.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// curve animation
if (Math.abs(window.scrollX - nextScrollX) > 1) {
let val = (nextScrollX - window.scrollX) / 8; // You can change 8 to another value to adjust the animation speed.
val = val > 0 ? Math.max(val, 1) : Math.min(val, -1);
window.scroll(window.scrollX + val, window.scrollY);
} else {
window.scroll(nextScrollX, window.scrollY);
}
requestAnimationFrame(scroll);
});
This is a simple example of implementation for understanding. Here is a link to the project using the code. If you are interested, please refer to the following link to understand Javascript animation. For your information, it would be more convenient to make an animation using a known library such as anime.js or popmotion.
This is the script code that fits your structure. Remove the existing wheel listener and insert this content.
const wrap = document.querySelectorAll('.elementor-section-wrap')[1]
let sectionAnchorPointer = [];
let resultX = 0;
const resizeHandler = () => {
sectionAnchorPointer = [...new Set([...wrap.children].map(el => el.offsetLeft))];
};
addEventListener('resize', resizeHandler);
addEventListener('DOMContentLoaded', () => {
resizeHandler();
nextSectionIndex = getCurrentSectionIndex();
});
resizeHandler();
let nextSectionIndex = 0;
const getCurrentSectionIndex = () => sectionAnchorPointer.findIndex((leftValue, i, array) => {
const scrollX = Math.ceil(resultX); // Fixed a bug where scrollX was decimalized
const rightValue = array[i + 1] ?? Infinity;
return leftValue <= resultX && resultX < rightValue;
});
window.addEventListener('wheel', (ev) => {
const {deltaY} = ev;
const currentSectionIndex = getCurrentSectionIndex();
const add = Math.abs(deltaY) / deltaY;
nextSectionIndex = currentSectionIndex + add;
nextSectionIndex = Math.min(sectionAnchorPointer.length - 1, Math.max(0, nextSectionIndex)); // To avoid pointing to a section index that does not exist
console.log(sectionAnchorPointer[nextSectionIndex]);
});
const SCROLL_SPEED = 70; // It can be changed for speed control.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// linear animtion
if (Math.abs(resultX - nextScrollX) > SCROLL_SPEED) {
const val = -Math.abs(resultX - nextScrollX) / (resultX - nextScrollX);
resultX = resultX + val * SCROLL_SPEED;
} else {
resultX = nextScrollX;
}
window.scroll(resultX , 0);
requestAnimationFrame(scroll);
});

How to combine intersection observer with parallax?

The problem with this parallax is that it runs on all sections of the .parallaxBg class page at the same time.
Therefore, I would like to use the Intersection Observer to run parallax only when the section enters the viewport.
// Parallax -------------------------------------------------------------------------
window.addEventListener("scroll", function parallaxFunction() {
let bg = document.querySelectorAll(".parallaxBg");
let distance = window.pageYOffset;
bg.forEach(parallaxBg => {
parallaxBg.style.top = distance * -0.2 + "px";
})
});
// Intersection observer --------------------------------------------------------------
const bgImages = document.querySelectorAll('.parallaxBg');
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
console.log(entries);
if (entry.intersectionRatio > 0) {
entry.target.parallaxFunction;
} else {
entry.target.parallaxFunction;
}
});
});
bgImages.forEach(image => {
observer.observe(image);
});
Yo. Bit late to the party but I needed to figure this out today. Three essential parts to this:
Intersection Observer tracks when the parallax class is in the viewport.
Scroll listener gets triggered when the parallax class is in the viewport, and is removed when the parallax class leaves the viewport.
While the scroll listener is active, we update the transform based on the scroll position
I'm running this through a debounce function to make it slightly smoother, but this is optional.
I think the top position can be calculated better, and I'd be interested to know how we can optimise this, but this answers your question of how to only transform the element when it is in the viewport - you can look in the console and check the dynamically added css inline on the img tag - ONLY when in the viewport :)
document.addEventListener('DOMContentLoaded', function () {
var parallaxImages =[].slice.call(
document.querySelectorAll(".parallax img")
)
console.log(parallaxImages);
if ("IntersectionObserver" in window && 'IntersectionObserverEntry' in window) {
// Intersection Observer Configuration
const observerOptions = {
root: null,
rootMargin: '0px 0px', // important: needs units on all values
threshold: 0
};
var observer = new IntersectionObserver(handleIntersect, observerOptions);
var el;
function handleIntersect(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
el = entry.target;
window.addEventListener('scroll', parallax, false);
} else {
window.removeEventListener('scroll', parallax, false);
}
});
}
parallaxImages.forEach(function(parallaxImage) {
observer.observe(parallaxImage);
});
var parallax = debounce(function() {
amount = Math.round( window.pageYOffset * 0.2 );
el.style.webkitTransform = 'translateY(-'+amount+'px)';
}, 10);
}
}, false);
/*************************************
Function: Debounce
*************************************/
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
/* PARALLAX STYLING */
.parallax {
height: 40vh;
overflow: hidden;
}
.parallax img {
width: 100%;
}
/* NICE TO HAVE */
body {
margin: 0;
padding: 0;
font-family: arial;
}
h1 {
margin: 0;
color: white;
}
header {
height: 110vh;
background-color: steelblue;
display: flex;
justify-content: center;
align-items: center;
}
section.spacer {
height: 110vh;
background-color: seagreen;
display: flex;
justify-content: center;
align-items: center;
}
footer {
height: 50vh;
background-color: firebrick;
}
<header>
<h1>Scroll down!</h1>
</header>
<main>
<section class="parallax">
<img src="https://picsum.photos/1920/1920" alt="">
</section>
<section class="spacer">
<h1>Keep scrolling!</h1>
</section>
<section class="parallax">
<img src="https://picsum.photos/1920/1920" alt="">
</section>
</main>
<footer></footer>

How to fade in / fade out a button on scrolling down / up respectively

I am working on my portfolio website and I am a complete beginner in Javascript.
I would like a button which has its position fixed, to slowly fade in when I scroll down (suppose when I scroll to >=20px from the top of the document, it should fade in) and when I scroll back up to the original position, it should gradually fade out.
I have already tried my hand and written a code for this. It is working perfectly when you scroll down and up. But when you quickly scroll and stop scrolling in the mid-way, it behaves pretty abnormally (suddenly appears or disappears).
HTML:
<div class="a_large_page">
<div class="enclose bordar black" id="bottomtoup">hello</div>
</div>
JS:
mybutton = document.getElementById("bottomtoup")
// initially, the button stays hidden
visible = false
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function() {
scrollFunction()
};
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
if (!visible) { // if the button is not visible,
unfade(mybutton); // function to gradually fadein button
visible = true; // button is visible so, set visible = false to true.
}
} else {
if (visible) { // if the button is visible,
fade(mybutton); // function to gradually fadeout button
visible = false; // set visible = true back to false
}
}
}
function unfade(element) {
var op = 0.1; // initial opacity
element.style.display = 'flex';
var timer = setInterval(function() {
if (op >= 1) {
clearInterval(timer);
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op += op * 0.1;
}, 10);
}
function fade(element) {
var op = 1; // initial opacity
var timer = setInterval(function() {
if (op <= 0.1) {
clearInterval(timer);
element.style.display = 'none';
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op -= op * 0.1;
}, 50);
}
Here is the fiddle: https://jsfiddle.net/P0intMaN/Lmp6u5ft/23/
My code is pretty substandard for sure. That's why it is behaving in this way. Hence, I am looking for an efficient way to achieve this. I have seen people making use of JQuery to do this, but I don't know JQuery at all. So, it would be much appreciated if the code is in pure JS.
I've changed your code and removed setInterval usage. This can be solved with it but may be harder to understand for newer coders.
There are also flags to keep track of whether you are currently fading or unfading to ensure you do not stack or "overlap" timeout/intervals.
mybutton = document.getElementById("bottomtoup")
// initially, the button stays hidden
var visible = false
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function() {
scrollFunction()
};
function scrollFunction() {
var threshold = 20;
var below_threshold = document.body.scrollTop > threshold || document.documentElement.scrollTop > threshold;
if (below_threshold) {
if (!visible) { // if the button is not visible,
unfade(mybutton); // function to gradually fadein button
}
return;
}
if (visible) { // if the button is visible,
fade(mybutton); // function to gradually fadeout button
}
}
var current_opacity = 0.1;
var is_unfading = false;
var is_fading = false;
function unfade(element) {
if(!visible){
element.style.display = 'flex';
visible = true;
}
is_fading = false;
is_unfading = true;
unfade_step(element);
}
function unfade_step(element){
element.style.opacity = current_opacity;
element.style.filter = 'alpha(opacity=' + current_opacity * 100 + ")";
if (current_opacity >= 1){
// end
is_unfading = false;
current_opacity = 1;
return;
}
current_opacity += 0.01;
if(is_unfading){
setTimeout(function(){
unfade_step(element);
}, 10);
}
}
function fade(element) {
if(!visible){
return;
}
is_fading = true;
is_unfading = false;
fade_step(element);
}
function fade_step(element) {
element.style.opacity = current_opacity;
element.style.filter = 'alpha(opacity=' + current_opacity * 100 + ")";
if (current_opacity <= 0.001){
// end
is_fading = false;
visible = false;
current_opacity = 0.1;
element.style.display = 'none';
return;
}
current_opacity -= 0.01;
if(is_fading){
setTimeout(function(){
fade_step(element);
}, 10);
}
}
There is no need to have so much JS when you can do in so little:
If you feel to change the timing of
// Set a function onscroll - this will activate if the user scrolls
window.onscroll = function() {
// Set the height to check for
var appear = 20
if (window.pageYOffset >= appear) {
// If more show the element
document.getElementById("bottomtop").style.opacity = '1'
document.getElementById("bottomtop").style.pointerEvents = 'all'
} else {
// Else hide it
document.getElementById("bottomtop").style.opacity = '0'
document.getElementById("bottomtop").style.pointerEvents = 'none'
}
}
.a_large_page{
background-color: gray;
height: 2000px;
}
.enclose{
height: 40px;
width: 40px;
position:fixed;
margin-right: 20px;
margin-bottom: 20px;
right:0;
bottom:0;
pointer-events:none;
opacity:0;
justify-content: center;
align-items: center;
color:white;
/* This determines how fast animation takes place, you can change it as per your choice. */
transition:all 0.6s;
}
.enclose:hover{
cursor: pointer;
}
<div class="a_large_page">
<div class="enclose bordar black" id="bottomtop">hello</div>
</div>
There is no need to sense the scroll event in more modern browsers as you can use IntersetionObserver to tell you when scrolling has gone past 20px;
You can do this by placing a tiny element at the top of the page with height 20px. You then ask the system to tell you when this has gone out of, or comes back into, the viewport. At these points you can set the opacity of the Hello to 1 or 0 as appropriate.
The extra bonus is that you get rid of a lot of code and there isn't the possible clash between set intervals as we use transition on the opacity to do the gradual fade in/out.
// See MDN for more info on IntersectioObserver
let callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
mybutton.style.opacity = 0;
} else {
mybutton.style.opacity = 1;
}
});
};
const mybutton = document.getElementById("bottomtoup")
const observer = new IntersectionObserver(callback);
const observed = document.getElementById("observed");
observer.observe(observed);
.a_large_page {
background-color: gray;
height: 2000px;
position: relative;
}
#observed {
position: absolute;
top: 0;
left: 0;
height: 20px;
width: 10px;
z-index: -999;
}
.enclose {
height: 40px;
width: 40px;
position: fixed;
margin-right: 20px;
margin-bottom: 20px;
right: 0;
bottom: 0;
justify-content: center;
align-items: center;
color: white;
opacity: 0;
transition: opacity 1s linear;
}
<div class="a_large_page">
<div id="observed"></div>
<div class="enclose bordar black" id="bottomtoup">hello</div>
</div>

Sprite PNG Javascript animation not working

I'm trying to create a Sprite animation using the following image:
To do so I am using it as a background and am trying to manipulate the background's position when animating. Somehow I can't get it working though - it shows the last frame from the very beginning.
Image: https://i.imgur.com/06vjVVj.png - 30800x1398 and 27 frames
Here's a codepen: https://codepen.io/magiix/pen/MWewdYo
#skull {
position: absolute;
border: 1px solid red;
width: 1140px;
height: 1398px;
background: url("https://i.imgur.com/06vjVVj.png") 1140px 0;
}
const animateSkull = () => {
const interval = 50;
let pos = 30800 / 27;
tID = setInterval(() => {
document.getElementById("skull").style.backgroundPosition = `-${pos}px 0`;
if (pos < 30800) {
pos = pos + 1140;
}
}, interval);
};
If you check (with a console for example), you'll see that your animateSkull function is never called, because your addEventListener does not work. Change it to the following so it will be called (but your animateSkull function has another bug (or maybe your css I didn't checked) so it's not fully working after that but you should be able to fix that easily):
document.addEventListener('DOMContentLoaded', () => {
animateSkull();
});
This should do the work, but the frames in your sprite don't have the same width. So the animation looks buggy. (that is one huge image just for the animation)
const animateSkull = () => {
const interval = 1000;
let pos = -1140;
tID = setInterval(() => {
if (pos > -30800) {
pos -= 1140;
}
document.getElementById("skull").style.backgroundPosition = `${pos}px 0`;
}, interval);
};
animateSkull();
#skull {
position: absolute;
border: 1px solid red;
width: 1140px;
height: 1398px;
background-image: url("https://i.imgur.com/06vjVVj.png");
background-position: -1140px 0;
background-size: cover;
}
</style>
<p id="skull"></p>

Categories