pomelovico / keep

A learning notebook
7 stars 0 forks source link

canvas 学习笔记 (三) - 动画 #20

Open pomelovico opened 6 years ago

pomelovico commented 6 years ago

动画循环

动画是一种持续的循环,由于浏览器是单线程的,所以不能通过一个while(true){}的形式来达到持续执行动画代码的目的,否则将会使页面失去响应,取而代之的是让浏览器有一个休息的时间,每个一小段时间来执行一次动画

实现循环的方式大致有两类:

  1. setTimeout && setInterval: 这两种浏览器提供的定时器函数都不是特别精准,这依赖于当前的事件队列,此外,重要的是,使用定时器函数来让浏览器定时执行动画,是一种命令式的操作,它要求浏览器这么做,所以绘制动画的开发人员必须知道绘制下一帧动画精准时机,也就是间隔时间,而这个间隔时间往往会因浏览器的不同而略有差别。

  2. window.requesAnimationFrame() 方法是W3C标准中的全局方法,通过该方法可以向浏览器注册动画绘制函数,浏览器在绘制下一帧(浏览器在实时的绘制页面,就算你对页面没有任何操作,他也在不停的刷新和绘制当前页面)的时候会自动调用注册的函数,从而达到绘制动画的效果,该方法的优点在于,是浏览器主动通知用户绘制,绘制的时机交给了更有经验的浏览器,用户更加专注于绘制函数本身

requestAnimationFrame()

所以在现代浏览器的动画绘制中,都应当使用requestAnimationFrame()这个API,与之对应的是cancleRequestAnimationFrame(handle)方法,其中handle参数是一个句柄,由requestAnimationFrame返回,可用于取消回调函数的执行.

动画函数调用规则:

参数

requestAnimationFrame(callback,optional Element)中,callback回调函数的参数是一个time,表示此次动画执行的时间,由于有些浏览器的bug,time可能为undefined,所以需要手动更正。第二个参数是一个可选的HTML元素引用,表示,当该HTML元素不可见时,该方法也就不会再调用传入的动画回调函数了。

pomelovico commented 6 years ago

帧速率(fps)计算

动画的帧速率就是动画每秒钟播放的帧的次数,在window.requestAnimationFrame()注册的回调函数里,将上次绘制动画的时间从当前时间中减去得到时间差,就是两次帧之间的时间差,取倒数就是帧速率了。

let lastTime = 0;
function fps(){
    var now = (+new Date()),
          f = 1000 / (now - lastTime);
    lastTime = now;
    return f;
}

之所以会有帧速率这个概念,是因为在一个复杂的动画中,可以能有些动画不需要太高频次的速度运行,所以在复杂动画 中,可以把不懂的任务安排在不同的帧速率上执行。大致过程就是用不同的变量记录上次动画执行的时刻,然后与当前时刻做差值,把不同的任务放到不同的差值范围内进行绘制,这样可以避免一次动画中绘制太多内容影响性能。

在这里个人有个问题,如果把不同的绘制任务放到不同的帧速率中,按上诉方式来看,那么针对慢速率的动画内容,就不会在每次的requestAnimationFrame()注册函数中执行,然而由于canvas的特点以及动画要求,每次绘制都会清除原来画板中的内容,那么在这次绘制中,没有绘制出来的内容岂不就是被擦除掉变成空白。所以我理解的慢速率绘制应该是要保证在人眼能够接受动画的正常帧速率以上,大概就是24帧以上,才能给人一种连续性的感觉(视觉暂留效应)

pomelovico commented 6 years ago

恢复动画背景

在动画绘制过程中,在下一帧绘制动画时,由于canvas中并没有层级概念,在canvas绘制都是后者覆盖前者,所以需要考虑如何处理背景(不变的内容),有以下几种方式:

在复杂的背景情况下,每次都重绘背景,那会导致绘制耗时太长。

利用剪辑区域处理背景(context.clip)

利用剪辑区域,可以将需要绘制的背景的区域缩减到上一帧动画元素的内容区域,也就是将上一帧动画物体所在的位置恢复为背景图。此种方式其实也是调用绘制整个背景的方法,但是在context.clip()方法的约束下,绘制背景只会影响到clip所剪辑到的路径区域里,具体做法如下:

  1. 调用context.save()保存绘图环境(必须要调用保存状态,clip方法不可逆,只会越来越小)
  2. context.beginPath()开始一条新路径
  3. 根据上一帧动画物体所在的路径绘制新路径
  4. context.clip()剪辑区域,将绘制区域缩减到上一帧动画物体所在的路径里(当然也不一定非要是上一帧动画物体路径,可以是某个圆形,矩形区域,重点是此区域要包含上一帧动画物体的区域)
  5. 擦除剪辑区域内的内容
  6. 调用绘制背景的方法(此处背景的绘制只会影响到剪辑区域),相当于使用背景覆盖掉了上一帧动画物体的内容
  7. context.resotre()恢复绘图环境

此种方式给我的感觉是有点牵强,虽然将背景绘制区域限制在了剪辑区域里,看似绘制的区域变小了,但是绘制背景的方法仍然是绘制全局背景的方法,这跟擦除重绘没有什么区别吧。除非我们要针对背景绘制函数做优化,做到可以实时根据路径来绘制背景。

离屏内容复制处理动画背景(drawImage)

我们设置一个离屏canvas,将整个背景绘制到该canvas中,同理,使用上述的剪辑方法,但是在第6步中,不再绘制,而是从离屏canvas中取出该区域的背景图,直接“修复”到当前的canvas中,也就是复制图像块,使用drawImage()等方法绘制。

一般来说,图像块复制的方式比剪辑区域的速度快,但是多了一个离屏canvas,占据更多内存,但是个人觉得,一个离屏canvas会让整个绘制过程简单很多。

pomelovico commented 6 years ago

双缓冲技术绘制canvas

在动画的绘制过程中,由于擦除可能会带来瞬间的空白从而导致动画看起来有点闪烁,而利用双缓冲可以避免此问题,双缓冲就是利用一个离屏canvas来绘制所有内容,然后将整个离屏canvas的内容一次性的复制到可见的canvas中。

但是现代浏览器中默认使用了双缓冲技术,所以不需要人手动的实现

pomelovico commented 6 years ago

基于时间的运动

由于各个设备的性能以及刷新频率的不同,要保证动画在每中设备下都有流畅的动画体验,需要使用统一的速率计算方式,要让动画以稳定的速度运行而不受帧率的影响,就要根据物体的速度计算出它在两帧之间所移动的像素数,即:

移动像素数/帧 = (像素/秒) (帧/秒) = 物体运动速度 fps

而fps(每秒播放的帧数,计算方式前面有提到过)就是两帧之间的时间差的倒数

知道了物理每帧之间所移动的像素数,那自然就可以计算到下一帧物体的具体坐标位置了。

定时动画

背景滚动

视差动画

为了实现三维精神效果,不同的物体由于远近的不同,应该有不同的移动速度,以此来形成视差效果