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="http://wow.techbrood.com/libs/jquery/jquery.min.js"></script> 
<script type="text/javascript" src="/path/to/asteroids.js"></script> 
</head> 

<body> 
<canvas id="myCanvas" width="500" height="500"> 
    <!-- Insert fallback content here --> 
</canvas> 
<div id="myButtons"> 
    <button id="startAnimation">Start</button> 
    <button id="stopAnimation">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 < 0) { 
tmpAsteroid.x = tmpAsteroid.radius; 
tmpAsteroid.vX *= -1; 
} else if (tmpAsteroid.x+tmpAsteroid.radius > canvasWidth) { 
tmpAsteroid.x = canvasWidth-tmpAsteroid.radius; 
tmpAsteroid.vX *= -1; 
}; 

if (tmpAsteroid.y-tmpAsteroid.radius < 0) { 
tmpAsteroid.y = tmpAsteroid.radius; 
tmpAsteroid.vY *= -1; 
} else 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 < 0) { 
tmpAsteroid.x = tmpAsteroid.radius; 
tmpAsteroid.vX *= -1; 
tmpAsteroid.aX *= -1; 
} else if (tmpAsteroid.x+tmpAsteroid.radius > canvasWidth) { 
tmpAsteroid.x = canvasWidth-tmpAsteroid.radius; 
tmpAsteroid.vX *= -1; 
tmpAsteroid.aX *= -1; 
}; 

if (tmpAsteroid.y-tmpAsteroid.radius < 0) { 
tmpAsteroid.y = tmpAsteroid.radius; 
tmpAsteroid.vY *= -1; 
tmpAsteroid.aY *= -1; 
} else 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(使小行星停止运动)。

最终的效果如下: