How to set a duration for requestAnimationFrame - javascript

requestAnimationFrame is good solution to creating javascript based animations. But i can not set a duration for this function. I want to play animations for a certain time. I tried some fps solutions but these are not smooth.
How can i fill this water in x seconds?
const water = document.querySelector('.water')
let scale = 0
const fillGlass = () => {
scale += 0.01
water.style.transform = `scaleY(${scale})`
if (scale <= 1) {
requestAnimationFrame(fillGlass)
}
}
requestAnimationFrame(fillGlass)
.glass {
margin: 100px;
width: 150px;
height: 250px;
border: 3px solid #000;
border-top: 0;
border-bottom-width: 12px
}
.water {
height: 100%;
background-color: #BBDEFB;
transform: scaleY(0);
transform-origin: bottom center;
}
<div class="glass">
<div class="water"></div>
</div>

Simply, you can control it with animation-duration. If you want to add dynamic value, I have just added a parameter to pass value to CSS
.glass {
margin: 100px;
width: 150px;
height: 250px;
border: 3px solid #000;
border-top: 0;
border-bottom-width: 12px
}
.water {
height: 100%;
background-color: #BBDEFB;
transform-origin: bottom center;
animation-name: animation;
animation-duration: var(--value);
}
#keyframes animation {
from { transform:scaleY(0) }
to { transform:scaleY(1) }
}
<div class="glass">
<div class="water" style='--value:20s'></div>
</div>
you can change --value in HTML, it will affect on CSS
As per your need, in JS
const water = document.querySelector('.water')
let scale = 0
let time = 2000 // 20 second * 100
const myInterval = setInterval(()=> {
scale += 1/time
water.style.transform = `scaleY(${scale})`
if (scale >= 1) {
clearInterval(myInterval);
}
}, 10);
.glass {
margin: 100px;
width: 150px;
height: 250px;
border: 3px solid #000;
border-top: 0;
border-bottom-width: 12px
}
.water {
height: 100%;
background-color: #BBDEFB;
transform: scaleY(0);
transform-origin: bottom center;
}
<div class="glass">
<div class="water"></div>
</div>
More Functions
const start = document.querySelector('.start')
const stop = document.querySelector('.stop')
const fill = document.querySelector('.fill')
const empty = document.querySelector('.empty')
const water = document.querySelector('.water')
let scale = 0
let time = 2000 // 20 second * 100
let myInterval;
start.addEventListener('click', () => {
myInterval = setInterval(() => {
scale += 1 / time
water.style.transform = `scaleY(${scale})`
if (scale >= 1) {
clearInterval(myInterval);
}
}, 10);
})
stop.addEventListener('click', () => {
clearInterval(myInterval);
})
fill.addEventListener('click', () => {
scale = 1
water.style.transform = `scaleY(${scale})`
})
empty.addEventListener('click', () => {
scale = 0
water.style.transform = `scaleY(${scale})`
})
.glass {
margin: 100px;
width: 150px;
height: 250px;
border: 3px solid #000;
border-top: 0;
border-bottom-width: 12px
}
.water {
height: 100%;
background-color: #BBDEFB;
transform: scaleY(0);
transform-origin: bottom center;
}
<button type="button" class="start">Start</button>
<button type="button" class="stop">Stop</button>
<button type="button" class="fill">Instant Fill</button>
<button type="button" class="empty">Instant Empty</button>
<div class="glass">
<div class="water"></div>
</div>

Defining simple animation like this is always better with pure CSS, also in this case, it gives you more control over the animation itself.
.water {
height: 100%;
background-color: #BBDEFB;
transform-origin: bottom center;
animation-name: animation;
animation-duration: 1s;
}
#keyframes animation {
from { transform:scaleY(0) }
to { transform:scaleY(1) }
}

For animation and smoothness you can divide the scale value by duration you want
const water = document.querySelector('.water')
let scale = 0
let time = 5000 //in seconds 1000=1sec
let duration = 100/time;
const fillGlass = () => {
scale += duration;
if(scale > 1){
scale = 1;
}
water.style.transform = `scaleY(${scale})`
if (scale <= 1) {
requestAnimationFrame(fillGlass)
}
}
requestAnimationFrame(fillGlass)

Related

GPT injecting between content of infinite scroll

I have created a simple boilerplate to test GPT injection using infinite scroll however when the unit loads its at the bottom of the page, but I need it to load at the bottom of the page and then more content load below it.
I can't see what mistake i have made and the changes i need to make to remedy this. All help is great and thankful.
I want it to show 7,8,9 ad 10,11,12
the code snippet doesn't work correctly so for example use link:
Live test page example
const cardContainer = document.getElementById("card-container");
const cardCountElem = document.getElementById("card-count");
const cardTotalElem = document.getElementById("card-total");
const loader = document.getElementById("loader");
const cardLimit = 99;
const cardIncrease = 9;
const pageCount = Math.ceil(cardLimit / cardIncrease);
let currentPage = 1;
cardTotalElem.innerHTML = cardLimit;
var throttleTimer;
const throttle = (callback, time) => {
if (throttleTimer) return;
throttleTimer = true;
setTimeout(() => {
callback();
throttleTimer = false;
}, time);
};
const getRandomColor = () => {
const h = Math.floor(Math.random() * 360);
return `hsl(${h}deg, 90%, 85%)`;
};
const createCard = (index) => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = index;
card.style.backgroundColor = getRandomColor();
cardContainer.appendChild(card);
};
const addCards = (pageIndex) => {
currentPage = pageIndex;
const startRange = (pageIndex - 1) * cardIncrease;
const endRange =
currentPage == pageCount ? cardLimit : pageIndex * cardIncrease;
cardCountElem.innerHTML = endRange;
for (let i = startRange + 1; i <= endRange; i++) {
createCard(i);
}
};
const handleInfiniteScroll = () => {
throttle(() => {
const endOfPage =
window.innerHeight + window.pageYOffset >= document.body.offsetHeight;
if (endOfPage) {
moreContent();
addCards(currentPage + 1);
}
if (currentPage === pageCount) {
removeInfiniteScroll();
}
}, 1000);
};
const removeInfiniteScroll = () => {
loader.remove();
window.removeEventListener("scroll", handleInfiniteScroll);
};
window.onload = function () {
addCards(currentPage);
};
window.addEventListener("scroll", handleInfiniteScroll);
body {
font-family: "Roboto", sans-serif;
}
#card-container {
display: flex;
flex-wrap: wrap;
}
.card {
height: 55vh;
width: calc((100% / 3) - 16px);
margin: 8px;
border-radius: 3px;
transition: all 200ms ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.card:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.card-actions {
margin: 8px;
padding: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
#loader {
display: flex;
}
.skeleton-card {
height: 55vh;
width: calc((100% / 3) - 16px);
margin: 8px;
border-radius: 3px;
transition: all 200ms ease-in-out;
position: relative;
background-color: #eaeaea;
}
.skeleton-card::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0));
animation: load 1s infinite;
}
#keyframes load {
100% {
transform: translateX(100%);
}
}
#media screen and (prefers-reduced-motion: reduce) {
.skeleton-card::after {
animation: none;
}
}
<script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script src="https://naughty-swanson.78-24-89177.plesk.page/Matrix/test_matrix.js"></script>
<div id="leaderboard">
<script>
// Call display() to register the slot as ready and fetch an ad.
googletag.cmd.push(function() {
googletag.display('leaderboard');
});
</script>
</div>
<div id="card-container">
</div>
<div>
</div>
<div id="loader">
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
</div>
<div class="card-actions">
<span>Showing
<span id="card-count"></span> of
<span id="card-total"></span> cards
</span>
</div>

Making a Javascript music app based on tutorial, hit roadblock

I am currently trying to create a music app based on the following video: https://youtu.be/OafpiyPa63I?t=13127
I am at the linked timestamp, and I have written the code exactly as shown in the video, but for some reason, when I try to seek, the input is set back to 0 and therefore so is the song. My code for this part:
let currentStart = document.getElementById('currentStart');
let currentEnd = document.getElementById('currentEnd');
let seek = document.getElementById('seek');
let bar2 = document.getElementById('bar2');
let dot = document.getElementsByClassName('dot')[0];
music.addEventListener('timeupdate', () => {
let music_curr = music.currentTime;
let music_dur = music.duration;
let min1 = Math.floor(music_dur / 60);
let sec1 = Math.floor(music_dur % 60);
if (sec1 < 10) {
sec1 = `0${sec1}`;
};
currentEnd.innerText = `${min1}:${sec1}`;
let min2 = Math.floor(music_curr / 60);
let sec2 = Math.floor(music_curr % 60);
if (sec2 < 10) {
sec2 = `0${sec2}`;
};
currentStart.innerText = `${min2}:${sec2}`;
let progressBar = parseInt((music_curr / music_dur) * 100);
seek.value = progressBar;
let seekbar = seek.value;
bar2.style.width = `${seekbar}%`;
dot.style.left = `${seekbar}%`;
});
seek.addEventListener('change', () => {
music.currentTime = seek.value * music.duration / 100;
});
<div class="bar">
<input type="range" id="seek" min="0" max="100">
<div class="bar2" id="bar2"></div>
<div class="dot"></div>
</div>
header .master_play .bar {
position: relative;
width: 43%;
height: 2px;
background: rgb(105,105,170,.1);
margin: 0px 15px 0px 10px;
}
header .master_play .bar .bar2 {
position: absolute;
background: #36e2ec;
width: 0%;
height: 100%;
top: 0;
transition: 1s linear;
}
header .master_play .bar .dot {
position: absolute;
width: 5px;
height: 5px;
background: #36e2ec;
border-radius: 50%;
left: 0%;
top: -1.5px;
transition: 1s linear;
}
header .master_play .bar .dot::before {
content: '';
position: absolute;
width: 15px;
height: 15px;
border: 1px solid #36e2ec;
border-radius: 50%;
left: -6.5px;
top: -6.5px;
box-shadow: inset 0px 0px 3px #36e2ec;
}
header .master_play .bar input {
position: absolute;
width: 100%;
top: -7px;
left: 0;
cursor: pointer;
z-index: 999999999999;
opacity: .5;
}
I'm not sure if I am in the wrong, or if the tutorial is outdated.
I figured out it was the platform I was running the code on. Somewhere it had clashed, so I was able to move it to a new host and it worked.

memory js game pictures won't close

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>

Add disappearing circle element to click function

I have a page where circles randomly appear in a certain place.
When you click on the circle, it disappears.
So, I have another circle-smoke, and I want that when the clicked circle disappears, this circle with the circle-smoke class appears in its place, and also dissolves in just a second.
I tried to add new element creation to my click function, but it didn't work
let div1 = document.createElement('div');
div1.classList.add("circle-smoke");
Isn’t it possible for the smoky circle to hold on a little more and steam out? The first one quickly disappeared.
In general, the whole point is to remain like this in the place of the disappearing circle, and then also disappear
How can I best implement this?
//create circle
var clickEl = document.getElementById("clicks");
var spawnRadius = document.getElementById("spawnRadius");
var spawnArea = spawnRadius.getBoundingClientRect();
const circleSize = 95; // Including borders
function createDiv(id, color) {
let div = document.createElement('div');
div.setAttribute('class', id);
if (color === undefined) {
let colors = ['#ebc6df', '#ebc6c9', '#e1c6eb', '#c6c9eb', '#c6e8eb', '#e373fb', '#f787e6', '#cb87f7', '#87a9f7', '#87f7ee'];
randomColor = colors[Math.floor(Math.random() * colors.length)];
div.style.borderColor = randomColor;
}
else {
div.style.borderColor = color;
}
// Randomly position circle within spawn area
div.style.top = `${Math.floor(Math.random() * (spawnArea.height - circleSize))}px`;
div.style.left = `${Math.floor(Math.random() * (spawnArea.width - circleSize))}px`;
div.classList.add("circle", "animation");
// Add click handler
let clicked = false;
div.addEventListener('click', (event) => {
if (clicked) { return; } // Only allow one click per circle
clicked = true;
div.style.animation = 'Animation 200ms linear forwards';
setTimeout(() => { spawnRadius.removeChild(div); }, 220);
});
spawnRadius.appendChild(div);
}
let i = 0;
const rate = 3000;
setInterval(() => {
i += 1;
createDiv(`circle${i}`);
}, rate);
html, body {
width: 100%;
height: 100%;
margin: 0;
background: #0f0f0f;
}
.circle {
width: 80px;
height: 80px;
border-radius: 80px;
background-color: #0f0f0f;
border: 3px solid #000;
position: absolute;
}
#spawnRadius {
top: 55%;
height: 250px;
width: 500px;
left: 50%;
white-space: nowrap;
position: absolute;
transform: translate(-50%, -50%);
background: #0f0f0f;
border: 2px solid #ebc6df;
}
#keyframes Animation {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(.8);
}
100% {
transform: scale(1);
opacity: 0;
}
}
.circle-smoke {
position: relative;
height: 500px;
width: 500px;
filter: url(#wave);
transform: scale(0.3);
}
.circle-smoke::before {
content: "";
position: absolute;
top: 100px;
left: 100px;
right: 100px;
bottom: 100px;
border: 10px solid #fff;
border-radius: 50%;
box-shadow: 0 0 50px #ebc6df, inset 0 0 50px #ebc6df;
filter: url(#wave) blur(10px);
}
svg {
display: none;
}
<html>
<body>
<div id="spawnRadius"></div>
</body>
</html>
My quick and dirty solution is here (the filters were disabled and the rate was increased for debugging purposes):
//create circle
var clickEl = document.getElementById("clicks");
var spawnRadius = document.getElementById("spawnRadius");
var spawnArea = spawnRadius.getBoundingClientRect();
const circleSize = 95; // Including borders
function createDiv(id, color, isSmoke) {
let div = document.createElement('div');
div.setAttribute('class', id);
if (!isSmoke) {
if (color === undefined) {
let colors = ['#ebc6df', '#ebc6c9', '#e1c6eb', '#c6c9eb', '#c6e8eb', '#e373fb', '#f787e6', '#cb87f7', '#87a9f7', '#87f7ee'];
randomColor = colors[Math.floor(Math.random() * colors.length)];
div.style.borderColor = randomColor;
} else {
div.style.borderColor = color;
}
}
// Randomly position circle within spawn area
div.style.top = `${Math.floor(Math.random() * (spawnArea.height - circleSize))}px`;
div.style.left = `${Math.floor(Math.random() * (spawnArea.width - circleSize))}px`;
div.classList.add("circle", "animation");
// Add click handler
let clicked = false;
if (!isSmoke) {
div.addEventListener('click', (event) => {
if (clicked) {
return;
} // Only allow one click per circle
clicked = true;
div.style.animation = 'Animation 200ms linear forwards';
setTimeout(() => {
spawnRadius.removeChild(div);
}, 220);
let newDiv = createDiv(id, color, true);
newDiv.style.top = div.style.top;
newDiv.style.left = div.style.left;
newDiv.classList.add("circle-smoke");
newDiv.classList.remove("circle");
newDiv.id = null;
});
} else {
div.style.animation = 'Animation 500ms linear forwards';
setTimeout(() => {
spawnRadius.removeChild(div);
}, 220);
}
spawnRadius.appendChild(div);
return div;
}
let i = 0;
const rate = 1000;
setInterval(() => {
i += 1;
createDiv(`circle${i}`);
}, rate);
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: #0f0f0f;
}
.circle {
width: 80px;
height: 80px;
border-radius: 80px;
background-color: #0f0f0f;
border: 3px solid #000;
position: absolute;
}
#spawnRadius {
top: 55%;
height: 250px;
width: 500px;
left: 50%;
white-space: nowrap;
position: absolute;
transform: translate(-50%, -50%);
background: #0f0f0f;
border: 2px solid #ebc6df;
}
#keyframes Animation {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(.8);
}
100% {
transform: scale(1);
opacity: 0;
}
}
.circle-smoke {
position: absolute;
margin: -5px;
height: 80px;
width: 80px;
border: 10px solid rgba(255, 255, 255, .5);
border-radius: 50%;
box-shadow: 0 0 50px #ebc6df, inset 0 0 50px #ebc6df;
//filter: url(#wave) blur(10px);
}
svg {
display: none;
}
<html>
<body>
<div id="spawnRadius"></div>
</body>
</html>
We can animate box-shadow:
let noOfCircles = 0;
const totalCircles = 10;
const colors = ['#ebc6df', '#ebc6c9', '#e1c6eb', '#c6c9eb', '#c6e8eb', '#e373fb', '#f787e6', '#cb87f7', '#87a9f7', '#87f7ee'];
const circleSize = 95; // Including borders
let i = 0;
const rate = 1000;
const clickEl = document.getElementById("clicks");
const spawnRadius = document.getElementById("spawnRadius");
const spawnArea = spawnRadius.getBoundingClientRect();
function createDiv(id, color) {
let div = document.createElement('div');
div.classList.add("circle", id);
if (!color)
color = colors[Math.floor(Math.random() * colors.length)];
div.style.setProperty('--bStart', color);
// Randomly position circle within spawn area
div.style.top = `${(Math.floor(Math.random() * spawnArea.height + circleSize)) % (spawnArea.height - circleSize)}px`;
div.style.left = `${(Math.floor(Math.random() * spawnArea.width + circleSize)) % (spawnArea.width - circleSize)}px`;
// Add click handler
div.addEventListener('click', (event) => {
div.style.setProperty('border-color', color+'ee');
div.style.setProperty('pointer-events', 'none');
div.classList.add("circle-smoke");
setTimeout(() => {
spawnRadius.removeChild(div);
noOfCircles--;
}, 1500);
}, { once: true });
spawnRadius.appendChild(div);
noOfCircles++;
}
setInterval(() => {
if (noOfCircles >= totalCircles) return;
createDiv(`circle${++i}`);
}, rate);
*{
box-sizing:border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #0f0f0f;
}
.circle {
--bStart: #fff;
width: 80px;
height: 80px;
border-radius: 50%;
background-color: transparent;
border: 3px solid var(--bStart);
position: absolute;
}
#spawnRadius {
height: 100vh;
width: 100vw;
background: #0f0f0f;
border: 2px solid #ebc6df;
}
#keyframes Animation {
0% {
box-shadow: 0 0 0px 0px var(--bStart), inset 0 0 0px 0px var(--bStart);
opacity: 1;
}
100% {
box-shadow: 0 0 30px 25px #0000, inset 0 0 30px 25px #0000;
border-color: #0000;
opacity: 0;
}
}
.circle-smoke {
animation: Animation 1.5s linear forwards;
}
<div id="spawnRadius"></div>
{ once: true } removes the click event handler after one click.
div.style.setProperty('pointer-events', 'none') this allows us to click on multiple circles that are on top of each other.
For the demo I am allowing only 10 circles at a time.

Why blocked pointer events are fired while timed-out function with no delay is running?

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">

Categories