通过添加eventListener来取消画布动画

时间:2017-06-07 01:19:58

标签: javascript animation canvas cancelanimationframe

我正在尝试从调整浏览器大小时的起点重置矩形动画,代码如下:

  Company Load (161.2ms)  SELECT `companies`.* FROM `companies` WHERE `companies`.`slug` = '1403045688' LIMIT 1
  EXPLAIN (162.2ms)  EXPLAIN SELECT `companies`.* FROM `companies` WHERE `companies`.`slug` = '1403045688' LIMIT 1
EXPLAIN for: SELECT  `companies`.* FROM `companies`  WHERE `companies`.`slug` = '1403045688' LIMIT 1
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                                               |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | Impossible WHERE noticed after reading const tables |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
1 row in set (0.20 sec)

  Company Load (1.6ms)  SELECT `companies`.* FROM `companies` WHERE `companies`.`id` = '1403045688' LIMIT 1
  EXPLAIN (80.7ms)  EXPLAIN SELECT `companies`.* FROM `companies` WHERE `companies`.`id` = '1403045688' LIMIT 1
EXPLAIN for: SELECT  `companies`.* FROM `companies`  WHERE `companies`.`id` = '1403045688' LIMIT 1
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table     | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | companies | const | PRIMARY       | PRIMARY | 182     | const |    1 |       |
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.08 sec)

=> #<Company id: "1403045688", name: "AAA Electric", slug: "aaa-electric-inc", created_at: "2014-06-17 22:54:48", updated_at: "2016-03-12 05:13:26">

我在想每次浏览器调整大小时,必须将矩形重置为起始点/*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函数需要目标动画timeID,并且无法在cancellAnimationFrame()函数中跟踪此timeID,因为如果我调用animate()它将会出现巨大的滞后峰值在我的浏览器上

只是我想要修改的越多,我就会失去这个,所以我的问题是:

1,在这里使用var timeID = requestionAnimationFrame(animate)作为重置动画的第一步是明智的吗?

2,有没有其他好办法实现这种“重置”的目的?

有人可以帮帮我吗?提前谢谢。

3 个答案:

答案 0 :(得分:2)

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

您只需在窗口大小调整时相应地更改/重置xy变量的值(位置),这将重置矩形起始位置/点。

因为animate函数以递归方式运行,因此对任何变量 (在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

答案 1 :(得分: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;
   }
}

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

时间和控制器。

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

控制器非常强大,应该在每个游戏/动画编码器知识集中。

循环三角波形

对于此动画,循环三角形控制器最适合模仿您拥有的动画。Example of cyclic triangle wave 上面的图像来自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命中率(由于画布调整大小),降低了鼠标捕获率(鼠标事件被丢弃,后续事件提供了丢失的数据)使窗口调整大小看起来很迟钝。

去抖事件

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

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;
}

这可确保画布在调整大小之前调整等待时间,以便在调整大小之前为后续调整大小事件提供机会。

虽然这比直接处理resize事件更好,但它仍然有点问题,因为在下一帧或两帧之后画布没有调整大小时延迟是显而易见的。

帧同步调整大小

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

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(回调)函数时,将使用第一个参数作为时间调用回调。时间以毫秒1/1000秒为单位,精确到微秒1 / 1,000,000秒。例如(时间> 1000.010)是加载超过1.00001秒的时间。

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

实施例

因此,尽管如此,让我们使用控制器和同步调整大小事件来重写动画。

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

此外,我还有原始动画,可以显示它是如何慢慢失去同步的。动画的全部应该是每个周期都在画布的左边缘。动画速度设置为以每秒像素为单位的移动速度,以匹配每帧1像素的原始近似值,速度设置为每秒60像素。虽然对于sin和sinTri动画来说速度是平均速度,而不是瞬时速度。

&#13;
&#13;
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);
}
&#13;
canvas {
  position : absolute;
  top : 0px;
  left : 0px;
}
&#13;
<canvas id="canvas"></canvas>
&#13;
&#13;
&#13;

答案 2 :(得分:0)

发布的代码看起来确实很混乱,导致矩形悬挂并因为我从未完全理解的原因而颤动。解决两个问题很大程度上解决了这个问题。

  1. 从resize处理程序排队冗长的计算可以减少系统开销。可以使用简单的计时器(比如说100毫秒)来延迟启动所需的反应。

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

  3. 请注意,使用window.innerHeightwindow.innerWidth作为画布大小会导致显示滚动条。一个原因是body元素填充和边距的效果将动画放在视口外。我没有采取进一步措施,仅从演示目的中减去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没有遇到明显的开销。)