本节书摘来自异步社区《HTML5+JavaScript动画基础》一书中的第2章,第2.3节,作者:【美】Billy Lamberta , Keith Peters著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.3 用代码实现动画
在准备好HTML5文件的基本结构之后,我们已经了解了足够多的基础知识,可以开始编码了。我们需要一个文本编辑器用来输入示例代码以及一个支持HTML5的Web浏览器运行这些示例。同时我们需要熟悉该浏览器内置的开发者控制台。在准备好这些工具后(它们很可能已经在你的电脑上)我们就可以出发了,让我们深入学习一些动画吧。
2.3.1 动画循环
几乎所有的程序动画都会表现为某种形式的循环。我们会创建一个展现一系列图像的流程图以实现逐帧动画,其中每一帧只需要绘制出来即可,如图2-2所示。
当你开始绘制图像时,尽管每幅图有细小的差别,JavaScript代码也不会为每一帧创建并保存一幅新的图像,即便是逐帧动画。我们会为每一帧图像保存其绘制到canvas上的每一个对象的位置、尺寸、颜色及其他属性。因此如果要实现一个球滚过屏幕的动画,我们会在每一帧中保存球的位置信息。例如,第1帧标明球处在距离左边缘第10个像素的位置,第2帧则标明在第15个像素的位置,诸如此类。代码会读取这些相关的数据,并根据这些数据将对象绘制出来,从而显示这些帧。据此过程可以衍生出如下流程图,如图2-3所示。
而描述一个动态的,编码的动画的流程图则如图2-4所示。
从图2-4中可以看出,其中并不存在第1帧,第2帧等概念。程序动画通常并总是可以通过一帧就完成动画。由此可以看出我们所指的循环的含义。
首先,设置初始状态,比如,用canvas内置的绘图API向屏幕上绘制一个圆形,渲染并显示这一帧。然后应用规则,规则可以简单如“球向右移动5个像素”,当然也可能由几十条遵循复杂三角函数的线构成。本书中的示例包含从简单到复杂的各种情况。应用规则后会变迁到新的状态,此时会根据一个新的图像描述渲染并显示。随后同样的规则会重复应用。
同一套规则会不断重复地应用,而不会出现为第一帧关联一套规则而又为第二帧关联另一套规则的情况。所以这里的挑战在于如何拿出一套规则,使之能够处理场景中可能出现的各种情况。当球滚出canvas的右边界怎么办?这套规则需要考虑到这种情况。你是否希望用户可以使用鼠标与球产生互动?这套规则也需要将其处理好。
这听上去有些令人气馁,不过实际上并没想象中那么复杂。可以通过创建一两个规则实现一些简单的行为,以此为基础再不断添加新的规则。这里所指的规则实际上就是程序语句。每条规则可以由一条或多条语句构成。以球向右移动5个像素为例,该规则在JavaScript中可以通过以下语句表达:
ball.x = ball.x + 5;
以上语句表示在当前球体所处的x轴(水平轴)上右移5个像素,以此作为球体在x轴上的新位置。甚至可以将语句简化成以下这样:
ball.x += 5;
+=操作符会将右侧的值与左侧的变量相加,并把结果赋给左边的变量。
下面是一套更加高级的规则,它会出现在本书的后续章节中:
var dx = mouse.x - ball.x,
dy = mouse.y - ball.y,
ax = dx * spring,
ay = dy * spring;
vx += ax;
vy += ay;
vy += gravity;
vx *= friction;
vy *= friction;
ball.x += vx;
ball.y += vy;
ball.draw(context);
现在我们不用担心以上语句的含义,只需知道这些代码会重复执行,从而不断产生新的帧。
那么,这些循环如何运行呢?下面的解释是许多入门的程序员都会有的一种误解:循环的运行依赖于几乎存在于所有编程语言中的while循环结构。通过以下代码设置一个无限循环用于更新球体的位置:
while (true) {
ball.x += 1;
}
这段代码看上去很简单:因为ture值永远为真,while子句的条件判断永远成立,所以循环不断执行。在循环中球体的x轴坐标每次增加1个像素,从0到1、2、3、4等。球体在canvas上从左向右不断地移动。
如果你也犯过类似的错误,你会知道通过以上代码你并不会看到球体穿越canvas的动画,实际上你压根看不到球体,因为它已经越过了屏幕的右边界。为什么球体没有移动到循环中的各个位置?实际上,它已经移动到那些位置。你之所以看不到它是因为我们没有更新canvas元素的显示。图2-5的流程图展现了本质上发生的事件:
应用规则并把球体移动到新的位置,新的图像得以创建,不过我们始终没有机会将其显示出来,因为在帧结束的时候并没有将对象绘制到canvas上。而这才是重点。
为了实现动画,需要为每一帧执行以下操作:
(1)执行该帧所要调用到的代码;
(2)将所有对象绘制到canvas上;
(3)重复这一过程渲染下一帧。
将这些步骤记在脑子里,为此创建一个函数用于不断更新对象的位置并将它绘制到canvas元素上。然后创建一个JavaScript定时器启动循环:
function drawFrame () {
ball.x += 1;
ball.draw(context);
}
window.setInterval(drawFrame, 1000/60);
以上代码定义drawFrame函数用于更新球体的位置并使用draw方法(尚未创建)将其绘制到canvas上。然后将drawFrame作为参数传递给window.setInterval方法,该方法会根据第二个参数指定的间隔时间以毫秒为单位重复执行drawFrame函数。在本例中,间隔时间为1000/60,即一秒60帧,差不多17ms一帧。
在相当长时间内,开发者通过以上方式使用JavaScript建立一个动画循环。如果你坚持,你仍旧可以在本书的所有示例代码中运用此方式。不过问题在于,JavaScript定时器并不是为实现动画设计的。因为它无法达到毫秒级的精确度(每个浏览器的定时器分辨率不同),所以无法依赖它实现高质量的动画。除此之外,通过第二个参数指定的间隔时间仅仅是请求在那一时刻执行而已。如果同一时刻在浏览器的任务队列中有其他任务的话,动画代码的执行任务将不得不等待在那里。
由于动画并不是之前HTML规范所包含的功能,因此浏览器厂商并没有将此种优化放在一个优先级很高的位置上。不过,随着HTML5中canvas元素的引入以及对多媒体内容的关注,不同的浏览器之间再次在性能和速度上展开了激烈竞争。由于认识到动画已成为Web应用中一个日益重要的组成部分,浏览器厂商开始不断提出新的解决方案以应对这一需求。
2.3.2 使用requestAnimationFrame的动画循环
由于开发者对基于HTML5的动画兴趣日增,因此Web浏览器为此专门为JavaScript的开发者实现了一个API,通过它提供了基于浏览器的优化实现。window.requestAnimationFrame函数接收一个回调函数作为参数,并确保在重绘屏幕前执行该回调函数。在某些浏览器中,还支持一个可选的第二参数,可以通过它指定的一个HTML元素提供动画的可视区域。在回调函数中对程序的修改必定发生在下一个浏览器重绘事件之前。可以通过对requestAnimationFrame函数的链式调用实现动画循环:
(function drawFrame () {
window.requestAnimationFrame(drawFrame, canvas);
//animation code...
}());
这看上去只是一小段代码,但是了解其工作方式非常重要,因为这是实现动画循环的核心思想,它贯穿了本书的所有示例。这里定义了drawFrame函数,其中包含了每一帧将要执行的动画代码。该函数的第一行代码调用了window.requestAnimationFrame函数并将draw Frame函数自身的引用作为参数值传入。第二个可选参数是要绘制的canvas。你可能会觉得惊讶,我们居然可以在完成一个函数的定义之前就把它作为一个参数值传入另一个函数。切记,在函数运行到需要将它作为参数值传入时,它早已定义好了。
当执行drawFrame函数时,window.requestAnimationFrame将drawFrame函数放入队列等待在下一个动画间隔中再次执行,而当它再次执行时又会重复这一过程。由于不断地请求执行该函数,因此就串联成了一个循环。所以,该函数中定义的代码会不断地调用,使得我们可以在canvas上以细微的间隔时间绘制动画。
为了启动循环,在定义好drawFrame函数后,就用一个圆括号将其包起来并立即调用它。这是一种更节省空间,也更加清晰(这里有争议)的做法,另一种传统的方式是首先定义函数,然后立即在下一行代码中调用它。
由于requestAnimationFrame是一个相对新的功能,因此目前的浏览器还致力于各自的实现。如果你希望代码具备更好的跨平台性,下面这一小段代码就可以用来规范该函数在不同浏览器中的实现。
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000/60);
});
}
这段代码先检查了window.requestAnimationFrame函数的定义是否存在,如果不存在,就遍历已知的各种浏览器实现并替代该函数。如果它还是找不到一个与浏览器相关的实现,它最终会采用基于JavaScript定时器的动画以每秒60帧的间隔调用setTimeout函数。
由于以上针对浏览器的环境检查会被所有示例用到,因此把这个函数放入了utils.js文件中以导入HTML5文件中。如此,就可以确保动画循环可以工作在多个浏览器上,而通过这种方式也使得脚本更加简洁,从而可以把注意力放在理解每个示例的核心思想上。