Search code examples
javascripthtmlcanvasdrawimage

Loading images before rendering JS canvas


I'm writing one of those simple games to learn JS and I'm learning HTML5 in the process so I need to draw things on canvas.

Here's the code:

 let paddle = new Paddle(GAME_WIDTH,GAME_HEIGHT);

 new InputHandler(paddle);

 let lastTime = 0;

 const ball = new Image();
 ball.src = 'assets/ball.png';

 function gameLoop(timeStamp){
   let dt = timeStamp - lastTime;
   lastTime = timeStamp;

   ctx.clearRect(0,0,600,600);
   paddle.update(dt);
   paddle.draw(ctx);

   ball.onload = () => {
    ctx.drawImage(ball,20,20);
  }

  window.requestAnimationFrame(gameLoop);
 }

gameLoop();

screenshot: no ball before comment

now I comment out the clearRect():

after comment

hello ball.

There's also a paddle at the bottom of the canvas that doesn't seem to be affected by the clearRect() method. It works just fine. What am I missing here?


Solution

  • It doesn't make much sense to put the image's onload handler inside the game loop. This means the game has to begin running before the image's onload function is set, leading to a pretty confusing situation.

    The correct sequence is to set the onload handlers, then the image sources, then await all of the image onloads firing before running the game loop. Setting the main loop to an onload directly is pretty easy when you only have one image, but for a game with multiple assets, this can get awkward quickly.

    Here's a minimal example of how you might load many game assets using Promise.all. Very likely, you'll want to unpack the loaded images into more descriptive objects rather than an array, but this is a start.

    const canvas = document.createElement("canvas");
    document.body.appendChild(canvas);
    canvas.width = 400;
    canvas.height = 250;
    const ctx = canvas.getContext("2d");
    
    const assets = [
      "https://picsum.photos/120/100",
      "https://picsum.photos/120/120",
      "https://picsum.photos/120/140",
    ];
    const assetsLoaded = assets.map(async url => {
      const img = new Image();
      img.src = url;
      await img.decode();
      return img;
    });
    
    Promise
      .all(assetsLoaded)
      .then(images => {
        (function gameLoop() {
          requestAnimationFrame(gameLoop);
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          images.forEach((e, i) =>
            ctx.drawImage(
              e, 
              i * 120, // x
              Math.sin(Date.now() * 0.005) * 20 + 40 // y
            )
          );
        })();
      })
      .catch(err => console.error(err));