1

This code make the counter/animation start when in view, but I would like for it to restart when scrolling out of view and then in view again. Can't seem to solve it.

If you would like to see it live link here - scroll down to the bottom just before the footer. https://easyrecycle.dk/Serviceomraader.html

var a = 0;
$(window).scroll(function() {
  var oTop = $('#counter').offset().top - window.innerHeight;
  if (a == 0 && $(window).scrollTop() > oTop) {
    $('.counter-value').each(function() {
      var $this = $(this),
        countTo = $this.attr('data-count');
      $({
        countNum: $this.text()
      }).animate({
          countNum: countTo
        },
        {
          duration: 3000,
          easing: 'swing',
          step: function() {
            $this.text(Math.floor(this.countNum));
          },
          complete: function() {
            $this.text(this.countNum);
            //alert('finished');
          }
        });
    });
    a = 1;
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="counter">
  <div class="counter-container">

    <div class="counter-box">
      <div class="counter-value" data-count="30">0</div>
      <span></span>
      <p>Antal medarbejdere</p>
    </div>

    <div class="counter-box">
      <div class="counter-value" data-count="51000">0</div>
      <span></span>
      <p>Processeret udstyr i KG pr. md.</p>
    </div>

    <div class="counter-box">
      <div class="counter-value" data-count="51">0</div>
      <span></span>
      <p>Antal afhentninger pr. md.</p>
    </div>

  </div>
</div>
Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313

2 Answers2

1

Don't use the .scroll() Event listener. It's a performance killer. Same goes for JavaScript's "scroll" EventListener - if used to perform heavy tasks. Actually you don't need jQuery at all.

Your task basically involves the use of:

  • the Intersection Observer API to watch for elements entering / exiting the viewport — instead of the pretty expensive scroll listener you used
  • a counterStart() function (triggered on viewport enter) that uses requestAnimationFrame — the most performant way to handle animations on the web.
  • a counterStop() function (triggered on viewport exit) that cancels the AnimationFrame and resets the element counter back to its start value.
  • an easing function in which you'll pass the linearly mapped time fraction (float 0.0 ... 1.0) in order to retrieve the easing value to be used to derive your integers/counters.

//gist.github.com/gre/1650294
const easeInOutQuad = (t) => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;

const inViewportCounter = (el) => {

  const duration = +el.dataset.duration || 3000;
  const start = +el.textContent || 0;
  const end = +el.dataset.count || 100;
  let raf;

  //stackoverflow.com/a/70746179/383904
  const counterStart = () => {
    if (start === end) return; // If equal values, stop here.

    const range = end - start;
    let curr = start; // Set current to start
    const timeStart = Date.now();

    const loop = () => {
      let elaps = Date.now() - timeStart;
      if (elaps > duration) elaps = duration;
      const frac = easeInOutQuad(elaps / duration); // Get the time fraction with easing
      const step = frac * 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) raf = requestAnimationFrame(loop); // Loop
    };

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

  const counterStop = (el) => {
    cancelAnimationFrame(raf);
    el.textContent = start;
  };

  //stackoverflow.com/a/70746179/383904
  const inViewport = (entries, observer) => {
    entries.forEach(entry => {
      // Enters viewport:
      if (entry.isIntersecting) counterStart(entry.target);
      // Exits viewport:
      else counterStop(entry.target);
    });
  };
  const Obs = new IntersectionObserver(inViewport);
  const obsOptions = {}; //developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options
  // Attach observer to element:
  Obs.observe(el, obsOptions);
};

document.querySelectorAll('[data-count]').forEach(inViewportCounter);
[data-count] { margin: 55vh 0; font: 3rem/1.4 sans-serif; }
Scroll down...
<div data-count="300">0</div>
<div data-count="51000">0</div>
<div data-count="1000">0</div>

For a better understanding of the above main two chunks of code head to their original base answers which I basically reused and adapted:

with the basic differences being:

  • in the counter function I passed the linear fraction of time into the easing function
  • and for the Intersection Observer one, I used an if .. else block; the else used to reset the element's textContent back to "0" (or any start value).

Sorry for not using jQuery as I probably would've years ago, but hopefully you learned something exciting and new. If some parts are not completely clear, feel always free to ask.

Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313
  • It's up and live now - https://easyrecycle.dk/Serviceomraader.html, thank you so much, I can't press the arrow up = usefull comment,I don't have the rights to it yet. Brand new here and in the coding world. – Lasse Bo Jensen Aug 17 '22 at 07:24
  • @LasseBoJensen (that was not a *comment*, is was an *Answer* - that's how we call it here.) Thank you for sharing, looks expectedly good! PS: you can finally remove the jQuery library since it seems it's unused right now. Also, lower your images sizes on the thumbnails. A ~1400px image width for just a small preview is loss of data and wasted bandwidth. PS take a **[tour]** to learn how stuff works. Happy coding – Roko C. Buljan Aug 17 '22 at 10:33
-1

As you can see from my example below, i add a simple if (if is not in view) and reset the text/variable.

var a = 0;
$(window).scroll(function() {
  var oTop = $('#counter').offset().top - window.innerHeight;
  if (a == 0 && $(window).scrollTop() > oTop) {
    $('.counter-value').each(function() {
      var $this = $(this),
        countTo = $this.attr('data-count');
      $({
        countNum: $this.text()
      }).animate({
          countNum: countTo
        },

        {

          duration: 3000,
          easing: 'swing',
          step: function() {
            $this.text(Math.floor(this.countNum));
          },
          complete: function() {
            $this.text(this.countNum);
            //alert('finished');
          }

        });
    });
    a = 1;
  } else {
    $('.counter-value').text('0');
    a = 0;
  }
});
.fakespace{
  height:500px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='fakespace'></div>
<div id="counter">
  <div class="counter-container">

    <div class="counter-box">
      <div class="counter-value" data-count="30">0</div>
      <span></span>
      <p>Antal medarbejdere</p>
    </div>

    <div class="counter-box">
      <div class="counter-value" data-count="51000">0</div>
      <span></span>
      <p>Processeret udstyr i KG pr. md.</p>
    </div>

    <div class="counter-box">
      <div class="counter-value" data-count="51">0</div>
      <span></span>
      <p>Antal afhentninger pr. md.</p>
    </div>

  </div>

</div>
Simone Rossaini
  • 8,115
  • 1
  • 13
  • 34