Adding throttling using requestAnimationFrame? - javascript

In the code below I'm successfully changing the color of the left and right arrows based on how far a div (shadeFinder) has scrolled on the x axis. I want to add throttling to this code. Can someone explain how? I have the example from w3Schools below but finding it hard to merge it into my code.
//change the color of mobile arrows based on where the shadefinder is on X-axis
function changeArrowColor(){
var shadeFinder = document.querySelector('.fms-wrapper');
let leftArrow = document.querySelector('.prev');
let rightArrow = document.querySelector('.next');
let last_known_scroll_position = 0;
let ticking = false;
function doSomething(scroll_pos) {
scroll_pos = parseInt(scroll_pos/10);
leftArrow.style.color = `rgb(${scroll_pos},${scroll_pos},${scroll_pos})`;
rightArrow.style.color = `rgb(${scroll_pos},${scroll_pos},${scroll_pos})`;
}
shadeFinder.addEventListener('scroll', function(e) {
last_known_scroll_position = shadeFinder.scrollLeft;
doSomething(last_known_scroll_position);
});
}
Mozilla scroll event example with throttling:
// Reference: http://www.html5rocks.com/en/tutorials/speed/animations/
let last_known_scroll_position = 0;
let ticking = false;
function doSomething(scroll_pos) {
// Do something with the scroll position
}
window.addEventListener('scroll', function(e) {
last_known_scroll_position = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(last_known_scroll_position);
ticking = false;
});
ticking = true;
}
});

The requestAnimationFrame API takes a callback function that returns a timestamp (I'll use RAF as shorthand hereon). If you wanted to use JS to animate, you can use the timestamp to call cancelAnimationFrame later. But to use just for throttling, you don't necessarily need to store this value, for now.
Since you already have a variable called last_known_scroll_position scoped for used within doSomething, you don't have to pass the scrollLeft value to the RAF function, but instead pass the doSomething as the callback after setting the last_known_scroll_position to the current scrollLeft value at the time of the scroll event.
That callback will receive a timestamp parameter, but you don't need it. The callback can update the color based on a calculation from the last_known_scroll_position.
See the example below.
function changeArrowColor(){
var shadeFinder = document.querySelector('.fms-wrapper');
let leftArrow = document.querySelector('.prev');
let rightArrow = document.querySelector('.next');
let last_known_scroll_position = 0;
function doSomething(timestamp) {
let scroll_pos = parseInt(last_known_scroll_position/10);
let color = `rgb(${scroll_pos},${scroll_pos},${scroll_pos})`;
console.clear()
console.log({timestamp, color, last_known_scroll_position});
leftArrow.style.color = color;
rightArrow.style.color = color;
}
shadeFinder.addEventListener('scroll', function(e) {
last_known_scroll_position = shadeFinder.scrollLeft;
requestAnimationFrame(doSomething);
});
}
document.addEventListener("DOMContentLoaded", changeArrowColor);
body {
padding:0;
margin:20px 0;
}
.fms-wrapper {
height: 80px;
width: 100%;
max-width: 100vw;
overflow-y: hidden;
overflow-x: scroll;
position: relative;
padding: 0 30px;
margin: 0 5px;
box-sizing: border-box;
z-index:0;
margin: 0;
}
.prev, .next {
position: fixed;
z-index: 5;
width: 30px;
height: 50px;
font-size: 28px;
line-height: 50px;
box-sizing: border-box;
cursor:pointer;
color: black;
background: white;
box-shadow: 0 0 5px rgba(0,0,0,.5);
text-align: center;
pointer-events: auto;
}
.prev {
left: 5px;
}
.next {
right: 5px;
}
.fms-content {
width: 400%;
height: 50px;
background: linear-gradient(90deg, #f0f0f0 0%, #f0f0f0 25%, #919191 25%, #919191 50%, #f0f0f0 50%, #f0f0f0 75%, #919191 75%, #919191 100%);
box-sizing: border-box;
margin: 0;
padding: 0;
}
<div class="fms-wrapper">
<div class="prev" tabindex="0"><</div>
<div class="next" tabindex="0">></div>
<div class="fms-content"></div>
</div>

Related

Using getBoundingClientRect() when resizing the window

I have this navbar and everytime I click an option in the navbar the absolute positioned indicator gets the position of the option on the left and the width with the help of getBoundingClientRect() and it is moved to the target.
The problem is when I resize the window the indicator changes it's position and moves away.To stay in the same place when I resize the window I applied an eventListener to the window and everytime is resized I get the new values of left and width with getBoundingClientRect().
It works but I wonder if that is a bad way to do it because of the calculations that happen everytime the window is resized and if that is the case what is a better way to do this.
Here is the code:
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.getBoundingClientRect().left}px`;
navigator.style.width = `${firstOption.getBoundingClientRect().width}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.getBoundingClientRect().left}px`;
navigator.style.width = `${e.target.getBoundingClientRect().width}px`;
};
});
window.addEventListener('resize', function() {
let navOptionActive = nav.querySelector('.nav-option-active');
navigator.style.left = `${navOptionActive.getBoundingClientRect().left}px`;
navigator.style.width = `${navOptionActive.getBoundingClientRect().width}px`;
});
* {
margin: 0;
padding: 0;
}
nav {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin: 100px auto;
padding: 7vh 30vw;
width: auto;
background:#eeeeee;
}
.nav-option {
padding: 0 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
You can make your <nav> element tightly wrap the buttons, then position the underline relative to the <nav>. A new wrapper <div> around the <nav> takes care of the margins and gray background. Instead of getBoundingClientRect() you then need to use offsetLeft and offsetWidth.
Note that this doesn't handle the changes in response to your #media query. For that, you could add a resize listener that specifically only handles changes across the 1200px threshold. Alternatively, you could reparent the underline to be a child of the actual nav button while it's not animating. Neither solution is great, but both would get the job done.
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.offsetLeft}px`;
navigator.style.width = `${firstOption.offsetWidth}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.offsetLeft}px`;
navigator.style.width = `${e.target.offsetWidth}px`;
};
});
* {
margin: 0;
padding: 0;
}
.nav-wrapper {
margin: 100px 0;
display: flex;
justify-content: center;
background: #eeeeee;
}
nav {
position: relative;
display: flex;
}
.nav-option {
padding: 7vh 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<div class="nav-wrapper">
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
</div>
If you have to use getBoundingClientRect (which honestly has nothing wrong with it), you can throttle the call, so that only the last resize after sufficient time has passed will execute. There are zillion ways of doing this, I will leave one example:
window.onresize = (function(id = null, delay = 600, oEvent = null){
return function fire(event){
return (new Promise(function(res,rej){
if (id !== null){
oEvent = event;
rej("busy");
return;
}
id = setTimeout(function(){
res(oEvent || event);
},delay);
})).then(function(event){
id = null;
console.log(event, "do getBoundingClientRect call");
}).catch(function(){void(0);});
};
}());
Replace console.log with what you want to do.
Your other option is to switch to intersection observer, if you can restructure your rendering logic. That will require some work

How to apply different class to all previous, current and all next elements using JavaScript?

I am working on range slider for my website. I am stuck in to make it possible that on clicking specific box all previous steps gets applied with class ".previous" and all steps next to current one get applied with ".next" class, which have different styling for previous steps and next steps, according to the data-label value of that specific div. Lets take an example for a simple scenario if I click on step 7 then step 5 and 6 gets red, and step 8 and 9 gets grey, if I click on step 9 all previous gets applied with ".previous" class. In simple words all steps having number greater than current active step get applied with ".next" class and all those having number less then current active gets applied with ".previous" class .Attached is the source code, Any suggestion or help would be appreciated.
const rangeSlider = document.querySelector('#price_slider');
rangeSlider.addEventListener("input", rangeScript);
const customProgress = document.querySelector('#customProgress');
for (let i = 0; i < rangeSlider.max - rangeSlider.min; i++) {
const step = document.createElement('div');
step.classList.add('step');
step.setAttribute('data-label', +rangeSlider.min + i + 1);
step.addEventListener('click', (e) => {
let val = e.target.dataset.label;
document.querySelector('#price_slider').value = val
rangeScript({
target: rangeSlider
})
})
customProgress.appendChild(step);
}
customProgress.querySelector(`.step[data-label="${rangeSlider.value}"]`)
.classList.add('current')
function rangeScript(e) {
const target = document.getElementById('progress');
let newValue = parseInt(e.target.value);
const currentStep = customProgress.querySelector(`.step.current`);
if (currentStep) {
currentStep.classList.remove('current');
}
nextStep = customProgress.querySelector(`.step[data-label="${newValue}"]`);
if (nextStep) {
nextStep.classList.add('current')
}
}
#customProgress {
display: flex;
width: 100%;
height: 25px;
margin-top: 44px;
}
.step {
position: relative;
background: #f5f5f5;
height: 40px;
width: 100%;
border-radius: 4px 0 0 4px;
border-right: 1px solid white;
}
.step::after {
content: attr(data-label);
position: absolute;
top: -1.5em;
right: -0.25em;
}
.step ~ .current,
.step.current {
background: #a6983e;
}
.previous {
background: red;
}
.next {
background: #f5f5f5;
}
<div id="customProgress"></div>
<div id="progress" style="width: 100%">
<input id="price_slider" type="range" min="4" max="9" value="" style="display:none" />
</div>
You can do something like this
const rangeSlider = document.querySelector('#price_slider');
rangeSlider.addEventListener("input", rangeScript);
const customProgress = document.querySelector('#customProgress');
for (let i = 0; i < rangeSlider.max - rangeSlider.min; i++) {
const step = document.createElement('div');
step.classList.add('step');
step.setAttribute('data-label', +rangeSlider.min + i + 1);
step.addEventListener('click', (e) => {
let val = e.target.dataset.label;
document.querySelector('#price_slider').value = val
rangeScript({
target: rangeSlider
})
})
customProgress.appendChild(step);
}
customProgress.querySelector(`.step[data-label="${rangeSlider.value}"]`)
.classList.add('current')
function rangeScript(e) {
const target = document.getElementById('progress');
let newValue = parseInt(e.target.value);
const currentStep = customProgress.querySelector(`.step.current`);
if (currentStep) {
currentStep.classList.remove('current');
}
nextStep = customProgress.querySelector(`.step[data-label="${newValue}"]`);
if (nextStep) {
nextStep.classList.add('current')
}
}
#customProgress {
display: flex;
width: 100%;
height: 25px;
margin-top: 44px;
}
.step {
position: relative;
background: #f5f5f5;
height: 40px;
width: 100%;
border-radius: 4px 0 0 4px;
border-right: 1px solid white;
}
.step::after {
content: attr(data-label);
position: absolute;
top: -1.5em;
right: -0.25em;
}
.step ~ .current,
.step.current {
background: #a6983e;
}
.step {
background: green;
}
.current ~ div {
background: red;
}
<div id="customProgress"></div>
<div id="progress" style="width: 100%">
<input id="price_slider" type="range" min="4" max="9" value="" style="display:none" />
</div>

How do I stop elements from moving when the media query is triggered?

I'm a complete novice at coding.
I am styling a recent project for a course I have worked on. I have put in a media query to change the properties of the H1 and Controls class. However, when I resize the browser to trigger the media query, it is also moving the button and score out of place. Is there a reason it is doing this and how do I fix it?
Many thanks in advance!
Ray
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class='mobile'>
<div class="info">
<h1>Snake Game</h1>
<button id="start">Lets go!</button>
<h2>Score <span id="score"></span></h2>
</div>
<div class="grid"></div>
<div class="nokia"></div>
<div class="controls">
<h3>Controls</h3>
<ul><span class="direction">Up</span> - Up arrow key</ul>
<ul><span class="direction">Right</span> - Right arrow key</ul>
<ul><span class="direction">Down</span> - Down arrow key</ul>
<ul><span class="direction">Left</span> - Left arrow key</ul>
</div>
</div>
<script src="main.js"></script>
</body>
.mobile {
display: flex;
justify-content: center;
}
.nokia {
position: absolute;
top: 190px;
display: block;
width: 700px;
height: 983px;
background-image: url("https://i.imgur.com/3SeVxgS.jpg");
background-repeat: no-repeat;
background-size: 100% 100%;
}
.controls {
position: absolute;
top: 100px;
display: flex;
}
#media (max-width: 930px) {
.controls {
top: 50px;
display: block;
font-size: 70%;
}
h1 {
font-size: 20px;
}
}
.grid {
position: absolute;
top: 420px;
z-index: 9999;
display: flex;
flex-wrap: wrap;
width: 200px;
height: 200px;
border: 2px solid rgba(0, 0, 0, 0.5);
background-color: white;
}
.info {
position: absolute;
z-index: 9999;
top: 0;
text-align: center;
}
h2 {
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
position: absolute;
top: 750px;
left: 40px;
}
button {
position: absolute;
top: 663px;
left: -5px;
height: 64px;
width: 172px;
border-style: solid;
border-bottom: 50px;
border-radius: 50%;
text-align: center;
display: inline-block;
font-size: 20px;
outline: none;
}
button:active {
transform: translateY(2px);
}
.square {
width: 20px;
height: 20px;
}
.snake {
background-color:#12c258
}
.apple {
background-color: red;
border-radius: 20%;
height: 20px;
width: 20px;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
transform: scale(1);
animation: pulse 2s infinite;
}
#keyframes pulse {
0% {
transform: scale(0.35);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
}
50% {
transform: scale(1);
/* box-shadow: 0 0 0 10px #12c258; */
}
100% {
transform: scale(0.35);
/* box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); */
}
}
const grid = document.querySelector(".grid");
const startButton = document.getElementById("start");
const scoreDisplay = document.getElementById("score");
let squares = [];
let currentSnake = [2, 1, 0];
let direction = 1;
const width = 10;
let appleIndex = 0;
let score = 0;
let intervalTime = 1000;
let speed = 0.9;
let timerId = 0;
function createGrid() {
//create 100 of these elements with a for loop
for (let i = 0; i < width * width; i++) {
//create element
const square = document.createElement("div");
//add styling to the element
square.classList.add("square");
//put the element into our grid
grid.appendChild(square);
//push it into a new squares array
squares.push(square);
}
}
createGrid();
currentSnake.forEach(index => squares[index].classList.add("snake"));
function startGame() {
//remove the snake
currentSnake.forEach(index => squares[index].classList.remove("snake"));
//remove the apple
squares[appleIndex].classList.remove("apple");
clearInterval(timerId);
currentSnake = [2, 1, 0];
score = 0;
//re add new score to browser
scoreDisplay.textContent = score;
direction = 1;
intervalTime = 1000;
generateApple();
//readd the class of snake to our new currentSnake
currentSnake.forEach(index => squares[index].classList.add("snake"));
timerId = setInterval(move, intervalTime);
}
function move() {
if (
(currentSnake[0] + width >= width * width && direction === width) || //if snake has hit bottom
(currentSnake[0] % width === width - 1 && direction === 1) || //if snake has hit right wall
(currentSnake[0] % width === 0 && direction === -1) || //if snake has hit left wall
(currentSnake[0] - width < 0 && direction === -width) || //if snake has hit top
squares[currentSnake[0] + direction].classList.contains("snake")
)
return clearInterval(timerId);
//remove last element from our currentSnake array
const tail = currentSnake.pop();
//remove styling from last element
squares[tail].classList.remove("snake");
//add square in direction we are heading
currentSnake.unshift(currentSnake[0] + direction);
//add styling so we can see it
//deal with snake head gets apple
if (squares[currentSnake[0]].classList.contains("apple")) {
//remove the class of apple
squares[currentSnake[0]].classList.remove("apple");
//grow our snake by adding class of snake to it
squares[tail].classList.add("snake");
console.log(tail);
//grow our snake array
currentSnake.push(tail);
console.log(currentSnake);
//generate new apple
generateApple();
//add one to the score
score++;
//display our score
scoreDisplay.textContent = score;
//speed up our snake
clearInterval(timerId);
console.log(intervalTime);
intervalTime = intervalTime * speed;
console.log(intervalTime);
timerId = setInterval(move, intervalTime);
}
squares[currentSnake[0]].classList.add("snake");
}
function generateApple() {
do {
appleIndex = Math.floor(Math.random() * squares.length);
} while (squares[appleIndex].classList.contains("snake"));
squares[appleIndex].classList.add("apple");
}
generateApple();
// 39 is right arrow
// 38 is for the up arrow
// 37 is for the left arrow
// 40 is for the down arrow
function control(e) {
if (e.keyCode === 39) {
console.log("right pressed");
direction = 1;
} else if (e.keyCode === 38) {
console.log("up pressed");
direction = -width;
} else if (e.keyCode === 37) {
console.log("left pressed");
direction = -1;
} else if (e.keyCode === 40) {
console.log("down pressed");
direction = +width;
}
}
document.addEventListener("keyup", control);
startButton.addEventListener("click", startGame);
The button and the score were in that "out of place" position by default but the "Snake Game" text was pushing it to the left, you can solve this issue by putting the "Snake Game" text out of the div that has the button in it.

Jquery animate not detecting scroll properly

I am trying to make a feature where a button slides up to take you back to the top after scrolling. However for some reason it just won't work. It works fine when I use
if (scroll >= 84) {
$("#ID").fadeIn();
} else {
$("#ID").fadeOut();
}
But when it just doesnt work with either animate or css.
The way i keep track of the scroll is with a eventlistener, and declaring a let with the value:
let scroll = this.scrollY;
I hope this is enough information to give you an idea of what im going for. Thanks for the help in advance
Try this (run and scroll down):
var timeout;
for (var i = 0; i < 200; i++) {
document.body.innerHTML += "Example Text<br>"
}
window.addEventListener("scroll", () => {
var scrollDistance = window.scrollY
if (scrollDistance >= 100) {
clearTimeout(timeout);
document.querySelector("button").style.display = "";
setTimeout(function() {
document.querySelector("button").style.opacity = "1"
}, 1);
document.querySelector("button").style.cursor = "pointer";
} else {
clearTimeout(timeout);
document.querySelector("button").style.opacity = "0";
document.querySelector("button").style.cursor = "";
timeout = setTimeout(function() {
document.querySelector("button").style.display = "none"
}, 500);
}
})
document.querySelector("button").addEventListener("click", () => {
window.scroll({
top: 0,
left: 0,
behavior: 'smooth'
});
})
button {
position: fixed;
right: 10px;
bottom: 10px;
border-radius: 50%;
height: 50px;
width: 50px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffcccb;
border: 2px solid black;
opacity: 0;
outline: none;
transition: opacity 0.5s;
}
button>div {
width: 20px;
height: 20px;
border-width: 5px 5px 0px 0px;
border-style: solid;
border-color: black;
transform: translate(0, 4px) rotate(-45deg);
}
<button>
<div></div>
</button>
Note that most of the code is just setup. This is the actual code that does the scrolling work:
document.querySelector("button").addEventListener("click", () => {
window.scroll({
top: 0,
left: 0,
behavior: 'smooth'
});
})
It simply listens for the click of the button, and scrolls to the top.

JS slider image disappears when clicking Prev/Next arrow

I've tried to look for a solution for this but have failed miserably. It's my first ever time using JS (I'm trying to learn) so the possibility of my just not understanding the answers in the search results properly is quite high - sorry about that.
I am wanting a JS carousel, generated from an array, with Prev/Next buttons (ideally responsive etc but that'll come at a later stage), preferably with captions underneath. I can get the carousel to work but I end up getting a text link when I click on either Prev or Next. And I've no idea how to add the caption array underneath (I've taken out the JS for the captions for now because it was messing everything else up even further).
Relevant HTML:
<body onload="changePilt()">
<span id="prev" class="arrow">❮</span>
<div class="karussell" id="karussell">
<img class="karu" name="esislaid">
</div>
<span id="next" class="arrow">❯</span>
<div class="caption">
<h3 name="esikiri"></h3>
</div>
</body>
CSS, just in case:
.karussell {
position: relative;
width: 100%;
max-height: 600px;
overflow: hidden;
}
.arrow {
cursor: pointer;
position: absolute;
top: 40%;
width: auto;
color: #00A7E0;
margin-top: -22px;
padding: 16px;
font-weight: bold;
font-size: 18px;
transition: 0.6s ease;
border-radius: 0 3px 3px 0;
}
#next {
right: 0;
border-radius: 3px 0 0 3px;
}
#prev {
left: 0;
}
.arrow:hover {
background-color: rgba(0,0,0,0.8);
}
.caption {
text-align: center;
color: #00A7E0;
padding: 2px 16px;
}
.karu {
max-width: 75%;
}
#media (max-width:767px){.karu{max-width: 95%;}}
And finally, the dreaded JS:
var i = 0;
var s = 0;
var esileht = [];
var aeg = 5000;
//Image List
esileht[0] = 'img/tooted/raamat/graafvanalinn2016.jpg';
esileht[1] = 'img/tooted/kaart/kaart_taskus_esipool.jpg';
esileht[2] = 'img/tooted/kaart/graafkaart_esikylg.jpg';
//Change Image
function changePilt (){
document.esislaid.src = esileht[i];
if(i < esileht.length -1){
i++;
} else {
i = 0;
}
setTimeout("changePilt()", aeg);
}
document.onload = function() {
}
// Left and Right arrows
//J2rgmine
function jargmine(){
s = s + 1;
s = s % esileht.length;
return esileht [s];
}
//Eelmine
function eelmine(){
if (s === 0) {
s = esileht.length;
}
s = s -1;
return esileht[s];
}
document.getElementById('prev').addEventListener('click', function (e){
document.getElementById('karussell').innerHTML = eelmine();
}
);
document.getElementById('next').addEventListener('click', function (e) {
document.getElementById('karussell').innerHTML = jargmine();
}
);
I'm sure the solution is dreadfully obvious, I just cannot seem to be able to figure it out...
instead of innerHTML change src attribute of image
document.querySelector('#karussell img').src = eelmine();
And
document.querySelector('#karussell img').src = jargmine();

Categories