动画的基本原理

动画是指由许多帧静止的画面,以一定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误以为画面活动的作品。为了得到活动的画面,每个画面之间都会有细微的改变。而画面的制作方式,最常见的是手绘在纸张或赛璐珞片上。

而基于 HTML5 Canvas 的图形一旦绘制出来,它就是一直静止在那里。如果需要移动它,我们不得不对所有图形(包括之前的)进行重绘。也就是说,实现动画效果就是在画布上绘制图像后,清空画布,然后再绘制图像,再清空...即在动画未完成播放之前不断的重复这样的操作!


动画的基本步骤

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

  1. 清空 canvas
  • 除非接下来要画的内容会完全充满 canvas (例如背景图),否则你需要清空所有。最简单的做法就是用 clearRect 方法。
  1. 保存 canvas 状态
  • 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
  1. 绘制动画图形(animated shapes)
  • 这一步才是重绘动画帧。
  1. 恢复 canvas 状态
  • 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。

操控动画

为来实现动画,我们需要一些可以定时执行重绘的方法。有两种方法可以实现这样的动画操控。首先可以通过 setIntervalsetTimeout 方法来控制在设定的时间点上执行重绘。

可用的定时执行方法:

// 当设定好间隔时间后,function会定期执行。
setInterval(function, delay)

// 在设定好的时间之后执行函数
setTimeout(function, delay)

// 告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。
requestAnimationFrame(callback)

关于 requestAnimationFrame ,这是专门为实现高性能的帧动画而设计的一个 API 。不同的浏览器有不同的实现,我们也可以通过扩展自行统一这个方法。

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

 if (!window.cancelAnimationFrame) {
      window.cancelAnimationFrame = (window.cancelRequestAnimationFrame ||
                                     window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame ||
                                     window.mozCancelAnimationFrame || window.mozCancelRequestAnimationFrame ||
                                     window.msCancelAnimationFrame || window.msCancelRequestAnimationFrame ||
                                     window.oCancelAnimationFrame || window.oCancelRequestAnimationFrame ||
                                     window.clearTimeout);
    }

动画:实现一个小球的移动

通过对上述知识的了解,我们可以尝试实现将一个向下移动的小球。


1. 准备

<html>
<body>
    <canvas id="myCanvas" width="200" height="200" style="border: 1px solid">
        你的浏览器不支持canvas,请升级你的浏览器
    </canvas>
</body>
</html>

<script>
    // 准备画布
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");
    
    // 绘制小球,参数 vy 是指在 y 轴上的偏移量,y 轴的数值越大,小球的位置就越靠下
    function drawBall(vy) {
        ctx.save();
        ctx.beginPath();
        ctx.fillStyle = "red";
        ctx.arc(100, 55 + vy, 15, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
    
    // 初始化,绘制出一个静止的小球
    drawBall(0)
</script>


2.使用定时器重绘小球

    // 定义偏移量
    var i = 0; 
    //定义要使小球向下移动 100 个像素
    var distance = 100; 
    //开启一个定时器
    var timer = setInterval(function(){
        // 每次重绘都要增加偏移量的值
        i++; 
        // 在每个增加量偏移量的位置上绘制小球
        drawBall(i); 
        if(i > distance){
            // 如果小球完成了移动 100 像素的任务,则销毁定时器
            clearInterval(timer);
        }
    })

由上图可见,小球在向下移动轨迹中的每一次偏移都被绘制出来,这样的画面显然不是我们想要的,我们只需要显示一个小球,于是我们只需要在每次绘制小球之前把之前的绘制清空掉。最终代码如下:

<html>
<body>
    <canvas id="myCanvas" width="200" height="200" style="border: 1px solid">
        你的浏览器不支持canvas,请升级你的浏览器
    </canvas>
</body>
</html>
<script>
    // 准备画布
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");
    
    // 绘制小球,参数 vy 是指在 y 轴上的偏移量,y 轴的数值越大,小球的位置就越靠下
    function drawBall(vy) {
        ctx.save();
        ctx.beginPath();
        ctx.fillStyle = "red";
        ctx.arc(100, 55 + vy, 15, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
    
    // 初始化,绘制出一个静止的小球
    drawBall(0)

    // 定义偏移量
    var i = 0; 
    //定义要使小球向下移动 100 个像素
    var distance = 100; 
    //开启一个定时器
    var timer = setInterval(function(){
        // 每次重绘都要增加偏移量的值
        i++; 
        // 清空画布
        ctx.clearRect(0,0,200,200); 
        // 在每个增加量偏移量的位置上绘制小球
        drawBall(i);
        if(i > distance){
            // 如果小球完成了移动 100 像素的任务,则销毁定时器
            clearInterval(timer);
        }
    })
</script>


动画:实现一个弹跳的小球

上面的例子里,我们已经可以将小球移动起来,如果想要实现在下落到地面后回弹的效果,该怎么实现?

我的思路:

  1. 准备初期,我们需要画一条线当作地面,画一个正圆形当作小球,再画一个椭圆形当作小球在接触地面时的挤压效果。
  2. 将小球移动至地面,显示椭圆形小球,完成整个挤压过程后,回弹时显示圆形小球。

尝试代码如下:

<html>

<body>
    <canvas id="myCanvas" width="200" height="200" style="border: 1px solid">
        你的浏览器不支持canvas,请升级你的浏览器
    </canvas>
</body>

</html>
<script>
    // 准备画布
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");
    // 设置默认的填充颜色,就是小球的颜色
    ctx.fillStyle = "red";

    // 正圆形小球移动的距离,地面的 y 值 - 小球起始位置的 y 值 - 小球的半径
    var distance = 160 - 55 - 15
    // 椭圆形小球因挤压而变形的量
    var degree = 5

    // 画出地面
    function drawGround() {
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(0, 160);
        ctx.lineTo(200, 160);
        ctx.stroke();
        ctx.restore();
    }

    // 画出圆形小球
    function drawBall(vy) {
        ctx.save();
        ctx.beginPath();
        // 随着偏移量的增加而向下移动
        ctx.arc(100, 55 + vy, 15, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }

    // 画出椭圆形小球
    function drawBall2(v) {
        ctx.save();
        ctx.beginPath();
        // 随着挤压度的变化进行变形,在变形的过程中,因为椭圆的高度越来越小,所以会增加椭圆 y 轴的坐标值,呈现小球紧挨地面的效果
        ctx.ellipse(100, 145 + v, 15 + v, 15 - v, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }

    // 清空画面并恢复场景
    function clear() {
        ctx.clearRect(0, 0, 200, 200)
        drawGround()
    }

    // 初始化场景
    drawGround();
    drawBall(0);
    // 偏移量控制参数
    var i = 0 
    // 挤压度控制参数
    var i2 = 0 
    //是否是回弹
    var isBack = false

    setInterval(function () {
        if (i > -1 && i <= distance) {
            // 正圆形小球在空中移动的时候
            // 如果当前状态是回弹,则递减 y 轴的值,如果是下降,则递加 y 轴的值
            if (isBack) {
                i--;
            } else {
                i++;
            }
            // 绘制在空中移动的小球
            clear();
            drawBall(i);
        } else if (i == -1 && isBack == true) {
            // 当回弹的小球回到了起始位置,则将状态设置为下降,并重置偏移量控制参数
            // 相当于重新开始动画
            isBack = false
            i = 0
        }
        else {
            // 当小球已完成指定距离的移动,说明已经接触地面,则开始椭圆形小球的操作
            if (i2 > -1 && i2 <= degree) {
                // 小球处于挤压过程中
                // 操作挤压度控制参数,来呈现小球被压扁后又恢复原样的过程
                if (isBack) {
                    i2--;
                } else {
                    i2++;
                }
                // 绘制处于挤压过程中的小球
                clear();
                drawBall2(i2);
            } else if (i2 == -1 && isBack == true) {
                // 当小球在挤压后完成回复原样的任务,则将设置偏移量控制参数,在回弹过程中实现小球向上的效果
                // 因为是回弹,所以 i 一直会递减,小球会上升
                i = distance
            }
            else {
                // 当小球完成了挤压过程,即 i2 的不断增大,大于了最大挤压量 degree,则进入回弹状态
                isBack = true
                // 操作挤压度控制参数,使小球进入回弹计算过程 
                i2 = degree
            }
        }
    }, 20)

</script>
你的浏览器不支持canvas,请升级你的浏览器

当然,这个动画还可以继续升级,通过重力加速度来控制小球的下降速度等等。一些更人性化的效果可以通过各种数学算法一一实现,那就需要在今后的岁月里不断的学习和研究了。