Synchronizing jQuery setInterval for slideshow - javascript

I created a very basic slideshow by repeating the jQuery effects with setInterval. However, there is a mismatch in timing after each cycle of the setInterval. After a few cycles, it leads to a visible mismatch in two parallel slide effects.
The code is
$(document).ready(function(){
var frontslides =$("#first li").length,
slideTime=500,
slideCycle=parseInt(slideTime*frontslides);
function frontSlide(){
$("#first li").each(function(n){
$(this).delay(slideTime*n).fadeIn(400).fadeOut(100);
$("#second li").eq(n).delay(slideTime*n).fadeIn(400).fadeOut(100);
});}
frontSlide();setInterval(frontSlide, slideCycle);});​
The working example is here
To save your valuable time, its speed is fast, but it happens on any speed. After a few cycles, you can see that left and right slides are no longer synchronized.

First approach: setTimeout
You could move the transitioning to the next slide in a separate function and call it with setTimeout. Then, you just store the slide number in a variable and increment it after each function call.
$(document).ready(function(){
var firstSlides = $("#first li"),
secondSlides = $("#second li"),
nbSlides = firstSlides.length,
slideTime = 500,
nextSlide = 0;
function slideshow() {
firstSlides.eq(nextSlide).fadeIn(400).fadeOut(100);
secondSlides.eq(nextSlide).fadeIn(400).fadeOut(100);
nextSlide = (nextSlide+1) % nbSlides;
setTimeout(slideshow, slideTime);
}
slideshow();
});
Here's a fiddle of this first approach.
Second approach: promises
If you want to absolutely guarantee that both animations are completed when starting the next animation, you can use the new promises from jQuery 1.6. You can call $.promise() on both jQuery objects to get a promise which will be resolved when all animations of that element are completed. Then, you can set up a master promise with $.when(promises...) which will be resolved when all the given promises are resolved and set up a then handler.
$(document).ready(function(){
var firstSlides = $("#first li"),
secondSlides = $("#second li"),
nbSlides = firstSlides.length,
fadeInTime = 400,
fadeOutTime = 100,
nextSlide = 0;
function slideIn() {
var p1 = firstSlides.eq(nextSlide).fadeIn(400).promise();
var p2 = secondSlides.eq(nextSlide).fadeIn(400).promise();
$.when(p1, p2).then(slideOut);
}
function slideOut() {
var p1 = firstSlides.eq(nextSlide).fadeOut(100).promise();
var p2 = secondSlides.eq(nextSlide).fadeOut(100).promise();
nextSlide = (nextSlide+1) % nbSlides;
$.when(p1, p2).then(slideIn);
}
slideIn();
});
For a simple slideshow, you probably won't notice much of a difference. However, with promises you can make much more advanced transition sequences with funky timings while still maintaining synchronisation.
Here's a fiddle of that approach.

Set the animations to both run at the "same" time:
function frontSlide(){
$("#first li").each(function(n){
var _ = $(this);
setTimeout(function() {
_.fadeIn(400).fadeOut(100);
$("#second li").eq(n).fadeIn(400).fadeOut(100);
}, slideTime*n);
});
}

Since I opened my mouth, here is the version I came up with. I added a blocked flag in there to indicate an effects loop was currently running, hopefully preventing the script from overrunning itself. I also added a factor value so that if overrunning did occur, it would loop more quickly, to preserve the appearance that the loop was not disrupted.
If you have any other questions about it, just let me know. It's pretty straight forward.
$(document).ready(function() {
var $sel = $('#left, #right').find('li:first-child'),
slidetime = 200,
timein = 400,
timeout = 100,
blocked = false;
var fadeout = function() {
$sel.fadeOut(timeout, next);
};
var next = function() {
if ($sel.is(':last-child')) {
$sel = $sel.siblings(':first-child');
} else {
$sel = $sel.next();
}
blocked = false;
};
(function slider() {
var factor = .2;
if (!blocked) {
factor = 1;
blocked = true;
$sel.fadeIn(timein, fadeout);
}
setTimeout(slider, slidetime * factor);
})();
});​
http://jsfiddle.net/j63vH/

Related

How to handle click event being called multiple times with jquery animation?

I have a click event that has a Jquery animation in it.
How can i guarantee that the animation has finished when multiple click events are being fired.
$(this._ScrollBarTrack).click(function(e) {
if(e.target === this && _self._horizontalClickScrollingFlag === false){
_self._horizontalClickScrollingFlag = true;
if(_self._isVertical){
} else{ //horizontal
if(e.offsetX > (this.firstChild.offsetWidth + this.firstChild.offsetLeft)){ // Moving Towards Right
var scrollableAmountToMove = _self._arrayOfCellSizes[_self._scrollBarCurrentStep + 1]; // additional amount to move
var scrollableCurrentPosition = -($(_self._bodyScrollable).position().left);
var scrollBarCurrentPosition = $(_self._ScrollBarTrackPiece).position().left;
var scrollBarAmountToMove = _self.getScrollBarTrackPiecePositionBasedOnScrollablePosition(scrollableAmountToMove);
$(".event-scroll-horizontally").animate({left:-(scrollableCurrentPosition+ scrollableAmountToMove)});
$(_self._ScrollBarTrackPiece).animate({left: (scrollBarCurrentPosition + scrollBarAmountToMove)});
_self._scrollBarCurrentStep += 1;
} else{
var scrollableAmountToMove = _self._arrayOfCellSizes[_self._scrollBarCurrentStep - 1]; // additional amount to move
var scrollableCurrentPosition = -($(_self._bodyScrollable).position().left);
var scrollBarCurrentPosition = $(_self._ScrollBarTrackPiece).position().left;
var scrollBarAmountToMove = _self.getScrollBarTrackPiecePositionBasedOnScrollablePosition(scrollableAmountToMove);
$(".event-scroll-horizontally").animate({left:-(scrollableCurrentPosition - scrollableAmountToMove)});
$(_self._ScrollBarTrackPiece).animate({left: (scrollBarCurrentPosition - scrollBarAmountToMove)});
_self._scrollBarCurrentStep -= 1;
}
}
_self._horizontalClickScrollingFlag = false;
}
});
jQuery has a hidden (I'm not sure why it's not in the docs someplace) variable $.timers that you can test against.
I made this function a long time ago to handle situations like this. Mind you, this will test to make sure there are NO animations currently being executed.
function animationsTest (callback) {
var testAnimationInterval = setInterval(function () {
if ($.timers.length === 0) { // any page animations finished
clearInterval(testAnimationInterval);
callback(); // callback function
}
}, 25);
};
Useage: jsFiddle DEMO
animationsTest(function () {
/* your code here will run when no animations are occuring */
});
If you want to test against one individually you could do a class/data route.
$('#thing').addClass('animating').animate({ left: '+=100px' }, function () {
// your callback when the animation is finished
$(this).removeClass('animating');
});
You could declare a global boolean called isAnimating and set it to true right when you begin the animation. Then add a done or complete function to the animation that sets it back to false. Then set your click event to only begin the animation if isAnimating is false.

Allowing for maximum number of opened accordion sections using jQuery

I've looked all over the internet and I can't seem to find a good way to do this.
I've got an accordion menu that I've built primarily using addClass/removeClass and css. It has special functionality, the accordion tabs open after a delay on mouseover and they open and close on click. I can currently open all of them at once, but I'd like to limit this to 2 or 3 with the earliest selected panel closing after I hit that limit. So I'd either need to make the classes numbered and switch them on every action, or perhaps apply a variable that keeps track of the order in which the panels were selected and switch them.
Below is the code I have so far. I've only been able to get as far as keeping count of how many tabs there currently are open. Does anyone have an idea as to what the best way to approach this is?
var timer;
var counter = 0;
$('li.has-dropdown').mouseenter(function() {
dd_item = $(this);
if(!$(this).hasClass('expand-tab')){
timer = setTimeout ( function () {
$(dd_item).addClass('expand-tab');
counter++;
}, 200);
};
}).mouseleave(function(){
clearTimeout(timer);
console.log(counter);
}).click(function() {
if ($(this).hasClass('expand-tab')){
$(this).removeClass('expand-tab');
counter--;
console.log(counter);
}else{
$(this).addClass('expand-tab');
console.log(counter);
}
});
Add a incrementting data-index to each opened tab.
count the tabs on the end of the hover effect, if they are to many, sort them by the index, and hide the lowest/oldest.
var timer;
var index = 1;
$('li.has-dropdown').mouseenter(function() {
dd_item = $(this);
if(!$(this).hasClass('expand-tab')){
timer = setTimeout ( function () {
$(dd_item).addClass('expand-tab');
$(dd_item).attr('data-index', index++);
counter++;
}, 200);
};
}).mouseleave(function(){
clearTimeout(timer);
console.log(counter);
}).click(function() {
$(this).taggleClass('expand-tab'); // see jQuery toggleClass();
$(this).attr('data-index', index++);//this will add index on closed tabs also.. but it does not matter at the end.
});
if($('.expand-tab').length> 3){
//custom inline sorting function.
var expanded_tabs = $('.expand-tab').sort(function (a, b) {
return (parseInt( $(a).attr('data-index')) < parseInt( $(b).attr('data-index')) ? -1 : 1 ;
});
//time out .. effect etc.
expanded_tabs[0].removeClass('expand-tab');
}
P.S I don't like havving Hover and Click in the same place ... try to separate the events and call a unified collapseIfToMany function in on each event
This is a corrected version. I decided to use a variable for the maximum panels opened, this way you don't have to dig if you decide you want to change it, or if you add more to the code.
var timer;
var index = 1;
var maxOpen = 2;
$('li.has-dropdown').mouseenter(function() {
dd_item = $(this);
if(!$(this).hasClass('expand-tab')){
timer = setTimeout ( function () {
$(dd_item).addClass('expand-tab');
$(dd_item).attr('data-index', index++);
collapseIfTooMany();
}, 200);
};
}).mouseleave(function(){
clearTimeout(timer);
}).click(function() {
$(this).toggleClass('expand-tab'); // see jQuery toggleClass();
$(this).attr('data-index', index++);//this will add index on closed tabs also.. but it does not matter at the end.
});
function collapseIfTooMany(){
if($('.expand-tab').length > maxOpen){
//custom inline sorting function.
var expanded_tabs = $('.expand-tab').sort(function (a, b) {
return (parseInt( $(a).attr('data-index')) < parseInt( $(b).attr('data-index'))) ? -1 : 1 ;
});
//time out .. effect etc.
$(expanded_tabs[0]).removeClass('expand-tab');
}
};

jQuery animation inside recursive function very slow

I have a small animation of an arrow bouncing horizontaly over three car pictures. The arrow starts with 200ms per bounce and the time increases by 200ms each turn, until it stops on 7th turn on car #3.
It works on Chrome and Firefox almost smoothly. On Safari 7 it starts fast and after two laps it becomes very slow and skips a lot of frames.
The Javascript code is the following:
var fwd = true;
var cnt = 6;
var time = 200;
function play(){
var tgt = fwd ? '310px' : '10px';
$('#arrow').animate({left: tgt}, time, function() {
if (cnt > 0){
cnt--;
fwd = !fwd;
time += 200;
play();
} else {
finalTarget();
}
});
}
function finalTarget (){
$('#arrow').animate({left: '230px'}, 466, function(){
$('#car3').hide(0).show('pulsate', {times: 3}, 600, function(){
$('#car1, #car2').fadeTo('slow', 0.3);
});
});
}
The code is also on JSFiddle http://jsfiddle.net/MkeeE/1/
What's the problem with this code?
Am I not supposed to call the "play" function inside the callback function?
EDIT:
As #jfriend00 pointed on comments, there's no recursion on this code. "By the time the animation completion function is called and play() is called again, the original invocation of play() has long since completed."
The typical method of doing a 'looping' animation style is to alternate animate functions in the call back.
I modified the fiddle you provided: http://jsfiddle.net/Culyx/fLz5U/4/
Jquery:
cnt = 6;
var arrowSpeed = 400;
bounceLeft = function(){
$("#arrow").animate({left: "+=380px"},{duration:arrowSpeed, complete: bounceRight});
}
bounceRight = function(){
cnt--;
if(cnt>0){
$("#arrow").animate({left: "-=380px"},{duration:arrowSpeed, complete: bounceLeft});}else{
finalTarget();
}
}
bounceLeft();
function finalTarget (){
$('#arrow').animate({left: '230px'}, 466, function(){
$('#car3').hide(0).show('pulsate', {times: 3}, 600, function(){
$('#car1, #car2').fadeTo('slow', 0.3);
});
});
}
The animation is the same but I think i misjudged the animation width; Also switched it to on Document.Ready can easily be switched back to function with a button.

Can I detect if an arbitrary CSS transition has started

In my close function I want to do all my DOM clean-up stuff after css transitions have finished running. But there might not be any transitions running/might be multi-stage ones - (maintaining the stylesheets is out of my hands).
How would I go about writing a function something like the following
function close () {
myEl.removeClass('open');
if (animation is running/about to be run) {
// wait for transition to end, then recursively check to see if another
// one has started, wait for that ...
// then
cleanUpDOM();
} else {
cleanUpDOM();
}
}
My thoughts so far are to wrap the initial check in a timeout/requestAnimationFrame in order to give the animation a chance to start then checking to see if it's running. Unfortunately, without a transitionstart event I have no idea how to check if a transition has begun.
edit Answers recommending jquery are irrelevant as jquery animations are javascript animations, not CSS transitions
About transitionStart and transitionEnd events:
The transition can't starts from nowhere. Usually transition starts after some event, where you change the state of DOM element by changing styles by class or something else. So you know when transition starts because you start it in your code.
During the transition user I/O don't blocks, so transition is asynchronous and then transition will end you don't know right. So you needs transitionEnd event to do something then transition has finished in javascript.
About transitionEnd event:
Just look the jsfiddle
Here's my solution so far - a bit hacky and only works when which element might transition is known, and doesn't work with transition-property: all... but it's a promising start
function toCamelStyleProp (str) {
return str.replace(/(?:\-)([a-z])/gi, function ($0, $1) {
return $1.toUpperCase();
});
}
function toHyphenatedStyleProp (str) {
return str.replace(/([A-Z])/g, function (str,m1) {
return '-' + m1.toLowerCase();
}).replace(/^ms-/,'-ms-');
}
function getPrefixedStyleProp (prop) {
prop = toCamelStyleProp(prop);
prop = Modernizr.prefixed(prop);
return toHyphenatedStyleProp(prop);
}
function getStyleProperty (el, prop) {
return getComputedStyle(el,null).getPropertyValue(getPrefixedStyleProp(prop));
}
function doAfterTransition ($wrapper, cssClass, mode, $transitioningEl, callback) {
$transitioningEl = $transitioningEl || $wrapper;
var transitioningEl = $transitioningEl[0],
duration = +getStyleProperty(transitioningEl, 'transition-duration').replace(/[^\.\d]/g, ''),
transitioners = getStyleProperty(transitioningEl, 'transition-property').split(' '),
initialState = [],
changedState = [],
i,
callbackHasRun = false,
//makes sure callback doesn't get called twice by accident
singletonCallback = function () {
if (!callbackHasRun) {
callbackHasRun = true;
callback();
}
};
// if no transition defined just call the callback
if (duration === 0) {
$wrapper[mode + 'Class'](cssClass);
callback();
return;
}
for (i = transitioners.length - 1;i>=0;i--) {
initialState.unshift(getStyleProperty(transitioningEl, transitioners[i]));
}
$wrapper[mode + 'Class'](cssClass);
setTimeout(function () {
for (i = transitioners.length - 1;i>=0;i--) {
changedState.unshift(getStyleProperty(transitioningEl, transitioners[i]));
}
for (i = transitioners.length - 1;i>=0;i--) {
if (changedState[i] !== initialState[i]) {
$transitioningEl.transitionEnd(singletonCallback);
// failsafe in case the transitionEnd event doesn't fire
setTimeout(singletonCallback, duration * 1000);
return;
}
}
singletonCallback();
}, 20);
}
There is no way (that I know of) to detect if a transition is currently working in the background without knowing the element that is being transitioned.
However, if you can move away from transition to key frame animations, then you'd have the so needed event - animationStart and animationEnd and then it will be easy to figure out if there are running animations.
If you're planning to make css transition, you can check out jQuery Transit Plugin http://ricostacruz.com/jquery.transit/
Very powerfull and useful, you can get transform x value with. css('x') for example.
Have you tried the JQuery pseudo ":animated"?
if( $(elem).is(':animated') ) {...}
See More http://api.jquery.com/animated-selector/
Here is a function that waits for the page Html to become stable. i.e. when all animations are finished. In the example below it waits for the Html to be unchanging for 200 milliseconds and a maximum timeout of 2 seconds.
Call the function with ...
waitUntilHtmlStable(yourCallback, 200, 2000);
The function ...
waitUntilHtmlStable = function (callback, unchangedDuration, timeout, unchangedElapsed, html) {
var sleep = 50;
window.setTimeout(function () {
var newHtml = document.documentElement.innerHTML;
if (html != newHtml) unchangedElapsed = 0;
if (unchangedElapsed < unchangedDuration && timeout > 0)
waitUntilHtmlStable(callback, unchangedDuration, timeout - interval, unchangedElapsed + interval, newHtml);
else
callback();
}, sleep);
};
In my case I wanted to be sure new elements where present. If you want to track animation movement then change the document.documentElement.innerHTML to
JSON.stringify(Array.prototype.slice.call(document.documentElement.getElementsByTagName("*"), 0)
.map(function(e) {
var x = e;
var r = x.getBoundingClientRect();
while (r.width == 0 || r.height == 0) {
x = x.parentNode;
r = x.getBoundingClientRect();
}
return r;
}));
There is an unprefixed transitionstart event in IE10+. It is even cancelable.
https://msdn.microsoft.com/library/dn632683%28v=vs.85%29.aspx
On animation.css i found this.
You can also detect when an animation ends:
$('#yourElement').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', doSomething);
read full doc here
you could use Jquery which would be much easier for example you could use .animate like this
(function(){
var box = $('div.box')
$('button').on('click', function(){
box.animate({ 'font-size' : '40px'})
.animate({'color': 'red'});
})
})();
or simply do a callback function

jquery/javascript set multiple timeouts one after the other via loop to run independently of the other

I am trying to animate a handful of DIV's to scroll upwards but I want one to scroll up after a pause after the other after the other. And the best I can come up with at the moment is
$('.curtain').each(function()
{
var $elem = $(this);
setTimeout(function()
{
$elem.animate({"height":0+'px'}, 2000);
}, 1000);
});
Problem is they still all animate together without pause. How can I go about doing something in this fashion. The divs are dynamically generated and there can be 5 - 20 of them so doing a hardcoded logic is out, any ideas?
function animateIt () {
var elems = $('.curtain').get();
(function next() {
if(elems.length){
var elem = $(elems.shift());
elem.animate({"height":0+'px'}, 2000, next);
}
})();
}
animateIt();
running example
Another way like queue
function animateIt () {
var divs = $('.curtain');
divs.each( function(){
var elem = $(this);
$.queue(divs[0],"fun", function(next) { elem.animate({"height":0+'px'}, 2000, next); });
});
divs.eq(0).dequeue("fun");
}
Looks like a simple recursive function might work for you here -
function doAnimation(elapsed){
var iterations = $('.curtain').length;
var current = elapsed+1;
if (current <= iterations){
setTimeout(function(){
$('.curtain:eq(' + elapsed + ')').animate(...);
doAnimation(current);
},50);
}
}
doAnimation(0);
Here's a simple demo

Categories

Resources