3

I have the following pattern to repeat an animation (itself consisting of x frames), t times:

sprite.prototype.loop = function(t,animation_name,frame_delay) {
    if(t > 0) {
        function next_run() {
            this.loop(t-1,animation_name,frame_delay);
        } 
        this.run(animation_name,frame_delay,next_run);
      }
    };

sprite.prototype.run = function(animation_name,frame_delay,frame_count,callback) {
    frame_count ||= 0;
    var s = this;
    if(frame_count < this.paths[animation_name].length) { // x frames
        setTimeout( function () {
            s.blit(s.paths[animation_name][frame_count]);
            s.run(animation_name,frame_delay,frame_count+1);
            }, frame_delay );
        }
    } else {
        if(!!callback) callback();
    }

super_mario.loop(10000,'mushroom_death',40);

Clearly if x*t is larger than the maximum stack depth, a stack overflow will occur.

Question: Can this pattern be extended to the case of running an animation an infinite number of times, or is there a more correct way to do the infinite loop case?

Cris Stringfellow
  • 3,714
  • 26
  • 48

2 Answers2

2

setTimeout doesn't inherit the caller's stack frame, so you'd only have to worry about t here, though there's really no reason this can't be trivially written iteratively so that stack size isn't a concern:

sprite.prototype.loop = function(t, animation_name, frame_delay){
    while (t--){
        this.run(animation_name, frame_delay);
    }
};

However, it feels like this code isn't doing what you expect it to do. This is literally running the animation t times in parallel, since the setTimeout calls in run will interleave one another. I'm not sure I can understand how that would be an intended effect, since each one of the t deferred callbacks would each perform the exact same blit operation here, so there would be no discernible difference.

Justin Summerlin
  • 4,938
  • 1
  • 16
  • 10
  • It's not that a call to `setTimeout` isn't pushed onto the call stack. Every function is. What happens when you call `setTimeout` is that it defers the execution of the specified function. Hence both `setTimeout` and the calling function return before the specified function is executed. Thus the call stack is cleared before the specified function executes. – Aadit M Shah Mar 10 '13 at 10:28
  • This was the right answer until I updated my code to include the callback for the next run. The callback is recursing the loop function, so it can't be unrolled like that anymore. – Cris Stringfellow Mar 10 '13 at 10:33
2

I would rewrite your loop function as follows:

sprite.prototype.loop = function (t, animation_name, frame_delay) {
    var s = this;
    var frames = s.paths[animation_name];
    var x = frames.length;

    for (var i = 0; i < t; i++) {
        for (var frame_count = 0; frame_count < x; frame_count++) {
            setTimeout(function (frame_count) {
                s.blit(frames[frame_count]);
            }, (i * x + frame_count) * frame_delay, frame_count);
        }
    }
};

That's it. There won't be any stack overflow because there's no recursion.

Edit: As just2n mentioned there's another problem with your code. There won't be any delay between two same frames as you're in effect calling setTimeout for all the same frames at the same time. Hence the animation will only occur once no matter what the value of t.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Is there any (memory/execution) problem with setting up this many `setTimeout`s in a row? – Cris Stringfellow Mar 10 '13 at 10:34
  • There is the issue that `setTimeout` has no guarantee that actions will occur at the given delay. Instead, the renderer should look at the time and decide what frame it should be on in something like a `requestAnimationFrame` loop. This way if someone were to minimize the window or switch tabs and then come back, rendering would continue at the correct place. – Justin Summerlin Mar 13 '13 at 17:53