HTML5 画布中最简单的幻灯片,canvas.context.clearRect 不适用于 setTimeout

2022-01-17 00:00:00 canvas javascript html html5-canvas

这是一个非常简单的幻灯片的代码,它应该在 4 秒内显示 4 张图像,每秒一张图像.相反,我得到了 4 秒的延迟,然后所有图像都被绘制在彼此之上.我做错了什么?

Here is a code of a very simple slideshow, that should show 4 images in 4 seconds, one image per second. Instead, I get a 4-second delay and then all the images get drawn on top of each other. What am I doing wrong?

<html>
<head>
<script langugage="javascript">
// 4 images
var image0 = new Image();
image0.src = "img/image0.png";
var image1 = new Image();
image1.src = "img/image1.png";
var image0 = new Image();
image2.src = "img/image2.png";
var image3 = new Image();
image3.src = "img/image3.png";
// array of 4 images
images = new Array(image0, image1, image2, image3);

// this is the main function
function draw(){
    myCanvas = document.getElementById('myCanvas');
    ctx = myCanvas.getContext('2d');
    counter=0; // this is the index of the next image to be shown
    for (var i=0;i<images.length;i++){
        setTimeout(draw_next_image, 1000);
        ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
    }
}
// this is the function called after each timeout to draw next image
function draw_next_image(){
    ctx.drawImage(images[counter], 0, 0);
    counter++;
    if (counter>images.length) {counter=0;}
}

window.onload = draw;
</script>
</head>
<body>
    <canvas id="myCanvas" width="800" height="600"></canvas>
</body>
</html>

更新:答案是:

在上面的代码中,我错误地假设 getTimeout 函数是同步的,即我预期,在调用它时程序执行将停止,等待 1000 毫秒,然后调用 draw_next_image 然后才执行 ctx.clearRect.

In the code above I mistakingly assumed that getTimeout function is synchronous, i.e. I expected, that upon its call the program execution is going to stop, wait 1000 milliseconds, then call the draw_next_image and only then execute ctx.clearRect.

实际上 Javascript 并不是这样工作的.实际上 getTimeout 是异步的,即 getTimeout 设置一个 Timeout 并几乎立即返回并且代码继续执行,因此 ctx.clearRect 被正确调用离开,实际上是 在 draw_next_image 之前.因此,当 Timeout 到期并调用 draw_next_image 时,代码的执行可能会到达任意行代码.在我的情况下,所有 4 个 clearRect 将几乎同时被调用,远在超时到期之前.然后 1000 毫秒后,所有 4 个超时将几乎一个接一个地立即到期,并且所有 4 个图像也将几乎同时绘制,没有 clearRects,它执行了很长时间之前.

In reality Javascript doesn't work like that. In fact getTimeout is asynchronous, i.e. getTimeout sets a Timeout and returns almost instantly and the code execution continues, so that ctx.clearRect gets called right away and actually prior to draw_next_image. So, by the time Timeout expires and calls draw_next_image, execution of code may reach and arbitrary line of code. In my case all the 4 clearRect are going to be called almost at the same time, long before expiration of Timeouts. Then 1000 milliseconds later, all the 4 timeouts are going to expire almost immediately one after another, and all the 4 images going to be drawn almost at the same time, too, without clearRects, which got executed long before.

推荐答案

问题在于,在您的代码中,您将异步函数视为同步函数.

The problem is that in your code you are treating asynchronous functions as if they where synchronous.

要点在这里:

image0.src = "img/image0.png";
image1.src = "img/image1.png";
image2.src = "img/image2.png";
image3.src = "img/image3.png";
...
setTimeout(draw_next, delayInMilliseconds);

由于这些调用一旦被调用就会失败,并且您的代码在这些结果(可能)准备好之前开始执行下一步.

As these calls just falls through once they are invoked, and your code starts to execute the next step before the result from these are (possibly) ready.

因此,您需要根据事件链接您的调用,例如:

You therefor need to chain your calls based on events, for example:

//image counter as there is no guarantee that the last images loaded
//is the last one to finish
var loaded = 0, numOfImages = 4;

//first part of chain, invoke async load
var image0 = document.createElement('img'); //this will work in new Chrome
var image1 = document.createElement('img'); //instead of new Image
var image2 = document.createElement('img');
var image3 = document.createElement('img');

//common event handler when images has loaded with counter
//to know that all images has loaded
image0.onload = image1.onload = 
image2.onload = image3.onload = function(e) {
    loaded++;
    if (loaded === numOfImages)
        draw();   // <-- second part of chain, invoke loop
}

//show if any error occurs
image0.onerror = image1.onerror = 
image2.onerror = image3.onerror = function(e) {
    console.log(e);
}

//invoke async loading... you can put these four into your
//window.onload if you want to
image0.src = "img/image0.png";
image1.src = "img/image1.png";
image2.src = "img/image2.png";
image3.src = "img/image3.png";

// this is the main function
function draw() {

    var images = new Array(image0, image1, image2, image3),
        counter = 0,
        delayInMilliseconds = 4000,
        maxNum = images.length - 1,

        myCanvas = document.getElementById('myCanvas'),
        ctx = myCanvas.getContext('2d'),

        me = this; //this we need for setTimeout()

    //third part of chain, have a function to invoke by setTimeout
    this._draw = function() {

        //if the next image will cover the canvas
        //there is no real need to clear the canvas first.
        //I'll leave it here as you ask for this specifically
        ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
        ctx.drawImage(images[counter++], 0, 0);
        if (counter > maxNum) counter = 0;

        setTimeout(me._draw, delayInMilliseconds); //use me instead of this
    }
    this._draw(); //START the loop
}

这里的工作演示:
http://jsfiddle.net/AbdiasSoftware/dhxNz/

_draw() 被包裹在 draw() 中以本地化变量并确保 _draw() 不会结束在 window 对象上.出于同样的原因,我们存储对 this 的引用,因为当调用其代码时,this 将更改为 window 对象.me(或你想调用的)确保我们调用的是正确的对象,以便我们可以访问局部变量(canvas、ctx、counter 等).

The _draw() is wrapped in draw() to localize the variables and also to make sure that _draw() doesn't end up on the window object. For the same reason we store a reference to this as this is changed to window object when its code is invoked. me (or what you want to call it) makes sure that we are calling on the right object so that we have access to the local variables (canvas, ctx, counter etc.).

相关文章