0

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

/*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、有没有其他好的方法可以达到这个“重置”的目的?

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

4

3 回答 3

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>

另外,请参阅JSBin 上的演示

于 2017-06-07T04:13:06.490 回答
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 等控制器。动画控件可以是固定时间的(在固定的时间内只播放一次),它们有一个开始时间和一个结束时间,或者它们可以是具有固定周期的循环。控制器还可以轻松集成关键帧动画。

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

循环三角波

对于此动画,循环三角形控制器最适合模仿您拥有的动画。循环三角波示例 上图来自Desmos 计算器,显示了随时间变化的三角形函数(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 命中(由于画布调整大小),降低了鼠标捕获率(鼠标事件被丢弃,随后的事件提供了丢失的数据)使得窗口调整大小看起来很慢。

去抖事件

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

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 事件设置一个标志以指示窗口已调整大小。然后在动画主循环中监视标志,如果为真则调整大小。这意味着调整大小与显示速率同步,并且忽略额外的调整大小事件。它还为动画设置了更好的开始时间(与帧时间同步)

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 秒。例如(时间 > 1000.010)是自加载超过 1.00001 秒以来的时间。

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

例子

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

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

我也有原始动画,它会显示它是如何慢慢不同步的。动画的所有内容应在每个循环中位于画布的左边缘。动画速度设置为以每秒像素为单位的移动速度,以匹配每帧 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>

于 2017-06-07T11:04:08.747 回答
0

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

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

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

请注意,对画布大小使用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. usingcancelAnimationFrame是干净地停止当前动画的好方法,只要它再次启动。

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

于 2017-06-07T03:45:13.950 回答