let pf = document.querySelectorAll('.pf');
for (let i of pf) {
Object.assign(i.style, {
left: '400px'
})
}
function shiftLetters() {
let start = performance.now();
let dist = -400;
let dur = 500;
const logoAnimate = (timestamp) => {
var runtime = timestamp - start
var progress = Math.min(runtime / dur, 1)
const position = progress * dist;
if (runtime < dur) {
for (let i = 0; i < pf.length; i++) {
(function(i) {
setTimeout(function() {
pf[i].style.transform = `translate3d(${position}px,0,0)`
}, 100 * i)
})(i);
}
requestAnimationFrame(logoAnimate)
}
}
requestAnimationFrame(logoAnimate)
}
document.getElementsByTagName('button')[0].addEventListener('click', shiftLetters);
#wrapper {
display: flex;
position: absolute;
-webkit-transform: translate(-50%, 10%);
transform: translate(-50%, 10%);
top: 50%;
left: 50%;
}
.pf {
display: inline-block;
position: relative;
width: 100px;
height: 100px;
margin: 2px;
background-color: red;
}
button {
display: block;
margin-top: 50px;
}
<div id="wrapper">
<div class="pf"></div>
<div class="pf"></div>
<div class="pf"></div>
<div class="pf"></div>
</div>
<button>animate</button>
I have 4 elements when button is clicked their supposed to end at the exact distance of the dist variable. Instead it end at random integers and never exactly -400px like stated in in my dist variable of -400. Has to be something simple. I've written the variables outside of scope etc etc..
The reason is that you never apply the final transform. On the last iteration of shiftLetters when position finally = -400 runtime is greater than dur so you never enter your if statement and apply the transform. Slightly refactored code below.
let pf = document.querySelectorAll('.pf');
for (let i of pf) {
Object.assign(i.style, {
left: '400px'
})
}
function shiftLetters() {
let start = performance.now();
let dist = -400;
let dur = 500;
const logoAnimate = (timestamp) => {
var runtime = timestamp - start
var progress = Math.min(runtime / dur, 1)
const position = progress * dist;
applyTransform(position);
if (runtime < dur) {
requestAnimationFrame(logoAnimate)
}
}
requestAnimationFrame(logoAnimate)
}
function applyTransform(position) {
for (let i = 0; i < pf.length; i++) {
setTimeout(function() {
pf[i].style.transform = `translate3d(${position}px,0,0)`
}, 100 * i)
}
}
document.getElementsByTagName('button')[0].addEventListener('click', shiftLetters);
#wrapper {
display: flex;
position: absolute;
-webkit-transform: translate(-50%, 10%);
transform: translate(-50%, 10%);
top: 50%;
left: 50%;
}
.pf {
display: inline-block;
position: relative;
width: 100px;
height: 100px;
margin: 2px;
background-color: red;
}
button {
display: block;
margin-top: 50px;
}
<div id="wrapper">
<div class="pf"></div>
<div class="pf"></div>
<div class="pf"></div>
<div class="pf"></div>
</div>
<button>animate</button>
Related
I created a slideshow with 3 slides but for some reason, it keeps adding an additional slide
const slideshow = document.getElementById("slideshow");
const slides = slideshow.children;
let currentSlide = 0;
function goToSlide(n) {
slides[currentSlide].classList.remove("active");
currentSlide = (n + slides.length) % slides.length;
slides[currentSlide].classList.add("active");
updateSlideshowCounter();
}
function nextSlide() {
goToSlide(currentSlide + 1);
}
function prevSlide() {
goToSlide(currentSlide - 1);
}
function updateSlideshowCounter() {
const slideshowCounter = document.getElementById("slideshow-counter");
slideshowCounter.textContent = `${currentSlide + 1} / ${slides.length}`;
}
const prevButton = document.getElementById("prev-button");
prevButton.addEventListener("click", prevSlide);
const nextButton = document.getElementById("next-button");
nextButton.addEventListener("click", nextSlide);
updateSlideshowCounter();
#slideshow {
position: relative;
text-align: center;
width: 400px;
height: 300px;
border: 1px black solid;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 1s;
}
.slide.active {
opacity: 1;
}
#slideshow-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
}
#prev-button,
#next-button {
padding: 10px 20px;
border: none;
background-color: #333;
color: #fff;
cursor: pointer;
}
#prev-button {
margin-right: 20px;
}
#next-button {
margin-left: 20px;
}
#slideshow-counter {
margin: 0 20px;
}
<div id="slideshow">
<div class="slide">Slide 1</div>
<div class="slide">Slide 2</div>
<div class="slide">Slide 3</div>
<div id="slideshow-controls">
<button id="prev-button">Prev</button>
<span id="slideshow-counter"></span>
<button id="next-button">Next</button>
</div>
</div>
Can someone tell me what my mistake is and how I can get 3 slides in the output instead of 4.
You're defining your slides with the statement const slides = slideshow.children;. Your slideshow has a total of 4 direct children, so the counter is technically correct (see slide 1, slide 2, slide 3, and slideshow-controls).
One approach to get just the slides you want is to use const slides = document.getElementsByClassName("slide"). I hope this helps!
The problem is your slides variable is not assigned to the correct list of elements, as the previous answer said, you should replace slideshow.children with either document.getElementsByClassName('slide') or document.querySelectorAll('.slide'), use any of the two.
By using slideshow.children, you're not getting .slide classes, you're getting all children of #slideshow.
So, your variable in line 67, should be as the following:
const slides = document.querySelectorAll('.slide');
or
const slides = document.getElementsByClassName('.slide');
You should keep slideshow controls out of your slideshow div. I am attaching Code Below. Run it and check.
const slideshow = document.getElementById("slideshow");
const slides = slideshow.children;
let currentSlide = 0;
function goToSlide(n) {
slides[currentSlide].classList.remove("active");
currentSlide = (n + slides.length) % slides.length;
slides[currentSlide].classList.add("active");
updateSlideshowCounter();
}
function nextSlide() {
goToSlide(currentSlide + 1);
}
function prevSlide() {
goToSlide(currentSlide - 1);
}
function updateSlideshowCounter() {
const slideshowCounter = document.getElementById("slideshow-counter");
slideshowCounter.textContent = `${currentSlide + 1} / ${slides.length}`;
}
const prevButton = document.getElementById("prev-button");
prevButton.addEventListener("click", prevSlide);
const nextButton = document.getElementById("next-button");
nextButton.addEventListener("click", nextSlide);
updateSlideshowCounter();
#slideshowbox {
position: relative;
width: 400px;
height: 300px;
}
#slideshow {
position: relative;
text-align: center;
width: 400px;
height: 300px;
border: 1px black solid;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 1s;
}
.slide.active {
opacity: 1;
}
#slideshow-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
}
#prev-button,
#next-button {
padding: 10px 20px;
border: none;
background-color: #333;
color: #fff;
cursor: pointer;
}
#prev-button {
margin-right: 20px;
}
#next-button {
margin-left: 20px;
}
#slideshow-counter {
margin: 0 20px;
}
<div id="slideshowbox">
<div id="slideshow">
<div class="slide">Slide 1</div>
<div class="slide">Slide 2</div>
<div class="slide">Slide 3</div>
</div>
<div id="slideshow-controls">
<button id="prev-button">Prev</button>
<span id="slideshow-counter"></span>
<button id="next-button">Next</button>
</div>
</div>
Your slideshow div childs is throwing 4 because your 4th div is slideshow-controls. You may want to add -1 to the counter or redifine the way you make your div. Best of luck!
I found one game on js, and I want to add it to myself by redoing it a bit
https://jsfiddle.net/a7Lx1c98/
So, I want to replace emoji with pictures here, I do this
const emojis = ['https://i.imgur.com/GLS9S5f.jpg', 'https://i.imgur.com/IN9C2qz.jpg', 'https://i.imgur.com/Ke2ubzv.jpg', 'https://i.imgur.com/PbvJDyR.jpg', 'https://i.imgur.com/L3ysai2.jpg',
'https://i.imgur.com/1NxzhTV.jpg', 'https://i.imgur.com/aksV9O3.jpg', 'https://i.imgur.com/gYsZdE4.jpg', 'https://i.imgur.com/LXo6iW3.jpg', 'https://i.imgur.com/wYrEwNR.jpg']
const picks = pickRandom(emojis, (dimensions * dimensions) / 2)
const items = shuffle([...picks, ...picks])
const cards = `
<div class="board" style="grid-template-columns: repeat(${dimensions}, auto)">
${items.map(item => `
<div class="card">
<div class="card-front"></div>
<div class="card-back"><img src="${item}"></div>
</div>
`).join('')}
</div>
`
As a result, the pictures appear, but the game itself does not work, when you select two pictures and they are different, they do not close, but you can choose everything in a row
What could be wrong?
const selectors = {
boardContainer: document.querySelector('.board-container'),
board: document.querySelector('.board'),
moves: document.querySelector('.moves'),
timer: document.querySelector('.timer'),
start: document.querySelector('button'),
win: document.querySelector('.win')
}
const state = {
gameStarted: false,
flippedCards: 0,
totalFlips: 0,
totalTime: 0,
loop: null
}
const shuffle = array => {
const clonedArray = [...array]
for (let index = clonedArray.length - 1; index > 0; index--) {
const randomIndex = Math.floor(Math.random() * (index + 1))
const original = clonedArray[index]
clonedArray[index] = clonedArray[randomIndex]
clonedArray[randomIndex] = original
}
return clonedArray
}
const pickRandom = (array, items) => {
const clonedArray = [...array]
const randomPicks = []
for (let index = 0; index < items; index++) {
const randomIndex = Math.floor(Math.random() * clonedArray.length)
randomPicks.push(clonedArray[randomIndex])
clonedArray.splice(randomIndex, 1)
}
return randomPicks
}
const generateGame = () => {
const dimensions = selectors.board.getAttribute('data-dimension')
if (dimensions % 2 !== 0) {
throw new Error("The dimension of the board must be an even number.")
}
const emojis = ['https://i.imgur.com/GLS9S5f.jpg', 'https://i.imgur.com/IN9C2qz.jpg', 'https://i.imgur.com/Ke2ubzv.jpg', 'https://i.imgur.com/PbvJDyR.jpg', 'https://i.imgur.com/L3ysai2.jpg',
'https://i.imgur.com/1NxzhTV.jpg', 'https://i.imgur.com/aksV9O3.jpg', 'https://i.imgur.com/gYsZdE4.jpg', 'https://i.imgur.com/LXo6iW3.jpg', 'https://i.imgur.com/wYrEwNR.jpg']
const picks = pickRandom(emojis, (dimensions * dimensions) / 2)
const items = shuffle([...picks, ...picks])
const cards = `
<div class="board" style="grid-template-columns: repeat(${dimensions}, auto)">
${items.map(item => `
<div class="card">
<div class="card-front"></div>
<div class="card-back"><img src="${item}"></div>
</div>
`).join('')}
</div>
`
const parser = new DOMParser().parseFromString(cards, 'text/html')
selectors.board.replaceWith(parser.querySelector('.board'))
}
const startGame = () => {
state.gameStarted = true
selectors.start.classList.add('disabled')
state.loop = setInterval(() => {
state.totalTime++
selectors.moves.innerText = `${state.totalFlips} moves`
selectors.timer.innerText = `time: ${state.totalTime} sec`
}, 1000)
}
const flipBackCards = () => {
document.querySelectorAll('.card:not(.matched)').forEach(card => {
card.classList.remove('flipped')
})
state.flippedCards = 0
}
const flipCard = card => {
state.flippedCards++
state.totalFlips++
if (!state.gameStarted) {
startGame()
}
if (state.flippedCards <= 2) {
card.classList.add('flipped')
}
if (state.flippedCards === 2) {
const flippedCards = document.querySelectorAll('.flipped:not(.matched)')
if (flippedCards[0].innerText === flippedCards[1].innerText) {
flippedCards[0].classList.add('matched')
flippedCards[1].classList.add('matched')
}
setTimeout(() => {
flipBackCards()
}, 1000)
}
// If there are no more cards that we can flip, we won the game
if (!document.querySelectorAll('.card:not(.flipped)').length) {
setTimeout(() => {
selectors.boardContainer.classList.add('flipped')
selectors.win.innerHTML = `
<span class="win-text">
You won!<br />
with <span class="highlight">${state.totalFlips}</span> moves<br />
under <span class="highlight">${state.totalTime}</span> seconds
</span>
`
clearInterval(state.loop)
}, 1000)
}
}
const attachEventListeners = () => {
document.addEventListener('click', event => {
const eventTarget = event.target
const eventParent = eventTarget.parentElement
if (eventTarget.className.includes('card') && !eventParent.className.includes('flipped')) {
flipCard(eventParent)
} else if (eventTarget.nodeName === 'BUTTON' && !eventTarget.className.includes('disabled')) {
startGame()
}
})
}
generateGame()
attachEventListeners()
html {
width: 100%;
height: 100%;
background: linear-gradient(325deg, #6f00fc 0%,#fc7900 50%,#fcc700 100%);
font-family: Fredoka;
}
.game {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.controls {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
button {
background: #282A3A;
color: #FFF;
border-radius: 5px;
padding: 10px 20px;
border: 0;
cursor: pointer;
font-family: Fredoka;
font-size: 18pt;
}
.disabled {
color: #757575;
}
.stats {
color: #FFF;
font-size: 14pt;
}
.board-container {
position: relative;
}
.board,
.win {
border-radius: 5px;
box-shadow: 0 25px 50px rgb(33 33 33 / 25%);
background: linear-gradient(135deg, #6f00fc 0%,#fc7900 50%,#fcc700 100%);
transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
backface-visibility: hidden;
}
.board {
padding: 20px;
display: grid;
grid-template-columns: repeat(4, auto);
grid-gap: 20px;
}
.board-container.flipped .board {
transform: rotateY(180deg) rotateZ(50deg);
}
.board-container.flipped .win {
transform: rotateY(0) rotateZ(0);
}
.card {
position: relative;
width: 100px;
height: 100px;
cursor: pointer;
}
.card-front,
.card-back {
position: absolute;
border-radius: 5px;
width: 100%;
height: 100%;
background: #282A3A;
transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg) rotateZ(50deg);
font-size: 28pt;
user-select: none;
text-align: center;
line-height: 100px;
background: #FDF8E6;
}
.card.flipped .card-front {
transform: rotateY(180deg) rotateZ(50deg);
}
.card.flipped .card-back {
transform: rotateY(0) rotateZ(0);
}
.win {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
background: #FDF8E6;
transform: rotateY(180deg) rotateZ(50deg);
}
.win-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 21pt;
color: #282A3A;
}
.highlight {
color: #6f00fc;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧠Memory Game in JavaScript</title>
<link rel="stylesheet" href="assets/styles.css" />
<script src="assets/game.js" defer></script>
</head>
<body>
<div class="game">
<div class="controls">
<button>Start</button>
<div class="stats">
<div class="moves">0 moves</div>
<div class="timer">time: 0 sec</div>
</div>
</div>
<div class="board-container">
<div class="board" data-dimension="4"></div>
<div class="win">You won!</div>
</div>
</div>
</body>
</html>
Because every pair is a match:
if (flippedCards[0].innerText === flippedCards[1].innerText)
Your elements have no text, so innerText is always an empty string. So any two cards, regardless of their images, match.
Probably the quickest solution is to compare the HTML instead:
if (flippedCards[0].innerHTML === flippedCards[1].innerHTML)
Assuming the rest of the HTML is always the same, the only different should be the src on the <img> element(s).
As an added exercise, you could also look into being more explicit in that comparison. Perhaps give each element a data-* property and compare those instead of relying on the text or HTML. Or perhaps specifically read the src property and compare those values instead of comparing the entire contents of the element(s).
The issue is comparing the innerText of the two cards. There is no inner text, so you can compare the card back image URLs.
const selected = [...flippedCards].map(card => card.querySelector('.card-back img').src);
if (allEqual(selected)) {
flippedCards[0].classList.add('matched')
flippedCards[1].classList.add('matched')
}
I also added an allEqual function to make sure all the items match:
const allEqual = arr => arr.every(v => v === arr[0]);
Working example
const allEqual = arr => arr.every(v => v === arr[0]);
const selectors = {
boardContainer: document.querySelector('.board-container'),
board: document.querySelector('.board'),
moves: document.querySelector('.moves'),
timer: document.querySelector('.timer'),
start: document.querySelector('button'),
win: document.querySelector('.win')
};
const state = {
gameStarted: false,
flippedCards: 0,
totalFlips: 0,
totalTime: 0,
loop: null
};
const shuffle = array => {
const clonedArray = [...array];
for (let index = clonedArray.length - 1; index > 0; index--) {
const randomIndex = Math.floor(Math.random() * (index + 1));
const original = clonedArray[index];
clonedArray[index] = clonedArray[randomIndex];
clonedArray[randomIndex] = original;
}
return clonedArray;
}
const pickRandom = (array, items) => {
const clonedArray = [...array];
const randomPicks = [];
for (let index = 0; index < items; index++) {
const randomIndex = Math.floor(Math.random() * clonedArray.length);
randomPicks.push(clonedArray[randomIndex]);
clonedArray.splice(randomIndex, 1);
}
return randomPicks;
}
const generateGame = () => {
const dimensions = selectors.board.getAttribute('data-dimension');
if (dimensions % 2 !== 0) {
throw new Error("The dimension of the board must be an even number.");
}
const emojis = [
'https://i.imgur.com/GLS9S5f.jpg',
'https://i.imgur.com/IN9C2qz.jpg',
'https://i.imgur.com/Ke2ubzv.jpg',
'https://i.imgur.com/PbvJDyR.jpg',
'https://i.imgur.com/L3ysai2.jpg',
'https://i.imgur.com/1NxzhTV.jpg',
'https://i.imgur.com/aksV9O3.jpg',
'https://i.imgur.com/gYsZdE4.jpg',
'https://i.imgur.com/LXo6iW3.jpg',
'https://i.imgur.com/wYrEwNR.jpg'
];
const picks = pickRandom(emojis, (dimensions * dimensions) / 2);
const items = shuffle([...picks, ...picks]);
const cards = `
<div class="board" style="grid-template-columns: repeat(${dimensions}, auto)">
${items.map(item => `
<div class="card">
<div class="card-front"></div>
<div class="card-back"><img src="${item}"></div>
</div>
`).join('')}
</div>
`;
const parser = new DOMParser().parseFromString(cards, 'text/html');
selectors.board.replaceWith(parser.querySelector('.board'));
}
const startGame = () => {
state.gameStarted = true
selectors.start.classList.add('disabled');
state.loop = setInterval(() => {
state.totalTime++;
selectors.moves.innerText = `${state.totalFlips} moves`;
selectors.timer.innerText = `time: ${state.totalTime} sec`;
}, 1000);
}
const flipBackCards = () => {
document.querySelectorAll('.card:not(.matched)').forEach(card => {
card.classList.remove('flipped');
})
state.flippedCards = 0;
}
const flipCard = card => {
state.flippedCards++;
state.totalFlips++;
if (!state.gameStarted) {
startGame();
}
if (state.flippedCards <= 2) {
card.classList.add('flipped');
}
if (state.flippedCards === 2) {
const flippedCards = document.querySelectorAll('.flipped:not(.matched)')
const selected = [...flippedCards].map(card => card.querySelector('.card-back img').src);
if (allEqual(selected)) {
flippedCards[0].classList.add('matched')
flippedCards[1].classList.add('matched')
}
setTimeout(() => {
flipBackCards();
}, 1000);
}
// If there are no more cards that we can flip, we won the game
if (!document.querySelectorAll('.card:not(.flipped)').length) {
setTimeout(() => {
selectors.boardContainer.classList.add('flipped');
selectors.win.innerHTML = `
<span class="win-text">
You won!<br />
with <span class="highlight">${state.totalFlips}</span> moves<br />
under <span class="highlight">${state.totalTime}</span> seconds
</span>
`;
clearInterval(state.loop);
}, 1000);
}
}
const attachEventListeners = () => {
document.addEventListener('click', event => {
const eventTarget = event.target;
const eventParent = eventTarget.parentElement;
if (eventTarget.className.includes('card') && !eventParent.className.includes('flipped')) {
flipCard(eventParent);
} else if (eventTarget.nodeName === 'BUTTON' && !eventTarget.className.includes('disabled')) {
startGame();
}
})
}
generateGame();
attachEventListeners();
html {
width: 100%;
height: 100%;
background: linear-gradient(325deg, #6f00fc 0%, #fc7900 50%, #fcc700 100%);
font-family: Fredoka;
}
.game {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.controls {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
button {
background: #282A3A;
color: #FFF;
border-radius: 5px;
padding: 10px 20px;
border: 0;
cursor: pointer;
font-family: Fredoka;
font-size: 18pt;
}
.disabled {
color: #757575;
}
.stats {
color: #FFF;
font-size: 14pt;
}
.board-container {
position: relative;
}
.board,
.win {
border-radius: 5px;
box-shadow: 0 25px 50px rgb(33 33 33 / 25%);
background: linear-gradient(135deg, #6f00fc 0%, #fc7900 50%, #fcc700 100%);
transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
backface-visibility: hidden;
}
.board {
padding: 20px;
display: grid;
grid-template-columns: repeat(4, auto);
grid-gap: 20px;
}
.board-container.flipped .board {
transform: rotateY(180deg) rotateZ(50deg);
}
.board-container.flipped .win {
transform: rotateY(0) rotateZ(0);
}
.card {
position: relative;
width: 100px;
height: 100px;
cursor: pointer;
}
.card-front,
.card-back {
position: absolute;
border-radius: 5px;
width: 100%;
height: 100%;
background: #282A3A;
transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg) rotateZ(50deg);
font-size: 28pt;
user-select: none;
text-align: center;
line-height: 100px;
background: #FDF8E6;
}
.card.flipped .card-front {
transform: rotateY(180deg) rotateZ(50deg);
}
.card.flipped .card-back {
transform: rotateY(0) rotateZ(0);
}
.win {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
background: #FDF8E6;
transform: rotateY(180deg) rotateZ(50deg);
}
.win-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 21pt;
color: #282A3A;
}
.highlight {
color: #6f00fc;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
<div class="game">
<div class="controls">
<button>Start</button>
<div class="stats">
<div class="moves">0 moves</div>
<div class="timer">time: 0 sec</div>
</div>
</div>
<div class="board-container">
<div class="board" data-dimension="4"></div>
<div class="win">You won!</div>
</div>
</div>
We need advice on how to do what would happen with normal scrolling of the page (with a wheel, or in a mob with a finger) after the block has been screwed up to the top of the screen, it began to scroll horizontally and after the edge of the block is reached, the standard scroll continues, respectively, if the scroll goes up, then that's it in reverse order. (There may be several such blocks on a page)
* {
box-sizing: border-box;
margin: 0;
}
body {
overflow-x: hidden;
}
.simple {
height: 100vh;
background: #1f69c0;
border-bottom: 2px solid #777;
}
.simple2 {
height: 400px;
background: #EEAA07;
border-bottom: 2px solid #777;
}
.simple3 {
height: 400px;
background: #07eed9;
border-bottom: 2px solid #777;
}
.outer {
height: 150px;
background: #7b8e39;
overflow-y: hidden;
overflow-x: scroll;
}
.inner {
height: 100%;
margin: 0 100px;
display: flex;
}
.cube {
min-width: 200px;
height: 100px;
background: #f6f6f6;
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
}
<div class="simple"></div>
<div class="outer">
<div class="inner">
<div class="cube">1</div>
<div class="cube">2</div>
<div class="cube">3</div>
<div class="cube">4</div>
<div class="cube">5</div>
<div class="cube">6</div>
<div class="cube">7</div>
<div class="cube">8</div>
<div class="cube">9</div>
<div class="cube">10</div>
</div>
</div>
<div class="simple2"></div>
<div class="simple3"></div>
Here is an approximate structure, until block 2 has scrolled to the end, block 3 should be visible and after 2 has scrolled horizontally to the end, the standard scroll will continue
PS Here is an example (https://horizontalscrolling.wpdemos.net/horizontal-scrolling/) of how scrolling should work, although there can be more than 1 block on one screen
This technique needs to use javascript called "GSAP" which is a high-performance javascript animation library.
I found a similar animation that you were looking for.
Hopefully, this will help :)
//https://codepen.io/osublake/pen/e72106811a34efcccff91a03568cc790.js?v=3
class SmoothScroll {
constructor(options) {
this.endThreshold = 0.05;
this.requestId = null;
this.maxDepth = 10;
this.viewHeight = 0;
this.halfViewHeight = 0;
this.maxDistance = 0;
this.viewWidth = 0;
this.halfViewWidth = 0;
this.maxDistanceWidth = 0;
this.scrollHeight = 0;
this.endScroll = 0;
this.returnCurrentScroll = 0;
this.currentScroll = 0;
this.scrollTransform = 0;
this.horizontalScroll = 0;
this.resizeRequest = 1;
this.scrollRequest = 0;
this.scrollItems = [];
this.lastTime = -1;
this.maxElapsedMS = 100;
this.targetFPMS = 0.06;
// this.scrollBody = options.scrollBody;
// this.scrollSpacer = options.scrollSpacer;
this.target = options.target;
this.scrollEase = options.scrollEase != null ? options.scrollEase : 0.1;
this.maxOffset = options.maxOffset != null ? options.maxOffset : 500;
this.horizontalScrollWrapper = options.horizontalScrollWrapper;
this.horizontalScrollTarget = options.horizontalScrollTarget;
this._horziontalSetHeihgt();
this.childElements = this._childElements();
this.rectHorStart = this.horizontalScrollWrapper.getBoundingClientRect();
this.horzItemStart = {
top: this.rectHorStart.top,
bottom: this.rectHorStart.bottom,
height: this.rectHorStart.height
}
this.addItems();
window.addEventListener("resize", this._onResize);
window.addEventListener("scroll", this._onScroll);
//this.scrollBody.addEventListener("scroll", this._onScroll);
this._update();
}
_childElements = (event) => {
const childElementsNode = this.target.querySelectorAll("*[data-color]");
return childElementsNode;
}
_horizonstalScrollRect = (event) => {
const horzintalRect = this.horizontalScrollTarget.getBoundingClientRect();
return horzintalRect;
}
_lastScrollRect = (event) => {
const lastScrollRect = this.horizontalScrollTarget.lastElementChild.getBoundingClientRect();
return lastScrollRect;
}
_horziontalSetHeihgt = (event) => {
let horScrHeight = 0;
if (
this.horizontalScrollTarget !== null &&
this.horizontalScrollWrapper !== null
) {
const lastScrollRect = this._lastScrollRect();
horScrHeight = this.horizontalScrollTarget.scrollWidth - lastScrollRect.width + this._horizonstalScrollRect().height;
this.horizontalScrollWrapper.style.height = horScrHeight + "px";
}
}
_onResize = (event) => {
this.resizeRequest++;
if (!this.requestId) {
this.lastTime = performance.now();
this.requestId = requestAnimationFrame(this._update);
}
};
_onScroll = (event) => {
this.scrollRequest++;
if (!this.requestId) {
this.lastTime = performance.now();
this.requestId = requestAnimationFrame(this._update);
}
};
_horizonstalScroll = (scrollY,dt) => {
if (this.horizontalScrollWrapper !== null) {
const rectHor = this.horizontalScrollWrapper.getBoundingClientRect();
const lastScrollRect = this._lastScrollRect();
const itemHor = {
target: this.horizontalScrollTarget,
targetRect: this._horizonstalScrollRect(),
top: rectHor.top,
bottom: rectHor.bottom + scrollY,
topScroll: rectHor.top + scrollY,
horizonstalMove: 0,
};
itemHor.horizonstalMove += this.currentScroll - this.horzItemStart.top;
if(scrollY >= this.horzItemStart.top && scrollY <= this.horzItemStart.bottom - itemHor.targetRect.height){
itemHor.target.style.position = 'fixed';
itemHor.target.style.transform = `translate3d(-${itemHor.horizonstalMove}px,0px,0px)`;
//this._paralaxHorizontal(dt);
if(lastScrollRect.x <= (lastScrollRect.width/2)){
this.scrollTransform = this.horzItemStart.bottom - itemHor.targetRect.height;
itemHor.target.style.top = this.horzItemStart.bottom - itemHor.targetRect.height+'px';
}else {
this.scrollTransform = this.horzItemStart.top;
itemHor.target.style.top = this.horzItemStart.top+'px';
}
}
}
};
_changeColorBody = (event) => {
if(this.childElements.length > 0){
this.childElements.forEach(child => {
const wrapper = document.querySelector('.change_color_page');
const childRect = child.getBoundingClientRect();
const childAttr = child.getAttribute('data-color');
if(childRect.y <= this.halfViewHeight && childRect.bottom >= this.halfViewHeight){
if(childAttr == "off_white"){
if(!document.body.classList.contains('white')){
document.body.classList.add('white');
}
if(!wrapper.classList.contains('white')){
wrapper.classList.add('white');
}
}else if(childAttr == "dark"){
if(document.body.classList.contains('white')){
document.body.classList.remove('white');
}
if(wrapper.classList.contains('white')){
wrapper.classList.remove('white');
}
}
}
});
}
}
_update = (currentTime = performance.now()) => {
let elapsedMS = currentTime - this.lastTime;
if (elapsedMS > this.maxElapsedMS) {
elapsedMS = this.maxElapsedMS;
}
const deltaTime = elapsedMS * this.targetFPMS;
const dt = 1 - Math.pow(1 - this.scrollEase, deltaTime);
const resized = this.resizeRequest > 0;
const scrollY = window.pageYOffset;
//const scrollY = this.scrollBody.scrollTop;
if (resized) {
this._horziontalSetHeihgt();
const height = this.target.clientHeight;
document.body.style.height = height + "px";
//this.scrollSpacer.style.height = height + "px";
this.scrollHeight = height;
this.viewHeight = window.innerHeight;
this.halfViewHeight = this.viewHeight / 2;
this.maxDistance = this.viewHeight * 2;
this.resizeRequest = 0;
this.viewWidth = window.innerWidth;
this.halfViewWidth = this.viewWidth / 2;
this.maxDistanceWidth = this.viewWidth * 2;
}
this.endScroll = scrollY;
// this.scrollTransform += (scrollY - this.scrollTransform) * this.scrollEase;
this.scrollTransform += (scrollY - this.scrollTransform) * dt;
this.currentScroll += (scrollY - this.currentScroll) * dt;
if (Math.abs(scrollY - this.currentScroll) < this.endThreshold || resized) {
this.currentScroll = scrollY;
this.scrollRequest = 0;
}
if (
Math.abs(scrollY - this.scrollTransform) < this.endThreshold ||
resized
) {
this.scrollTransform = scrollY;
this.scrollRequest = 0;
}
///change color section
this._changeColorBody();
///horizontal scroll
this._horizonstalScroll(this.currentScroll,dt);
// const scrollOrigin = scrollY + this.halfViewHeight;
const scrollOrigin = this.currentScroll + this.viewHeight;
this.target.style.transform = `translate3d(0px,-${this.scrollTransform}px,0px)`;
//items
for (let i = 0; i < this.scrollItems.length; i++) {
const item = this.scrollItems[i];
const distance = scrollOrigin - item.top;
const offsetRatio = distance / this.maxDistance;
item.endOffset = Math.round(
this.maxOffset * item.depthRatio * offsetRatio
);
if (Math.abs(item.endOffset - item.currentOffset < this.endThreshold)) {
item.currentOffset = item.endOffset;
} else {
// item.currentOffset += (item.endOffset - item.currentOffset) * this.scrollEase;
item.currentOffset += (item.endOffset - item.currentOffset) * dt;
}
if(item.direction == "y"){
item.target.style.transform = `translate3d(0px,${item.currentOffset}px,0px)`;
}else if(item.direction == "x"){
item.target.style.transform = `translate3d(${item.currentOffset}px,0px,0px)`;
}
}
this.lastTime = currentTime;
this.requestId =
this.scrollRequest > 0 ? requestAnimationFrame(this._update) : null;
};
addItems() {
this.scrollItems = [];
const elements = document.querySelectorAll("*[data-depth]");
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const depth = +element.getAttribute("data-depth");
const direction_item = element.getAttribute("data-direction");
const rect_item = element.getBoundingClientRect();
const item = {
rect: rect_item,
target: element,
top: rect_item.top + window.pageYOffset,
//top: rect_item.top + this.scrollBody.scrollTop,
depth: depth,
depthRatio: depth / this.maxDepth,
currentOffset: 0,
endOffset: 0,
direction: direction_item
};
this.scrollItems.push(item);
}
return this;
}
currentScrollReturn() {
return this.currentScroll;
}
}
document.documentElement.style.setProperty(
"--scrollbar-size",
getScrollbarSize() + "px"
);
var scroller = new SmoothScroll({
// scrollBody: document.querySelector(".scroll-content"),
// scrollSpacer: document.querySelector(".spacer"),
target: document.querySelector(".scroll-container"), // element container to scroll
scrollEase: 0.05,
horizontalScrollWrapper: document.querySelector(".horizontal-scroll-wrapper"),
horizontalScrollTarget: document.querySelector(".horizontal-scroll")
});
function getScrollbarSize() {
var div = document.createElement("div");
div.classList.add("scrollbar-test");
document.body.appendChild(div);
var size = div.offsetWidth - div.scrollWidth;
document.body.removeChild(div);
return size;
}
$white: #fbe8ee;
$black: #0a0a0a;
:root {
--scrollbar-size: 0px;
}
*, :after, :before {
box-sizing: border-box;
}
body {
}
.viewport {
overflow: hidden;
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
body {
overflow-x: hidden;
overflow-y: scroll;
padding: 0;
margin: 0;
font-family: "Courier New", Courier, monospace;
*:not(.change_color_page) {
color: $white;
transition: color 0.5s ease-in-out, border-color 0.5s ease;
border-color: $white;
}
&.white {
*:not(.change_color_page) {
color: $black;
transition: color 0.5s ease-in-out, border-color 0.5s ease;
border-color: $black;
}
}
}
.change_color_page {
position: fixed;
display: block;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: $black;
transition: background-color 0.5s ease;
backface-visibility: hidden;
transform-style: preserve-3d;
&.white {
background-color: $white;
}
}
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.5s ease-in-out;
background-color: $white;
&.white {
background-color: $black;
}
}
.scrollbar-test {
position: absolute;
visibility: hidden;
overflow: scroll;
width: 100px;
height: 100px;
top: -99999px;
left: -99999px;
pointer-events: none;
user-select: none;
}
.fixed-content {
position: absolute;
display: block;
top: 0;
left: 0;
right: var(--scrollbar-size, 0px);
bottom: 0;
z-index: 2;
pointer-events: none;
}
.scroll-container {
position: absolute;
overflow: hidden;
z-index: 10;
backface-visibility: hidden;
transform-style: preserve-3d;
width: 100%;
}
.content {
overflow: hidden;
position: relative;
width: 100%;
}
.spacer {
background: transparent;
}
.single-item {
flex: 0 0 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 70px;
&.left {
justify-content: flex-start;
}
p {
width: 300px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
border-width: 2px;
border-style: solid;
}
}
.horizontal-scroll-wrapper {
position: relative;
}
.horizontal-scroll {
display: flex;
}
.horizontal-scroll .single-item {
flex: 0 0 100vw;
p {
width: 600px;
height: 600px;
max-width: 80%;
max-height: 80%;
}
}
<div class="change_color_page"></div>
<header></header>
<div class="viewport">
<div class="scroll-container">
<div class="content">
<div class="single-item active" data-color="off_white">
<p>1</p>
</div>
<div class="single-item" data-color="dark">
<p data-depth="-7" data-direction="y">2</p>
</div>
<div class="single-item" data-color="off_white">
<p class="item_to_move">3</p>
</div>
<div class="single-item" data-color="dark">
<p data-depth="-3" data-direction="y">4</p>
</div>
<div class="single-item" data-color="off_white">
<p data-depth="-3" data-direction="y">5</p>
</div>
<div class="horizontal-scroll-wrapper" data-color="dark">
<div class="horizontal-scroll">
<div class="single-item">
<p><span data-depth-hor="-3" data-direction="left">6</span></p>
</div>
<div class="single-item">
<p><span data-depth-hor="-3" data-direction="left">7</span></p>
</div>
<div class="single-item">
<p><span data-depth-hor="-3" data-direction="left">8</span></p>
</div>
<div class="single-item">
<p><span data-depth-hor="-3" data-direction="left">9</span></p>
</div>
<div class="single-item">
<p><span data-depth-hor="-3" data-direction="left">10</span></p>
</div>
</div>
</div>
<div class="single-item" data-color="off_white">
<p data-depth="-3" data-direction="y">11</p>
</div>
<div class="single-item" data-color="dark">
<p data-depth="-3" data-direction="y">12</p>
</div>
<div class="single-item left" data-color="off_white">
<p data-depth="15" data-direction="x">13</p>
</div>
</div>
</div>
</div>
source code from: https://codepen.io/duty47/pen/vYYEgam
I've been trying to develop a CSS keyframes animation for a navigation bar.
You can see in the code snippet how the animation works - the red line is animated when the user clicks an element of the nav bar. The first element of the nav bar is active by default (the red line is under this element). When an element is clicked, the JS takes the properties of the animation element, as well as the properties of the element that was clicked. These properties are incorporated into new keyframes that are inserted into the single keyframes rule.
When the second element is clicked, the animation runs successfully from element 1 --> 2. The animation also runs successfully from element 1 --> 3.
But after the animation plays from element 1 --> 2, it won't play from element 2 --> 3. The animationend event does not trigger (I checked this). As of now, I'm only concerned with the animation going forwards.
After researching, I tried several methods to fix this. Removing and reattaching the animation class does not work, even with a DOM reflow being triggered. Changing the animation-play-state from 'running' to 'paused' does not work either. Other solutions, such as changing the animation-name to 'none' and then back, only generate more problems, like the position of the animation element being reset upon the ending of the animation. I truly do not know how to fix this.
I would prefer to make a flexible keyframes animation, such as this, rather than brute-forcing it. A brute force scenario would include making 6 different keyframes rules, and I want the code to be applicable to any number of elements in the navigation bar. Adding keyframes rules for every addition of an element would require exponentially more code each addition.
Thanks.
~ Code for demo ~
var keyframes = findKeyframesRule('movey');
$(document).ready(() => {
$('div.one').click(() => {
if (!($('div.one').hasClass('active'))) {
/* unfinished */
}
})
$('div.two').click(() => {
if (!($('div.two').hasClass('active'))) {
/* transfer active class */
$('div.active').removeClass('active');
$('div.two').addClass('active');
var left = ( parseInt($('div.absolute').css('left')) / $(window).width() ) * 100;
/* reset keyframes before animation */
clearKeyframes();
/* add new keyframes for when div.two is clicked */
keyframes.appendRule("0% { width: 15%; left: " + left + "%;}");
keyframes.appendRule("49.99% { width: 30%; left: " + left + "%; right: 70%;}");
keyframes.appendRule("50% { width: 30%; left: unset; right: 70%;}");
keyframes.appendRule("100% { width: 15%; right: 70%;}");
/* first animation - add animation class */
if (!($('div.absolute').hasClass('animateMovey'))) {
$('div.absolute').addClass('animateMovey');
/* animations after first - remove and reattach animation class with new keyframes */
} else {
$('div.absolute').removeClass('animateMovey');
$('div.absolute').addClass('animateMovey');
}
/* ensure animation occurs */
$('div.animateMovey').on('animationend', () => {
console.log('Animation ended');
})
}
})
$('div.three').click(() => {
if (!($('div.three').hasClass('active'))) {
$('div.active').removeClass('active');
$('div.three').addClass('active');
var left = ( parseInt($('div.absolute').css('left')) / $(window).width() ) * 100;
var width = 45 - left;
clearKeyframes();
keyframes.appendRule("0% { width: 15%; left: " + left + "%;}");
keyframes.appendRule("49.99% { width: " + width + "%; left: " + left + "%; right: 55%;}");
keyframes.appendRule("50% { width: " + width + "%; left: unset; right: 55%;}");
keyframes.appendRule("100% { width: 15%; right: 55%;")
if (!($('div.absolute').hasClass('animateMovey'))) {
$('div.absolute').addClass('animateMovey');
} else {
$('div.absolute').removeClass('animateMovey');
$('div.absolute').addClass('animateMovey');
}
$('div.animateMovey').on('animationend', () => {
console.log('Animation ended');
})
}
})
})
function findKeyframesRule(rule) {
var ss = document.styleSheets;
for (var i = 0; i < ss.length; ++i) {
for (var j = 0; j < ss[i].cssRules.length; ++j) {
if (ss[i].cssRules[j].type == window.CSSRule.KEYFRAMES_RULE && ss[i].cssRules[j].name == rule)
return ss[i].cssRules[j];
}
}
return null;
}
function clearKeyframes() {
for (var i = 0; i <= 3; ++i) {
if (keyframes[0]) {
var keyToRemove = keyframes[0].keyText;
keyframes.deleteRule(keyToRemove);
}
}
}
body {
margin: 0;
}
div.nav {
position: relative;
display: block;
overflow: hidden;
width: 100%;
}
div.nav div {
float: left;
width: 15%;
height: 75px;
}
div.nav div:hover {
opacity: 0.5;
}
div.one {
background-color: #7a7a7a;
}
div.two {
background-color: #9e9e9e;
}
div.three {
background-color: #bdbdbd;
}
.active {
box-shadow: inset 3px 5px 6px #000;
}
div.animateMovey {
animation-name: movey;
animation-duration: 0.6s;
animation-fill-mode: forwards;
animation-timing-function: ease-in-out;
}
div.relative {
position: relative;
width: 100%;
height: 20px;
}
div.absolute {
position: absolute;
background-color: #ff8c69;
width: 15%;
height: 100%;
}
#keyframes movey {
100% { }
}
<div>
<div class="nav">
<div class="one active"></div>
<div class="two"></div>
<div class="three"></div>
</div>
<div class="relative">
<div class="absolute"></div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
Interesting question. I'm not sure why the event is not re-triggering in this case, but will suggest a few changes to your approach:
Aim to animate transform and opacity instead of width and left, right
(https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance)
One way to do this is to use a separate red element under each box, and slide it left or right using transform
Use animation-delay to create the lengthening and shortening effect
Try to reuse the animation logic, so it will work regardless of the number of items.
The challenging part of this effect is managing the opacity of each line. I've used animationEnd to help with that, and it appears to work fine.
Additional comments in the example code. It could be improved by handling clicks while animation is active, consolidating animation functions etc. You could also vary the animation duration depending on the number of items.
let boxes = null;
let lines = null;
let fromIndex = 0;
let toIndex = 0;
const ANIMATION_DURATION = 0.1; // seconds
const animation = {
animating: false,
lines: [],
direction: "right",
inOrOut: "in"
};
function getEls() {
boxes = [...document.querySelectorAll(".box")];
lines = [...document.querySelectorAll(".line")];
}
function setAnimationDuration() {
lines.forEach((line) => {
line.style.animationDuration = `${ANIMATION_DURATION}s`;
});
}
function addEvents() {
boxes.forEach((box, index) => {
box.addEventListener("click", () => {
// User has clicked the currently active box
if (fromIndex === index) return;
// Line is currently animating
if (animation.animating) return;
toIndex = index;
updateActiveBox();
handleLineAnimation();
});
});
document.addEventListener("animationend", (e) => {
// Maintain opacity on lines that animate in
if (animation.inOrOut === "in") {
e.target.style.opacity = 1;
}
});
}
function updateActiveBox() {
boxes[fromIndex].classList.remove("active");
boxes[toIndex].classList.add("active");
}
function updateActiveLine(line) {
lines[fromIndex].classList.remove("active");
line.classList.add("active");
}
function handleLineAnimation() {
animation.animating = true;
animation.lines = [];
if (toIndex > fromIndex) {
animation.direction = "right";
for (let i = fromIndex; i <= toIndex; i++) {
animation.lines.push(lines[i]);
}
} else {
animation.direction = "left";
for (let i = fromIndex; i >= toIndex; i--) {
animation.lines.push(lines[i]);
}
}
animate();
}
function animate() {
const wait = (animation.lines.length - 1) * ANIMATION_DURATION * 1000;
animation.inOrOut = "in";
animateIn();
setTimeout(() => {
resetLine();
updateActiveLine(lines[toIndex]);
animation.inOrOut = "out";
animateOut();
setTimeout(() => {
resetLine();
onAnimationComplete();
}, wait);
}, wait);
}
function animateIn() {
const {
direction,
lines
} = animation;
lines.forEach((line, index) => {
// index = 0 is currently active, no need to animate in
if (index > 0) {
line.classList.add(`animate-in-${direction}`);
line.style.animationDelay = `${(index - 1) * ANIMATION_DURATION}s`;
}
});
}
function animateOut() {
const {
direction,
lines
} = animation;
lines.forEach((line, index) => {
// lines.length - 1 is new active, don't animate out
if (index < lines.length - 1) {
line.classList.remove(`animate-in-${direction}`);
line.classList.add(`animate-out-${direction}`);
line.style.animationDelay = `${index * ANIMATION_DURATION}s`;
}
});
}
function resetLine() {
const {
direction,
lines,
inOrOut
} = animation;
lines.forEach((line) => {
line.classList.remove(`animate-${inOrOut}-${direction}`);
line.style.animationDelay = null;
// After animating out, remove inline opacity
if (inOrOut === "out") {
if (!line.classList.contains("active")) {
line.style.opacity = "";
}
}
});
}
function onAnimationComplete() {
animation.animating = false;
fromIndex = toIndex;
}
function init() {
getEls();
setAnimationDuration();
addEvents();
}
function reset() {
fromIndex = 0;
init();
lines.forEach((line, index) => {
line.classList.remove('active');
line.style.opacity = "";
boxes[index].classList.remove('active');
});
boxes[0].classList.add("active");
lines[0].classList.add("active");
}
init();
// DEBUG
document.getElementById("debug").addEventListener("change", (e) => {
document.querySelector("nav").classList.toggle("debug-on");
});
document.getElementById("add").addEventListener("click", (e) => {
const div = document.createElement("div");
div.classList.add("box");
div.innerHTML = '<div class="new"></div><span class="line"></span>';
document.querySelector("nav").appendChild(div);
reset();
});
document.getElementById("remove").addEventListener("click", (e) => {
const indexToRemove = boxes.length - 1;
if (indexToRemove > 0) {
const box = boxes[indexToRemove];
box.parentNode.removeChild(box);
reset();
}
});
nav {
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
.debug-on .line {
border: 1px solid;
box-sizing: border-box;
opacity: 0.2;
}
.box {
display: flex;
flex-direction: column;
position: relative;
float: left;
flex: 0 0 15%;
/* Allows the line to slide left or right with opacity: 1 */
overflow: hidden;
}
.box>div {
cursor: pointer;
height: 75px;
}
.one {
background-color: #7a7a7a;
}
.two {
background-color: #9e9e9e;
}
.three {
background-color: #bdbdbd;
}
.new {
background-color: pink;
border: 1px solid;
box-sizing: border-box;
}
.line {
background-color: #ff8c69;
height: 20px;
opacity: 0;
pointer-events: none;
width: 100%;
animation-fill-mode: forwards;
animation-timing-function: linear;
}
.active>div {
box-shadow: inset 3px 5px 6px #000;
}
.box:hover div {
opacity: 0.5;
}
.line.active {
opacity: 1;
}
.line.show {
opacity: 1;
}
.animate-in-right {
animation-name: SLIDE_IN_RIGHT;
}
.animate-out-right {
animation-name: SLIDE_OUT_RIGHT;
}
.animate-in-left {
animation-name: SLIDE_IN_LEFT;
}
.animate-out-left {
animation-name: SLIDE_OUT_LEFT;
}
#keyframes SLIDE_IN_RIGHT {
from {
opacity: 1;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
#keyframes SLIDE_OUT_RIGHT {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 1;
transform: translateX(100%);
}
}
#keyframes SLIDE_IN_LEFT {
from {
opacity: 1;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
#keyframes SLIDE_OUT_LEFT {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 1;
transform: translateX(-100%);
}
}
/* for demo only */
.debug {
background: #eee;
padding: 1rem;
display: inline-flex;
flex-direction: column;
font: 14px/1 sans-serif;
position: fixed;
bottom: 0;
right: 0;
}
.debug button {
margin-top: 1rem;
padding: .25rem;
}
<nav>
<div class="box active">
<div class="one"></div>
<span class="line active"></span>
</div>
<div class="box">
<div class="two"></div>
<span class="line"></span>
</div>
<div class="box">
<div class="three"></div>
<span class="line"></span>
</div>
</nav>
<br><br>
<div class="debug">
<label for="debug">Debug Lines <input type="checkbox" id="debug">
</label>
<button id="add">Add cell</button>
<button id="remove">Delete cell</button>
</div>
Why after clicking the load button, both buttons load and dummy covered by the loader's overlay still register clicks?
Sometimes when clicking the load button, the loader is not even displayed.
Buttons correctly don't register clicks
if we for example display the loader from the start by commenting the line 5 loader.hide();
add some timeout delay (but I don't want that)
Example (best to run in Full Page mode):
const iterations = 1e3;
const multiplier = 1e9;
const loader = $('.css-loader-fullscreen');
const dummyBtn = $('#dummy');
const loadBtn = $('#load');
loader.hide();
dummyBtn.on('click', () => console.log('dummy clicked'));
loadBtn.on('click', jsHeavyTask);
function calculatePrimes(iterations, multiplier) {
var primes = [];
for (var i = 0; i < iterations; i++) {
var candidate = i * (multiplier * Math.random());
var isPrime = true;
for (var c = 2; c <= Math.sqrt(candidate); ++c) {
if (candidate % c === 0) {
// not prime
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(candidate);
}
}
return primes;
}
function jsHeavyTask(){
console.log('heavy function started');
loader.show();
setTimeout(() => {
const start = performance.now();
calculatePrimes(iterations, multiplier);
const end = performance.now();
loader.hide();
console.log('heavy function ended in '+ (end - start).toFixed() +' ms');
});
}
input {width: 150px}
.css-loader-background {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border-radius: 10px;
font-size: 12px;
}
.css-loader-fullscreen {
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100000;
position: fixed;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.css-loader-fullscreen .css-loader-background {
width: 100px;
height: 100px;
}
.css-loader-animation {
width: 40px;
height: 40px;
border-radius: 50%;
border: 8px solid transparent;
border-top-color: purple;
border-bottom-color: purple;
text-indent: -9999em;
animation: spinner 0.8s ease infinite;
transform: translateZ(0);
}
#-webkit-keyframes spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="css-loader-fullscreen">
<div class="css-loader-background">
<div class="css-loader-animation"></div>
</div>
</div>
<input id="load" type="button" value="load">
<input id="dummy" type="button" value="dummy">
Run times of jsHeavyTask() are different on every machine. For me it's around 5s. You can change iterations and multiplier constants to modify the run time.
There is lot more of weird I observed related to this no delay timed-out calc-heavy function, especially in Webkit, but first I am curious about this one.
as first I would try to call the heavy function differently:
loadBtn.on('click', () => {
loader.show();
jsHeavyTask
});
If that doesn't do the trick, I would try different approach with the show/hide method and use opacity with ponter-events combination for better performance and disabling passing through the clicks.
JavaScript
const loader = $('.css-loader-fullscreen');
const dummyBtn = $('#dummy');
const loadBtn = $('#load');
const content = $('#content');
const cssHidden = 'css-hidden';
const cssLoading = 'css-loading';
loader.addClass(cssHidden);
dummyBtn.on('click', () => console.log('dummy clicked'));
loadBtn.on('click', () => {
content.addClass(cssLoading);
loader.removeClass(cssHidden);
jsHeavyTask
});
function jsHeavyTask(){
console.log('heavy function started');
setTimeout(() => {
for ( var i = 0; i < 2e7; i++){
Math.sqrt(Date.now());
}
loader.addClass(cssHidden);
content.removeClass(cssLoading);
console.log('heavy function ended');
});
}
CSS (only changes)
.css-loader-fullscreen {
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100000;
position: fixed;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
opacity: 99.99999;
pointer-events: auto;
}
.css-hidden {
opacity: 0.000001;
pointer-events: none;
}
.css-loading {
pointer-events: none;
}
HTML
<div class="css-loader-fullscreen">
<div class="css-loader-background">
<div class="css-loader-animation"></div>
</div>
</div>
<div id="content">
<input id="load" type="button" value="load">
<input id="dummy" type="button" value="dummy">
</div>
Sometimes when clicking the load button, the loader is not even displayed.
Obviously because of this setTimeout function
setTimeout(() => {
for (var i = 0; i < 2e7; i++) {
Math.sqrt(Date.now());
}
console.log('heavy function ended');
});
The setTimeout makes your code, kind of async, so the inside runs smoothly and with the provided delay. Here, you have almost no delay, it's just a simple timeout that runs instantly, the functions inside as there's little to no delay value.
On the other hand, the for loop has dynamic run times.
var t0 = performance.now();
for (var i = 0; i < 2e7; i++) {
Math.sqrt(Date.now());
}
var t1 = performance.now();
console.log("Took " + (t1 - t0) + " milliseconds.");
So sometimes it runs fast enough and that means that your loader.hide(); runs instantly and hides your overlay and sometimes not so you see the loader.
Why after clicking the load button, both buttons covered by the loader's overlay still register clicks?
I don't know what you mean there but if you mean that you can click as the overlay displays then no, try debugging by not allowing the loaders to go and then click the other button, you'll notice that the click is not registered.
const loader = $('.css-loader-fullscreen');
const dummyBtn = $('#dummy');
const loadBtn = $('#load');
loader.hide();
dummyBtn.on('click', () => console.log('dummy clicked'));
loadBtn.on('click', jsHeavyTask);
function jsHeavyTask(){
console.log('heavy function started');
loader.show();
setTimeout(() => {
for ( var i = 0; i < 2e6; i++){
Math.sqrt(Date.now());
}
// loader.hide();
console.log('heavy function ended');
});
}
input {width: 150px}
.css-loader-background {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border-radius: 10px;
font-size: 12px;
}
.css-loader-fullscreen {
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100000;
position: fixed;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.css-loader-fullscreen .css-loader-background {
width: 100px;
height: 100px;
}
.css-loader-animation {
width: 40px;
height: 40px;
border-radius: 50%;
border: 8px solid transparent;
border-top-color: purple;
border-bottom-color: purple;
text-indent: -9999em;
animation: spinner 0.8s ease infinite;
transform: translateZ(0);
}
#-webkit-keyframes spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="css-loader-fullscreen">
<div class="css-loader-background">
<div class="css-loader-animation"></div>
</div>
</div>
<input id="load" type="button" value="load">
<input id="dummy" type="button" value="dummy">
NOTE: Some of your stated behaviors are inconsistent with the given observations in the questions, I suggest you adequately validate them.
It simply seems that the browser is memorizing all the clicks while the JS calculation is running. But those clicks are not applied to the painted layout existing before AND during the heavy JS calculation. They are applied to the repainted layout which includes changes introduced while the JS calculation was running. So no loader overlay to catch those clicks.
I would expect the clicks to be applied to the original existing layout, when the loader overlay is still displayed.
edit: adding a correct solution below without a need to add some delay
const iterations = 1e3;
const multiplier = 1e9;
const loader = $('.css-loader-fullscreen');
const dummyBtn = $('#dummy');
const loadBtn = $('#load');
loader.hide();
dummyBtn.on('click', () => console.log('dummy clicked'));
loadBtn.on('click', jsHeavyTask);
function calculatePrimes(iterations, multiplier) {
var primes = [];
for (var i = 0; i < iterations; i++) {
var candidate = i * (multiplier * Math.random());
var isPrime = true;
for (var c = 2; c <= Math.sqrt(candidate); ++c) {
if (candidate % c === 0) {
// not prime
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(candidate);
}
}
return primes;
}
function renderLayoutAndRun(f){
window.requestAnimationFrame(() => {
window.requestAnimationFrame(f);
});
};
function jsHeavyTask(){
loader.show();
renderLayoutAndRun(() => {
console.log('heavy function started');
const start = performance.now();
calculatePrimes(iterations, multiplier);
const end = performance.now();
renderLayoutAndRun(() => loader.hide());
console.log('heavy function ended in '+ (end - start).toFixed() +' ms');
});
}
input {width: 150px}
.css-loader-background {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border-radius: 10px;
font-size: 12px;
}
.css-loader-fullscreen {
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100000;
position: fixed;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.css-loader-fullscreen .css-loader-background {
width: 100px;
height: 100px;
}
.css-loader-animation {
width: 40px;
height: 40px;
border-radius: 50%;
border: 8px solid transparent;
border-top-color: purple;
border-bottom-color: purple;
text-indent: -9999em;
animation: spinner 0.8s ease infinite;
transform: translateZ(0);
}
#-webkit-keyframes spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="css-loader-fullscreen">
<div class="css-loader-background">
<div class="css-loader-animation"></div>
</div>
</div>
<input id="load" type="button" value="load">
<input id="dummy" type="button" value="dummy">