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>
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:
px
argument is greater than 0
(element is in viewport) $(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>
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>
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With