canvas动画之绘制思路

前面学习了canvas的基础知识,现在是时候学习如何绘制酷炫的动画了。好吧,这里先整理下绘制动画的基本思路。

<!--more-->

参考

1. 相关概念

先理清动画的基本概念,然后在了解绘制动画的一些思路。

1.1. 动画

下面先来一段动画的概念,来源维基百科

动画是指由许多帧静止的画面,以一定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误以为画面活动的作品。为了得到活动的画面,每个画面之间都会有细微的改变。

为了实现动画,我们只需要准备静止的画面,然后以某种策略,连续播放画面即可。

人的眼睛能感知的最大帧数是每秒60帧,换句话说,每一帧我们可以使用的时间是1000/60=16.66ms。当然对于某些游戏场景而言,二三十帧可能就够了。

1.2. 帧

通过调用canvas的API,我们可以绘制出各种图形和图像

在canvas中,图像一旦绘制,就无法更改了,我们能做的就是对其进行重绘。

可以通过以下的步骤来画出一帧:

  • 清空 canvas,最简单的做法就是用 clearRect 方法。
  • 保存 canvas 状态,可以用save和restore方法
  • 绘制动画图形,调用canvas绘制接口进行绘制,绘制出来的结果即为当前帧能够看见的图像

接着只需要定时重复步骤1-3,然后重绘下一帧,周而复始,就可以实现连续的动画了。之前我们已经学习了如何绘制帧,现在让我们来看看定时器。

1.3. 定时器

window对象提供了两个定时器的接口 setTimeout

通过递归,可以实现setTimeout定时循环调用

let loop = ()=>{
    console.log('loop')
    setTimeout(() => {
        loop()
    }, 1000);
}
loop()

setInterval

setInterval的调用更加简单直观一些

setInterval(()=>{
    console.log('loop in setInterval')
},1000)

requestAnimationFrame 该方法接收一个回调函数,这个回调函数会在浏览器重绘之前被调用,回调的次数通常是每秒60次,但大多数浏览器通常匹配 W3C 所建议的刷新频率,更新频率是浏览器自动控制的,因此不需要我们手动设置时间间隔。

这个接口比传统的定时器效率更高,参考MDN文档

一个简单的兼容形式

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();

当然这里还需要考虑cancelAnimationFrame等形式,这里有一份参考代码

下面使用requestAnimFrame实现一个最简单的方块移动的动画


let width = 50,
    height = 50,
    x = 0,
    y = 0,
    speed = 1;

function update(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    x += speed;
    ctx.fillRect(x, y, width, height);

    requestAnimationFrame(update);
}
requestAnimationFrame(update);

如果我们在moveBall中绘制更精致的帧,就可以看见酷炫的动画啦~

2. 常见动画的实现

上面剖析了动画的概念,即连续播放的帧,而每帧的过渡也大有学问。大部分的动画内容都可以看作是位移、缩放、旋转的运动渐变。而这些概念与CSS动画基本是互通的~

以位移举例,在上面的例子中实际上就是位移动画的展示,位移动画的实现就是在后一帧更新前一帧的元素位置即可

// 水平方向平移,其中vx表示较前一帧水平位置的变化,可以通过控制vx的变化来实现加速度平移
x += vx
// 竖直方向平移,同上
y += vy

实际上关于动画的变化速度,可以利用许多数学函数来实现,最著名的就是tween.js,这个库实现了一个链式调用的缓动动画,如果我们只需要里面的数学计算公式,可以试试张鑫旭提供的[精简版],相关使用可以参考(https://github.com/zhangxinxu/Tween/blob/master/tween.js)

以线性速度为例

/*
 * t: current time(当前时间);
 * b: beginning value(初始值);
 * c: change in value(变化量);
 * d: duration(持续时间)。
*/
Tween.Linear = function (t, b, c, d) {
    return c * t / d + b;
},

如果需要实现一个线性运动的动画,实际上上面函数的b,c,d都是在设定之初就已经可以决定的,只要输入了当前时间,就可以返回对应时间点的位置值,然后,让canvas根据这个位置绘制出对应的图形即可。

那个,如何把定时器内的当前时间和Tween结合起来呢?通过一个计时变量维持当前时间的引用即可。

var time = 0
setInterval(() => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ball.draw();

    time++;
    if (time <= duration) {
        // 继续运动
        ball.y = Tween.Linear(time, begin, end, duration);

        requestAnimationFrame(update);
    } else {
        // 动画结束
    }
}, 1000);

如果在requestAnimationFrame中,计时器并不是以每秒一次的频率触发,因此每帧中time增加的幅度也不一样,这里需要注意下

3. 使用面向对象的思维来绘制图形

在刚开始学习编程的时候,面对C++的面向对象,我一脸懵逼,依稀还记得猫叫狗叫动物叫....

扯远了,从上面可以得知,我们需要在每一帧中完成整个画布的绘制,然后在下一帧中清空画布,重新绘制。如果画面比较复杂,则我们的绘制函数会变得十分庞大,每个图 形都有自己的绘制参数,这样维护起来就十分困难,因此,通过对象去管理每一个需要绘制的图形,是一个不错的选择。

我们来改造一下上面的代码

let sprite = {
    width: 50,
    height: 50,
    x: 0,
    y: 0,
    speed: 1,
    draw(){
        ctx.fillRect(this.x, this.y, this.width, this.height);
    },
    update(){
        this.x += this.speed;
        this.draw()
    }

function update(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    sprite.update()

    requestAnimationFrame(update);
}
requestAnimationFrame(update);

还可以通过sprite这个对象来保存它的其他绘制信息,比如颜色、速度、边界逻辑等,即使我们要再添加一个元素到画布上,也不会干扰之前的元素了。

回到这个对象身上,

  • 如果需要控制移动速度,只需要改变speed值的大小;
  • 如果需要添加加速度,则在每一帧中去改变speed
  • 如果需要添加边界判断,则在绘制时判断x和y的大小
  • 如果需要添加用户交互,只需要监听事件,然后在事件回调中去设置对象对应的属性

通过对象去管理需要绘制的属性,可以更直观更方便,为了练手,我整理了一个canvas练习项目(https://github.com/tangxiangmin/amazing-canvas),目前包括

  • 打砖块,只实现了基本的游戏逻辑,后面有空再继续完善。
  • 下雪花效果

不管怎么样,还是要多练习才行。

4. 性能优化

如果动画比较复杂,我们就不得不考虑性能优化的问题,尤其是在移动端,性能问题表现得更加显著。下面是在整理这篇博客时了解到性能优化方面的一些知识点,暂做记录。参考

4.1. 局部重绘

在上面的代码中,绘制每一帧,我们都清空了整个画布,这意味着在上一帧没有改变的元素,我们在这一帧仍旧需要重新绘制,这会导致频繁调用canvas绘制接口,带来性能的消耗。如果按需进行局部重绘,绘制面积要小多,因此显著提高绘制效率。

关于局部重绘,这里有一篇文章:HTML5 canvas实现脏区重绘,不妨移步阅读。

4.2. 硬件加速

现代浏览器大都可以利用GPU来加速页面渲染,可以通过在Chrome浏览器地址栏输入about:gpu来检测浏览器是否开启了canvas硬件加速。

硬件加速在移动端尤其有用,因为它可以有效的减少资源的利用。使用GPU可能会导致严重的性能问题,因为它增加了内存的使用,而且它会减少移动端设备的电池寿命 GPU是相对于CPU的一个概念,由于在现代的计算机中图形的处理变得越来越重要,需要一个专门的图形的核心处理器。GPU比CPU更擅长处理图形绘制。

关于gpu硬件加速,这里有一篇不错的文章

5. 小结

在研究动画绘制的时候,发现了一个貌似有点酷炫的软件:Animate CC,参考

讲道理,这种可视化的动画编辑对于开发和调试要方便得多,但是对于动画原理、性能优化等,还是需要扎实的底层基础才行。因此,学习canvas动画绘制怎么看都是一件很划算的事情。