views:

47

answers:

4

I am animating html elements (cards) using jquery. I need to animate a dealing action ('one you for you, one for me, one you you, one for me...') so want the cards to fully animate (move position) before the next one starts. I am creating the cards in a $.each function and calling the animation from there (to avoid looping again unnecessarily). When the card is added to the page the animation starts but the loop then moves on to the next card without waiting for the animation to finish. This makes all the cards appear to animate at exactly the same time rather than sequentially. How do I make the loop wait for the animation callback? Could this be done with triggers? Could I manually queue the items?

A cut down version of my (erroneous) code:

var card = [
  { id:1, pos:{ x:100, y:100 } },
  { id:2, pos:{ x:150, y:105 } },
  { id:3, pos:{ x:200, y:110 } }
];

$.each(card, function() {
  $('#card_' + this.id).animate({ top:this.pos.y, left:this.pos.x });
});

Demo @ http://jsbin.com/akori4

+3  A: 

You can't do this in the JavaScript loop, you have to use the callback parameter on the animate call to trigger the animation of the next card. Something like this:

var card = [
    { id:1, pos:{ x:100, y:100 } },
    { id:2, pos:{ x:150, y:105 } },
    { id:3, pos:{ x:200, y:110 } }
];

doOne(0);

function doOne(index) {
    var thisCard = card[index];
    if (thisCard) {
        $('#card_' + thisCard.id).animate({
            top:    thisCard.pos.y,
            left:   thisCard.pos.x
        }, function() {
            doOne(index + 1);
        });
    }
}

Here's a live example Naturally I'd have all this wrapped in a function to avoid creating unnecessary global symbols.

T.J. Crowder
+1 good and simple solution
Tauren
+1 the right answer, properly-designed async functions like `animate` always have the ability to call another function when they're finished. That's the place to trigger the next animation.
Daniel Earwicker
+2  A: 

You can use .delay(), like this:

$.each(card, function(i) {
  $('#card_'+this.id).delay(i*400).animate({ top:this.pos.y, left:this.pos.x });
});

Here's your updated/working demo.

Each animation (by default) takes 400ms, and the first parameter to the $.each() callback is the index, so the first is delayed 0*400, the second 1*400, etc...effectively running them one after the other.

Also if you don't need those x/y format variables, you can store them as top/left and pass those animation properties directly, like this:

var card = [
  { id:1, pos:{ left:100, top:100 } },
  { id:2, pos:{ left:150, top:105 } },
  { id:3, pos:{ left:200, top:110 } }
];

$.each(card, function(i) {
  $('#card_'+this.id).delay(i*400).animate(this.pos);
});
Nick Craver
With a bunch of cards, the uncertainties of timing are likely to become an issue.
T.J. Crowder
@TJCrowder - It won't, it's `setInterval()` under the covers.
Nick Craver
@Nick: `setInterval` has all *sorts* of uncertainties.
T.J. Crowder
@TJCrowder - I agree, but they're consistent internally, what delays one delays the other.
Nick Craver
@Nick: No, each interval is independent of the others. The only time they interact is when an interval comes due but code for another interval is already running, delaying it. And that's when it gets ugly.
T.J. Crowder
@TJCrowder - All of these are delayed, internally what causes one to be delayed will cause other later ones to be delayed as well, and since the animation itself is *on* an interval, that's how the jquery animations chose to prevent conflicts. Also, this is scheduling an interval *after* another, they're not overlapping. Test it yourself, with hundreds of cards: http://jsbin.com/akori4/4
Nick Craver
@Nick: We'll have to agree to disagree. I'm just not convinced that this will be reliable cross-browser, I've seen too many inconsistencies with the timings around `setInterval` and `setTimeout` (particularly `setInterval`). And I'm not really seeing that all of these timers are a better solution than the callbacks, but it's good to offer the OP options. :-)
T.J. Crowder
@TJCrowder - Show me an example of this breaking and I'll be a believer, but the way the animations intervals are structured I don't see how it can happen, feel free to test the above code with 500 cards in any browser...if you can show me it failing I'll happily delete this, but I've *never* seen or heard that behavior with `.animate()`. I agree if you're dealing with a *huge* number of cards go with another approach, but for just a *few* (and we're dealing cards here...) I think this is the simplest way. The large demo is just to illustrate the timers working :)
Nick Craver
@Nick: This particular example may work (or it may not, on IE on a slow computer). But as a general technique, if you have an event occuring every X ms and N times, with the timing resolution and vagaries of browsers, it's *not* reliable (in my experience) to assume that that will take exactly X * N milliseconds to complete. So scheduling something to occur at X * N on the assumption it'll happen after those iterations are complete is asking for trouble -- *especially* when there's a reliable mechanism available instead. Feel free to have the last word, but I'm agreeing to disagree. :-)
T.J. Crowder
@TJCrowder - I agree I don't think there's much reconciling this, my point was animations in jQuery run on a 13ms interval, so they're not taking `nn` total ms to complete they're stepped, if one step gets delayed that's fine, but it's not a 400ms continuous processing that's going to push an interval back...even if it delays one interval or timeout it'll be in sync or very, very close on the next step. Like I said I've never seen it be an issue due to the stepping, but if you can demo it with animations I would really love to see it so I can break it down and see what's causing it.
Nick Craver
@Nick: I should have said that I *don't* think you should delete this answer. I like that we've given the OP options. I was just raising a note of caution about the general technique, particularly with the odd behavior of `setInterval` when the code it runs is slow. You can see the times of this creeping up (getting increasingly out of sync with the origin time), for instance: http://jsbin.com/idase4 That doesn't use jQuery animations, but my point is about timing in browsers.
T.J. Crowder
A: 

This could get messy with normal callbacks, as each animation then needs to call the next in its callback. Of course you could use a timer or delay to create a pause, but this doesn't guarantee the full animation completes before the next one starts. And there could be a pause between each animation if the animation takes less time than your timeout.

An alternative is to look into Promises and Futures. Although I'm not performing animations, my applications have logic that must be processed in a certain order. I've been using the FuturesJS library to help simplify things. It may look intimidating at first, but it is really very powerful. Take a look at the .chainify() method.

I realize this is probably overkill for your simple use case, but knowing about Promises is something I believe is worthwhile.

Tauren
+1  A: 

Here's another approach:

var card = [
    {elem: $('#card_1'), id:1, pos:{ x:100, y:100 } },
    {elem: $('#card_2'), id:2, pos:{ x:150, y:105 } },
    {elem: $('#card_3'), id:3, pos:{ x:200, y:110 } }
];

(function loop(arr, len){
    if(len--){      
        arr[len].elem.animate({top: arr[len].pos.y, left: arr[len].pos.x}, 400, function(){
           loop(arr, len);
        });     
    }
}(card.reverse(), card.length));

In action: http://jsbin.com/akori4/6/edit

jAndy
This approach reverses the card animation order :)
Nick Craver
@Nick: Indeed, but it is just another way to deal with it. To restore the original order, it's an easy fix.
jAndy
For me this has the same timing issues (or doesn't, according to Nick!) as Nick's approach.
T.J. Crowder
@T.J. I agree. I realized that I actually have no reason for that previous pattern, so I changed it, putting the next call into the callback of `.animate()`.
jAndy
@jAndy: Yeah, that's my preferred approach as well. BTW, just FYI, on IE you're creating two functions there. ;-) Any time you have a named function declaration you also use as the right hand side of an expression, MS's JScript creates two of them -- one from the declaration, and then another from the expression. Your first call is calling the function generated by the expression, which is then calling the one created by the declaration. Doesn't matter in this case, but I like to point it out because with some patterns people end up with two copies of nearly every function they create! :-)
T.J. Crowder
@T.J.: Interesting. Anyway for this kind of approach using a self-invoking function which gets called again I see no other option, beside storing the `arguments.callee` reference and closing over that. This would be deprecated in ECMA edition 5 strict mode.
jAndy
@jAndy: There's no reason not to just declare the function and then call it *separately*, as I did in my answer. More on this topic: http://blog.niftysnippets.org/2010/09/double-take.html And yes, absolutely, stay away from `arguments.callee` whenever possible. (And paraphrasing the Dalai Lama: It's always possible.)
T.J. Crowder