goodson
goodson

Reputation: 757

Create a promise here, resolve it there

I thought I had the hang of promises in all situations, but I'm stuck here: I'd like an animation to happen on a timer. The only thing I can get from the graphics package is the frame number, so I compute an end frame based on a frame rate estimate.

The graphics package is p5.js, which exposes frameCount, that just runs up at about 60 fps on each draw cycle. (I named it currentFrame in this question, hoping that would clarify). As far as I can tell, this is the only visibility I get into the animation frame state in p5.js

doAnimation(duration) {
  // animation stuff
  this.endFrame = currentFrame + (60 * duration); // 60fps seems to be a good guess
}

draw() {
  if (currentFrame < this.endFrame) {
    // compute and draw the state of the animation
  } else if (currentFrame >= this.endFrame) {
    // animation done
  }
}

What I'm puzzled by is how to give the doAnimation caller a promise that resolves in the other method. I've tried this:

doAnimation(duration) {
  // animation stuff
  this.endFrame = currentFrame + (60 * duration);
  this.animationPromise = new Promise(resolve => {
    // I realize this is wrong, but it illustrates the problem. How do I run this test later?
    if (currentFrame >= this.endFrame) resolve();
  });
  return this.animationPromise;
}

draw() {
  if (currentFrame < this.endFrame) {
    // compute and draw the state of the animation
  } else if (currentFrame >= this.endFrame) {
    this.animationPromise.resolve();  // also wrong, I think
    // since resolve isn't an instance method of a promise
  }
}

Can somebody get me un-mixed up about this?

Upvotes: 1

Views: 100

Answers (3)

trincot
trincot

Reputation: 350725

As an alternative, you can also assign to draw within the promise constructor callback:

function basicDraw() {
    // ...
}
var draw = basicDraw; // Default function

doAnimation(duration) {
    // animation stuff
    this.endFrame = currentFrame + (60 * duration);
    return new Promise((resolve) => {
        draw = () => {
            basicDraw();
            if (currentFrame < this.endFrame) {
                // compute and draw the state of the animation
            } else if (currentFrame >= this.endFrame) {
                draw = basicDraw; // back to normal
                resolve();
            }
        };
    });
}

doAnimation(1000).then(() => {
     /* whatever you want to do next */ 
});

Upvotes: 1

T.J. Crowder
T.J. Crowder

Reputation: 1074959

In general, fulfilling or rejecting a promise should be done by the process that was started within the executor function you passed new Promise. It's very rare to need to fulfill or reject the promise from outside it (at least, in a way that the caller realizes it's a promise fulfillment).

I'd use a callback queue:

  1. Maintain a list of pending animations which has the frame they end on and a callback.

  2. Have draw look through the queue for animation callbacks it should call based on the (new) currentFrame.

  3. Have the doAnimation code queue a callback

Roughly:

activeAnimations = [];

doAnimation(duration) {
    // animation stuff
    const endFrame = currentFrame + (60 * duration); // 60fps seems to be a good guess
    return new Promise(resolve => {
        this.activeAnimations.push({
            endFrame,
            done: resolve, // Or possibly: `done: () => resolve()`
                           // so this code is in control of whether
                           // there's a fulfillment value
        });
    });
}

draw() {
    for (let index = 0; index < this.activeAnimations; ++index) {
        const animation = this.activeAnimations[index];
        if (currentFrame > animation.endFrame) {
            // Animation done
            animation.done();
            this.activeAnimations.splice(index, 1);
            --index;
        } else {
            // Continue animation
        }
    }
}

The reason for the done: () => resolve() comment is that in general, I try to avoid directly exposing the promise resolve/reject functions to code outside the promise executor, on the basis that code outside it shouldn't typically be directly in control of what happens to the promise. (I make an exception for setTimeout. :-) ) But that may be overkill here.

Upvotes: 3

Bergi
Bergi

Reputation: 664936

Instead of storing the promise to resolve, you need to store the resolver to call:

doAnimation(duration) {
  // animation stuff
  this.endFrame = currentFrame + (60 * duration);
  return new Promise(resolve => {
    this.onAnimationFinished = resolve;
  });
}

draw() {
  if (currentFrame < this.endFrame) {
    // compute and draw the state of the animation
  } else if (currentFrame >= this.endFrame) {
    this.onAnimationFinished();
  }
}

Of course you need to ensure that there is only one call to doAnimation() at a time, and that draw() is not called before doAnimation() (or more precisely, that onAnimationFinished is set up when an endFrame is set). You also might want to reset them (to undefined) once the end frame is reached.

Upvotes: 3

Related Questions