12.25 HTML Canvas动画 - 物理特性
本节将创建一个简单的太空场景,其中在太空深处有一个动态的小行星群。
创建小行星对象
本节开头的JavaScript代码并没有什么特别之处,实际上,它与上一章使用的代码几乎完全相同。主要的不同之处在于,上一章使用的对象是形状,而本章使用的对象是小行星。
将以下代码添加到一个外部JavaScript文件中,命名为astcroids.js:
$(document).ready(function() { var canvas = $("#myCanvas"); var context = canvas.get(0).getContext("2d"); var canvasWidth = canvas.width(); var canvasHeight = canvas.height(); $(window).resize(resizeCanvas); function resizeCanvas() { canvas.attr("width", $(window).get(0).innerWidth); canvas.attr("height", $(window).get(0).innerHeight); canvasWidth = canvas.width(); canvasHeight = canvas.height(); }; resizeCanvas(); var playAnimation = true; var startButton = $("#startAnimation"); var stopButton = $("#stopAnimation"); startButton.hide(); startButton.click(function() { $(this).hide(); stopButton.show(); playAnimation = true; animate(); }); stopButton.click(function() { $(this).hide(); startButton.show(); playAnimation = false; }); var Asteroid = function(x, y, radius) { this.x = x; this.y = y; this.radius = radius; }; var asteroids = new Array(); for (var i = 0; i < 10; i++) { var x = 20+(Math.random()*(canvasWidth-40)); var y = 20+(Math.random()*(canvasHeight-40)); var radius = 5+Math.random()*10; asteroids.push(new Asteroid(x, y, radius)); }; function animate() { context.clearRect(0, 0, canvasWidth, canvasHeight); context.fillStyle = "rgb(255, 255, 255)"; var asteroidsLength = asteroids.length; for (var i = 0; i < asteroidsLength; i++) { var tmpAsteroid = asteroids[i]; context.beginPath(); context.arc(tmpAsteroid.x, tmpAsteroid.y, tmpAsteroid.radius, 0, Math.PI*2, false); context.closePath(); context.fill(); }; if (playAnimation) { setTimeout(animate, 33); }; }; animate(); });
下一步就是构造CSS文件,让画布的尺寸与浏览器窗口同宽。另外,还要使用CSS来移动Start和Stop按钮,因为如果不这样做,当画布占据整个窗口时,这些按钮将位于浏览器之外。
将包含以下代码的外部CSS文件和JavaScript文件放在相同的目录下,命名为canvas.css:
* { margin: 0; padding: 0; } html, body { height: 100%; width: 100%; } canvas { display: block; } #myCanvas { background: #001022; } #myButtons { bottom: 20px; left: 20px; position: absolute; } #myButtons button { padding: 5px; }
最后,你需要建立一个HTML文件将所有文件整合到一起。这与上一章建立的HTML文件完全相同,不过文件要小得多,因为所有的JavaScript代码都在一个外部文件中。注意这里新加了一个script元素调用JavaScript文件。如果你的文件名不叫asteroids.js,就需要更改调用文件的名称。
将包含以下代码的HTML文件和其他文件放在相同的目录下,并命名为index.html:
<!DOCTYPE html> <html> <head> <title>Implementing advanced animation</title> <meta charset="utf-8"> <link href="/path/to/canvas.css" rel="stylesheet" type="text/css"> <script type="text/javascript" src="//www.xnip.cn/libs/jquery/jquery.min.js"></script> <script type="text/javascript" src="/path/to/asteroids.js"></script> </head> <body> <canvas width="500" height="500"> <!-- Insert fallback content here --> </canvas> <div> <button>Start</button> <button>Stop</button> </div> </body> </html>
如果使用现代浏览器加载该html页面,应该会看到一个全屏的蓝色背景(画布),在其左下角有一个Stop接钮和一群四散分布的小行星。单击Stop按钮可以让背景中的动画停下来。
速度
前面的教程介绍过通过增加或减少形状的x和y位置来移动形状,同样的方法也可以赋予每个形状速度。速度包括物体的速率和方向。速率是指像素单位时间移动的长度,方向是指向左和向右(x)、向上和向下(y)。
上一章中的速度存在的问题是,它们要么是完全随机的,要么是完全相同的。因此,我们可以将这两种情况中和一下,让每颗小行星采用不同的飞行速度。为此,你需要在Asteroid类中定义两个新属性,代码如下:
var Asteroid = function(x, y, radius, vX, vY) { this.x = x; this.y = y; this.radius = radius; this.vX = vX; this.vY = vY; };
适过添加vX和vY属性,现在每颗小行星可以拥有各自不同的速度.注意类函数中参数vX和vY的设置方法,当你创建一颗新的小行星时,可以通过这两个参数来设置速度。那么,接下来需要为每颗小行星设置不同的速度,速度定义了每个动画循环中小行星移动的像素数目。
为了在循环中创建所有的小行星,需要在radius变量下面添加以下代码:
var vX = Math.random()*4-2; var vY = Math.random()*4-2;
另外,还需要使用以下代码替换radius变量下一行的代码,以便把新的速度作为参数传递给Asteroid类:
asteroids.push(new Asteroid(x, y, radius, vX, vY));
在本示例中,在x轴和y轴上同时将速度设置为一个介于-2到2之间的随机数。Math.random方法将会产生一个介于0到1之间的小数。因此,为了得到一个介于-2到2之间的数,需要分两步完成。第一步,将随机数乘以4,得到一个介于0到4之间的随机数。第二步非常简单,只需将该随机数减去2,这样将得到一个介于-2(0减2)到2(4减2)之间的数。你可以使用该方法计算介于任意范围内的随机数。
仅对代码进行这些修改,还不能改变小行星的速度。你还需要使用新的速度属性来更新每颗小行星的x和y位置。在动画循环中的tmpAsteroid变量声明下面添加以下代码:
tmpAsteroid.x += tmpAsteroid.vX; tmpAsteroid.y += tmpAsteroid.vY;
小行星的当前位置增加了一个确定的像素数。现在每颗小行星各自有了不同的速度,这说明它们将会以不同的速率(每个循环移动的像素数)和方向运动。
刷新或加载该HTML文件,你应该会看到一群类似于小行星的物体在画布上运动。继续刷新页面,可以看到这些小行星将从不同的位置出发,并以不同的速度运动。 目前画布中还没有边界,当小行星运动到屏幕的边界快要消失时,你可以单击Stop按钮来留住它们。完整的效果参考下面的在线示例:
添加边界
为了防止小行星运动到画布之外,我们可以在画布上添加一个物理边界。所谓边界的实现思路是检测对象距离某特定x或y轴线的距离,如果相遇则反弹:
if (tmpAsteroid.x-tmpAsteroid.radius canvasWidth) { tmpAsteroid.x = canvasWidth-tmpAsteroid.radius; tmpAsteroid.vX *= -1; }; if (tmpAsteroid.y-tmpAsteroid.radius canvasHeight) { tmpAsteroid.y = canvasHeight-tmpAsteroid.radius; tmpAsteroid.vY *= -1; };
在画布上绘制小行星之前,以上代码中的两个条件语句用于检查每颗小行星的位置。如果小行星的边界位于某个边界之外,那么它将向边界内部运动,并且其速度也改变为相反的方向。 如果不改变小行星的运动方向,那么它要么停下来,要么完全飞出边界之外。因为这里用圆来代表小行星,因此(x,y)坐标位于圆形中心点。为此,你需要加上或减去圆的半径来计算边界处的x或y位置。
在下面的在线实例中,你应该会看到一群小行星在四处飘荡,并在浏览器的边缘处弹回。
加速度
加速度是速度在一段时间内的变化,也称为速率的增加。在小行星动画中添加加速度非常简单。实际上,这和添加速度几乎完全相同,因为加速度也包含大小和方向,大小指加速小行星的像素数目,方向指加速度沿x轴或y轴方向。
你需要让每个小行星拥有不同的加速度,因此第一步需要在Asteroid类中创建所需的属性,然后在构建每颗小行星时使用这些属性。在Asteroid类中添加以下代码:
this.aX = aX; this.aY = aY;
就像前面对速度参数所做的操作一样,请务必向类函数中添加aX和aY参数。以下是Asteroid类最终的代码:
var Asteroid = function(x, y, radius, vX, vY, aX, aY) { this.x = x; this.y = y; this.radius = radius; this.vX = vX; this.vY = vY; this.aX = aX; this.aY = aY; };
下一步是在创建小行星时使用这些新属性,因此在循环中创建小行星,并在速度变量之后添加以下代码:
var aX = Math.random()*0.2-0.1; var aY = Math.random()*0.2-0.1;
通过以上两行代码,小行星将获得一个介于-0.1到0.1之间的加速度。
在循环中所做的最后一件事,是在new Asteroid调用中添加新的aX和aY变量作为最后面的参数,代码如下所示:
asteroids.push(new Asteroid(x, y, radius, vX, vY, aX, aY));
仅对代码做这些修改,还无法看到加速度的效果,因为你还需要将加速度应用到每个具体的小行星。应用加速度就和把加速度添加到物体的当前速度一样,非常简单。 毕竟,加速度是物体速度的变化情况,也就是说,它是单位时间内先前速度与当前速度之间的差值。通过在动画循环中添加以下代码,将加速度应用到每个小行星。 以下代码需要放在每颗小行星的速度代码(x和y位置)之后:
tmpAsteroid.vX += tmpAsteroid.aX; tmpAsteroid.vY += tmpAsteroid.aY;
所有这些步骤都是通过加速度为每颗小行星增加速度,单位为像素。这并不会影响小行星的当前动画循环,但这意味着小行星在随后的循环中将会改变速度。
在让小行星加速之前,还需要在边界检查中添加几行代码。这样,当小行星碰到窗口的边缘时,边界检查将改变速度的方向,使物体沿着相反的方向运动。 但加速度没有改变,因此当某颗小行星改变方向时,其加速度将逐步使小行星恢复为原来的方向。因此我们还需要在改变速度方向的同时也改变加速度的方向。代码如下所示:
if (tmpAsteroid.x-tmpAsteroid.radius canvasWidth) { tmpAsteroid.x = canvasWidth-tmpAsteroid.radius; tmpAsteroid.vX *= -1; tmpAsteroid.aX *= -1; }; if (tmpAsteroid.y-tmpAsteroid.radius canvasHeight) { tmpAsteroid.y = canvasHeight-tmpAsteroid.radius; tmpAsteroid.vY *= -1; tmpAsteroid.aY *= -1; };
现在你就会看到期望的加速效果了。但是,这样小行星将永远处于加速状态。要想改变这种状态,需要为每个小行星设置一个最大速度:
if (Math.abs(tmpAsteroid.vX) < 10) { tmpAsteroid.vX += tmpAsteroid.aX; }; if (Math.abs(tmpAsteroid.vY) < 10) { tmpAsteroid.vY += tmpAsteroid.aY; };
以上代码的功能是,如果每个循环中小行星的速度小于10像素,就把加速度应用于该小行星。这种简单的检查可以限制小行星实际可以达到的速度,使小行星变得更易于控制。 还需要重点注意的是,Math.abs方法将一个数转化成了绝对值数,这种方法主要用于删除数值前面的符号,例如,删除负数前面的符号。 使用绝对值数意味着你仅处理正数,这样可以减少条件语句中的判断次数。效果如下:
力实质上体现为沿特定方向的加速度,例如,如果需要模拟重力,你可以沿y轴的正向(向下)创建均匀的加速度。
摩擦力/阻力
从技术上说,摩擦也是一种力,可以非常精确地计算摩擦力,然后将它作用于物体,使物体降低速度。但是,如果精确的计算非常复杂且需要耗费一些不必要的时间,那么我们可以模仿摩擦力! 显然,如果你对动画的仿真度要求很高,那么这种方法就不太适用了。但是在多数情况下,尤其是对游戏而言,模仿的摩擦力产生的效果和实际效果几乎看不出差别来。模仿物理量的优点在于计算时间更少且易于理解。
就摩擦力而言,你需要用它来降低物体的运动速度。正常情况下,必须根据物体及其经过的物体表面来计算真实的摩擦力,但是如果你采用模仿摩擦力的方法,就可以仅用物体的速度乘以一个摩擦系数来实现。 对非专业人员来说,这两种方法产生的效果是相同的:计算正确的摩擦力并将它作用于物体,将会使物体减速,通过速度乘以一个摩擦系数来模仿摩擦力也会使物体减速。 例如,如果物体的速度是2个像素,摩擦系数为0.9.最后的速度将会等于当前的速度乘以该系数,即等于1.8。在每个循环中使用相同的摩擦系数,物体的速度将很快趋近于0(假设该摩擦力使速度降低的幅度大于加速度使速度增加的幅度)。
如果需要在小行星示例中演示这种摩擦力,那么只需要在加速度代码后面添加以下代码即可:
if (Math.abs(tmpAsteroid.vX) > 0.1) { tmpAsteroid.vX *= 0.9; } else { tmpAsteroid.vX = 0; }; if (Math.abs(tmpAsteroid.vY) > 0.1) { tmpAsteroid.vY *= 0.9; } else { tmpAsteroid.vY = 0; };
以上代码把每颗小行星的速度都乘以0.9,其结果作为一个全局变量值。摩擦力将使每颗小行星逐渐减速,它们就好像在沿着台球桌往高处运动。 当物体的速度降低到一定值时,条件语句用于取消摩擦力,防止摩擦力占用系统资源。为此,当速度取非常小的数值时(速度非常小,看上去好像处于静止状态),可以将速度值设置为0(使小行星停止运动)。
最终的效果如下: