This is a follow up from my previous question.
I have a progressbar.js circle that animates on scroll. If there is just one circle it works as expected.
Now I want to create many of these animated circles by looping through an object with different key-values pairs.
For example:
var divsValues = {
'total-score-circle': 0.75,
'general-score-circle': 0.80,
'speed-score-circle': 0.85,
'privacy-score-circle': 0.90,
};
For each key-value pair, the key is a div ID and the value is number that tells the animation how far to go.
Below is the code where I try to implement my loop, but the problem is that only the last circle is animated on scroll. All the circles appear in their "pre-animation" state, but only the last circle actually becomes animated when you scroll to the bottom.
I need each circle to animate once it is in the viewport.
//Loop through my divs and create animated circle for each one
function makeCircles() {
var divsValues = {
'total-score-circle': 0.75,
'general-score-circle': 0.80,
'speed-score-circle': 0.85,
'privacy-score-circle': 0.90,
};
for (var i in divsValues) {
if (divsValues.hasOwnProperty(i)) {
bgCircles(i, divsValues[i]);
}
}
}
makeCircles();
// Check if element is scrolled into view
function isScrolledIntoView(elem) {
var docViewTop = jQuery(window).scrollTop();
var docViewBottom = docViewTop + jQuery(window).height();
var elemTop = jQuery(elem).offset().top;
var elemBottom = elemTop + jQuery(elem).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}
//Circle design and animation
function bgCircles(divid, countvalue) {
// Design the circle using progressbar.js
bar = new ProgressBar.Circle(document.getElementById(divid), {
color: '#ddd',
// This has to be the same size as the maximum width to
// prevent clipping
strokeWidth: 4,
trailWidth: 4,
easing: 'easeInOut',
duration: 1400,
text: {
autoStyleContainer: false
},
from: {
color: '#ddd',
width: 4
},
to: {
color: '#888',
width: 4
},
// Set default step function for all animate calls
step: function(state, circle) {
circle.path.setAttribute('stroke', state.color);
circle.path.setAttribute('stroke-width', state.width);
var value = Math.round(circle.value() * 100);
if (value === 0) {
circle.setText('');
} else {
circle.setText(value + '%');
}
}
});
bar.text.style.fontFamily = '"Montserrat", sans-serif';
bar.text.style.fontSize = '1.7rem';
bar.trail.setAttribute('stroke-linecap', 'round');
bar.path.setAttribute('stroke-linecap', 'round');
//Animate the circle when scrolled into view
window.onscroll = function() {
if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
else bar.animate(0); // or bar.set(0)
}
}
#total-score-circle,
#general-score-circle,
#speed-score-circle,
#privacy-score-circle {
margin: 0.8em auto;
width: 100px;
height: 100px;
position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js"></script>
<div id="total-score-circle"></div>
<div id="general-score-circle"></div>
<div id="speed-score-circle"></div>
<div id="privacy-score-circle"></div>
While researching this problem I learned that JavaScript will only output the last value of a loop, which I thought could be the cause of my problem.
So I tried to replace the for loop with these solutions...
Solution 1: Same problem as before, only the last loop animates on scroll.
for (var i in divsValues) {
(function(){
var ii = i;
if (divsValues.hasOwnProperty(ii)) {
bgCircles(ii, divsValues[ii]);
}
})();
}
Solution 2: Again, same problem as before, only the last loop animates on scroll.
for (var i in divsValues) {
let ii = i;
if (divsValues.hasOwnProperty(ii)) {
bgCircles(ii, divsValues[ii]);
}
}
Solution 3: Again, same problem as before, only the last loop animates on scroll.
for (var i in divsValues) {
try{throw i}
catch(ii) {
if (divsValues.hasOwnProperty(ii)) {
bgCircles(ii, divsValues[ii]);
}
}
}
So now I'm thinking maybe the problem is not the loop, but something I can't see or figure out.
Looping an animation causes it to repeat. You can loop animations in Advanced mode. Each element's animation can be looped separately, or you can loop a more complex animation involving multiple elements.
You can play your animation by pressing Ctrl + Enter on Windows or Command + Enter on Mac. That will loop through all the frames on the main timeline, unless you have scripted it to stop.
Here are the fixes:
In the function bgCircles(...)
, use var
to declare the bar
in that function's scope:
var bar = new ProgressBar.Circle(document.getElementById(divid), {
When you set the animate scrolled into view checker event, you assign a new function over-and-over to window.onscroll
. Since you are using jQuery, consider jQuery's .scroll event handler and use it like this:
$(window).scroll(function () {
if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
else bar.animate(0); // or bar.set(0)
});
//Loop through my divs and create animated circle for each one
function makeCircles() {
var divsValues = {
'total-score-circle': 0.75,
'general-score-circle': 0.80,
'speed-score-circle': 0.85,
'privacy-score-circle': 0.90,
};
for (var i in divsValues) {
if (divsValues.hasOwnProperty(i)) {
bgCircles(i, divsValues[i]);
}
}
}
makeCircles();
// Check if element is scrolled into view
function isScrolledIntoView(elem) {
var docViewTop = jQuery(window).scrollTop();
var docViewBottom = docViewTop + jQuery(window).height();
var elemTop = jQuery(elem).offset().top;
var elemBottom = elemTop + jQuery(elem).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}
//Circle design and animation
function bgCircles(divid, countvalue) {
// Design the circle using progressbar.js
var bar = new ProgressBar.Circle(document.getElementById(divid), {
color: '#ddd',
// This has to be the same size as the maximum width to
// prevent clipping
strokeWidth: 4,
trailWidth: 4,
easing: 'easeInOut',
duration: 1400,
text: {
autoStyleContainer: false
},
from: {
color: '#ddd',
width: 4
},
to: {
color: '#888',
width: 4
},
// Set default step function for all animate calls
step: function(state, circle) {
circle.path.setAttribute('stroke', state.color);
circle.path.setAttribute('stroke-width', state.width);
var value = Math.round(circle.value() * 100);
if (value === 0) {
circle.setText('');
} else {
circle.setText(value + '%');
}
}
});
bar.text.style.fontFamily = '"Montserrat", sans-serif';
bar.text.style.fontSize = '1.7rem';
bar.trail.setAttribute('stroke-linecap', 'round');
bar.path.setAttribute('stroke-linecap', 'round');
//Animate the circle when scrolled into view
$(window).scroll(function () {
if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
else bar.animate(0); // or bar.set(0)
});
}
#total-score-circle,
#general-score-circle,
#speed-score-circle,
#privacy-score-circle {
margin: 0.8em auto;
width: 100px;
height: 100px;
position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js"></script>
<div id="total-score-circle"></div>
<div id="general-score-circle"></div>
<div id="speed-score-circle"></div>
<div id="privacy-score-circle"></div>
Since I didn't edit any of your circle animation/circle visibility checking functions, I assume you intended the current state of your animate-when-scrolled-and-in-view functionality the way, that it is right now. In this current state, your script does/has the following side-effects:
If you don't scroll the page, not at all, your circles won't start to animate, even, when they are visible. Solution: encapsulate the visibility checker lines to a separate function and run, when creating the circles.
If you scroll through a circle, its animation with the percent will going to go to its default state, which is 0%. Solution: change the visibility checker function, when the particular element is not visible because of overscroll, return that state as visible too. This way your circles will stay at 100%, even when you scroll over them.
When using jQuery, make sure to call jQuery(...)
or its shorthand $(...)
as few times as possible. Use variables to store elements, properties, and data.
It's better to separate longer/larger, monolithic functions into smaller functions with a more narrow, but also more clear scope of functionality.
When using event listeners, make sure to run as few of them as possible. Structure your HTML and your JavaScript code to have clear and performant ways to access and modify your crucial elements, properties, and data.
The loop you have will run so fast that the browser engine wont be able to render the changes, I would suggest either you use setInterval()
method or continuous setTimeout()
method which will add some delay to your code so that the browser can render the changes you are making.
For your special case I would suggest:
var i = 0;
var tobecleared = setInterval(timer,1000);
function timer(){
var p = get_ith_key_from_divsvalues(i);//implement this method
console.log(p);
bgCircles(p, divsValues[p]);
i++;
if(i == Object.keys(divsValues).length)
clearInterval(tobecleared);
}
function get_ith_key_from_divsvalues(i){
var j = -1;
for(var property in divsValues){
j++;
if(j==i)
return property;
}
}
Note : window.onscroll
is being overwritten in each call that is why only the last circle responds.
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