面向 JavaScript 游戏动画仿真的物理学教程(四)

原文:Physics for JavaScript Games, Animation, and Simulations

协议:CC BY-NC-SA 4.0

八、回复力:弹簧和振动

弹簧是你会遇到的最有用的工具之一,尤其是用于创建有趣的物理效果。许多系统可以用弹簧和类似弹簧的运动来模拟。因此,学习弹簧运动的基本原理是值得的。但是要小心:春天会让人奇怪地上瘾!

本章涵盖的主题包括以下内容:

  • 自由振荡:物体将在弹簧力的作用下振荡,也称为回复力。
  • 阻尼振荡:阻尼消耗振荡的能量,使振荡随时间而消失。
  • 强迫振荡:振荡可以由抵抗阻尼力的外力驱动。
  • 耦合振荡器:使用多个弹簧和物体耦合在一起,可以产生有趣的效果。

弹簧和振动:基本概念

在讨论正弦波及其组合时,我们在第三章中介绍了振荡(参见“使用触发函数制作动画”一节)。振荡是围绕某一中心位置的重复运动。振荡系统的简单例子包括钟摆和秋千。

自然系统中也有大量的振荡现象:树木在风的吹拂下振荡,漂浮的物体在水面上振荡是因为经过的波浪。包括弹簧在内的人造机械会产生振动。因此,振荡和弹簧之间有一种直观的联系;事实上,我们将交替使用振荡和类似弹簧的运动这两个术语。这种联系不仅仅是口头上的:振荡系统通常可以用虚拟的“弹簧”来建模。

类似弹簧的运动

你已经在这本书里遇到了春天般的运动,可能还没有意识到。前一章中的浮球模拟提供了一个例子,其中球在水面上以类似弹簧的方式摆动。许多其他东西也经历类似弹簧的运动:汽车的悬挂装置;树、树枝和树叶在风中摇摆。其他东西,虽然它们不一定看起来像弹簧,但仍然可以使用弹簧建模:可变形的物体,如绳子、衣服和头发。当然,这些系统一定有一些共同的特征,使得它们可以用弹簧来建模。那么,振荡系统的一般特征是什么呢?

回复力、阻尼和作用力

振荡系统通常包括以下成分:

  • 一个平衡位置,如果物体不运动,它将保持在这个位置。
  • 如果物体发生位移,将物体拉回到平衡位置的恢复力。
  • 随时间减少振荡的阻尼力。
  • 将物体从平衡位置移开的驱动力。

其中,前两个是产生振荡的关键,而后两个可能存在,也可能不存在,这取决于系统。虽然更复杂的系统可能看起来没有这些特征,但它们可以由具有这些特征的组件组成(或由这些组件建模),例如,一根绳子可以表示为一串弹簧。

在理解恢复力、阻尼和作用力的作用时,一个相关的概念是振幅。振动的振幅是从平衡位置的最大位移。如果你把一个秋千拉离它的平衡位置,然后放开,初始位移将是振荡的振幅。

回复力随时间改变位移,但不改变振幅(最大位移)。振幅取决于系统中的能量大小。阻尼力从系统中带走能量,因此振幅随时间减小。如果在最初移动秋千之后,让它自己摆动,就会发生这种情况。驱动力将能量注入系统,因此倾向于增加振荡的振幅。当阻尼和强迫同时存在时,输入系统的能量有可能正好补偿阻尼损失的能量。在这种情况下,振幅随时间保持不变,就好像只有恢复力存在一样。在挥杆的例子中,这是通过用适当的力量定期推动挥杆来实现的。

胡克定律

在大多数振荡系统中,支配恢复力的力定律被称为胡克定律(因为一位名叫罗伯特·胡克的绅士在历史的某个时刻发现了它)。

胡克定律很简单。我们先解释弹簧,因为这是定律的原始形式。看一下图 8-1 ,它显示了一个自然长度为 l 的弹簧,固定在一端,然后被拉伸 x,所以它的长度变成 l + x。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-1。

A stretched spring experiences a restoring force proportional to the extension x

胡克定律表明,弹簧将被一个 F 级的力拉回,该力由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

换句话说,回复力与延伸量 x 成比例。比例常数 k 称为弹簧常数,它是弹簧刚度的一个度量。k 值越大,对于给定的拉伸,力就越大,因此弹簧在拉伸时被拉回的力就越大。

负号表示力的方向与延伸方向相反。因此,如果弹簧被压缩,力会反推增加其长度。

方程的矢量形式是这样的,其中 r 被解释为弹簧自由端点的位移矢量:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于我们在这一章将要做的大部分事情(可能也是你想要做的),我们不会太关心实际的春天。我们更感兴趣的是附在弹簧末端的质点的运动。这样一个物体会如何运动?每个孩子都知道,它会在平衡位置附近摆动。

事实上,我们可以完全去掉弹簧,只考虑恢复力对粒子的影响。这就是我们在很多例子中要做的。在这种情况下,虎克定律中的位移矢量 r 被解释为粒子相对于其振荡的平衡位置的位移,如图 8-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-2。

A particle oscillating about an equilibrium position

在我们结束这一节之前,有一个警告:不应该假设所有的振荡都遵循胡克定律;许多人不知道。然而,许多类型的运动系统遵循胡克定律,至少近似如此。所以在这一章中我们将坚持胡克定律。

自由振荡

让我们从模拟自由振荡开始。这意味着,系统纯粹在恢复力的作用下振荡,没有任何其他的力。当然,对于一个要振荡的物体来说,首先一定有什么东西(一个施加的力)把它从平衡位置移开了。但是我们在这里感兴趣的是,在初始力被移除,振荡系统被留给它自己之后会发生什么。

弹簧力函数

首先,我们需要在Forces对象中为恢复力创建一个新的力函数。姑且称之为spring()。这是一个非常简单的功能:

Forces.spring = function(k,r){

return r.multiply(-k);

}

函数spring()有两个参数,分别是弹簧常数k和位移向量r(这只是前面公式中的向量 r)。它返回恢复力 F =–k r。

现在让我们用这个函数来创建一个基本振荡器。

创建基本振荡器

你知道接下来会发生什么,不是吗?下面是从文件basic-oscillator.js中提取的一些代码,该文件创建了一个对象,我们希望该对象作为名为ballBall对象进行振荡(我们省略了画布上的标准行和上下文变量)。我们实际上并没有创建一个弹簧(在视觉上),而是创建了另一个Ball对象,我们称之为attractor,它的位置将是平衡位置。两者的初速度都是零,通过给它们不同的位置向量,它们被放置在一定距离之外pos2D:

var ball;

var displ;

var center = new Vector2D(0.5*canvas.width,0.5*canvas.height);

var m = 1;

var kSpring = 1;

var t0, dt;

var acc, force;

var animId;

window.onload = init;

function init() {

// create a ball

ball = new Ball(15,'#0000cc',m,0,true);

ball.pos2D = new Vector2D(100,50);

ball.draw(context);

// create an attractor

var attractor = new Ball(2,'#000000');

attractor.pos2D = center;

attractor.draw(context_bg);

// make the ball move

t0 = new Date().getTime();

animFrame();

}

动画代码相当标准;唯一的新颖之处在于calcForce()方法:

function calcForce(){

displ = ball.pos2D.subtract(center);

force = Forces.spring(kSpring,displ);

}

这段代码非常简单:它首先计算物体相对于吸引子的位移向量displ,然后用它来计算吸引子对物体施加的弹力。参数kSpring是弹簧常数 k。

运行代码,你会看到球在吸引子周围振荡,就像预期的那样。现在将弹簧常数值从 1 更改为 10。这给你一个硬弹簧。如果你再次运行代码,你会看到球摆动得更快了。如果你想让它以不同的振幅振荡,只需改变basic-oscillations.js中的初始位置。

接下来将 k 的值改回 1,然后将球的初速度改为(200,0):

ball.velo2D = new Vector2D(200,0);

当您运行代码时,您现在应该看到球以某种椭圆形轨迹“环绕”吸引子。有点像你在第六章看到的重力。重力和弹力都是吸引力,总是作用在一个点上。但是重力随着距离的增加而减小,而弹力随着距离的增加而增加。这是因为重力与 1/r 2 成正比,而弹力与 r 成正比,其中 r 是与引力中心的距离。

简谐运动

你刚才看到的这种振荡运动在技术上被称为简谐运动(SHM)。如果作用在物体上的唯一力是弹簧力,物体就会经历 SHM,就像前面的例子一样。

因为 SHM 中唯一的力是回复力 F =–k r,而牛顿第二定律表明 F = m a,所以这两个方程一起告诉我们,对于 SHM,以下是正确的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将等式两边除以 m 得出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里 m 是振荡粒子的质量,k 是弹簧常数,所以比率 k/m 是常数。因此,这个方程告诉我们,振动粒子的加速度与它离中心的位移矢量成正比。比例常数为负,这意味着加速度总是与位移矢量相反(它总是指向中心,因为位移矢量根据定义总是指向远离中心的方向)。参见图 8-3 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-3。

The acceleration in SHM always points toward the center, opposite to displacement

还记得在第三章第一节中,我们谈到了导数,加速度是位移的二阶导数吗?这意味着以下情况成立:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,我们可以将 SHM 方程 a =–(k/m)r 写成等价形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以这种方式表述清楚了支配 SHM 的方程是一个二阶微分方程,如第五章中所述。basic-oscillator.js中的动画代码所做的就是使用欧拉格式数值求解这个二阶微分方程(再次如第五章所述)。

事实上,前面的微分方程也可以解析求解,以给出作为时间函数的位移公式。这是大学水平微积分中的一个练习题,下面是解法,其中 A 和 B 是依赖于初始条件的常矢量,ω是振荡的角速度(见第三章):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,SHM 由正弦和余弦函数组成。这并不奇怪,因为 SHM 基本上是一种振荡运动,正如你从第三章中所知,sin 和 cos 是振荡函数。ω的值决定了振动的频率和周期,而 A 和 B 的值决定了振动的振幅(物体从平衡位置的最大位移)。

稍微懂点微积分的人,都可以通过对前面的 r 的表达式求导,立刻写下速度向量 v,因为 v = d r/dt。结果是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

A、B 和ω的值分别是多少?a 和 B 由您在开始时为对象的位移和速度指定的初始条件设定(时间 t = 0 时的 r 和 v 值)。我们称初始位移矢量 r 0 和初始速度矢量 v 0 。如果我们现在将 t = 0 代入前面的 r 和 v 方程,因为 cos(0) = 1,sin(0) = 0,我们得到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们还得到以下信息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这马上告诉我们,A = r 0 和 B = v 0 /ω。

如果你要解这个微分方程,你也会发现角速度ω实际上是由这个公式给出的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这称为系统的固有频率。如果给振荡系统一个初始扰动(使物体离开其平衡位置),但此后任其自生自灭,它将以该频率振荡,并且仅在没有阻尼等其他影响的情况下以该频率振荡。

现在,如果您还记得(再次从第三章)ω= 2πf 和 f = 1/T,其中 f 是频率(每秒的振荡次数),T 是振荡周期(完成一次振荡的时间),您可以使用前面的公式根据参数 k 和 m 获得振荡的频率和周期:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些公式告诉你,如果你增加弹簧刚度 k,振荡的频率将增加,其周期将减少(它将振荡得更快)。如果你增加振荡粒子的质量,情况会相反,它会振荡得更慢。去试试吧!

这是仅有的两个影响频率和周期的参数;物体的初始位置和速度不变。你可能会想,如果物体最初离中心较远,完成一次摆动就需要更长的时间。但不是和 SHM。发生的情况是,如果物体离得更远,它在开始时会经历更大的加速度,因此平均来说,它会获得更大的速度,这补偿了它完成一次振荡所必须行进的更长的距离。怀疑?尝试用秒表计时振荡或输出时间。

事实上,我们可以做得更好。让我们画一张图表。

振荡和数值精度

为了绘制图表,我们通过改变振荡物体的初始位置来修改basic-oscillations.js,如下所示:

object.pos2D = new Vector2D(100,50);

我们也改变吸引子的位置:

var center = new Vector2D(0.5*canvas.width,50);

在添加了一些额外的代码来绘制显示对象相对于时间的位移的图形,并在固定的持续时间(20 秒)内运行模拟之后,我们将文件保存为free-oscillations.js

下面是设置和绘制图表的代码:

function setupGraph(){

//graph= new Graph(context,xmin,xmax,ymin,ymax,xorig,yorig,xwidth,ywidth);

graph = new Graph(context_bg,0,20,-250,250,50,300,600,300);

graph.drawgrid(5,1,50,50);

graph.drawaxes('t (s)','displacement (px)');

}

function plotGraph(){

graph.plot([t], [displ.x], '#ff0000', false, true);

graph.plot([t], [displ.y], '#0000ff', false, true);

}

在每个时间步调用的plotGraph()方法调用Graphplot()方法来绘制球相对于吸引子的位移的 x 和 y 坐标。

如果您现在运行代码,您将看到类似图 8-4 的内容。果然,物体随时间的水平位移(x)是一个正弦波,我们在上一节已经说过了。由于我们选择的初始条件,垂直位移始终为零。但是很容易改变它,给物体一个速度的初始垂直分量,或者一个不同的初始垂直位置。那么 y 位移也将正弦变化。你也可以改变 k、m 的值和物体的初始位置,以验证我们在上一节结束时所做的关于振荡的频率和周期如何随这些参数变化(或不变化)的陈述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-4。

Plotting the displacement of an oscillating object as a function of time

现在,让我们通过将振荡物体的轨迹与上一节给出的解析解预测的轨迹进行比较,来看看我们的模拟有多精确。

为此,我们首先通过改变振荡物体的初始速度将free-oscillations.js修改为free-oscillations2.js,如下所示:

object.velo2D=new Vector2D(0,50);

然后我们添加代码,使用之前给出的等式外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传、A = r 0 和 B = v 0 /ω计算 A、B 和ω的值(在代码中定义为变量ABomega)。这是在init()方法中完成的,因此它包含这些附加行:

omega = Math.sqrt(kSpring/m);

A = ball.pos2D.subtract(center);

B = ball.velo2D.multiply(1/omega);

然后,我们修改代码,创建两个Graph对象graphXgraphY(通过适当修改的setupGraph()方法建立),在plotGraph()中分别绘制 x 和 y 位移。在plotGraph()方法中,我们插入以下附加行来绘制解析解 r = A cos (ωt) + B sin (ωt)的 x 和 y 分量:

var r = A.multiply(Math.cos(omega*t)).add(B.multiply(Math.sin(omega*t)));

graphX.plot([t], [r.x], '#00ff00', false, true);

graphY.plot([t], [r.y], '#ff00ff', false, true);

如果您现在运行代码,您将看到对象以拉长的轨道围绕吸引子运行,并且您将看到对象的每个水平和垂直位移的一对图形。在每对图表中,一个对应于通过指定公式计算的解析解,而另一个对应于通过模拟计算的数值解。你可以看到它们彼此非常接近,几乎重叠(见图 8-5 )。欧拉积分在这种情况下做得还不错。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-5。

Comparing the numerical and analytical solutions for the oscillator

然而,如果您现在将kSpring的值更改为20,例如,使“弹簧”更硬,对象移动更快,您会看到两者之间的差异更显著,表明欧拉正在失去准确性。如果你尝试让kSpring的值大于大约100,那么你可能会发现欧拉给出的完全是垃圾,物体在错误的时间出现在错误的地方。所以在这里你开始看到,如果你不小心对待你的积分器,你可能会得到错误的物理结果,特别是在弹簧的情况下。你会在第十四章中看到如何克服这个问题。同时,在本章的其余部分,我们将避免非常高的弹簧常数值。

阻尼振荡

在前面的例子中,振荡会一直持续下去。实际上,这种情况很少发生。振荡系统通常是阻尼的。这意味着随着能量从系统中移除,振荡会减少并及时消失。这类似于曳力,它耗散了运动物体的动能,使其减速。为了在弹簧中实现阻尼,我们需要一个阻尼力。

阻尼力

阻尼力通常被建模为与运动物体的速度成比例。这意味着,在任何时刻,它由下式给出,其中 c 是一个称为阻尼系数的常数,负号表示力的方向与速度相反:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,这与线性阻力的力定律的形式完全相同。因此,我们可以使用linearDrag()函数来实现弹簧运动的阻尼。然而,尽管阻力确实是阻尼的一种形式,但它并不是阻尼的唯一形式,尽管这两个术语有时可以互换使用。阻力是流体对浸入其中的运动物体施加的阻力。另一方面,弹簧阻尼可由流体阻力或外部摩擦等外部因素引起,或由弹簧的分子材料特性引起的内部因素如内部摩擦引起。事实上,你可以同时拥有由弹簧阻力产生的内部阻尼和由流体阻力产生的阻力(当然,系数不同)。例如,悬挂在弹簧上并在空气或水等流体中振荡的质量既受到内部摩擦,又受到流体阻力。出于这些原因,我们更喜欢为阻尼创建一个单独的函数。该函数的形式与linearDrag()的形式相同,如清单所示:

Forces.damping = function(c,vel){

var force;

var velMag = vel.length();

if (velMag>0) {

force = vel.multiply(-c);

}

else {

force = new Vector2D(0,0);

}

return force;

}

阻尼对振荡的影响

我们现在将修改free-oscillations.js来处理弹簧力之外的阻尼。新档名为damped-oscillations.js,基本上引入了一个新的力:阻尼。因此,与free-oscillator.js的一个不同之处在于,它声明了一个阻尼系数cDamping,并给它赋值:

var cDamping = 0.5;

另一个不同点是,它在calcForce()中包含了一个阻尼力,并将其与弹簧力相加,计算出合力:

function calcForce(){

displ = ball.pos2D.subtract(center);

var restoring = Forces.spring(kSpring,displ);

var damping = Forces.damping(cDamping,ball.velo2D);

force = Forces.add([restoring, damping]);

}

就这样。如果你用kSpring = 10cDamping = 0.5运行代码,你会看到如图 8-6 所示的东西。正如你所料,振荡会及时消失,物体最终会停留在平衡位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-6。

Damped oscillations

此时,你应该摆弄一下阻尼常数cDamping,看看你得到了什么。选择一个较低的值,你会看到振荡持续更长时间,而一个较高的值会更快杀死它们。你可能认为 c 值越大,物体停止的越快。事实上,存在一个临界值 c,使振荡在最短时间内消失。在这个值下,根本没有振荡。物体只是平稳地移动到平衡位置,而不会振荡超过它。这称为临界阻尼,如图 8-7 所示。对于我们模拟中的kSpring = 10,cDamping的临界值约为 5.5,物体在 1.5 秒左右到达平衡位置。如果你增加cDamping超过这个临界值,例如增加到 10 或 20,你会看到物体实际上需要更长的时间到达它的平衡位置。这是因为增加的阻尼使它慢了下来,尽管它没有超过平衡位置,但需要更长的时间才能到达。临界阻尼是一种非常有用的现象,应用于门上的阻尼机构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-7。

Critical damping

有阻尼振荡的解析解

这个简短的部分是为爱好者准备的。如果你不喜欢复杂的公式,你可以放心地跳过它。本节的重点是向您展示在阻尼存在的情况下,前面引用的自由振荡的解析解是如何变化的,以及我们的模拟在再现它方面做得有多好。因此,它的教育意义大于实用性。

在弹簧力 F =–k r 和阻尼力 F =–c v 的作用下,振动的牛顿第二定律如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这可以用导数的形式写成(两边除以 m 之后):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该微分方程的解析解由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中常数γ,ω 0 ,ω d ,A 和 B 由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将此与之前无阻尼的解决方案进行比较,您会注意到 r 的表达式中有一个额外的指数因子,这是正弦振荡衰减的原因(参见第三章)。新参数γ是由阻尼引起的。在没有阻尼的情况下,c = 0,所以γ= 0;于是,解就减少到没有阻尼的情况。还要注意,振荡的角频率现在是ω d (因为它出现在正弦和余弦曲线中),小于没有阻尼时的固有频率ω 0 (之前用ω表示)。

我们将这些方程编码成我们称之为damped-oscillations2.jsfree-oscillations.js的修改版本。如果你愿意,可以看一下代码。如果你运行代码,你会发现这个解析解和模拟计算的数值解之间又有一些差异。事实上,有时你可能会发现摆动的物体看起来很疯狂,最终停在了它不应该在的地方。从所有这些中得到的重要信息是,除了最简单的运动之外,欧拉积分在这种模拟中根本没有用。在第十四章中,我们将讨论解决这个问题的替代集成方案。

强迫振荡

在阻尼存在的情况下,系统的振荡会及时消失。因此,首先需要一个力来维持振荡或引发振荡。这就是所谓的驱动力或压力。

驱动力

驱动力可以是任何形式,所以让我们简单地用 f (t)来表示。这仅仅意味着力是时间的函数,而没有具体说明力定律的形式。

例如,我们可以有一个形式为 f = A cos (ωt) + B sin (ωt)的周期力,其中 A 和 B 是给出力的振幅(最大值)和方向的恒定矢量,ω是力的角频率。例如,这可以表示一个振荡器与另一个振荡器相互作用并驱动另一个振荡器,如周期性的风吹过吊桥并导致其振动。

先说个例子。

示例:周期性驱动力

我们从修改damped-oscillations2.js开始。我们不想在这里画出解析解,所以我们去掉了plotGraph()中的相关行,但是我们将保留在init()方法中计算gammaomega0omegad的三行。这些变量对应于常数γ、ω 0 和ω d 。删除任何不必要的变量声明后,我们将文件重命名为forced-oscillations.js

记住omega00 是无阻尼系统的固有角频率,omegadd )是有阻尼的角频率。让我们从给calcForce()添加以下几行开始:

var forcing = new Vector2D(200*Math.cos(2*omegad*t)+200*Math.sin(2*omegad*t),0);

force = Forces.add([restoring, damping, forcing]);

这增加了一个形式为 f = A cos (ωt) + B sin (ωt)的驱动力,其中 A 和 B 的幅度均为 200,并且在 x 方向上(我们需要相当大的值才能看到对振荡的作用力的显著差异),ω = 2 ω d 。因此,我们以两倍于阻尼系统频率的角频率,对系统施加随时间呈正弦变化的驱动力。

运行代码,你会看到振荡开始消失,就像没有外力时一样。然后,过了一会儿,物体开始以两倍于之前的频率振荡,但是振幅要小得多。这种情况会无限期持续下去。因此,正弦驱动力的作用是最终使系统以驱动频率振荡(尽管振幅减小),而不是以系统自己喜欢的频率振荡。你可以用不同的强制频率来尝试。例如,如果使用ω d /2,阻尼系统频率的一半,振荡的频率最终将是初始非受迫频率的一半。结论是,如果你强迫一个系统以不同于其固有频率的频率振荡,它仍然会振荡,但振幅会减小。

现在,通过将强制向量修改为以下形式,使强制频率恰好等于ω d :

var forcing = new Vector2D(200*Math.cos(omegad*t)+200*Math.sin(omegad*t),0);

当您运行代码时,您会看到振荡很快在原始频率ω d 处达到平衡,并保持较大的恒定振幅而不衰减。这个系统现在是“快乐的”,因为它被强迫以它喜欢的频率振荡。因此,它很快稳定下来,并以大幅度振荡。因此,虽然阻尼降低了系统的能量,但强迫将能量放回系统并维持振荡。

当强迫频率等于振荡系统的固有频率时,我们就有了所谓的共振。它发生在很多情况下,从推动孩子的秋千到调整无线电电路。共振也会产生不良影响,比如周期性阵风引起的桥梁有害振动。事实上,阵风引起的共振是 1940 年华盛顿塔科马海峡大桥倒塌的罪魁祸首。

示例:随机驱动力

接下来,我们来试试随机强制。这就像替换一行代码一样简单。让我们先试试这个:

var forcing = new Vector2D(1000*Math.random(),0);

这将在 x 轴正方向(向右)施加高达 1,000 个单位的随机力。如果这是唯一的力,它会使物体向右飞去,再也看不见了。但是回复力的存在(顾名思义)将物体拉回到吸引子。事实上,因为恢复力与吸引子的位移成正比,物体移动得越远,被拉回的力就越大。这创造了一个有趣的效果,你肯定想尝试一下。因为我们使力只作用于右边,所以物体大部分时间都在吸引子的右边,尽管偶尔会被稍微拉向左边。

要使吸引子周围的振荡更加对称,只需将该行代码改为:

var forcing = new Vector2D(1000*(Math.random()-0.5),0);

这会产生一个 x 分量在–500 和+500 之间的随机力,其中负号表示力指向左侧而不是右侧。运行代码,你会得到一个非周期振荡器(物体无规律地振荡)。

最后,通过将同一行代码修改为以下内容,让物体在 2D 的随机力的作用下振动:

var forcing = new Vector2D(1000*(Math.random()-0.5),1000*(Math.random()-0.5));

如果你运行代码,你会看到系统很快进入一种状态,其中 x 和 y 方向的振荡幅度相当,物体在吸引子周围盘旋(见图 8-8 )。在每个时间步长,对象被踢向不同的方向,但也会被拉向吸引子。不管作用力有多大,它都不能逃脱,因为它走得越远,被拉回来的力就越大。这是一个有趣的效果。这是某种疯狂的随机轨道运动。或者甚至像蜜蜂在花周围嗡嗡叫。一定要玩那个!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-8。

Oscillator with random forcing

重力作为驱动力:蹦极

现在让我们来看一些不同的东西:蹦极!有人从桥上或类似的建筑上跳下来,绳子的另一端系在建筑上的固定支架上。弹力绳有一个自然的未拉伸长度,比如说cordLength。因此,如果跳线与固定支架的距离小于cordLength,则重力和阻力是跳线上仅有的力。但是一旦这个距离超过cordLength,弹性(弹簧)力就会生效,将跳线拉回。

所以这里重力作为驱动力,所以 f(t) = mg,这里 m 是 jumper 的质量。这是一种持续的压力,不像之前讨论的那种压力。主要的阻尼机制是由空气中的阻力提供的,因此可以使用Forces.drag()方法来实现。最后,仅当跳线与固定支架的距离超过电线的自然未拉伸长度cordLength时,才施加弹簧力。这个距离最初可能小于cordLength(在跳跃者从一些支撑结构上跳下之前),但也可能在跳跃者跳回足够高的时候。

因为这个例子与前几个有一点不同,我们将列出完整的源代码,然后讨论要点。该文件简称为bungee.js:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var jumper;

var fixedPoint;

var displ = new Vector2D(0,0);

var center = new Vector2D(0.5*canvas.width,50);

var mass = 90;

var g = 20;

var kDamping = 0.02;

var kSpring = 25;

var cordLength = 100;

var t0, dt;

var acc, force;

var animId;

window.onload = init;

function init() {

// create a bungee jumper

jumper = new StickMan();

jumper.mass = mass;

jumper.pos2D = center;

jumper.draw(context);

// create a fixedPoint

fixedPoint = new Ball(2,'#000000');

fixedPoint.pos2D = center;

fixedPoint.draw(context);

// make the ball move

t0 = new Date().getTime();

animFrame();

};

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

move();

}

function move(){

context.clearRect(0, 0, canvas.width, canvas.height);

drawSpring(jumper);

moveObject(jumper);

calcForce(jumper);

updateAccel();

updateVelo(jumper);

}

function drawSpring(obj){

fixedPoint.draw(context);

context.save();

if (displ.length() > cordLength){

context.lineStyle = '#999999';

context.lineWidth = 2;

}else{

context.lineStyle = '#cccccc';

context.lineWidth = 1;

}

context.moveTo(center.x,center.y);

context.lineTo(obj.x,obj.y);

context.stroke();

context.restore();

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

obj.draw(context);

}

function calcForce(obj){

displ = obj.pos2D.subtract(center);

var gravity = Forces.constantGravity(mass,g);

var damping = Forces.drag(kDamping,obj.velo2D);

var extension = displ.subtract(displ.unit().multiply(cordLength));

var restoring;

if (displ.length() > cordLength) {

restoring = Forces.spring(kSpring,extension);

}else{

restoring = new Vector2D(0,0);

}

force = Forces.add([gravity, damping, restoring]);

}

function updateAccel(){

acc = force.multiply(1/mass);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

请注意,我们在这里创建了两个对象:一个固定点和一个跳线。jumper 是StickMan的一个实例,一个男人的穷人版,欢迎你通过编辑文件stickman.js来改进。

首先,注意我们为弹簧常数kSpring (25)使用的大值。这是因为我们在bungee.js中将跳线的质量指定为 90,而不是之前所有模拟中的 1。这反映了一个事实,如果你想让一个大家伙在一根弹性绳索的末端摆动,绳索最好是相当硬的!

接下来看看calcForce()方法,我们看到我们包括了重力、阻尼和回复力,如前所述。前两个通常以简单的方式实现。与前面的例子相比,我们计算恢复力的方法有两个不同之处。首先,必须用来计算回复力的位移矢量现在是弹性索(延伸部分)端点的位移矢量,正如本章开头所解释的,而不是物体离开固定支座的位移。因此,我们首先在下面一行中计算作为向量的延伸:

var extension = displ.subtract(displ.unit().multiply(cordLength));

请注意对新添加到Vector2D对象中的unit()方法的巧妙使用,其中vec.unit()返回一个长度为unit的向量,其方向为向量vec

然后我们提供extension作为Forces.spring()中的第二个参数。

第二个区别是,只有当物体离支撑物的距离大于绳子的长度时,回复力才不为零。那就等于说displ.length() > cordLength。这解释了if块中的代码:

if (displ.length() > cordLength) {

restoring = Forces.spring(kSpring,extension);

}else{

restoring = new Vector2D(0,0);

}

另一个新特性是我们包含了一个在move()方法中调用的drawSpring()方法,该方法在每个时间步长绘制一条线来表示弹性线。当绳子不拉伸时,线被拉得越来越细,越来越模糊。像往常一样,运行代码并随意使用参数。物体的初始位置与固定支撑的位置相同。这使得物体沿垂直直线振荡(这使得振荡 1D)。要使其成为 2D,只需在bungee.js中改变物体的初始 x 坐标即可;例如:

object.pos2D = new Vector2D(300,50);

演示的截图如图 8-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-9。

A bungee jump simulation

示例:用户交互的驱动力

用户交互也可以被视为某种强迫。在下面的例子中,我们将构建一个模拟,其中用户可以单击并拖动对象,然后在任何位置释放它。这将扰乱系统,使其振荡。

修改蹦极模拟来做我们想要的事情是相当简单的。我们将把这个新文件称为dragging-oscillations.js。在其他修改中,我们将对象的质量更改为 1,并添加代码以支持拖动。如果您想了解如何做到这一点,请查看该文件——对于我们目前的目的来说,细节并不特别有趣。这里的要点是,代码更改使您,即用户,能够随时通过拖动和释放对象来施加驱动力。因此,这种强迫不是由一个数学函数确定的,而是人类随机互动的产物。

然而,正如calcForce()方法所反映的那样,基本的物理原理并没有改变:

function calcForce(obj){

displ = obj.pos2D.subtract(center);

var gravity = Forces.constantGravity(mass,g);

var damping = Forces.drag(kDamping,obj.velo2D);

var extension = displ.subtract(displ.unit().multiply(springLength));

var restoring = Forces.spring(kSpring,extension);

force = Forces.add([gravity, damping, restoring]);

}

calcForce()中,我们像以前一样包括重力,同时使用Forces.damping()函数包括阻尼。然后,我们计算弹簧的伸长量,并像之前一样将其输入回复力。然而,因为这是一个也可以被压缩的弹簧,而不是一个弹性绳,所以即使伸长量为负,我们也会施加回复力,所以我们已经删除了前面蹦极例子中的if语句。

运行代码,你会看到物体在重力作用下下落,然后又被弹簧拉回来,由于阻尼作用,物体的振动幅度减小。如果你拖动物体并释放它,由于外力的作用,它会再次振荡。

耦合振荡器:多个弹簧和物体

到目前为止,在所有的例子中,我们只考虑了一个物体和弹簧系统。当您将多个对象和弹簧系统耦合在一起时,效果会变得更加有趣。事实上,您可以基于质量由弹簧连接的对象创建扩展系统。在下一小节中,您将看到一个可以做什么的示例。然后我们将在第十三章中看到更复杂的例子。

示例:由弹簧连接的对象链

你将创建的东西如图 8-10 所示:由弹簧连接的许多球,第一个球连接到一个支架上。然后支架会四处移动,导致悬浮球链也四处移动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-10。

A chain of objects held together by springs

将要建模的力包括重力、阻尼和恢复力。弹簧将有一个自然的长度,就像最后两个例子一样。因此,每个球都会受到重力、阻尼、上方弹簧产生的回复力和下方弹簧产生的回复力的作用(最后一个球除外)。弹簧本身将被假定为无质量的。

代码在一个名为coupled-oscillations.js的文件中。我们首先列出变量声明和init()函数:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var balls;

var support;

var center = new Vector2D(0.5*canvas.width,50);

var g = 20;

var kDamping = 0.5;

var kSpring = 10;

var springLength = 50;

var numBalls = 6;

var t0, t, dt;

var acc, force;

var animId;

window.onload = init;

function init() {

// create a support

support = new Ball(2,'#000000');

support.pos2D = center;

support.draw(context);

// create a bunch of balls

balls = new Array();

for (var i=0; i<numBalls; i++){

var ball = new Ball(15,'#0000ff',1,0,true);

ball.pos2D = new Vector2D(0.5*canvas.width,100+60*i);

ball.pos2D = new Vector2D(0.5*canvas.width+60*i,100+60*i);

ball.draw(context);

balls.push(ball);

}

// make the balls move

t0 = new Date().getTime();

t = 0;

animFrame();

}

和前面两个例子一样,我们首先创建一个Ball对象作为支撑。然后我们创建一大堆Ball对象,并将它们放入一个名为balls的数组中。

我们对代码的动画部分做了一些结构上的修改,为了便于参考,我们也在这里完整地复制了这些修改:

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

t += dt;

move();

}

function move(){

context.clearRect(0, 0, canvas.width, canvas.height);

drawSpring();

for (var i=0; i<numBalls; i++){

var ball = balls[i];

moveObject(ball);

calcForce(ball,i);

updateAccel(ball.mass);

updateVelo(ball);

}

}

function drawSpring(){

support.draw(context);

context.save();

context.lineStyle = '#999999';

context.lineWidth = 2;

context.moveTo(center.x,center.y);

for (var i=0; i<numBalls; i++){

var X = balls[i].x;

var Y = balls[i].y;

context.lineTo(X,Y);

}

context.stroke();

context.restore();

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

obj.draw(context);

}

function calcForce(obj,num){

var centerPrev;

var centerNext;

if (num > 0){

centerPrev = balls[num-1].pos2D;

}else{

centerPrev = center;

}

if (num < balls.length-1){

centerNext = balls[num+1].pos2D;

}else{

centerNext = obj.pos2D;

}

var gravity = Forces.constantGravity(obj.mass,g);

var damping = Forces.damping(kDamping,obj.velo2D);

var displPrev = obj.pos2D.subtract(centerPrev);

var displNext = obj.pos2D.subtract(centerNext);

var extensionPrev = displPrev.subtract(displPrev.unit().multiply(springLength));

var extensionNext = displNext.subtract(displNext.unit().multiply(springLength));

var restoringPrev = Forces.spring(kSpring,extensionPrev);

var restoringNext = Forces.spring(kSpring,extensionNext);

force = Forces.add([gravity, damping, restoringPrev, restoringNext]);

}

function updateAccel(mass){

acc = force.multiply(1/mass);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

注意move()方法的变化,它循环遍历balls数组的元素,依次对每个球应用moveObject(), calcForce(), updateAccel()updateVelo()方法。我们修改了方法calcForce(),加入了另一个参数Number。在move()中,当我们调用calcForce()时,我们包含一个额外的参数i,它是数组中相关粒子的索引号。为什么我们需要这样做?对于每一个球,我们需要计算与它相连的弹簧所施加的弹力。因此,如果我们确切地知道我们在处理哪个球,以及哪个球在它之前和之后,这将是很方便的。这就是额外参数的作用。

calcForce()中,我们通过首先定义变量centerPrevcenterNext,考虑到每个物体由于其最近的邻居而经受恢复力的事实。我们通过使用数组索引num给出当前球之前和之后球的位置,数组索引作为参数在calcForce()中传递。对于第一个球,centerPrev给出了固定支撑的位置;对于最后一个球,centerNext给出当前球的位置。然后,我们使用centerPrevcenterNext的值,以通常的方式计算前一个球和下一个球的位移矢量。然后,根据弹簧springLength的自然长度,计算出每个弹簧的弹簧延伸长度,如最后两个示例所示。最后,使用Forces.spring()计算每个弹簧产生的弹簧力,并将其添加到重力和阻尼力中。

move()方法调用而来的drawSpring()方法,现在从固定支撑开始通过每个球画一条线。

用不同的初始位置运行代码,看看球是如何在重力和弹簧力的作用下重新排列到它们的平衡位置的。

同样,不要害怕尝试。例如,添加下面的方法moveSupport()并在move()中调用它,使支撑正弦振动,同时拖动链条:

function moveSupport(){

support.x = 100*Math.sin(1.0*t)+0.5*canvas.width;

center = support.pos2D;

}

这给了你类似于图 8-10 中所示的东西。

试试不同的质量和不同的弹簧常数怎么样?还是允许球被拖动?或者看看能不能弄清楚怎么做封闭链。我们不打扰你了。

摘要

我们希望你能从本章的例子中获得很多乐趣。正如我们在本章开始时所说,弹簧不仅有趣,而且非常有用。弹簧可以以多种方式创造性地使用,例如,您可以将它们应用于速度,以便对象平稳地加速和减速,以获得给定的“平衡”速度。希望你能找到它们的许多用途。你的想象力是极限。在本书的后面,我们肯定会再次碰到春天。现在,是时候在下一章讨论一些不同的东西了:旋转运动。

九、向心力:旋转运动

从旋转木马到行星的轨道和自转,圆周运动和自转运动在自然界和人造机器中都很普遍。为了分析和模拟这种类型的运动,我们需要一些特殊的工具:第五章中介绍的线性运动学和动力学需要扩展到旋转运动。简而言之,这就是本章将要开始探讨的内容。

本章涵盖的主题包括以下内容:

  • 匀速圆周运动的运动学:旋转运动的运动学可以类比线性运动学来发展。
  • 向心加速度和向心力:匀速圆周运动的物体仍然受到加速度,因此受到一个指向旋转中心的力。
  • 非匀速圆周运动:如果除了向心分量之外还有一个切向分量的力,就会发生这种情况。

本章涵盖的材料也与第十三章中的刚体运动处理相关。

匀速圆周运动的运动学

在第四章中,我们用位移、速度和加速度等概念设计了描述运动的框架。正如我们在那里所说,这个框架在技术上被称为运动学。这些概念足以描述所谓的线性或平移运动。但是为了分析圆周或旋转运动,我们需要引入额外的概念。

事实上,旋转运动学的概念可以完全类比线性运动学的概念来发展。首先,我们有位移、速度和加速度的旋转模拟,分别称为角位移、角速度和角加速度。所以让我们从看它们开始。

角位移

回想一下,在第四章中,位移被定义为在指定方向上移动的距离。那么角位移的等价定义是什么呢?

首先,我们需要弄清楚什么概念等同于旋转运动中的距离和方向。看一下图 9-1 ,它比较了平移运动和旋转运动。在左边,一个物体沿给定方向移动距离 d。在右边,一个物体以给定的方向(在这种情况下是顺时针方向)绕某个中心旋转了θ角。所以很明显,在旋转运动学中,角度代替了距离,旋转感(顺时针或逆时针)代替了方向。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1。

Translation and rotation compared for particle motion

产生角位移的另一种情况是刚体绕固定轴旋转时,如图 9-2 所示。在这种情况下,刚体上的每个点都绕旋转轴移动相同的角度θ。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2。

Rotation of a rigid body about an axis

因此,在任何一种情况下(粒子绕中心旋转或刚体绕轴旋转),想法都是一样的。因此,我们将角位移定义为一个物体围绕一个特定的中心以特定的方式运动的角度。

这个定义意味着角位移的大小是一个角度。通常的惯例是用弧度来表示角度,所以我们用它来表示角位移。

角速度

与线速度类似,角速度被定义为角位移随时间的变化率。角速度的常用符号是ω(希腊字母 omega),因此它在微积分符号中的定义方程是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于较小的时间间隔δt 和角度变化δθ,它可以用以下离散形式表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

后一个等式也可以写成以下形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

等式的最后一种形式是我们在代码中最常使用的,用来计算在时间间隔δt 内角速度ω引起的角位移。

角速度用弧度每秒(rad/s)来度量,因为我们用角度除以时间来计算它。

你会经常遇到角速度恒定的情况。比如做匀速圆周运动的质点,虽然它的线速度不恒定(因为它的运动方向改变了),但是它的角速度是恒定的。另一个例子是一个匀速旋转的物体,比如地球。

角加速度

角加速度是角速度的变化率。它由希腊字母α (alpha)表示,其定义等式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

角加速度的定义意味着,如果角速度不变,角加速度为零。所以匀速圆周运动的物体,角加速度为零。这并不意味着它的线性加速度为零。实际上,线加速度不可能为零,因为运动的方向(因此线速度)是不断变化的。我们将在本章稍后回到这一点。

周期、频率和角速度

匀速圆周运动或旋转是周期运动的一种(见第三章中的“基础三角学”),因为物体以固定的间隔回到同一位置。事实上,正如在第三章中所讨论的,圆周运动与振荡、正弦运动有关。当时,我们定义了振荡的角频率、频率和周期,并表明它们与以下等式相关:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样的关系也适用于圆周运动,从图 3-16 中可以明显看出,图 3-16 说明了这两种运动。对于圆周运动,ω现在是角速度;它与相应正弦振荡的角频率相同。同样,f 是旋转的频率(每秒的转数)。最后,T 是旋转周期(完成一次完整旋转所需的时间)。

例如,让我们使用公式ω = 2π/T 来计算地球绕太阳运行的角速度。我们知道周期 T 约为 365 天。以秒为单位,它具有以下值:

t = 365×24×60×60s≈3.15×107s

因此,角速度具有以下值:

φ=2π/t =2π/(3.15×107)≈2.0×10–7rad/s

这是以弧度为单位的角度,地球每秒钟绕太阳旋转一周。

现在让我们用同样的公式计算地球绕轴旋转的角速度。在这种情况下,公转周期为 24 小时,因此 T 具有以下值:

T = 24 × 60 × 60 秒= 86400 秒

因此,角速度如下:

ω=2π/t =2π/86400 = 7.3×105rad/s

请注意,这是地球上每个点的角速度,包括地球表面和地球内部。这是因为地球是一个刚体,所以当它绕轴旋转时,每个点都在同一时间旋转相同的角度。但是,不同的点有不同的线速度。角速度和线速度有什么关系?是时候找出答案了!

角速度和线速度的关系

匀速圆周运动物体的线速度和角速度之间有一个简单的关系。为了得出这种关系,我们首先注意到,物体绕半径为 r 的圆移动一个角度δθ的位移δs 是旋转中心处对着该角度的弧的长度。因此,它由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是根据第三章给出的弧度定义得出的:一个弧度是一个长度等于圆半径的弧对着圆心的角度。所以,按比例,在中心成δθ弧度角的圆弧的长度一定是 rδθ(见图 9-3 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-3。

Relationship between length of arc and angle at center

现在,我们可以将这个等式两边除以物体移动通过该角度的时间间隔δt:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是根据定义,δs/δt = v 和δθ/δt =ω。因此,我们有以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个公式给出了物体以角速度ω在半径为 r 的圆周上运动的线速度。

它也适用于绕轴旋转的刚体。在这种情况下,它给出了距离旋转轴 r 处的线速度。对于给定的角速度ω,那么公式告诉我们,速度与离轴的距离成正比(离得越远,线速度越快)。

让我们将这个公式应用到上一节讨论的例子中。首先,我们将计算地球绕太阳运行的线速度。为了解决这个问题,我们需要知道地球轨道的半径(地球到太阳的距离):1.5 × 10 11 米。我们已经计算出了地球在其轨道上的角速度。它是 2.0×10–7弧度/秒

因此,地球的轨道速度由下式给出:

v = rω= 1.5×1011×2.0×10–7= 3×104m/s

这是 30 千米/秒,或 100 000 千米/小时,大约是飞机典型速度的 100 倍!

作为第二个例子,因为地球的半径是 6.4 × 10 6 米,其绕自身轴的角速度是 7.3×10–5rad/s(正如我们在上一节中计算出的),地球表面赤道上任意一点由于地球自转而产生的速度值如下:

v = rω= 6.4×106×7.3×10–5m/s≈470m/s

这大约是 1500 公里/小时,或者大约是飞机典型速度的一倍半。

示例:滚动的轮子

我们现在知道的足够多,可以把一些简单的例子放在一起。我们的第一个例子将是创造滚动的轮子。与前几章中的大多数例子不同,这一个将不涉及任何动力学,而仅涉及运动学。所以我们不会用力来计算运动,而只是通过直接指定速度来告诉物体如何运动。

首先,让我们创建一个轮子。我们希望轮子看起来如图 9-4 所示,有一个内轮缘和一个外轮缘,以及一些等间距的辐条(这样我们可以看到它旋转)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-4。

A rolling wheel

下面是一个完成这项工作的Wheel对象:

function Wheel(innerRadius,outerRadius,numSpokes){

this.ir = innerRadius;

this.or = outerRadius;

this.nums = numSpokes;

this.x = 0;

this.y = 0;

this.vx = 0;

this.vy = 0;

}

Wheel.prototype = {

get pos2D (){

return new Vector2D(this.x,this.y);

},

set pos2D (pos){

this.x = pos.x;

this.y = pos.y;

},

get velo2D (){

return new Vector2D(this.vx,this.vy);

},

set velo2D (velo){

this.vx = velo.x;

this.vy = velo.y;

},

draw: function (context) {

var ir = this.ir;

var or = this.or;

var nums = this.nums;

context.save();

context.fillStyle = '#000000';

context.beginPath();

context.arc(this.x, this.y, or, 0, 2*Math.PI, true);

context.closePath();

context.fill();

context.fillStyle = '#ffffaa';

context.beginPath();

context.arc(this.x, this.y, ir, 0, 2*Math.PI, true);

context.closePath();

context.fill();

context.strokeStyle = '#000000';

context.lineWidth = 4;

context.beginPath();

for (var n=0; n<nums; n++){

context.moveTo(this.x,this.y);

context.lineTo(this.x+ir*Math.cos(2*Math.PI*n/nums),this.y+ir*Math.sin(2*Math.PI*n/nums));

}

context.closePath();

context.stroke();

context.restore();

}

}

Wheel的构造函数有三个参数:内径、外径和辐条数。

现在我们将创建一个Wheel实例并让它滚动。从本书的可下载文件中,看看wheel-demo.js中的代码:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var r = 50;       // outer radius

var w = 1;        // angular velocity in radians per second

var dt = 30/1000; // timestep = 1/FPS

var fac = 1;      // slipping/sliding factor

var wheel = new Wheel(r-10,r,12);

wheel.x = 100;

wheel.y = 200;

wheel.draw(context);

var v = fac*r*w;     // v = r w

var angle = 0;

setInterval(onTimer, 1/dt);

function onTimer(evt){

wheel.x += v*dt;

angle += w*dt;

context.clearRect(0, 0, canvas.width, canvas.height);

context.save();

context.translate(wheel.x,wheel.y);

context.rotate(angle);

context.translate(-wheel.x,-wheel.y);

wheel.draw(context);

context.restore();

}

我们创建一个Wheel实例,然后设置一个调用onTimer()函数的setInterval()循环,该函数在每个时间步增加轮子的位置和角度方向。这是使用线速度v和角速度w通过以下公式完成的:

wheel.x += v*dt;

angle += w*dt;

第一个公式使用速度值v,该值是使用该公式预先计算的:

v = fac*r*w*;

这基本上是公式 v = rω,但包括了一个附加系数fac来模拟滑动或滑行(打滑)的影响,如果fac = 1为纯滚动。

第二个公式来自角速度的定义。变量angle表示车轮需要转动的角度。实际的转向由onTimer()中的其余代码处理。问题是没有办法旋转画布元素上的单个对象——您必须旋转整个画布!我们在下面的代码行中使用了context.rotate()方法:

context.rotate(angle);

然而,仅这样做将围绕画布原点(0,0)旋转画布,画布原点是画布元素的左上角。我们想围绕物体的中心旋转它。为了实现这一点,我们首先使用context.translate()方法移动画布,使其原点在旋转前位于对象的中心。旋转画布后,在绘制轮子之前,我们将画布平移回其原始位置:

context.translate(wheel.x,wheel.y);

context.rotate(angle);

context.translate(-wheel.x,-wheel.y);

wheel.draw(context);

请注意,整个代码块都包含在一对context.save()context.restore()命令中,以防止画布转换影响随后可能在同一画布上绘制的其他对象。

最后,dt是时间步长,被赋予 0.03 的值。这是对连续调用onTimer()的时间间隔的估计。我们期望dt大约等于 1/FPS,其中 FPS 是由setInterval()函数产生的帧速率,我们将其设置为 30。我们重申这只是近似的(参考第二章对setInterval()的讨论),但是对于这个简单的演示来说已经足够好了。

fac = 1运行代码,你会看到轮子移动,这样它的平移速度就和它的旋转速率一致了。这给人一种纯粹滚动的感觉,不会打滑。如果您将fac的值更改为小于 1,比如 0.2,车轮相对于其旋转速度向前移动的速度不够快;它看起来像陷在泥里,正在往下滑。如果你给fac一个大于 1 的值,比如说 5,轮子向前移动的速度会比它单纯旋转的速度快:它看起来像是在冰上打滑。

示例:围绕旋转地球的卫星

我们在这个例子中想要实现的本质上很简单:一个卫星在围绕旋转的地球的圆形轨道上的动画。问题是,我们希望卫星在轨道上运行,以便它总是在地球上的同一个地方。这被称为地球静止轨道;正如你可能猜到的,它对电信和间谍活动非常有用。

同样,这个例子只涉及运动学,不涉及动力学(没有力)。在这一章的后面,我们将加入力。

完整代码在文件satellite-demo.js中,在此复制:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var earth;

var satellite;

var r, x0, y0, omega;

var angle = 0;

window.onload = init;

function init() {

// create a stationary earth

earth = new Ball(70,'#0099ff',1000000,0,true,true);

earth.pos2D = new Vector2D(400,300);

earth.angVelo = 0.4;

earth.draw(context);

// create a moving satellite

satellite = new Satellite(8,'#0000ff',1);

satellite.pos2D = new Vector2D(600,300);

satellite.angVelo = earth.angVelo;

satellite.draw(context);

// set params

r = satellite.pos2D.subtract(earth.pos2D).length();

omega = earth.angVelo;

x0 = earth.x;

y0 = earth.y;

// make the satellite orbit the earth

t0 = new Date().getTime();

t = 0;

animFrame();

};

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

t += dt;

move();

}

function move(){

satellite.pos2D = new Vector2D(r*Math.cos(omega*t)+x0,r*Math.sin(omega*t)+y0);

angle += omega*dt;

context.clearRect(0, 0, canvas.width, canvas.height);

rotateObject(earth);

rotateObject(satellite);

}

function rotateObject(obj){

context.save();

context.translate(obj.x,obj.y);

context.rotate(angle);

context.translate(-obj.x,-obj.y);

obj.draw(context);

context.restore();

}

大部分代码都很简单,但是有一些值得注意的新特性。我们创建一个 E arth和一个satellite对象。这些物体的质量在这里是不相关的,因为没有动力学。您可以使用任何您想要的值,动画将以完全相同的方式运行。注意 E arthBall的一个实例,而satelliteSatellite的一个实例。后一个对象基本上类似于Ball,增加了在卫星上绘制三条线的代码,使其具有老式的类似 Sputnik 的外观。BallSatellite对象都有一个名为angVelo的新属性,它存储它们实例的角速度值。我们还在Ball中引入了一个可选的布尔参数line,如果设置为true,将在Ball实例上绘制一对交叉线。这样可以观察到球的旋转。

我们给地球一个非零的角速度。然后我们赋予卫星相同的角速度。卫星到地球的距离被计算出来并存储在变量r中。这个距离不变,因为轨道是圆形的。然后地球的角速度和 x、y 位置分别存储在变量omegax0y0中。

动作发生在move()方法中。我们在这里所做的就是告诉卫星它在任何时候的位置,使用这些公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可能会从第三章中回忆起,这样使用 cos 和 sin,会产生角频率为ω,半径为 r 的圆周运动,加上 x0 和 y0(地心坐标)就使卫星绕地球运行。另一个重要的是,ω选择为等于地球角速度。这使得卫星以与地球自转完全相同的速度绕地球旋转。运行模拟,你会发现事实确实如此。

此外,由于卫星绕其轴的角速度与地球相同,其天线始终指向地球(见图 9-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-5。

A spy satellite orbiting the Earth

产生地球和卫星旋转的代码在rotateObject()方法中,从前面的例子看应该很熟悉。注意,这里使用一个变量angle来计算两个对象在每个时间步必须旋转的角度。这是因为它们具有相同的角速度。

机警的读者会注意到,我们把这个例子称为动画,而不是模拟。这是因为我们基本上告诉卫星如何移动,而不是规定作用在它身上的力,让它们决定卫星的运动。虽然动画确实有一些物理真实感,但并不完全真实。一个原因是,我们已经迫使卫星以与地球绕其轴自转相同的速率(相同的角速度)绕地球旋转。你可以改变卫星离地球中心的距离,但它仍会以同样的速度运行。在现实中,卫星只能在离地球中心特定距离的地方这样做;换句话说,所有的地球同步卫星都必须与地球保持相同的距离!为什么会这样还不清楚,但是下一节将会帮助你澄清这一点,并帮助你在动画中构建这个特性。

向心加速度和向心力

既然我们已经讨论了基础知识,是时候继续讨论一些稍微难一点的概念了:向心加速度和向心力。实际上,它们并不难,但是经常被误解。因此,在我们进行的过程中,我们将试图澄清一些可能的混淆。

向心加速度

正如我们在角速度一节中所指出的,一个做圆周运动(或一般做曲线运动)的物体,其线速度是随时间变化的,因为它的方向一直在变化。因此,从加速度作为速度变化率的定义来看,这意味着它具有非零加速度——我们从未说过加速度仅仅意味着速度(速度)大小的变化;这也可能意味着方向的改变。

可能一点都不明显,但是对于做圆周运动的物体来说,这个加速度其实是指向圆心的。因此,它被称为向心加速度(来自拉丁语:centr = "center,"和 petere = "to seek ")。理解这一点的一种方法是将中心想象成拉动物体,从而不断改变其方向(见图 9-6 )。我们一会儿会看到如何计算向心加速度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-6。

Centripetal acceleration

向心加速度不能和角加速度混淆。正如本章前面所解释的,角加速度指的是角速度的变化率。在匀速圆周运动的情况下,角加速度为零是因为角速度不变(这就是“匀速”的意思),但向心加速度不为零。向心加速度是线速度变化率的一种度量,此时线速度仅改变方向,而其大小保持不变。

向心加速度、速度和角速度

我们如何计算向心加速度?有一个简单的公式。这不容易证明,所以最好的事情就是相信我们!下面是公式的一种形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

参考图 9-6 ,这个公式告诉我们一个物体以速度 v 做半径为 r 的圆周或圆弧运动时的向心加速度,由此我们推导出如果速度大,加速度也大(对于同样的半径);而如果半径小,加速度就大(对于同样的速度量级)。

记住 v = rω,您可以用 r 和ω替换前面公式中的 v,得出公式的另一种形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个公式现在告诉我们,角速度越大,向心加速度越大(半径相同)。但它也告诉我们,对于相同的角速度,半径越大,向心加速度越大。这与我们之前所说的公式的另一种形式并不矛盾,但乍一看似乎令人困惑!关键的一点是,这里的线速度的大小并没有被假定为不同半径的常数。事实上,如果角速度不变,那么线速度的大小一定随半径增加,因为 v = rω。

前面公式的一个结果是,对于绕轴旋转的刚体,离轴越远的点加速度越大(因为刚体上所有点的角速度都一样),速度也越大(因为 v = rω)。

向心力

我们已经看到,任何做匀速圆周运动的物体都必须经历一个朝向圆周轨迹中心的加速度。因此,根据牛顿第二定律,一定有一个合力也指向中心。这就是所谓的向心力。

向心力是使物体保持圆周运动所需的力。它需要由某个物理代理持续提供;否则,物体不能保持圆周运动。

举个例子,一个被绳子绕着旋转的物体从绳子的张力中获得向心力。围绕圆形轨道运动的汽车从地面对轮胎施加的摩擦力中获得向心力。由于太阳的引力,围绕太阳运行的行星受到向心力的作用。

解出向心力的公式非常容易。因为 F = ma,你只需要将向心加速度的公式乘以 m(运动物体的质量)就可以得到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了得到这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在向量形式中,第一个公式可以写成如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

或者可以这样写,其中 r u 是运动物体相对于中心的位置矢量 r 方向的单位矢量:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

负号的出现是因为力指向中心(与向量 r 相反)。类似的向量形式也适用于公式 F = mrω 2

向心力的常见误解

向心力可能是物理学中最容易被误解的概念之一。让我们澄清一些困惑。

  • 向心力不是一种独特的“类型”的力,就像重力和摩擦力是力的类型一样。它只是使一个物体做圆周运动所必须存在的力。不同的力可以起到向心力的作用,包括重力和摩擦力。
  • 有时人们会错误地谈论需要“平衡”向心力。例如,一个常见的误解是,围绕太阳运行的轨道上的行星的引力平衡了向心力。出于几个原因,这是没有意义的。第一,向心力一定是指向太阳的,引力也是,所以两者方向一致。第二,如果这两个力真的平衡了,行星上的合力将为零;因此,它的加速度为零,所以它必须以恒定速度沿直线运动(根据牛顿第一运动定律)。第三,向心力甚至不是一种“类型”的力。正确的思路是,引力提供了行星绕太阳运行所必需的向心力。
  • 你可能还听说过一个叫离心力的东西,它与向心力大小相同,但作用方向相反(远离旋转中心)。离心力不是真正的力,而是所谓的伪力。它起源于试图在牛顿定律的框架内分析运动,但是是从旋转参照系的角度。一个例子是从地球上的参考系(当然是旋转的)来描述大气运动。虽然离心力在分析这类问题的物理学中肯定有它的位置,但它也能引起许多混乱。因此,我们的建议是避免考虑离心力。在本章后面的“例子:汽车绕弯道行驶”一节中,我们给出了一个离心力的例子的向心力解释。

示例:重温卫星动画

作为向心力概念应用的第一个例子,让我们回到卫星动画,并像承诺的那样给它注入更多的真实感。

这里向心力是由卫星上的重力提供的。因此,我们可以将向心力的表达式与引力的表达式等同起来,以获得以下等式,其中 M 是卫星质量,M 是地球质量,r 是卫星到地心的距离,ω是卫星的角速度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很容易重新排列这个等式,得到如下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个公式告诉我们,为了以角速度ω绕轨道运行,卫星必须达到的半径。换句话说,卫星不可能在任何距离都有任何角速度;向心力决定了它必须以固定的角速度旋转,距离地球任意给定的距离!特别是,我们可以使用公式来计算它必须以与地球自转相同的角速度旋转的距离。如果你把 G、M(地球质量)和ω(我们之前算出的地球角速度)的值代入,你会发现地球静止轨道的半径是 42,400 公里,大约是地球半径的 6.6 倍。

回到我们的动画,我们需要让卫星意识到它与地球的距离和它绕地球的角速度之间的约束,如前面的等式所示。事实上,在动画中,我们将指定卫星的位置,从而指定到地球的距离 r,并希望计算出它的最终角速度ω,而不是将其设置为等于地球的角速度。

因此,我们需要一个用 r 表示的ω公式,利用上一个公式可以很容易地得出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是我们所需要的。我们复制了satellite-demo.js来创造satellite-demo2.js。如果您查看后一个文件,您会发现我们所做的主要更改是替换了这一行:

omega = earth.angVelo;

用下面一行:

omega = Math.sqrt(G*M/(r*r*r));

其中M是地球的质量(这里设为 1000000),引力常数G的值设为 1。我们现在也有单独的变量omegaEangleE来存储地球的角速度和角位移。

如果你现在用先前卫星和地球的位置和质量以及地球的角速度运行代码,你会看到卫星不再在地球静止轨道上。事实上,当它完成一个轨道时,它的触角已经指向错误的方向。如果使用前面的公式计算地球静止轨道的 r 值,给定值为 G = 1,M = 1000000,ω = 0.4,则 r = 184.2。这是卫星应该离地球中心的距离。假设地球位于(400,300),你只需要把卫星的位置换到 184.2 个单位以外的位置,比如satellite-demo2.js中的(584.2300):

satellite.pos2D = new Vector2D(584.2,300);

这样做,然后运行代码。你会看到卫星确实以和地球相同的角速度运行。

示例:重力作用下的圆形轨道

在本例中,我们回到完全动态模拟。事实上,我们已经在第六章的轨道模拟中做过了,我们模拟了一颗行星围绕太阳的运动。模拟已经“知道”万有引力是向心力,所以没有其他要补充的了。但是如果我们想要圆形轨道,我们可以用向心力公式,精确计算出我们需要给行星多大的速度。

同样,这只是一个向心力和引力表达式相等的问题。这次,我们用 F = mv 2 /r 的形式来表示向心力:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

稍加代数运算,我们就可以得出以下公式,这是行星在距离其中心 r 处围绕太阳运行的圆形轨道所需的速度大小:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们把这个编码到轨道模拟的修改版本中。新的源文件名为circular-orbits.js,这里显示了完整的代码:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var canvas_bg = document.getElementById('canvas_bg');

var context_bg = canvas_bg.getContext('2d');

var planet;

var sun;

var m = 1;       // planet's mass

var M = 1000000; // sun's mass

var G = 1;

var t0,dt;

var acc, force;

window.onload = init;

function init() {

// create a stationary sun

sun = new Ball(70,'#ff9900',M,0,true);

sun.pos2D = new Vector2D(400,300);

sun.draw(context_bg);

// create a moving planet

planet = new Ball(10,'#0000ff',m);

planet.pos2D = new Vector2D(400,50);

var r = planet.pos2D.subtract(sun.pos2D).length();

var v = Math.sqrt(G*M*m/r);

planet.velo2D = new Vector2D(v,0);

planet.draw(context);

// make the planet orbit the sun

t0 = new Date().getTime();

animFrame();

};

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

move();

}

function move(){

moveObject(planet);

calcForce();

updateAccel();

updateVelo(planet);

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

context.clearRect(0, 0, canvas.width, canvas.height);

obj.draw(context);

}

function calcForce(){

force = Forces.gravity(G,M,m,planet.pos2D.subtract(sun.pos2D));

}

function updateAccel(){

acc = force.multiply(1/m);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

大部分代码应该非常熟悉。这个模拟的新特点是,我们精确地给出了行星在圆形轨道上运动所需的初速度。这是在用粗体突出显示的两行中完成的,使用的是我们刚刚导出的公式外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用前面代码清单中给出的初始值运行模拟,您会发现行星确实描述了围绕太阳的圆形轨道。为了让自己相信这在离太阳任意远的地方都有效,将行星的初始位置改为如下:

planet.pos2D = new Vector2D(400,150);

这使得行星更靠近太阳。如果您现在运行代码,您将看到行星以更快的速度绕太阳运行,但轨道仍然是圆形的。

注意速度必须是切向的,没有径向分量(见图 9-7 )。如果有任何径向速度分量,行星要么靠近太阳,要么远离太阳(它的轨道不会是圆形的)。例如,当行星的初始位置为(400,50)时,尝试以下操作:

planet.velo2D = new Vector2D(v/Math.sqrt(2), v/Math.sqrt(2));

这给了行星一个与之前相同大小的初始速度v,但是方向不同,因此它有一个朝向太阳的径向分量和一个切向分量。你会发现行星的轨道现在是高度椭圆形的,行星离太阳非常近,然后又远离太阳。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-7。

Tangential velocity

鼓励你尝试不同的初始位置和速度,看看你得到的轨道类型。你应该会发现,如果不使用公式外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,通过简单的试错法,很难得到恰到好处的初始条件来产生甚至近似圆形的轨道!

示例:汽车在转弯处行驶

在下一个例子中,我们模拟了一辆汽车在环形轨道上的运动,并用它来演示汽车行驶过快时的打滑现象。

这里的基本物理原理是,当汽车绕着一个弯道或弯道行驶时,它需要这样做的向心力是由轮胎与路面接触时的摩擦力提供的(见图 9-8 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-8。

Friction provides the centripetal force for a car to negotiate a bend

回到向心力的公式,汽车移动得越快,保持它绕过弯道所需的向心力就越大:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

顺便说一句,该公式还表明,向心力更大的更尖锐的弯曲,弯曲半径较小。

摩擦力是静摩擦力,如第七章中所述,其最大值由 f = C s N 给出,其中 N 是法向力(这里等于汽车重量 mg),C s 是轮胎和路面之间的静摩擦系数。因此,如果通过弯道所需的向心力超过最大静摩擦力,汽车将无法跟随弯道的曲率——它将打滑。

我们现在构建一个模拟,可以以自然的方式处理这个场景。设置如图 9-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-9。

The skidding car simulation

下面是实现它的代码(在文件car-demo.js中):

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var canvas_bg = document.getElementById('canvas_bg');

var context_bg = canvas_bg.getContext('2d');

var center = new Vector2D(canvas.width/2,canvas.height/2);

var car;

var mass = 90; // car mass

var g = 10;

var Cs = 1;

var angle = 0;

var omega;

var t0,dt;

var acc, force;

window.onload = init;

function init() {

// create a circular track, e.g. a roundabout

context_bg.fillStyle = '#cccccc';

context_bg.beginPath();

context_bg.arc(canvas.width/2, canvas.height/2, 100, 0, 2*Math.PI, true);

context_bg.closePath();

context_bg.fill();

context_bg.fillStyle = '#ffffff';

context_bg.beginPath();

context_bg.arc(canvas.width/2, canvas.height/2, 50, 0, 2*Math.PI, true);

context_bg.closePath();

context_bg.fill();

// create a car

car = new Box(10,20,'#0000ff',mass);

car.pos2D = new Vector2D(center.x+75,center.y);

car.velo2D = new Vector2D(0,-10);

car.angVelo = -car.velo2D.length()/(car.pos2D.subtract(center).length());

omega = car.angVelo;

car.draw(context);

// make the car move

t0 = new Date().getTime();

animFrame();

};

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

move();

}

function move(){

moveObject(car);

calcForce();

updateAccel();

updateVelo(car);

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

angle += omega*dt;

context.clearRect(0, 0, canvas.width, canvas.height);

rotateObject(obj);

}

function rotateObject(obj){

context.save();

context.translate(obj.x,obj.y);

context.rotate(angle);

context.translate(-obj.x,-obj.y);

obj.draw(context);

context.restore();

}

function calcForce(){

var dist = car.pos2D.subtract(center);

var velo = car.velo2D.length();

var centripetalForce = dist.unit().multiply(-mass*velo*velo/dist.length());

var radialFriction = dist.unit().multiply(-Cs*mass*g);

if(radialFriction.length() > centripetalForce.length()) {

force = centripetalForce;

}

else{

force = radialFriction;

}

}

function updateAccel(){

acc = force.multiply(1/mass);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

如图所示,我们从init()方法开始,画一个圆形轨迹。下一步引入了一点艺术上的新奇。因为我们正在处理一辆汽车,一个Ball实例根本不行。相反,我们的车是一个Box实例。Box是一个本质上类似于Ball的类,除了它绘制一个填充的矩形而不是一个填充的圆形。Box有四个参数(widthheightcolormass),它们都有默认值,因此是可选的。将汽车创建为Box实例后,我们将它放置在轨道上,并给它一个初始切向速度。然后,下一条线通过使用公式ω = v/r,给出汽车的旋转角速度等于其“轨道”角速度,其中 v 是汽车的速度,r 是其距中心的距离。

代码的其余部分应该很容易理解。在calcForce()中,我们用向量形式的公式 F = mv 2 /r 计算向心力,用公式 F = C s mg 计算汽车上的最大摩擦力(见第七章),其中 m 为汽车质量。为简单起见,我们假设静摩擦力和动摩擦力是相同的。还有,我们假设小车上的合力是径向的摩擦力。if模块检查最大摩擦力是否足以提供向心力。如果是这样,合力就取为向心力相等;如果不是,合力等于最大摩擦力。

如果你以 10 或 20 px/s 的初速度运行模拟,你会发现赛车在赛道上运动没有问题。然而,如果你把车速提高到 30 英里以上,汽车就会打滑!

非匀速圆周运动

到目前为止,本章我们只讨论了匀速圆周运动。当角速度不恒定时,我们如何处理旋转?在最后一节中,我们将开始研究这个问题,并用一个例子来说明它。对非匀速转动的更全面的论述将涉及到对角动量和力矩等概念的深入研究。为此,你必须等待第十三章。

切向力和加速度

匀速圆周运动和非匀速圆周运动的根本区别在于,在前者中,向心力是唯一的力;而后者既有向心力又有切向力(见图 9-10 )。切向的字面意思是“沿着圆的切线”。非匀速圆周运动的向心力由与匀速圆周运动相同的公式给出。与向心力一样,切向力可能是由许多因素引起的,如重力或摩擦力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-10。

Tangential (Ft ) and centripetal (Fc) forces

注意向心力改变的只是运动的方向,这就是匀速圆周运动为什么是匀速的原因。但是因为切向力作用在速度矢量的方向上,所以它们会引起旋转物体速度的变化。这也意味着当存在切向力时,存在非零的角加速度。

底线:为了模拟非匀速圆周运动,我们需要提供向心力和切向分力的力,正如我们下一个也是最后一个例子所展示的。

例子:单摆

所谓的单摆由一个重锤组成,重锤用绳子吊在一个固定的枢轴上。通过轻微移动摆锤并释放摆锤,摆锤开始摆动。模拟单摆并不完全简单,需要一点思考来获得正确的物理原理。

图 9-11 显示了单摆的力图。绳索通常相对于垂直线倾斜一定角度θ。我们假设绳子的质量可以忽略不计,并且没有摩擦力或阻力。设 m 表示摆锤的质量。于是有两个力作用在摆锤上:重力垂直向下作用,大小为毫克;并且绳索中朝向固定枢轴作用的张力的大小为 t

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-11。

Geometrical and force diagram for a pendulum

显然,摆锤以枢轴为中心做圆弧运动。但是摆锤的角速度是不均匀的。事实上,当摆锤处于最高位置时,它是零,当摆锤处于最低点时,它是最大值。因此,有一个角加速度,因为有一个切向分力。因为张力是径向的(垂直于切向),所以没有切向分量。但是重力总的来说有一个切向分量,由 mg sin(θ)给出。这是引起角加速度的力。注意,因为角度θ随时间变化,所以切向力和角加速度不是恒定的。

提供向心力的径向分力是什么?从图 9-11 可以清楚地看出,重力有一个远离支点的分量 mg cos(θ)。因此,净径向力分量为 T–mg cos(θ),它必须等于向心力:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,这意味着张力 T 的大小由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从图 9-11 中可以清楚地看出,张力的方向与摆锤相对于枢轴的位置矢量 r 相反。

聪明的部分来了。我们所要做的就是指定重力 mg 和张力 T,就像上一个等式给出的那样。然后,一旦我们给它一个初始位置,摆锤应该知道如何在这两个力的作用下运动。让我们继续编码吧。

代码在文件pendulum .js中:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var bob;

var pivot;

var displ = new Vector2D(0,0);

var center = new Vector2D(0.5*canvas.width,50);

var mass = 1;

var g = 100;

var lengthP = 200;

var initAngle = 30;

var t0, t, dt;

var acc, force;

var animId;

window.onload = init;

function init() {

// create a pivot

pivot = new Ball(2,'#000000');

pivot.pos2D = center;

pivot.draw(context);

// create a bob

bob = new Ball(10,'#333333',mass);

bob.mass = mass;

var relativePos = new Vector2D(lengthP*Math.sin(initAngle*Math.PI/180),lengthP*Math.cos(initAngle*Math.PI/180));

bob.pos2D = pivot.pos2D.add(relativePos);

bob.draw(context);

// make the bob move

t0 = new Date().getTime();

t = 0;

animFrame();

};

function animFrame(){

animId = requestAnimationFrame(animFrame,canvas);

onTimer();

}

function onTimer(){

var t1 = new Date().getTime();

dt = 0.001*(t1-t0);

t0 = t1;

if (dt>0.2) {dt=0;};

t += dt;

move();

}

function move(){

context.clearRect(0, 0, canvas.width, canvas.height);

drawSpring(bob);

moveObject(bob);

calcForce(bob);

updateAccel();

updateVelo(bob);

}

function drawSpring(obj){

pivot.draw(context);

context.save();

context.strokeStyle = '#999999';

context.lineWidth = 2;

context.moveTo(center.x,center.y);

context.lineTo(obj.x,obj.y);

context.stroke();

context.restore();

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

obj.draw(context);

}

function calcForce(obj){

displ = obj.pos2D.subtract(center);

var gravity = Forces.constantGravity(mass,g);

var tension;

if (displ.length() >= lengthP) {

tension = displ.unit().multiply(-(gravity.projection(displ)+mass*bob.velo2D.lengthSquared()/lengthP));

}else{

tension = new Vector2D(0,0);

}

force = Forces.add([gravity, tension]);

}

function updateAccel(){

acc = force.multiply(1/mass);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

我们首先创建一个枢轴和一个摆锤。摆锤的位置应确保绳索与垂直线成 30 度的初始角度。为此,参考图 9-11 ,该图显示摆锤相对于枢轴的 x 和 y 坐标由下式给出,其中 l 是摆锤的“长度”,定义为摆锤中心到固定枢轴的距离:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

物理上,l 等于弦的长度和摆锤的半径之和。在代码中,我们用变量lengthP来表示。这个相对位置向量被计算并存储在变量relativePos中,然后被添加到枢轴的pos2D属性中以设置摆锤的位置。初始倾斜角度(以度为单位)由参数initAngle设定。

代码中动画部分的新颖之处基本上是在calcForce()方法中。我们用通常的方法计算重力。if代码块检查摆锤到枢轴的距离是否大于或等于绳索长度。如果是这样,那就意味着绳子是绷紧的,因此张力不为零;是用公式 T = mg cos(θ) + mv 2 /r 计算出来的,否则绳子没有拉紧,所以张力为零。在当前设置中,没有必要严格检查绳索是否拉紧,因为模拟从拉紧的绳索开始,并且一直保持拉紧状态。但是实现这种检查会使代码对未来可能的修改更加健壮。请注意,重力的径向分量 mg cos(θ)是使用漂亮的projection()方法计算的,我们将该方法引入到Vector2D对象中,以便vec1.projection(vec2)给出矢量vec1在矢量vec2方向上的投影。下面是projection()的定义:

function projection(vec){

var length = this.length();

var lengthVec = vec.length();

var proj;

if( (length == 0) || ( lengthVec == 0) ){

proj = 0;

}else {

proj = (this.x*vec.x + this.y*vec.y)/lengthVec;

}

return proj;

}

如果你运行代码并调整参数,你会发现如果绳子越短,g 值越大,钟摆摆动得越快。这在现实中是理所应当的。事实上,你可以通过比较振荡周期和理论值来测试你的模拟摆有多好。

物理学理论认为,对于小振幅的振荡,单摆的周期大致由下式给出,其中 l 是摆的长度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们在模拟中使用的值是 l = 200 和 g = 100。这样就得到 T = 2π,大约是 8.9 秒。检查模拟是否给出该值,例如,通过将时间输出到控制台并计时为 10 次振荡,然后将总时间除以 10。为了使这更容易,文件pendulum2.js包含了一些额外的代码,允许你通过点击鼠标来停止模拟。你应该得到一个非常接近 8.9 秒的值。正如你在图 9-12 中看到的,它几乎像真的一样工作!。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-12。

The pendulum simulation

摘要

本章介绍了处理圆周和旋转运动所需的一些物理学知识,重点是粒子和简单刚体旋转。本章为更详细地研究刚体转动奠定了基础,我们将在第十三章中介绍。这里介绍的一些概念也将在下一章——远程部队——中进一步应用和发展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值