简体   繁体   中英

How to add delays in animations in canvas?

I am trying to create a small javascript animation to understand an algorithm better . The algorithm works on a 2d array like this :-

function algorithm(){
    for(var i=0;i<gridSize;i++){
        for(var j=0;j<gridSize;j++){
            if(arr[i][j]=="D"){
                // fill every nearby emty place with 1
            if(i-1>=0 && j-1>=0 && arr[i-1][j-1]=="-"){
                arr[i-1][j-1]=1;
                // change the canvas here.
                    queue.enqueue(new Point(i-1,j-1));
                }
            }
        }
    }
}

and so on . My function to fill canvas based on array is as :

function execute(){
    for(var i=0;i<gridSize;i++){
        for(var j=0;j<gridSize;j++){
            drawRect(i,j);
        }
    }
}
function randomFill(){
    ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height)
    execute();
    for(var i=0;i<gridSize;i++){
        for(var j=0;j<gridSize;j++){
            if(arr[i][j]=="W")
                ctx.fillStyle = "red";
            else if(arr[i][j]=="D")
            ctx.fillStyle = "blue";
            else if(arr[i][j] == "-")
            ctx.fillStyle = "green";
            else
            ctx.fillStyle = "purple";
            ctx.font='30px Calibri';
            ctx.fillText(arr[i][j],i*40 + 40,(j+1)*40 + 40);
            }
        }
    }
}

So how can I call the randomFill() function to redraw on the canvas after say a 100 ms. I want to show the change in the array on the canvas but after a little delay , so that people can see.

The new and preferred way of creating animation loops is with window.requestAnimationFrame

It's preferred because it coordinates loop execution with the browser refresh rate to produce efficient redraws. It also suspends the animation loop if the browser switches to a different browser tab (saves battery on mobile devices).

Like setTimeout, requestAnimationFrame (I'll call it RAF for short) is given a callback function to execute. RAF will execute that callback function in a manor synchronized with the browser and hardware.

Here's what a typical RAF animation loop looks like:

function animate(timestamp){

    // execute RAF again to request the next loop
    requestAnimationFrame(animate);

}

RAF sends a timestamp to the function it calls (that's the timestamp in animate). You can use this timestamp to execute your randomFill() after 100ms.

That might look like this: http://jsfiddle.net/m1erickson/2afLc/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" />
<script src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
    $(function(){

        var canvas=document.getElementById("canvas");
        var ctx=canvas.getContext("2d");
        ctx.fillStyle="skyblue";
        ctx.strokeStyle="lightgray";
        ctx.lineWidth=4;

        // testing: rotate a rectangle every 100ms        
        var r=0;
        var interval=100;
        $("#interval").text("Call randomFill to rotate every "+interval+"ms");

        // a variable to hold when the animation loop last fired
        var lastTime;

        // start the animation loop
        requestAnimationFrame(animate);

        function animate(timestamp) {

            // initialize lastTime during the first run of the animate() loop
            if(!lastTime){lastTime=timestamp;}

            // calculate the elapsed time
            var elapsed=timestamp-lastTime;      


            if(elapsed>interval){
                // let randomFill complete before
                // resetting the timer and requesting another loop
                r+=Math.PI/120;
                randomFill(r);
                // reset the timer
                lastTime=performance.now();
            }
            // request another animation loop
            requestAnimationFrame(animate);

        }        

        function randomFill(r){
            ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height)
            ctx.save();
            ctx.translate(100,100);
            ctx.rotate(r);
            ctx.fillRect(-25,-25,50,50);
            ctx.strokeRect(-25,-25,50,50);
            ctx.restore();
        }

    }); // end $(function(){});
</script>
</head>
<body>
    <p id="interval">Call randomFill</p>
    <canvas id="canvas" width=350 height=350></canvas>
</body>
</html>

To delay an iteration you need to use something else than a for-loop as delaying would involve timers which are asynchronous without exception and can't therefor be used to "sleep" inside a for-loop iteration.

The solution is to break up the for-loops into counters which are incremented every 100ms. It becomes a bit different pattern but enables you to introduce a delay for each "iteration" (or increment).

Here is a basic example setup looping using counters and delays for each iteration - the example is only meant as skeleton which you can use to adopt for your scenario:

/* global variables */
var i = 0;                 // outer loop
var j = 0;                 // inner loop

function next() {

    if (j === gridSize) {
        i++;               // increase outer loop counter
        j = 0;             // reset inner loop counter

        if (i === gridSize) {
            /* ...loop has finished, re-initialize etc. here ... */
            return;
        }
    }

    /* ... use i, j here and update canvas - see example link below ... */

    j++;                   // iterate inner counter

    setTimeout(next, 100); // delayed calling of next() again
}
next();                    // start loop

EXAMPLE FIDDLE

The next() method simply increased inner loop ( j ) each time it's called. When first criteria is fulfilled (j = gridSize) then j is reset and outer loop is increased ( i ).

When also the outer loop meets the criteria (i = gridSize) then the loop is entering an exit point which can be used to re-initialize an array, reset i and j , restart loop and so on.

i and j are in the global scope as when you call setTimeout the code will be executed with the global window object as scope, so declaring i and j globally makes them available from inside the function. There are ways around this but I leave this topic out here for the sake of simplicity.

setTimeout

function randomFill(){
  setTimeout(function() {
    ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height)
    execute();
    for(var i=0;i<gridSize;i++){
        for(var j=0;j<gridSize;j++){
            if(arr[i][j]=="W")
                ctx.fillStyle = "red";
            else if(arr[i][j]=="D")
            ctx.fillStyle = "blue";
            else if(arr[i][j] == "-")
            ctx.fillStyle = "green";
            else
            ctx.fillStyle = "purple";
            ctx.font='30px Calibri';
            ctx.fillText(arr[i][j],i*40 + 40,(j+1)*40 + 40);
            }
        }
    }
  }, 100); // or however long the delay should be, in ms
}

Might also be advisable to make sure you don't overlap timeout requests with clearTimeout, eg:

  tid && clearTimeout(tid);
  tid = setTimeout(...)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM