15.5.4 创建太阳系的动画
至此,我们已经实现了动画库的所有类型和函数。虽然我们使用的是 F# Interactive,但是,把库的核心部分保存到一个单独的文件是个好主意(例如,Animations.fs)。然后,我们可以加载整个库,使用 #load 指令,将文件名作为参数。这种方式,也确保该内部类型扩展(包括我们添加到行为的运算符)正确加载,因为,内部类型扩展必须用相同的命令处理,作为它们扩展的类型。
动画的关键部分是对象彼此绕着旋转。我们刚刚创建的库可以把现有的基元组合成高级基元,解决特定的问题。我们可以把旋转封装到可重用的函数(或 C# 方法)中,稍后会用来描述仿真。这是一个精心设计的函数库的非常重要的属性,处理序列的函数按照相同的方式是可组合的。
我们新的基元实现动画围绕点 (0,0)、按指定的距离和速度旋转。清单 15.21 显示了这个实现。考虑到事情的复杂性,需要很少的代码。
Listing 15.21 Implementing rotation in F# and C#
// F# function
let rotate (dist:float32) speed img =
let pos = wiggle * dist.forever
img |> translate pos (wait 0.5f pos)
|> faster speed
// C# extension method
public static Behavior<IDrawing> Rotate
(this Behavior<IDrawing> img, float dist, float speed) {
var pos = Time.Wiggle * dist.Forever();
return img.Translate(pos, pos.Wait(0.5f))
.Faster(speed);
}
有点令人惊讶的是,我们只使用 translate 函数,就可以实现旋转。使用 wiggle 基元创建的运动是正弦波,这意味着,它给了我们旋转对象的一个坐标值。为获得第二个坐标值,我们需要延迟 0.5 秒的相位。如果使用余弦函数创建类似的基元,我们会得到希望的值。为了延迟行为,可以使用我们刚刚实现的 wait 函数。
我们可以使用流操作来指定完成动画应进行的操作顺序。指定旋转之后,还应用了 faster 函数,指定所需的旋转速度。在 C# 中,我们可以使用相同的编程风格,由于使用了扩展方法,把动画作为第一个参数,返回一个新动画作为结果。这是类似于在 LINQ 查询中应用多个运算符(过滤、投影、分组)。
使用这个基元来描述旋转,现在可以很容易地创建太阳系动画了。我们首先创建三个圆代表太阳、地球和月亮,然后,描述它们如何彼此围绕转动。图 15.6 显示了正在运行的动画,可以看到我们创建的内容。
图 15.6 运行中的太阳能系模拟 ;月球围绕地球旋转,它们两个围绕太阳旋转。
现在让我们看一下代码。清单 15.22 显示了两种语言的实现,因此,可以看到在 F# 和 C# 中,相关的构造如何彼此对应。
构造行星的代码非常简单。唯一值得注意的事情,是我们将使用一个圆基元来创建动画,因此,必须提供画笔和大小作为行为。这意味着,可以产生有趣的效果,例如,创建的太阳逐渐变大,并随着时间的推移更改颜色。
Listing 15.22 Creating solar system animation in F# and C#
// F# version
let sun = circle (forever Brushes.Goldenrod) 100.0f.forever
let earth = circle (forever Brushes.SteelBlue) 50.0f.forever
let moon = circle (forever Brushes.DimGray) 20.0f.forever
let planets =
sun --
(earth -- (moon |> rotate 40.0f 12.0f)
|> rotate 160.0f 1.3f)
|> faster 0.2f
// C# version
var sun = Anims.Circle(Time.Forever(Brushes.Goldenrod), 100.0f.Forever());
var earth = Anims.Circle(Time.Forever(Brushes.SteelBlue),50.0f.Forever());
var moon = Anims.Circle(Time.Forever(Brushes.DimGray), 20.0f.Forever());
var planets =
sun.Compose(
earth.Compose(moon.Rotate(50.0f, 12.0f))
.Rotate(150.0f, 1.0f))
.Faster(0.2f);
从旋转的对象来组合动画,更加有趣。我们先来从中间解释一下。使用 rotate 函数来创建围绕中心在 50 像素距离上旋转的月亮。再把这个动画与不旋转的地球组合起来,所以,结果就是月球绕地球旋转。这个结果的类型是一个动画,所以,我们可以再次启动,这一次它(组合的动画)在 150 像素的距离上旋转。当我们再把这个动画与太阳(不移动)组合起来,就得到了地球围绕太阳旋转的动画了。最后,我们使用 faster 基元,改变动画的速度。我们实际上降低了速度,因为我们使用的倍速数小于 1。注意在 F# 中,可以首先使用流操作符来写动画对象,然后,做各种转换。
我们实现的动画只有三个对象,但容易看出,如何添加其余的行星。框架的可组合性意味着,增量更改保持简单,即使随着总体结果变得越来越复杂。
更进一步的动画库
这个库还可以有很多有趣的补充。我们已经提到,可以添加更多绘图基元和转换。添加新基元的提升版本,将能够轻松地创建动画。还有其他更有趣的选项。
我们可以实现使用了其他上下文信息的行为。我们已经在 BehaviorContext 的类型中封装了上下文,因此,添加额外的信息将相当简单。我们可以在上下文中添加到当前光标位置,能够创建动画的形状追或逃离光标。
一个更复杂的扩展是能够创建非线性的动态系统。我们可以添加一个基元,告诉我们某些行为的值更改的速度有多快,可以根据其状态改变的速度,再用它来创建一个系统。
动画库提供了以函数式编程风格实现库的示例。你可能想知道,这种风格如何转化到更传统的业务应用程序的开发中。让我们通过另一个示例快速浏览一下,不会像讨论动画库一样详细,而是应该给你一个概念,即业务声明库(或 DSL)感觉可能像什么。