This question already has answers here:
Animate counter when in viewport
(4 answers)
Closed 6 years ago.
I'm trying to make a number count up when it's within the viewport, but currently, the script i'm using will interrupt the count on scroll.
How would I make it so that it will ignore the scroll and just count up when it's within the viewport? This needs to work on mobile, so even when a user is scrolling on touch. It cannot interrupt the count.
Please see here:
http://jsfiddle.net/Q37Q6/27/
(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 = 25; // Count up to 25x for first
var second = 4; // Count up to 4x for second
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 div
$(".first-count").each(function (i, el) {
var el = $(el);
if (el.visible(true)) {
countStuffUp(first, '.first-count', 1600);
}
});
// Output to div
$(".second-count").each(function (i, el) {
var el = $(el);
if (el.visible(true)) {
countStuffUp(second, '.second-count', 1000);
}
});
});
Your example is more complicated than you're aware, I think. You're doing things in a pretty unusual way, here, using a jQuery animate method on a custom property as your counter. It's kind of cool, but it also makes things a little more complicated. I've had to add a number of things to straighten up the situation.
I went ahead and rewrote your visible plugin, largely because I had no idea what yours was doing. This one's simple!
When your counters become visible, they get a "counting" class so that the counter isn't re-fired on them when they're already counting.
I save a reference to the object you have your custom counter animation on to the data attribute of the counter. This is vital: without that reference, you can't stop the animation when it goes offscreen.
I do some fanciness inside the step function to keep track of how much time is left so that you can keep your counter running at the same speed even if it stops and starts. If your counter runs for half a second and it's set to use one second for the whole animation, if it gets interrupted and restarted you only want to set it to half a second when you restart the counter.
http://jsfiddle.net/nate/p9wgx/1/
(function ($) {
$.fn.visible = function () {
var $element = $(this).eq(0),
$win = $(window),
elemTop = $element.position().top,
elemBottom = elemTop + $element.height(),
winTop = $win.scrollTop(),
winBottom = winTop + $win.height();
if (elemBottom < winTop) {
return false;
} else if (elemTop > winBottom) {
return false;
} else {
return true;
}
};
})(jQuery);
function padNum(num) {
if (num < 10) {
return " " + num;
}
return num;
}
var $count1 = $('.first-count');
var $count2 = $('.second-count');
// Scrolling Functions
$(window).scroll(function (event) {
var first = 25; // Count up to 25x for first
var second = 4; // Count up to 4x for second
function countStuffUp(points, selector, duration) {
//Animate count
var $selector = $(selector);
$selector.addClass('counting');
var $counter = $({
countNumber: $selector.text()
}).animate({
countNumber: points
}, {
duration: duration,
easing: 'linear',
step: function (now) {
$selector.data('remaining', (points - now) * (duration / points));
$selector.text(padNum(parseInt(this.countNumber)));
},
complete: function () {
$selector.removeClass('counting');
$selector.text(points);
}
});
$selector.data('counter', $counter);
}
// Output to div
$(".first-count").each(function (i, el) {
var el = $(el);
if (el.visible() && !el.hasClass('counting')) {
var duration = el.data('remaining') || 1600;
countStuffUp(first, '.first-count', duration);
} else if (!el.visible() && el.hasClass('counting')) {
el.data('counter').stop();
el.removeClass('counting');
}
});
// Output to div
$(".second-count").each(function (i, el) {
var el = $(el);
if (el.visible() && !el.hasClass('counting')) {
var duration = el.data('remaining') || 1000;
countStuffUp(second, '.second-count', duration);
} else if (!el.visible() && el.hasClass('counting')) {
el.data('counter').stop();
el.removeClass('counting');
}
});
});
There's a lot here. Feel free to ask me questions if anything's not clear.
Related
Hi I have some js code that do typing effect on my web page it start typing when you scroll down end of page. For first it work normally but when you start scroll faster down to up the typing effect goes crazy how can I fix that
demo page
code
$(window).scroll(function (e) {
var elem = $(".hello-page");
var scrollTop = $(window).scrollTop();
var blockTop = elem.offset().top;
var docHeight = $(document).height();
var windowH = $(window).height();
if (scrollTop >= blockTop) {
var helloPageA = $(".hello-page").find("a");
var text = helloPageA.attr("data-text");
helloPageA.text('');
$.each(text.split(''), function (i, letter) {
setTimeout(function () {
helloPageA.html(helloPageA.html() + letter);
}, 150 * i);
});
} else {
elem.find("a").text('');
}
});
jsfiddle example
Thanks for your help
So, here is the solution - http://jsfiddle.net/u3ojjx8r/1/
I borrowed initial structure of the code from previous answer here and it was removed unfortunately, therefore I can't mention one of the co-authors. Though the code looked quite similar to topic-starter's one.
The idea of the code below is to separate the queuing of characters to render and the actual rendering. Another important improvement is always have control over timeouts, i.e. never schedule more than one timeout. That allows you to cancel them any time without unpredicted/uncontrolled behavior.
var timeoutVar;
var queue = [];
var drawQueueTimeout = -1;
var helloPageA = $(".hello-page").find("a");
function pushQueue (element) {
console.log('pushQUeue', element.char);
queue.push(element);
checkQueue();
}
function flushQueue () {
console.log('flushQueue');
queue = [];
clearTimeout(drawQueueTimeout);
drawQueueTimeout = -1;
}
function checkQueue () {
console.log('checkQueue', queue.length, drawQueueTimeout);
if (queue.length > 0 && drawQueueTimeout < 0) {
console.log('schedule drawQueue');
drawQueueTimeout = setTimeout(drawQueue, 150);
}
}
function drawQueue () {
drawQueueTimeout = -1;
console.log('draw queue');
if (queue.length > 0) {
var element = queue.shift();
console.log('drawQueue', element.char);
helloPageA.html(helloPageA.html() + element.char);
}
checkQueue();
}
$(window).scroll(function (e) {
var elem = $(".hello-page");
var scrollTop = $(window).scrollTop();
var blockTop = elem.offset().top;
var docHeight = $(document).height();
var windowH = $(window).height();
if (scrollTop + windowH == docHeight) {
// Empty anything typed so far
helloPageA.empty();
flushQueue();
var text = helloPageA.attr("data-text");
helloPageA.text('');
$.each(text.split(''), function (i, letter) {
pushQueue({
char: letter,
index: i
});
});
} else {
helloPageA.empty();
flushQueue();
}
});
I have this javascript function I use that when clicked goes a certain distance. This is used within a scroller going left to right that uses about 7 divs. My question is how do I get the click to go the full distance first before the click can be used again? The issue is if the user rapidly clicks on the arrow button it resets the distance and sometimes can end up in the middle of an image instead of right at the seam. What code am I missing to accomplish this?
$(function () {
$("#right, #left").click(function () {
var dir = this.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ scrollLeft: dir + '251' }, 1000);
});
});
I would've thought that the easiest way would be to have a boolean flag indicating whether or not the animation is taking place:
$(function () {
var animating = false,
outerwrap = $(".outerwrapper");
$("#right, #left").click(function () {
if (animating) {return;}
var dir = (this.id === "right") ? '+=' : '-=';
animating = true;
outerwrap.animate({
scrollLeft: dir + '251'
}, 1000, function () {
animating = false;
});
});
});
works for me: http://jsfiddle.net/BYossarian/vDtwy/4/
Use .off() to unbind the click as soon as it occurs, then re-bind it once the animation completes.
function go(elem){
$(elem).off('click'); console.log(elem);
var dir = elem.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ left: dir + '251' }, 3000, function(){
$("#right, #left").click(go);
});
}
$("#right, #left").click(function () {
go(this);
});
jsFiddle example
You can see in this simplified example that the click event is unbound immediately after clicking, and then rebound once the animation completes.
Use an automatic then call like this
var isMoving = false;
$(function () {
$("#right, #left").click(function () {
if (isMoving) return;
isMoving = true;
var dir = this.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ scrollLeft: dir + '251' }, 1000).then(function(){isMoving = false}());
});
});
I think that you miss the fact that when you make stop() you actually position the slider at some specific point. I.e. if your scroller is 1000px and you click left twice very quickly you will probably get
scrollLeft: 0 - 251
scrollLeft: -2 - 251
So, I think that you should use an index and not exactly these += and -= calculations. For example:
$(function () {
var numberOfDivs = 7;
var divWidth = 251;
var currentIndex = 0;
$("#right, #left").click(function () {
currentIndex = this.id == "right" ? currentIndex+1 : currentIndex-1;
currentIndex = currentIndex < 0 ? 0 : currentIndex;
currentIndex = currentIndex > numberOfDivs ? numberOfDivs : currentIndex;
$(".outerwrapper").stop().animate({ scrollLeft: (currentIndex * divWidth) + "px" }, 1000);
});
});
A big benefit of this approach is that you are not disabling the clicking. You may click as many times as you want and you can do that quickly. The script will still works.
This will work perfectly fine:
var userDisplaysPageCounter = 1;
$('#inventory_userdisplays_forward_button').bind('click.rightarrowiventory', function(event) {
_goForwardInInventory();
});
$('#inventory_userdisplays_back_button').bind('click.leftarrowiventory', function(event) {
_goBackInInventory();
});
function _goForwardInInventory()
{
//$('#inventory_userdisplays_forward_button').unbind('click.rightarrowiventory');
var totalPages = $('#userfooterdisplays_list_pagination_container div').length;
totalPages = Math.ceil(totalPages/4);
// alert(totalPages);
if(userDisplaysPageCounter < totalPages)
{
userDisplaysPageCounter++;
$( "#userfooterdisplays_list_pagination_container" ).animate({
left: "-=600",
}, 500, function() {
});
}
}
function _goBackInInventory()
{
//$('#inventory_userdisplays_back_button').unbind('click.leftarrowiventory');
if(userDisplaysPageCounter > 1)
{
userDisplaysPageCounter--;
$( "#userfooterdisplays_list_pagination_container" ).animate({
left: "+=600",
}, 500, function() {
});
}
}
I second BYossarian's answer.
Here is a variation on his demo, which "skips" the animation when the user clicks several times quickly on the buttons :
$(function () {
var targetScroll = 0,
outerwrap = $(".outerwrapper");
$("#right, #left").click(function () {
// stop the animation,
outerwrap.stop();
// hard set scrollLeft to its target position
outerwrap.scrollLeft(targetScroll*251);
if (this.id === "right"){
if (targetScroll < 6) targetScroll += 1;
dir = '+=251';
} else {
if (targetScroll > 0) targetScroll -=1;
dir = '-=251';
}
outerwrap.animate({ scrollLeft: dir }, 1000);
});
});
fiddle
The code (from an old plugin that I am trying to make responsive) slides a set of images across every n seconds. It uses setInterval code as below, and works well on Firefox. On Chrome it runs once only, and debugging indicates that the second setInteral function is just not called. Please help as its diving me mad. Running example at http://lelal.com/test/site10/index.html (sorry about the load time)
play = setInterval(function() {
if (!busy) {
busy = true;
updateCurrent(settings.direction);
slide();
}
}, settings.speed);
The complete plugin code is below (sorry its long)
/*
* jQuery Queue Slider v1.0
* http://danielkorte.com
*
* Free to use and abuse under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*/
(function($){
var QueueSlider = function(element, options) {
var play = false,
busy = false,
current = 2,
previous = 2,
widths = [],
slider = $(element),
queue = $('ul.queue', slider),
numImages = $('img', queue).size(),
viewportWidth = slider.width(),
settings = $.extend({}, $.fn.queueSlider.defaults, options);
$(window).resize(function(){
if(busy !== false)
clearTimeout(busy);
busy = setTimeout(resizewindow, 200); //200 is time in miliseconds
});
function resizewindow() {
viewportWidth = slider.width();
if (settings.scale > 0) {
slider.css('height',viewportWidth * settings.scale);
computeQueueWidth();
}
queue.css('left', -getQueuePosition());
busy = false;
}
function requeue() {
$('li', queue).each(function(key, value) {
$(this).attr('class', 'slide-' + (key+1));
});
}
function updateCurrent(dir) {
current += dir;
if (current < 1) {
current = numImages;
} else if (current > numImages) {
current = 1;
}
}
function getQueuePosition() {
var i = 0, index = current-1,
queuePosition = (viewportWidth - widths[index]) / -2;
for (i = 0; i < index; i++) { queuePosition += widths[i]; }
return queuePosition;
}
function computeQueueWidth() {
var queueWidth = 0;
// factor = slider.height() / settings.imageheight;
// settings.imageheight = settings.imageheight * factor;
// Get the image widths and set the queue width to their combined value.
$('li', queue).each(function(key, value) {
var slideimg = $("img", this),
slide = $(this),
// width = slide.width() * factor,
width = slideimg.width();
slide.css('width', width+'px');
queueWidth += widths[key] = width;
});
queue.css('width', queueWidth + 500);
}
function slide() {
var animationSettings = {
duration: settings.transitionSpeed,
queue: false
};
// Emulate an infinte loop:
// Bring the first image to the end.
if (current === numImages) {
var firstImage = $('li.slide-1', queue);
widths.push(widths.shift());
queue.css('left', queue.position().left + firstImage.width()).append(firstImage);
requeue();
current--; previous--;
}
// Bring the last image to the beginning.
else if (current === 1) {
var lastImage = $('li:last-child', queue);
widths.unshift(widths.pop());
queue.css('left', queue.position().left + -lastImage.width()).prepend(lastImage);
requeue();
current = 2; previous = 3;
}
// Fade in the current and out the previous images.
if (settings.fade !== -1) {
$('li.slide-'+current, queue).animate({opacity: 1}, animationSettings);
$('li.slide-'+previous, queue).animate({opacity: settings.fade}, animationSettings);
}
// Animate the queue.
animationSettings.complete = function() { busy = false; };
queue.animate({ left: -getQueuePosition() }, animationSettings);
previous = current;
}
//
// Setup the QueueSlider!
//
if (numImages > 2) {
// Move the last slide to the beginning of the queue so there is an image
// on both sides of the current image.
if (settings.scale > 0) {
slider.css('height',viewportWidth * settings.scale);
}
computeQueueWidth();
widths.unshift(widths.pop());
queue.css('left', -getQueuePosition()).prepend($('li:last-child', queue));
requeue();
// Fade out the images we aren't viewing.
if (settings.fade !== -1) { $('li', queue).not('.slide-2').css('opacity', settings.fade); }
// Include the buttons if enabled and assign a click event to them.
if (settings.buttons) {
slider.append('<button class="previous" rel="-1">' + settings.previous + '</button><button class="next" rel="1">' + settings.next + '</button>');
$('button', slider).click(function() {
if (!busy) {
busy = true;
updateCurrent(parseInt($(this).attr('rel'), 10));
clearInterval(play);
slide();
}
return false;
});
}
// Start the slideshow if it is enabled.
if (settings.speed !== 0) {
play = setInterval(function() {
if (!busy) {
busy = true;
updateCurrent(settings.direction);
slide();
}
}, settings.speed);
}
}
else {
// There isn't enough images for the QueueSlider!
// Let's disable the required CSS and show all one or two images ;)
slider.removeClass('queueslider');
}
};
$.fn.queueSlider = function(options) {
return this.each(function(key, value) {
var element = $(this);
// Return early if this element already has a plugin instance.
if (element.data('queueslider')) { return element.data('queueslider'); }
// Pass options to plugin constructor.
var queueslider = new QueueSlider(this, options);
// Store plugin object in this element's data.
element.data('queueslider', queueslider);
});
};
$.fn.queueSlider.defaults = {
scale: 0,
imageheight: 500,
fade: 0.3, // Opacity of images not being viewed, use -1 to disable
transitionSpeed: 700, // in milliseconds, speed for fade and slide motion
speed: 7000, // in milliseconds, use 0 to disable slideshow
direction: 1, // 1 for images to slide to the left, -1 to silde to the right during slideshow
buttons: true, // Display Previous/Next buttons
previous: 'Previous', // Previous button text
next: 'Next' // Next button text
};
}(jQuery));
Have a look here:
http://www.w3schools.com/jsref/met_win_setinterval.asp
The setInterval() method will continue calling the function until clearInterval() is called, or the window is closed.
Looks like you're calling clearInterval after the first usage of play, which makes it stop working.
I have a Javascript file that I am using to try to animate a dropdown menu. I have the "Toggle" function in that file set to run when I click on a certain div. Here's the script I'm using:
var up = true;
function Toggle(x)
{
if (up)
{
for (var i = x.offsetTop; i <= 0; i++)
{
x.style.top = i;
if (i == 0)
{
up = false;
}
}
}
else if (up == false)
{
for (var i = x.offsetTop; i >= -50; i--)
{
x.style.top = i;
if (i == -50)
{
up = true;
}
}
}
}
In the HTML div I want to animate, I have the "onclick" property set to "onclick=Toggle(this)". The first for loop works as it should (it sets the div's top offset to 0). However, the second for loop doesn't set the offsetTop. I know that the for loop is activating because I've tested it and it gives me every integer between 0 and -50. Why isn't it setting the offset position?
1) You must specify a unit to the top ie: x.style.top = i +"px"
2) Your function won't animate instead of you use a setInterval or a setTimeout
Like you asked, an example. I wouldn't do it like this for one of my project, but i kept your function to make you more familiar with the code.
I Used setTimeout instead of setInterval because setInterval must be cleared when not needed and setTimeout is just launched one time :
var Toggle = (function() { // use scope to define up/down
var up = true;
return function(element) {
var top = parseInt(element.style.top, 10); // element.offsetTop ?
if ( !top ) {
top = 0;
}
if (up) {
if (element.offsetTop < 0) { // if we are not at 0 and want to get up
element.style.top = (top+1) + "px";
setTimeout(function() { Toggle(element); }, 10); // recall the function in 10 ms
} else { // we change up value
up = !up;
}
}
else {
if (element.offsetTop > -50) {
element.style.top = (top-1) + "px";
setTimeout(function() { Toggle(element); }, 10); // recall the function in 10 ms
} else {
up=!up;
}
}
}
})();
You'd have to use x.style.top = i + 'px' as top and similar css properties must define the type (px, em, %, etc.) unless they are 0, as this is 0 in any case.
But your script would actually snap the div directly to -50px, as you do not wait between those iteration steps.
I'd recommend to use a library like jQuery to use it's animate() method.
function Toggle(obj) {
$(obj).animate({
top: parseInt($(obj).css('top')) === 0 ? '-50px' : '0px'
})
}
I have a bug in Javascript where I am animating the margin left property of a parent container to show its child divs in a sort of next/previous fashion. Problem is if clicking 'next' at a high frequency the if statement seems to be ignored (i.e. only works if click, wait for animation, then click again) :
if (marLeft === (-combinedWidth + (regWidth) + "px")) {
//roll margin back to 0
}
An example can be seen on jsFiddle - http://jsfiddle.net/ZQg5V/
Any help would be appreciated.
Try the below code which will basically check if the container is being animated just return from the function.
Working demo
$next.click(function (e) {
e.preventDefault();
if($contain.is(":animated")){
return;
}
var marLeft = $contain.css('margin-left'),
$this = $(this);
if (marLeft === (-combinedWidth + (regWidth) + "px")) {
$contain.animate({
marginLeft: 0
}, function () {
$back.fadeOut('fast');
});
} else {
$back.fadeIn(function () {
$contain.animate({
marginLeft: "-=" + regWidth + "px"
});
});
}
if (marLeft > -combinedWidth) {
$contain.animate({
marginLeft: 0
});
}
});
Sometimes is better if you create a function to take care of the animation, instead of writting animation code on every event handler (next, back). Also, users won't have to wait for the animation to finish in order to go the nth page/box.
Maybe this will help you:
if (jQuery) {
var $next = $(".next"),
$back = $(".back"),
$box = $(".box"),
regWidth = $box.width(),
$contain = $(".wrap")
len = $box.length;
var combinedWidth = regWidth*len;
$contain.width(combinedWidth);
var currentBox = 0; // Keeps track of current box
var goTo = function(n) {
$contain.animate({
marginLeft: -n*regWidth
}, {
queue: false, // We don't want animations to queue
duration: 600
});
if (n == 0) $back.fadeOut('fast');
else $back.fadeIn('fast');
currentBox = n;
};
$next.click(function(e) {
e.preventDefault();
var go = currentBox + 1;
if (go >= len) go = 0; // Index based, instead of margin based...
goTo(go);
});
$back.click(function(e) {
e.preventDefault();
var go = currentBox - 1;
if (go <= 0) go = 0; //In case back is pressed while fading...
goTo(go);
});
}
Here's an updated version of your jsFiddle: http://jsfiddle.net/victmo/ZQg5V/5/
Cheers!
Use a variable to track if the animation is taking place. Pseudocode:
var animating = false;
function myAnimation() {
if (animating) return;
animating = true;
$(this).animate({what:'ever'}, function() {
animating = false;
});
}
Crude, but it should give you the idea.
Edit: Your current code works fine for me as well, even if I jam out on the button. On firefox.