I have implemented a countup which animates up to the data-target. How can I adjust the counter so that all counters stop at the same time?
Or is there a better way to implement this?
function animationEffect(){
const counters = document.querySelectorAll('.counter');
const speed = 2000;
counters.forEach((counter) => {
const updateCount = () => {
const target = counter.getAttribute('data-target');
const count = counter.innerText;
const inc = target / speed;
if(count < target) {
counter.innerText = Math.ceil(count inc);
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
}
updateCount();
});
}
<div data-target="299" onm ouseover="animationEffect()">0</div>
<div data-target="1299" onm ouseover="animationEffect()">0</div>
<div data-target="99" onm ouseover="animationEffect()">0</div>
CodePudding user response:
I don't know if it helps, but i got it working when i change this line:
const inc = target / speed;
to this:
const inc = 1/(speed / target);
AND get rid of the Math.ceil()
because this const speed = 2000;
which is actually a step creates floating values. Maybe try smaller speed
value
EDIT A little more tweaking and we have a smooth answer without floating values >:D
function animationEffect(){
if(animationEffect.called){return null}
animationEffect.called=true
//these 2 lines prevent this function from being called multiple times to prevent overlapping
//the code works without these lines though so remove them if u don't want them
const counters = document.querySelectorAll('.counter');
const incrementConstant = 2; //the higher the number the faster the count rate, the lower the number the slower the count rate
const speed = 2000;
counters.forEach((counter) => {
const updateCount = () => {
const target = counter.getAttribute('data-target');
counter.storedValue=counter.storedValue||counter.innerText-0; //saving a custom value in the element(the floating value)
const count = counter.storedValue; //accessing custom value(and rounding it)
const inc = incrementConstant/(speed / target); //the math thanks to @Tidus
if(count < target) {
counter.storedValue=count inc
counter.innerText = Math.round(counter.storedValue);
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
}
updateCount();
});
}
<div data-target="299" onm ouseover="animationEffect()">0</div>
<div data-target="1299" onm ouseover="animationEffect()">0</div>
<div data-target="99" onm ouseover="animationEffect()">0</div>
CodePudding user response:
Updated Answer
Looks like I misunderstood what you mean by at the same time
, but using setTimeout
like this is still a really bad practice. Here is my take on it:
const counters = Array.from(document.querySelectorAll(".counter"));
const counterValues = [];
const speed = 500;
const updateCount = (counter, target, count, index) => {
const inc = target / speed;
counterValues[index] = count inc;
if (count < target) {
counter.innerText = Math.floor(counterValues[index]);
} else {
counter.innerText = target;
}
};
counters.forEach((counter, index) => {
counterValues.push(0)
const interval = setInterval(() => {
const target = counter.getAttribute("data-target");
const count = counterValues[index];
if (target !== count) {
updateCount(counter, target, count, index)
} else {
clearInterval(interval);
}
}, 1)
});
<div data-target="32">0</div>
<div data-target="3000">0</div>
<div data-target="10">0</div>
Please check my old answer for why setInterval
is better for this problem.
Other than that here is what is going on in this snippet:
I defined an array called counterValues
which will hold the count values in a float format. In your example when you store the ceiled number to be used in your calculation later again, you are not doing a correct calculation.
If I remember correct one of your counters must be increased by 0.145, while you are incrementing it by 1 every time. By the way flooring is the right method for this as it won't reach to the target until it really reaches to it. If the target is 10
, but your counter is at 9.5
it will be written as 10 in your code although it is not there yet.
updateCount
is almost the same function. It now uses floor
. It updates the amount of counter by using the previous floating value and then while writing to the DOM, it uses the floored value.
For each counter, it adds an interval which will update the counter and cancel itself whenever the counter reaches to the target value.
I used a shared state and indexed calculation for simplicity.
Old Answer
If you paste this code to the top of your code and run, when you log window.activeTimers
you will see that there are hundreds of timers defined. It is because every time updateCount
is called you are setting a new timer to updateCount. Although your animationEffect
function is like the main function of your program, if you invoke it every time I mouseover your counters, it will set new timers which means faster update on your counters every time. To sum up, you don't have control at all at the moment.
For periodic calls you should use setInterval. It takes a function and a delay parameter (there are also optional args. You can check the docs). It repeatedly calls a function or executes a code snippet, with a fixed time delay between each call (From Mozilla docs)
. It also returns an interval ID so that you can cancel it later (meaning we can control it).
So in your case, just to stop in a desired moment first thing you should do is to get rid of onm ouseover calls to the animationEffect and add a button to stop execution of updateCounters.
<div data-target="299">0</div>
<div data-target="1299">0</div>
<div data-target="99">0</div>
<button id="stop">Stop</button>
After assigning variables counters
and speed
we can define an array to hold the intervals' IDs to cancel them later.
const counters = document.querySelectorAll(".counter");
const speed = 2000;
const cancelButton = document.getElementById('stop')
const countIntervals = [];
cancelButton.addEventListener('click', () => {
countIntervals.forEach(interval => clearInterval(interval))
})
As you can see I defined an event listener to our button. When you click on it it will iterate the interval IDs in countIntervals
and clear those intervals. For simplicity, I didn't implement features like pausing and resetting and tried not to change your code much. You can experiment later.
Now first thing you should do is to comment or delete setTimeout
line in your if statement. Then we will push the returned interval ID to our array countIntervals
:
counters.forEach((counter) => {
const updateCount = () => {
const target = counter.getAttribute("data-target");
const count = counter.innerText;
const inc = target / speed;
if (count < target) {
counter.innerText = Math.ceil(count inc);
// setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
};
countIntervals.push(setInterval(updateCount, 10))
});
Now your counters will stop once you hit the Stop
button. I ignored the speed feature, but If you understand setInterval
you can implement it easily.