通过添加 eventListener 取消 canvas 动画

cancel canvas animation by adding eventListener

我试图在调整浏览器大小时从起点重置我的矩形动画,代码是这样的:

/*from basic setup*/
canvas = document.getElementById("myCanvas");
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
ctx = canvas.getContext("2d");

/*the "resize" event and the reset function should be triggered */
window.addEventListener("resize",function(){
     canvas.height = window.innerHeight;
     canvas.width = window.innerWidth;
     //cancelAnimationFrame(timeID)
     //theRect();
});

/*drawing the rectangle*/
var x = 0,y = canvas.height - 100,w = 100, h = 100,dx=1;

function theRect(){
 ctx.fillStyle = "lightblue";
 ctx.fillRect(x,y,w,h)
 x+=dx;
 if(x+w>canvas.width/2 || x<0){
    dx=-dx;
 }
}

/*start animation*/

function animate(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    theRect();
    requestAnimationFrame(animate);

}
//var timeID = requestAnimationFrame(animate)

animate();

我在想,如果每次浏览器调整大小时,矩形都必须重新设置为起点ctx.fillRect(0,canvas.height-100,100,100)并继续其动画。

所以我最初解决这个问题的方法是使用cancelAnimationFrame()这个方法先杀死动画然后在我的eventlistener中再次调用这个函数所以每次触发事件时,动画都会停止并再次 运行。

但是 cancellAnimationFrame() 函数需要目标动画时间 ID,并且无法在我的 animate() 函数中跟踪这个时间 ID,因为如果我调用 var timeID = requestionAnimationFrame(animate) 它会变得很大我的浏览器延迟峰值

只是我越想修改越多,我对此感到迷茫,所以我的问题是:

1,在此处使用 cancelAnimationFrame 作为重置动画的第一步是否明智?

2、请问还有什么好的方法可以达到这个"reset"目的吗?

有人可以帮我解决这个问题吗?提前致谢。

所发布的代码似乎确实令人困惑,并导致矩形悬挂并颤抖,原因我从未完全理解。解决两个问题在很大程度上解决了问题。

  1. 调整大小处理程序对冗长的计算进行排队可以减少系统开销。可以使用一个简单的计时器(例如 100 毫秒)来延迟开始所需的反应。

  2. 在 window 调整大小后需要重新计算动画的所有参数。

请注意,使用 window.innerHeightwindow.innerWidth 作为 canvas 大小会导致显示滚动条。一个原因是主体元素填充和边距的效果将动画置于视口之外。我没有更进一步,只是为了演示目的从值中减去 20px。

此工作示例显示为 HTML 来源,因为它使用了整个视口:

<!DOCTYPE html><html><head><meta charset="utf-8">
    <title>animation question</title>
</head>
<body>
    <canvas id="myCanvas"></canvas>
<script>

/*from basic setup*/
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var delayTimer = 0;
var animateTimer = 0;

function sizeCanvas() { // demonstration
    canvas.height = window.innerHeight -20;
    canvas.width = window.innerWidth -20;
}
sizeCanvas();  // initialize


/* monitor "resize" event */

window.addEventListener("resize",function(){
     clearTimeout( delayTimer);
     if( animateTimer) {
         cancelAnimationFrame( animateTimer);
         animateTimer = 0;
     }
     delayTimer = setTimeout( resizeCanvas, 100);
});

function resizeCanvas() {
    delayTimer = 0;
    sizeCanvas();
    x = 0; y = canvas.height - 100;
    animate(); // restart
}

/*drawing the rectangle*/
var x = 0,y = canvas.height - 100,w = 100, h = 100,dx=2;

function theRect(){
 ctx.fillStyle = "lightblue";
 ctx.fillRect(x,y,w,h)
 x+=dx;
 if(x+w>canvas.width/2 || x<0){
    dx=-dx;
 }
}

/*start animation*/

function animate(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    theRect();
    animateTimer = requestAnimationFrame(animate);
}
animate();

</script>
</body>
</html>

所以回答你的问题:

  1. 使用 cancelAnimationFrame 是干净地停止当前动画的好方法,只要它再次启动即可。

  2. 如果您愿意离开动画 运行,这并不是解决方案的一部分。采用这种方法的解决方案可能仍然需要通过测试调整大小计时器是否在 animate 内 运行 来停止动画,并且似乎没有任何清晰度,所以我没有发布代码。 (我没有遇到记录从 requestAnimationFrame 返回的 requestId 的明显开销。)

在这种情况下没有需要使用cancelAnimationFrame()功能。

您可以根据 window 调整大小,相应地更改/重置 xy 变量的值 (position)将重置矩形起始位置/点。

因为 animate 函数是以递归方式 运行 因此,更改任何变量 (在内部使用动画功能) 值将立即生效。

/*from basic setup*/
canvas = document.getElementById("myCanvas");
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
ctx = canvas.getContext("2d");

/*drawing the rectangle*/
var x = 0,
   y = canvas.height - 100,
   w = 100,
   h = 100,
   dx = 1;

/*the "resize" event and the reset function should be triggered */
window.addEventListener("resize", function() {
   canvas.height = window.innerHeight;
   canvas.width = window.innerWidth;
   x = 0; //reset starting point (x-axis)
   y = canvas.height - 100; //reset starting point (y-axis)
});

function theRect() {
   ctx.fillStyle = "lightblue";
   ctx.fillRect(x, y, w, h);
   x += dx;
   if (x + w > canvas.width / 2 || x < 0) {
      dx = -dx;
   }
}

/*start animation*/
function animate() {
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   theRect();
   requestAnimationFrame(animate);
}
animate();
body{margin:0;overflow:hidden}
<canvas id="myCanvas"></canvas>

另外,看到一个demo on JSBin

动画

动画最好用系统时间或页面时间处理,单个时间变量保存动画的开始时间,当前动画位置是通过从当前时间减去开始时间找到的。

迭代动画

这意味着您制作动画的方式必须改变,因为迭代动画不适合条件分支,就像您对...

// Code from OP's question
function theRect() {
   ctx.fillStyle = "lightblue";
   ctx.fillRect(x, y, w, h);
   x += dx;
   if (x + w > canvas.width / 2 || x < 0) {
      dx = -dx;
   }
}

如果时间不重要,上面的方法很有效,但是你无法找到任何特定时间点的位置,因为你需要迭代两者之间的位置。此外,任何丢帧都会改变动画对象的速度,并且随着时间的推移,位置会随着时间的推移变得不确定。

时间和控制器。

另一种方法是使用控制器来制作动画。控制器只是根据输入产生形状输出的功能。这就是大多数 CSS 动画的完成方式,使用诸如 easeIn、easeOut 等控制器。动画控件可以是固定时间的(在固定的时间内只播放一次),它们具有开始时间和结束时间,或者它们可以是具有固定周期的循环。控制器也可以轻松集成关键帧动画。

控制器非常强大,应该在每个 game/animation 编码员的知识集中。

循环三角波

对于此动画,循环三角形控制器最适合模仿您的动画。 上图来自 Desmos calculator,显示了随时间变化的三角函数(x 轴是时间)。所有控制器的频率为 1,振幅为 1。

控制器作为 Javascript 函数

// waveforms are cyclic to the second and return a normalised range 0-1
const waveforms = {
    triangle (time) { return Math.abs(2 * (time - Math.floor(time + 0.5))) },
}

现在您可以获得矩形的位置作为时间的函数。您只需要开始时间、当前时间、对象的速度(以像素为单位)以及行进的距离。

我创建了一个对象来定义矩形和动画参数

const rect = {
    startTime : -1, // start time if -1 means start time is unknown
    speed : 60, // in pixels per second
    travelDist : canvas.width / 2 - 20,
    colour : "#8F8",
    w : 20,
    h : 20,
    y : 100,
    setStart(time){
        this.startTime = time / 1000;  // convert to seconds
        this.travelDist = (canvas.width / 2) - this.w;
    },
    draw(time){
        var pos = ((time / 1000) - this.startTime); // get animation position in seconds
        pos = (pos * this.speed) / (this.travelDist * 2)
        var x = waveforms.triangle(pos) * this.travelDist;
        ctx.fillStyle = this.colour;
        ctx.fillRect(x,this.y,this.w,this.h);
    }
}   

因此,一旦设置了开始时间,您只需提供当前时间就可以随时找到矩形的位置

这是一个简单的动画,矩形的位置也可以作为时间的函数来计算。

 rect.draw(performance.now());  // assumes start time has been set

调整大小

调整大小事件可能会产生问题,因为它们会像鼠标采样率一样频繁触发,而鼠标采样率可能是帧率的许多倍。结果是代码 运行 比必要次数多了很多次,增加了 GC 命中率(由于 canvas 调整大小),降低了鼠标捕获率(鼠标事件被丢弃,后续事件提供了丢失的数据)使得window 调整大小似乎很慢。

去抖事件

解决此问题的常用方法是使用去抖动的调整大小事件处理程序。这只是意味着实际的调整大小事件会延迟一小段时间。如果在去抖动期间触发了另一个调整大小事件,则取消当前计划的调整大小并安排另一个调整大小。

var debounceTimer;  // handle to timeout
const debounceTime = 50; // in ms set to a few frames
addEventListener("resize",() => {
    clearTimeout(debounceTimer); 
    debounceTimer = setTimeout(resizeCanvas,debounceTime);
}
function resizeCanvas(){
    canvas.width = innerWidth;
    canvas.height = innerHeight;
}

这可确保 canvas 调整大小在调整大小之前等待,从而使后续调整大小事件有机会在调整大小之前发生。

尽管这比直接处理调整大小事件要好,但仍然存在一些问题,因为延迟很明显 canvas 直到下一帧或两帧之后才调整大小。

帧同步调整大小

我找到的最佳解决方案是将调整大小同步到动画帧。您可以只让调整大小事件设置一个标志来指示 window 已调整大小。然后在动画主循环中监视标志,如果为真则调整大小。这意味着调整大小与显示速率同步,并且忽略额外的调整大小事件。它还为动画设置了更好的开始时间(与帧时间同步)

var windowResized = true; // flag a resize at start so that its starts
                          // all the stuff needed at startup
addEventListener("resize",() => {windowResized = true});  // needs the {}
function resizeWindow(time){
    windowResized = false; // clear the flag
    canvas.width = innerWidth;
    canvas.height = innerHeight;
    rect.setStart(time); // reset the animation to start at current time;
}

然后在主循环中,您只需检查 windowResized 标志,如果为真,则调用 resizeWindow

动画时间

当您使用 requestAnimationFrame(callback) 函数时,将使用第一个参数作为时间调用回调。时间以毫秒 1/1000 秒为单位,精确到微秒 1/1,000,000 秒。例如 (time > 1000.010) 是加载超过 1.00001 秒后的时间。

function mainLoop(time){ // time in ms since page load.
    requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);

例子

综上所述,让我们使用控制器和同步调整大小事件重写动画。

为了强调定​​时动画和控制器的实用性,我添加了两个额外的波形 sinsinTri

我还有原始动画,可以显示它是如何慢慢失去同步的。所有的动画都应该在canvas的左边缘每个循环一次。动画速度设置为每秒像素的移动速度,以匹配每帧 1 像素的原始近似值,速度设置为每秒 60 像素。虽然对于sin和sinTri动画来说速度是平均速度,不是瞬时速度。

requestAnimationFrame(animate); //Starts when ready
var ctx = canvas.getContext("2d"); // get canvas 2D API

// original animation by OP
var x = 0;
var y = 20;
var h = 20;
var w = 20;
var dx = 1;
// Code from OP's question
function theRect() {
   ctx.fillStyle = "lightblue";
   ctx.fillRect(x, y, w, h);
   x += dx;
   if (x + w > canvas.width / 2 || x < 0) {
      dx = -dx;
   }
}

// Alternative solution

// wave forms are cyclic to the second and return a normalised range 0-1
const waveforms = {
    triangle (time) { return Math.abs(2 * (time - Math.floor(time + 0.5))) },
    sin(time) { return Math.cos((time + 0.5) * Math.PI * 2)*0.5 + 0.5 },
    // just for fun the following is a composite of triangle and sin
    // Keeping the output range to 0-1 means that multiplying any controller
    // with another will always keep the value in the range 0-1
    triSin(time) { return waveforms.triangle(time) * waveforms.sin(time) } 
    
}

// object to animate is easier to manage and replicate
const rect1 = {
    controller : waveforms.triangle,
    startTime : -1, // start time if -1 means start time is unknown
    speed : 60, // in pixels per second
    travelDist : canvas.width / 2 - 20,
    colour : "#8F8",
    w : 20,
    h : 20,
    y : 60,
    setStart(time){
        this.startTime = time / 1000;  // convert to seconds
        this.travelDist = (canvas.width / 2) - this.w;
    },
    draw(time){
        var pos = ((time / 1000) - this.startTime); // get animation position in seconds
        pos = (pos * this.speed) / (this.travelDist * 2)
        var x = this.controller(pos) * this.travelDist;
        ctx.fillStyle = this.colour;
        ctx.fillRect(x,this.y,this.w,this.h);
    }
}    
// create a second object that uses a sin wave controller

const rect2 = Object.assign({},rect1,{
    colour : "#0C0",
    controller : waveforms.sin,
    y : 100,
})
const rect3 = Object.assign({},rect1,{
    colour : "#F80",
    controller : waveforms.triSin,
    y : 140,
})

// very simple window resize event, just sets flag.
addEventListener("resize",() => { windowResized = true });

var windowResized = true; // will resize canvas on first frame
function resizeCanvas(time){
    canvas.width = innerWidth;  // set canvas size to window inner size
    canvas.height = innerHeight;
    rect1.setStart(time); // restart animation
    rect2.setStart(time); // restart animation
    rect3.setStart(time); // restart animation
    x = 0; // restart the stepped animation

    // fix for stack overflow title bar getting in the way
    y = canvas.height - 4 * 40;
    rect1.y = canvas.height - 3 * 40;
    rect2.y = canvas.height - 2 * 40;
    rect3.y = canvas.height - 1 * 40;

    windowResized = false; // clear the flag
    // resizing the canvas will reset the 2d context so
    // need to setup state
    ctx.font = "16px arial";    
}
function drawText(text,y){
    ctx.fillStyle = "black";
    ctx.fillText(text,10,y);
}
function animate(time){ // high resolution time since  page load in millisecond with presision to microseconds
    if(windowResized){  // has the window been resize
        resizeCanvas(time);  // yes than resize for next frame
    }
    ctx.clearRect(0,0,canvas.width,canvas.height);
    drawText("Original animation.",y-5);
    theRect(); // render original animation
    // three example animation using different waveforms
    drawText("Timed triangle wave.",rect1.y-5);
    rect1.draw(time); // render using timed animation
    drawText("Timed sin wave.",rect2.y-5);
    rect2.draw(time); // render using timed animation
    drawText("Timed sin*triangle wave.",rect3.y-5);
    rect3.draw(time); // render using timed animation
    ctx.lineWidth = 2;
    ctx.strokeRect(1,1,canvas.width - 2,canvas.height - 2);
    requestAnimationFrame(animate);
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;
}
<canvas id="canvas"></canvas>