最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Animate counter from start to end value - Stack Overflow

programmeradmin1浏览0评论

I want to put a counter on my site.
The following code works for very large numbers, but low numbers like 3 or 95.5 do not work. But it works with numbers over 1000.Where do you think the problem is with the JavaScript code written?
Thanks in advance for your guidance.

const counters = document.querySelectorAll('.count');
const speed = 200;
counters.forEach((counter) => {
  const updateCount = () => {
  const target = parseInt(counter.getAttribute('data-target'));
    const count = parseInt(counter.innerText);
    const increment = Math.trunc(target / speed);
    if (count < target) {
       counter.innerText = count + increment;
       setTimeout(updateCount, 1);
    } else {
       counter.innerText = target;
    }
  };
 updateCount();
});
<div>
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="3" class="count">0</span>
       <span>Years</span>
    </div>
    <p>example1</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="50000" class="count">0</span>
    </div>
    <p>example2</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
      <span data-target="95" class="count">0</span>
      <span>%</span>
    </div>
    <p>example3</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span data-target="10000" class="count">0</span>
      <span>%</span>
    </div>
    <p>example4</p>                     
  </div>
</div>

I want to put a counter on my site.
The following code works for very large numbers, but low numbers like 3 or 95.5 do not work. But it works with numbers over 1000.Where do you think the problem is with the JavaScript code written?
Thanks in advance for your guidance.

const counters = document.querySelectorAll('.count');
const speed = 200;
counters.forEach((counter) => {
  const updateCount = () => {
  const target = parseInt(counter.getAttribute('data-target'));
    const count = parseInt(counter.innerText);
    const increment = Math.trunc(target / speed);
    if (count < target) {
       counter.innerText = count + increment;
       setTimeout(updateCount, 1);
    } else {
       counter.innerText = target;
    }
  };
 updateCount();
});
<div>
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="3" class="count">0</span>
       <span>Years</span>
    </div>
    <p>example1</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="50000" class="count">0</span>
    </div>
    <p>example2</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
      <span data-target="95" class="count">0</span>
      <span>%</span>
    </div>
    <p>example3</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span data-target="10000" class="count">0</span>
      <span>%</span>
    </div>
    <p>example4</p>                     
  </div>
</div>

Share Improve this question edited Jan 17, 2022 at 20:44 Roko C. Buljan 207k41 gold badges327 silver badges339 bronze badges asked Jan 17, 2022 at 18:42 h.m.p.frontendDeveloperh.m.p.frontendDeveloper 5051 gold badge6 silver badges14 bronze badges 1
  • maybe try with Math.ceil – cmgchess Commented Jan 17, 2022 at 18:53
Add a ment  | 

3 Answers 3

Reset to default 6

Animate counter using CSS only

NOTICE: missing Firefox support (2024.)

You can animate a CSS Var integer by defining a @property and by using animation property and CSS counter on the ::before/after pseudo element:

@property --from {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.counter {
  transition: --from 1s;
  counter-reset: int var(--from);
  animation: counter var(--time, 1000) forwards ease-in-out;
}

.counter::after {
  content: counter(int);
}

@keyframes counter {
  to {
    --from: var(--to, 100);
  }
}
<br>Count from 0 to 100
<br><span class="counter" style="--from:0; --to:100; --time:4s;"></span>
<br>You don't necessarily have to start at 0
<br><span class="counter" style="--from:1000; --to:2024; --time:4s;"></span>
<br>You can also count in reverse
<br><span class="counter" style="--from:9999; --to:0; --time:4s;"></span>
<br>or even to negative values
<br><span class="counter" style="--from:1000; --to:-1000; --time:4s;"></span>
<br>This one will not count
<br><span class="counter" style="--from:666; --to:666; --time:4s;"></span>

Animate counter using JavaScript

MDN Docs: The Math.trunc() function returns the integer part of a number by removing any fractional digits.

Therefore in Math.trunc(target / 200)
target has to be at least 200 for you to finally get a 1. But you always get a 0 and you try to do a 0 + 0.


What not to do

  • Don't use textContent to grab values from DOM Nodes on every loop iteration. Simply, increment an integer variable

  • Never use setTimeout/setInterval() set at a small milliseconds value. Such will clog the main thread, and your application performance might suffer from it. Even worse if this operation happens for multiple elements on a single page.

  • Don't animate different counters at a different timing duration. A proper implementation would be to animate all counters during a constant time, going from 0to10 or from 0to9999999 should be "animated" during a constant time duration — for the user to perceive those visual clues/changes and make sense of it, for multiple counter elements.

Animate the range during a defined time period

Here's a bulletproof example on how to properly create animated counters using JavaScript:

  • use the proper, more performant requestAnimationFrame (instead of setTimeout or setInterval) to not cog the main thread.
  • use elements with [data-counter] attribute. The counter will consider textContent as the start value, and count to the end value which is stored in the actual data-counter attribute
  • count either up or down — depending on the start/end values
  • will not count if the start value equals the end value

const ease = {
  linear: t => t,
  inOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
  // Find out more at: https://gist.github./gre/1650294
};

const counter = (EL) => {

  const duration = 4000; // Animate all counters equally for a better UX

  const start = parseInt(EL.textContent, 10); // Get start and end values
  const end = parseInt(EL.dataset.counter, 10); // PS: Use always the radix 10!

  if (start === end) return; // If equal values, stop here.

  const range = end - start; // Get the range
  let curr = start; // Set current at start position
  
  const timeStart = Date.now();

  const loop = () => {
    let elaps = Date.now() - timeStart;
    if (elaps > duration) elaps = duration; // Stop the loop
    const norm = ease.inOutQuad(elaps / duration); // normalised value + easing
    const step = norm * range; // Calculate the value step
    curr = start + step; // Increment or Decrement current value
    EL.textContent = Math.trunc(curr); // Apply to UI as integer
    if (elaps < duration) requestAnimationFrame(loop); // Loop
  };

  requestAnimationFrame(loop); // Start the loop!
};

document.querySelectorAll("[data-counter]").forEach(counter);
<br>Count from 0 to 10
<br><span data-counter="10">0</span>
<br>You don't necessarily have to start at 0
<br><span data-counter="2022">1000</span>
<br>You can also count in reverse
<br><span data-counter="0">9999</span>
<br>or even to negative values
<br><span data-counter="-1000">1000</span>
<br>This one will not count
<br><span data-counter="666">666</span>
<br>
<br>And for a better UX - all the counters finished simultaneously  during 4000ms

You're trying to increment zeroes.

const increment = Math.trunc(target / speed);

When your target is less than 200 (which is your hard-coded speed) it will return a 0.

Math.trunc(3/200) will be 0 because according to the MDN Web Docs:

The Math.trunc() function returns the integer part of a number by removing any fractional digits. https://developer.mozilla/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc

That's why it won't work on lower numbers than 200.

What you can do is remove the Math.trunc and increment those decimal values instead.

Try this:

function animateValue(id, start, end, duration) {
  const INTERVAL_TIME = 10;
        if (duration < 100) {
            duration = 100;
        } else if (duration > 10000) {
            duration = 10000;
        }

        let obj      = document.getElementById(id);
        let decimals = (end % 1 != 0) ? (end.toString().split('.')[1] || '').length : 0;
        
        if (end === start) {
         obj.innerHTML = start.toFixed(decimals);
         return;
        }
        
        const stepTime = Math.ceil(duration / INTERVAL_TIME);
        let doneLoops = 0;

        let quantityPerLoop = end / stepTime;

        let increment = (end - start) / stepTime;
        let current   = start;

        const INTERVAL = setInterval(function () {
            current += quantityPerLoop;
            obj.innerHTML = current.toFixed(decimals);
            if ((increment > 0 && current >= end) || (increment < 0 && current <= end)) {
                clearInterval(INTERVAL);
                obj.innerHTML = parseFloat(end).toFixed(decimals);
            }
        }, INTERVAL_TIME);
}

window.onload = function () {
  animateValue('quantity1', 0, document.getElementById('quantity1').innerText.trim(), 100);
  animateValue('quantity2', 0, document.getElementById('quantity2').innerText.trim(), 3000);
  animateValue('quantity3', 0, document.getElementById('quantity3').innerText.trim(), 500);
}
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <table>
            <thead>
                <th>Product ID</th>
                <th>Quantity</th>
            </thead>
            <tbody>
                <tr>
                    <td>1</td>
                    <td id="quantity1">500.55</td>
                </tr>
                <tr>
                    <td>2</td>
                    <td id="quantity2">22.31</td>
                </tr>
                <tr>
                    <td>3</td>
                    <td id="quantity3">3800</td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

You just need to pass the id of the element that will receive the animation, the initial value, the end value and the duration of the animation.

It's great for using in Ajax request returns.

发布评论

评论列表(0)

  1. 暂无评论