18

I have a counter which animates to a final number which is defined in the HTML. However I would like this animation to happen once it's in the viewport.

I have a fiddle here which shows how scrolling seems to effect the counter number.

$(document).ready(function() {
      $(function($, win) {
        $.fn.inViewport = function(cb) {
          return this.each(function(i, el) {
            function visPx() {
              var H = $(this).height(),
                r = el.getBoundingClientRect(),
                t = r.top,
                b = r.bottom;
              return cb.call(el, Math.max(0, t > 0 ? H - t : (b < H ? b : H)));
            }
            visPx();
            $(win).on("resize scroll", visPx);
          });
        };
      }(jQuery, window));

      $(".fig-number").inViewport(function(px) {
        $(this).each(function() {
          $(this).prop('Counter', 0).animate({
            Counter: $(this).text()
          }, {
            duration: 1000,

            step: function(now) {
              $(this).text(Math.ceil(now));
            }
          });
        });
      });
    });

I've tried multiple things but I cant seem to achieve what I'm after.

$(document).ready(function() {
  $(function($, win) {
    $.fn.inViewport = function(cb) {
      return this.each(function(i, el) {
        function visPx() {
          var H = $(this).height(),
            r = el.getBoundingClientRect(),
            t = r.top,
            b = r.bottom;
          return cb.call(el, Math.max(0, t > 0 ? H - t : (b < H ? b : H)));
        }
        visPx();
        $(win).on("resize scroll", visPx);
      });
    };
  }(jQuery, window));

  $(".fig-number").inViewport(function(px) {
    $(this).each(function() {
      $(this).prop('Counter', 0).animate({
        Counter: $(this).text()
      }, {
        duration: 1000,

        step: function(now) {
          $(this).text(Math.ceil(now));
        }
      });
    });
  });
});
html,
body {
  height: 100%;
}
#upper-push {
  height: 100%;
  width: 100%;
  display: block;
  background: red;
  color: white;
}
<div id="upper-push">
  Scroll down
</div>
<div id="numbers">
  <span class="fig-number">25</span>
  <span class="fig-number">78</span>
</div>
Ani Menon
  • 27,209
  • 16
  • 105
  • 126
probablybest
  • 1,403
  • 2
  • 24
  • 47
  • So what do you want the scrolling should start once you scroll down and the counter text visible inside viewport and then should not be affected if scrolled back up again /? – codefreaK Apr 29 '16 at 19:28

4 Answers4

15

The .inViewport() plugin triggers a callback on every scroll event.
It's by design. (Helps to keep the source of a plugin in code! ;) )

On the "plugin page" you can see how to use it:

$("selector").inViewport(function(px) {
  console.log( px ); // `px` represents the amount of visible height
  if(px){
    // do this if element enters the viewport // px > 0
  }else{
    // do that if element exits  the viewport // px = 0
  }
}); // Here you can chain other jQuery methods to your selector 

that means:

  1. You have to listen for the px argument is greater than 0 (element is in viewport)
  2. To prevent chaining additional animations creating buildups, you should use a flag variable
  3. (The $(this).each() inside the callback is not needed. The plugin already operates over a collection of elements.)

Edited jsFiddle demo

jQuery(function($) { // DOM ready and $ in scope

  $(".fig-number").inViewport(function(px) {
    // if px>0 (entered V.port) and
    // if prop initNumAnim flag is not yet set = Animate numbers
    if(px>0 && !this.initNumAnim) { 
      this.initNumAnim = true; // Set flag to true to prevent re-running the same animation
      // <<< DO SOME COOL STUFF HERE! 
    }
  });

});

Snippet example:

// inViewport jQuery plugin
// https://stackoverflow.com/a/26831113/383904
$(function($, win) {
  $.fn.inViewport = function(cb) {
    return this.each(function(i,el){
      function visPx(){
        var H = $(this).height(),
            r = el.getBoundingClientRect(), t=r.top, b=r.bottom;
        return cb.call(el, Math.max(0, t>0? H-t : (b<H?b:H)));  
      } visPx();
      $(win).on("resize scroll", visPx);
    });
  };
}(jQuery, window));


jQuery(function($) { // DOM ready and $ in scope

  $(".fig-number").inViewport(function(px) { // Make use of the `px` argument!!!
    // if element entered V.port ( px>0 ) and
    // if prop initNumAnim flag is not yet set
    //  = Animate numbers
    if(px>0 && !this.initNumAnim) { 
      this.initNumAnim = true; // Set flag to true to prevent re-running the same animation
      $(this).prop('Counter',0).animate({
        Counter: $(this).text()
      }, {
        duration: 1000,
        step: function (now) {
          $(this).text(Math.ceil(now));
        }
      });         
    }
  });

});
html,
body {
  height:100%;
}

#upper-push {
  height:100%;
  width:100%;
  display:block;
  background:red;
  color:white;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="upper-push">
  Scroll down
</div>
<div id="numbers">
  <span class="fig-number">25</span>
  <span class="fig-number">78</span>
</div>
Community
  • 1
  • 1
Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313
  • i'm trying to do this in a div which is has own scrollbar. When i use page scroll it works fine but i need it to work when this div scrolled and appeared in this div. What should i do? – Çağdaş Takış Apr 09 '17 at 11:57
  • I'm using your code on multiple elements on one page. Unfortunately it "stutters" when I'm scrolling the page. Is there any solution for that? – Cray Feb 27 '19 at 08:55
  • 1
    @Cray that means you have a large set of elements and scroll listeners assigned - yes, this could be a performance killer. You can always extend the code to proxy it into a function **throttle** or **debounce** (with a tiny *ms* timeout). If you don't know how to do it - could you explain what's your specific case.? – Roko C. Buljan Feb 27 '19 at 13:34
  • I added an own question for that: https://stackoverflow.com/questions/54902102/jquery-animtion-stops-when-user-scrolls – Cray Feb 27 '19 at 13:51
10

This solves it if you don't mind change of code. jsfiddle

    var $findme = $('#numbers');
var exec = false;
function Scrolled() {
  $findme.each(function() {
    var $section = $(this),
      findmeOffset = $section.offset(),
      findmeTop = findmeOffset.top,
      findmeBottom = $section.height() + findmeTop,
      scrollTop = $(document).scrollTop(),
      visibleBottom = window.innerHeight,
      prevVisible = $section.prop('_visible');

    if ((findmeTop > scrollTop + visibleBottom) ||
      findmeBottom < scrollTop) {
      visible = false;
    } else visible = true;

    if (!prevVisible && visible) {
     if(!exec){
              $('.fig-number').each(function() {
          $(this).prop('Counter', 0).animate({
            Counter: $(this).text()
          }, {
            duration: 1000,

            step: function(now) {
              $(this).text(Math.ceil(now));
              exec = true;
            }
          });
        });
      }
    }
    $section.prop('_visible', visible);
  });

}

function Setup() {
  var $top = $('#top'),
    $bottom = $('#bottom');

  $top.height(500);
  $bottom.height(500);

  $(window).scroll(function() {
    Scrolled();
  });
}
$(document).ready(function() {
  Setup();
});
html,
body {
  height: 100%;
}

#upper-push {
  height: 100%;
  width: 100%;
  display: block;
  background: red;
  color: white;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="upper-push">
  Scroll down
</div>
<div id="numbers">
  <span class="fig-number">25</span>
  <span class="fig-number">78</span>
</div>
Stack learner
  • 1,726
  • 2
  • 11
  • 21
  • Thank you so much, thats great. Is there a way so that it only animates once? – probablybest Apr 28 '16 at 14:36
  • @probablybest Edited the code to fit it, please accept the answer if you are satissfied – Stack learner Apr 28 '16 at 15:09
  • Setting `exec` as global kills the purpose of a code that operates over a class collection of elements. Also using/creating a plugin helps in not making spaghetti code and improves reusability ;) – Roko C. Buljan May 02 '16 at 10:46
  • I used jsfiddle.net/2v3mq3nh/4 and example set by you excellent one ...can you pls help i want to recount when visible ..but failed even after i remove this.initNumAnim = true;..pls help modified https://jsfiddle.net/ax6q4y1c/ not working properly – user1526780 Aug 06 '22 at 11:25
1

Here's my solution which uses IntersectionObserver and only animates once upon entering the viewport. Supports configurable duration and floats.

const initAnimatedCounts = () => {
  const ease = (n) => {
    // https://github.com/component/ease/blob/master/index.js
    return --n * n * n + 1;
  };
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // Once this element is in view and starts animating, remove the observer,
        // because it should only animate once per page load.
        observer.unobserve(entry.target);
        const countToString = entry.target.getAttribute('data-countTo');
        const countTo = parseFloat(countToString);
        const duration = parseFloat(entry.target.getAttribute('data-animateDuration'));
        const countToParts = countToString.split('.');
        const precision = countToParts.length === 2 ? countToParts[1].length : 0;
        const startTime = performance.now();
        const step = (currentTime) => {
          const progress = Math.min(ease((currentTime  - startTime) / duration), 1);
          entry.target.textContent = (progress * countTo).toFixed(precision);
          if (progress < 1) {
            animationFrame = window.requestAnimationFrame(step);
          } else {
            window.cancelAnimationFrame(animationFrame);
          }
        };
        let animationFrame = window.requestAnimationFrame(step);
      }
    });
  });
  document.querySelectorAll('[data-animateDuration]').forEach((target) => {
    target.setAttribute('data-countTo', target.textContent);
    target.textContent = '0';
    observer.observe(target);
  });
};
initAnimatedCounts();
div {
  font-size: 30px;
  text-align: center;
  padding: 30px 0;
}
div > span {
  color: #003d82;
}
div.scrollpad {
  height: 100vh;
  background-color: #eee;
}
<div>
  <span>$<span data-animateDuration="1000">987.45</span></span> was spent on
  about <span><span data-animateDuration="1000">5.8</span>M</span> things.
</div>
<div class="scrollpad">keep scrolling</div>
<div>
  There are <span><span data-animateDuration="1000">878</span>K</span> people involved.
  <br/>
  And <span><span data-animateDuration="1000">54</span></span> cakes.
</div>
<div class="scrollpad">keep scrolling</div>
<div>
  Additionally, <span>$<span data-animateDuration="3000">300</span>B</span> went to waste.
  <br/>
  Because <span>$<span data-animateDuration="2000">54</span></span> was spent on each cake.
</div>
<div class="scrollpad">keep scrolling</div>
<div>
  Lastly, <span><span data-animateDuration="4000">3.5334583</span>T</span> ants said hello.
  <br/>
  But <span><span data-animateDuration="2000">4</span></span> of them said goodbye.
</div>
Dane Iracleous
  • 1,659
  • 2
  • 16
  • 35
0

Snippet :

(function($) {

  $.fn.visible = function(partial, hidden) {

    var $t = $(this).eq(0),
      t = $t.get(0),
      $w = $(window),
      viewTop = $w.scrollTop(),
      viewBottom = viewTop + $w.height(),
      _top = $t.offset().top,
      _bottom = _top + $t.height(),
      compareTop = partial === true ? _bottom : _top,
      compareBottom = partial === true ? _top : _bottom,
      clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true;

    return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop));
  };

})(jQuery);


// Scrolling Functions
$(window).scroll(function(event) {
  function padNum(num) {
    if (num < 10) {
      return "" + num;
    }
    return num;
  }

  var first = parseInt($('.c1').text());
  var second = parseInt($('.c2').text());

  function countStuffUp(points, selector, duration) { //Animate count
    $({
      countNumber: $(selector).text()
    }).animate({
      countNumber: points
    }, {
      duration: duration,
      easing: 'linear',
      step: function() {
        $(selector).text(padNum(parseInt(this.countNumber)));
      },
      complete: function() {
        $(selector).text(points);
      }
    });
  }

  // Output to first-count
  $(".first-count").each(function(i, el) {
    var el = $(el);
    if (el.visible(true)) {
      countStuffUp(first, '.first-count', 1600);
    }
  });

  // Output to second count
  $(".second-count").each(function(i, el) {
    var el = $(el);
    if (el.visible(true)) {
      countStuffUp(second, '.second-count', 1000);
    }
  });

});
.block {
  height: 1000px;
  background: #eeeeee;
}
.dontShow {
  //display:none;

}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<div class="block">Scroll down to bottom to see counter</div>
<div>
  <span class="first-count">0</span>
  <span class="second-count">0</span>
</div>
<div class="dontShow">
  Max Value of count 1 : <span class="c1">25</span>
  <br />Max Value of count 2 : <span class="c2">78</span>
</div>

Refer : Similar

Community
  • 1
  • 1
Ani Menon
  • 27,209
  • 16
  • 105
  • 126
  • Please [don't post identical answers to multiple questions](https://meta.stackexchange.com/q/104227). Post one good answer, then vote/flag to close the other questions as duplicates. If the question is not a duplicate, *tailor your answers to the question*. – Martijn Pieters Apr 30 '16 at 14:37