requestAnimationFrame 动画

现在我们知道如果绘制炫酷的东西,并且让它们动起来。 首先我们知道动画其实就是重复绘制相同的东西。 当调用绘制函数时,图像马上显示在画布上。 制作动画,等待某时刻又重新绘制。 当然不想等待忙碌的循环中阻塞浏览器。相反,你希望浏览器间隔某些事件通知重新你绘制图形。 最简单的方法就是使用计时器 setInterval() 函数。它会每间隔N毫秒调用一次。

然而,最好不要使用 setInterval。setInterval 会以相同的时间间隔执行绘制函数无论用户使用的是什么电脑,用户使用电脑期间做什么, 也不管页面是否正在后台运行。简单来说,能工作但效率不高。所以我们推荐使用一个新的接口 requestAnimationFrame。

requestAnimationFrame 是为了解决动画的流畅性及运行效率而提供的。 将绘制函数作为参数调用。以后,当浏览器就绪时,浏览器会调用绘制函数。这让浏览器完全控制图像的绘制,能减少能耗。 做出来的动画更加平滑,能以60帧/每秒左右的频率刷新。递归调用requestAnimationFrame。

requestAnimationFrame 正在成为一个标准,但大多浏览器现在只支持前缀版本(译者注:现在都已支持了,可以直接使用)。 例如,Chrome 浏览器使用 webkitRequestAnimationFrame 以及 Mozilla 支持 mozRequestAnimationFrame。 使用 shim 脚本兼容Paul Irish's shim script. shim 函数大致如下: requestAnimFrame

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
            window.setTimeout(callback, 1000 / 60);
            };
})();

先来一个矩形运动的例子。

var x = 0;
function drawIt() {
    window.requestAnimFrame(drawIt);
    var canvas = document.getElementById('canvas');
    var c = canvas.getContext('2d');
    c.fillStyle = "red";
    c.fillRect(x,100,200,100);
    x+=5;
}
window.requestAnimFrame(drawIt);

交互 requestAnimFrame() 实例

使用 requestAnimFrame 的矩形运动(点击运行)

清除背景

你会注意到一个问题。矩形跑出去屏幕了,每100毫秒(10FPS)更新5像素,原来的矩形还在。 看上去就是矩形变长了而已。需要注意的是 Canvas 只是一块像素的缓冲区。除非你作过改变否则设置过的像素一直保留。 所以在每一帧绘制矩形之前需要清除 canvas 画布。

var x = 0;
function drawIt() {
    window.requestAnimFrame(drawIt);
    var canvas = document.getElementById('canvas');
    var c = canvas.getContext('2d');
    c.clearRect(0,0,canvas.width,canvas.height);
    c.fillStyle = "red";
    c.fillRect(x,100,200,100);
    x+=5;
}

window.requestAnimFrame(drawIt);

交互 requestAnimFrame 实例

清除背景的绘制矩形 (点击运行)

粒子模拟器

那就是制作动画的全部。重复一次又一次绘制图形。 我们尝试复杂一点的东西:一个粒子模拟器。 我们让一些粒子从画布上项雪花一样飘下。 为了实现粒子模拟器,我们会实现经典的粒子模拟器算法:

粒子模拟器包括一组需要循环的粒子。每一帧都会通过一些等式来更新每个粒子的位置,接着满足条件之后清除/创建粒子, 最后绘制粒子。 下面是一个简单的下雪粒子。

var canvas = document.getElementById('canvas');
var particles = [];
var tick = 0;
function loop() {
    window.requestAnimFrame(loop);
    createParticles();
    updateParticles();
    killParticles();
    drawParticles();
}
window.requestAnimFrame(loop);

首先,我们先创建了一个粒子模拟器。 这个就是每隔 N 毫秒调用的 loop 函数。 我们需要的数据结构仅有一个空的粒子数组particles以及控制时间的tick计数器。 每次调用 loop 函数都会调用粒子模拟器的四部分功能。

function createParticles() {
    // 检查是否为10的倍数
    if(tick % 10 == 0) {
        // 少于100则新增粒子
        if(particles.length < 100) {
            particles.push({
                    x: Math.random()*canvas.width, // 0 ~ 画布宽度小数
                    y: 0,
                    speed: 2+Math.random()*3, // 2 ~ 5之间小数
                    radius: 5+Math.random()*5, // 5 ~ 10 之间小数
                    color: "white",
            });
        }
    }
}

createParticles 创建粒子函数检查是否小于100个粒子,如果是则新创建一个粒子。 注意只有在 tick 计数器为 10 的倍数是才被创建。这样画布就减慢生成粒子,而不是开始的时候立刻就生成所有的100个粒子。 你可以根据效果需要调整该数值。 我用了Math.random() 随机数及一些计算让雪花出现在不同的位置,让它们看起来不全是一样的,让雪花看上去更自然。

function updateParticles() {
    for(var i in particles) {
        var part = particles[i];
        part.y += part.speed;
    }
}

updateParticles 更新粒子函数很简单。 它只更新每个粒子的y坐标,y坐标加上粒子本身的速度。这让雪花从画布上落下。

function killParticles() {
    for(var i in particles) {
        var part = particles[i];
        if(part.y > canvas.height) {
            part.y = 0;
        }
    }
}

上面的是清除粒子函数 killParticles 代码。 它检查每个粒子是否位于画布canvas的底部。 在一些模拟器中,会清除粒子并将其从粒子集合中移除。 为了让雪花持续地运动落下,我们通过设置该粒子的y坐标为0,回收该粒子。

function drawParticles() {
    var c = canvas.getContext('2d');
    c.fillStyle = "black";
    c.fillRect(0,0,canvas.width,canvas.height);
    for(var i in particles) {
        var part = particles[i];
        c.beginPath();
        c.arc(part.x,part.y, part.radius, 0, Math.PI*2);
        c.closePath();
        c.fillStyle = part.color;
        c.fill();
    }
}

最后绘制所有的粒子。 这也很简单:首先清除背景,然后通过粒子提供的圆心(x,y)、 半径以及颜色绘制填充圆形。

现在看起来应该是这样

交互 雪花模拟器

飘雪粒子模拟器。 (点击运行)

我喜欢粒子模拟器的原因是使用简单的数学知识合着一点精心挑选的随机数就能够创建非常复杂、系统和看上去非常自然的动画

精灵图动画

什么是精灵图?

最后一类重要的动画就是精灵图动画。那么什么是精灵图动画?

精灵图是能够快速绘制到画布上的小图片。通常精灵图都是从主图像扣出来的一部分。 扣下来的图片可能包含不同内容的多个片段,像游戏中的不同人物。精灵图或者包括相同人物的不同姿势。 这就是提供不同动画帧的原因。这就是经典动画的形式:快速翻转不同页面。

为什么和什么时候使用精灵图?

精灵图有点:

绘制精灵图

Sprites are easy to draw using the drawImage function. This function can draw and stretch a portion of an image by specifying different source and destination coordinates. For example, suppose we have this sprite sheet and we just want to draw the sprite in the center (5th from the left).

精灵图很容易通过 drawImage 函数绘制。 该函数通过指定不同的源和目标坐标就能绘制和拉伸图像的一部分。 例如,假设我们有张精灵图,然后将该精灵图添加到画布中心(从左数第五张)。

通过制定源坐标绘制单独的某帧图像。

context.drawImage(
    img,         // 精灵图
    65,0,13,13, // 源坐标   (x,y,w,h)
    0,0,13,13,  // 目标坐标 (x,y,w,h)
);

精灵图动画

如上所见到整张精灵图,在动画不同帧中绘制相同的对象,现在切换不同的精灵图让它动起来。 我们会通过一个 tick 计数器保持动画帧执行。

var frame = tick % 10; 
var x = frame * 13;
context.drawImage(
    img,        //  精灵图
    x,0,13,13,  // 源坐标      (x,y,w,h)
    0,0,13,13,  // 目标坐标 (x,y,w,h)
);
tick++;

画布更新通过 tick 计算的当前所在帧。 通过模(%)操作10 意味着frame 重复地从0 ~ 9变化。 通过第几帧图片的下标计算出x的坐标。 接着绘制图片并更新tick计数器。 当然动画如果执行得太快,在模运算之前你可以用tick计数器除2或3减慢动画速度。

交互 精灵图动画

动画有十帧,放大更多细节(点击运行)

下一章节中我们会创建一个简单的游戏。 这个游戏会演示如何使用基本的精灵动画,键盘事件以及爆炸的粒子效果。