skip to Main Content

I want to draw a rectangle on an HTML5 canvas, and have it move based on coordinates originating from an asynchronous source. I used setTimeOut to simulate the asynchronous source:

async function getX() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(Math.random() * canvas.width);
         }, 500);
    });
}
async function animate() {
    requestAnimationFrame(animate);
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const x = await getX();
    ctx.strokeRect(x, 100, 200, 100);
}

animate();

However, this doesn’t seem to work. Trying to draw it without animating (i.e. – without using requestAnimationFrame()), works, so I believe the problem arises from some "collision" between frames wanting to move forward and the async function.

How could I fix this?

2

Answers


  1. The problem is a race condition between requestAnimationFrame(animate) which calls animate before the next frame and getX which stalls the method for 500 ms. Resulting in ctx.clearRect being called at least one time before each frame.

    You can avoid the race condition by rearranging your code like this:

    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Async Animation</title>
        <style>
            canvas {
                border: 1px solid #000;
            }
        </style>
    </head>
    
    <body>
        <canvas id="myCanvas" width="800" height="600"></canvas>
        <script>
            const canvas = document.getElementById('myCanvas');
            const ctx = canvas.getContext('2d');
    
            async function getX() {
                return new Promise((resolve) => {
                    setTimeout(() => {
                        resolve(Math.random() * canvas.width);
                    }, 500);
                });
            }
    
            async function animate() {
                const x = await getX();
    
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.strokeRect(x, 100, 200, 100);
    
                requestAnimationFrame(animate);
            }
    
            animate();
        </script>
    </body>
    
    </html>
    
    Login or Signup to reply.
  2. I would use setInterval instead of setTimeout that should simplify a lot in your code, also you are already using global variable I would add those elements you are drawing as globals as well…

    Here is an example with a bit more complexity

    const canvas = document.getElementById('x');
    const ctx = canvas.getContext('2d');
    var bricks = [ {x:10, y:100}, {x:10, y:50}, {x:10, y:10} ]
    var circle = {x: 100, y: 90}
    
    function updateBricks() {
      bricks.forEach((b) => {
        b.x = Math.random() * canvas.width
      })
    }
    
    function updateCircle() {
      circle.y += 2
      if (circle.y > canvas.height)
        circle.y = 0
    }
    
    function animate() {    
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath()
        bricks.forEach((b) => {
          ctx.strokeRect(b.x, b.y, 40, 20);
        })
        ctx.arc(circle.x, circle.y, 10, 0, 2 * Math.PI);
        ctx.fill();
        requestAnimationFrame(animate);
    }
    
    setInterval(updateBricks, 500);
    setInterval(updateCircle, 100);
    animate();
    <canvas id="x" ></canvas>

    You can see see I have two variables bricks and circle, I still do all the drawing like you in the animate function, but I added a couple of new functions to update the position of the elements.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search