Tuesday 22 May 2018

Async/await with Animations

Years ago I had fun coding an animation matrix (the sample has been moved here). Most cases involved a sequence of animations, each one starting after the previous one had finished. It was one more case where I had to simulate an asynchronous loop, where at the end of each "iteration" I would callback into an "orchestrator function" that would launch the next iteration. I realized that now that we have async/await, this could be done in a much more straight forward way, so I've prepared some sample code.

For the kind of animation that I'm doing it would have been more simple to just animate divs rather than drawing squares on a canvas, but lately I've been playing around with particle systems on a canvas, and I've preferred to continue with the canvas route.

Whatever item we are animating, the thing is that starting the animation should return a Promise that will get resolved when the animation completes. For this sample I'm animating a square that falls from the top to the bottom of the canvas and bounces a couple of times until it stops. You can see it here. The code (yep, I've jumped into the TypeScript trend) looks like this:


abstract class AnimationItem{
 currentPosition:Vector;
    endPosition:Vector;
    moving:boolean;
 resolveCurrentAnimationFunc:any;

 public abstract update():void;
 public abstract draw():void;
 
 public animate():Promise<void>{
  this.moving = true;
        return new Promise<void>((res, rej) => {
            this.resolveCurrentAnimationFunc = res;
        });
    }
}


//in our child, concrete class:
    public update():void{
        if(!this.moving){
            return;
        }

        this.ySpeed += this.gravity;
        //this.logger.log("speed: " + this.ySpeed + ", pos:" + this.currentPosition.y + ", " + this.acceleration);
        this.currentPosition.y += this.ySpeed;
        if(this.currentPosition.y >= this.endPosition.y){
            //let's place it just in the limit
            this.currentPosition.y = this.endPosition.y;
            if(this.bounces > 0){
                this.bounces--;
                this.ySpeed = -1 * this.ySpeed/2;
                this.logger.log("bouncing: pos:" + this.currentPosition.y + ", speed: " + this.ySpeed + ", " + this.acceleration);
            }
            else{
                //stop it
                this.moving = false;
                this.resolveCurrentAnimationFunc();
            }
        }
    }

So we have an animate method that returns a newly created Promise and stores in our class a reference to the resolve handle of this Promise. The update method takes care of updating the position of the object and resolving the Promise once the final position is reached.

I'm animating several of these items (rows and columns), one after another. With the power of async/await our code is so simple as just writing normal loops like this:

for (let y=0; y<2; y++){
        for (let x=0; x<3; x++){
            let item:AnimationItem = new FallingSquare(ctx, 
                new Vector(x * size, 0), //currentPosition:Vector, 
                new Vector(x * size, canvas.height - (size * (y + 1))), //endPosition:Vector,
                0, //speed:number, 
                size,
                "red",
                logger);
            
            animationSystem.addItem(item);   
            await item.animate();
        }
    }

I've uploaded along with the transpiled and bundled javascript file the original typescript source code and the source map, so to see the full code just launch the debugger!

No comments:

Post a Comment