扩展 WPF 动画类
Charles Petzold
http://msdn.microsoft.com/msdnmag/issues/07/07/Foundations/Default.aspx?loc=zh#S4
Get the sample code for this article.
对 Microsoft® Windows® Presentation Foundation 中动画的支持大部分收集在 System.Windows.Media.Animation 命名空间中。这是一个大型命名空间,其中定义了 164 个类专用于设置 22 种特定数据类型的动画属性。这 22 种数据类型包括基本数据类型(如 byte、int 和 double),也包括更特殊的类型(如 Matrix、Point3D 和 Quaternion)。
Windows Presentation Foundation 中的动画资源毫无疑问是引人入胜的作品,但恐怕只有非编程人员才会认为这 22 种数据类型就足够满足各种应用程序。我个人经常开发希望对整个对象集合实现动画效果的应用程序,尤其是坐标点的集合。现有的 API 为我们提供了类(如 PointAnimation 和 Point3DAnimation)来制作单独的 2D 或 3D 坐标的动画,但我希望 PointCollectionAnimation 和 Point3DCollectionAnimation 类通过在两个集合的相应成员之间插入来制作整个坐标点集合的动画。
通常,我们可能会责怪 Microsoft .NET Framework(尤其是 Windows Presentation Foundation)使得定义对象集合如此轻松,使用起来如此便利,就像使用对象本身一样。Windows Presentation Foundation 已定义了 PointCollection 和 Point3DCollection 类,而且在 XAML 中,这些集合的使用方法非常简单。在 XAML 中,分配 Point 类型的属性和分配 PointCollection 类型的属性之间的差别就像键入“100 50”和“100 50, 40 25, 35 80, 50 100”之间的差别一样。因此,我想为什么使这些类型的属性具有动画效果不能像这般简单呢?
假定 PointCollectionAnimation 类将会使 PointCollection 类型的属性具有动画效果。Windows Presentation Foundation 中内置的五个类具有该类型的属性:Polyline、Polygon(两者均源于 Shape)、PolyLineSegment、PolyBezierSegment 和 PolyQuadraticBezierSegment(源于 PathSegment)五个类的属性均指定为 Points。通过使这些 Points 属性具有动画效果,您可以将图形数字从一个形状更改到另一个具有单个动画的形状。
据我所知,Point3DCollection 仅能在一个类中显示,即位于 Windows Presentation Foundation 3D 图形系统中心的 MeshGeometry3D 类。通过制作 Point3DCollection would 动画,可以非常轻松地实现 3D 对象变形(通常被认为是相当高级的 3D 编程任务)。
正是在计算机屏幕上切换、变形和改变 2D 和 3D 图的形状的想象促使我在扩展 Windows Presentation Foundation 动画类的道路上不断前进。
插入值
在 Windows Presentation Foundation 中,动画基本上为插值,通常情况下为线性插值。使新动画类编码复杂化的因素并非插值操作本身,而是确定要在其间插入的确切值。
我们来看一个涉及到 Point 值动画的简单示例。您可以在 XAML 中画一个实心圆形,如下所示:
<Path Fill=”Blue”> <Path.Data> <EllipseGeometry x:Name=”ellipse” RadiusX=”10” RadiusY=”10” /> </Path.Data> </Path>
在此标记中未指定 EllipseGeometry 的 Center 属性,这是 Point 类型的属性。您可以通过 PointAnimation 元素使 Center 属性具有动画效果,如下所示:
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” From=”10 10” To=”100 100” Duration=”0:0:3” />
Center 属性在三秒钟内从点(10,10)移动到点(100,100),这表示 PointAnimation 类基于这段时间在两个值之间执行线性插入。
您还可以在 EllipseGeometry 标记中指定 Center 属性,如下所示:
<EllipseGeometry x:Name=”ellipse” Center=”50 50” ...
此值称为基值,因为动画可以基于此值构建。您可以通过省略 PointAnimation 标记中的 From 或 To 属性来充分利用此基值,如以下标记中所示:
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” To=”100 100” Duration=”0:0:3” />
现在该动画从 EllipseGeometry 标记中指定的中心点(50,50)开始,到点(100,100)结束。允许使用以下语法:
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” From=”100 100” Duration=”0:0:3” />
现在该动画从点(100,100)开始,到点(50,50)结束。如果 From 或 To 属性缺失,而且 EllipseGeometry 标记未指定 Center 属性,则该动画将采用 Center 属性的默认值,即(0,0)。
好像越来越乱了。To 属性的一个备选项是 By 属性,即起始点的增量。
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” From=”100 100” By=”125 125” Duration=”0:0:3” />
现在此动画在(100,100)到(225,225)之间移动。但无需显示 From 属性。
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” By=”125 125” Duration=”0:0:3” />
现在该动画从 EllipseGeometry 标记中指定的点(50,50)开始,到点(175,175)结束。
动画的基值通常是最初为需要动画效果的属性定义的值,但不必一定是该值。请考虑以下两个顺序 PointAnimation 对象:
<PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” To=”100 100” Duration=”0:0:3” /> <PointAnimation Storyboard.TargetName=”ellipse” Storyboard.TargetProperty=”Center” BeginTime=”0:0:5” Duration=”0:0:3” />
在前三秒内,初始动画将 Center 属性从其基值(50,50)更改到 To 值(100,100),在此处,该值再停留两秒钟,直到第二个动画到达。此动画完全没有 From、To 或 By 属性!该动画开始于基值(100,100),结束于 EllipseGeometry 标记中指定的值(50,50)。
您可以看到,一个类(如 PointAnimation)具有两个默认值用于动画开始和结束;如果没有为特定动画对象定义 From、To 和 By 属性,则动画将使用这些默认值。这些类还定义了两个 Boolean 属性,分别为 IsAdditive(使动画值添加到基值)和 IsCumulative(如果动画不断重复,则累积动画值)。这些属性均无法使动画在逻辑上更加简单。
类的结构
各种动画类通常通过名称(如 <Type>AnimationBase,它指示名为 DoubleAnimationBase、PointAnimationBase 等的 22 个类)被集体引用。但不要根据此方便的表示法就认为动画类将按此泛型实现;事实上它们并非如此,因为每个类要根据具体数据类型执行计算。图 1 显示了动画类常见的类层次结构(尽管 22 种动画类型各有不同)。
如果您决定要编写使自定义类型具有动画效果的类,则可能要模仿现有的动画类的结构。通常,您首先定义抽象的 <Type>AnimationBase 类,从该类您可能会生成一个纯 <Type>Animation 类以基于线性插值执行动画操作。我希望使 PointCollection 类型的属性具有动画效果,所以我从名为 PointCollectionAnimationBase 的类开始。该类从 AnimationTimeline 派生,并且定义为抽象。您将需要重写只读 TargetPropertyType,并返回 typeof(PointCollection)。
在您的新 <Type>AnimationBase 类中,您还需要重写由 AnimationTimeline 定义的 GetCurrentValue 方法:
public override object GetCurrentValue( object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock) { ... }
这就是您的类必须为动画计算插入值的方法。参数名称即为文档中使用的名称。前两个参数提供了动画将开始和结束的默认基值。在缺少明确的 From、To 和 By 设置时,需要使用这些参数。AnimationClock 对象包括名为 CurrentProgress 的 double 类型的属性,其值范围为 0 到 1。这是您用于插入的值。
因为您已重写了 TargetPropertyType 属性,所以 WPF 知道您要使其具备动画效果的对象的类型。如果您的类要使 PointCollection 类型的对象具有动画效果,则 defaultOriginValue 和 defaultDestinationValue 参数将为 PointCollection 类型,并且 WPF 预期此方法会返回一个 PointCollection 类型的对象。
如果您希望模拟现有的动画类,则 GetCurrentValue 只需将其参数转换到正确的类型,然后调用此类中定义的另一个 GetCurrentValue 方法即可,此方法接下来将调用名为 GetCurrentValueCore 的方法。为了使 PointCollection 对象具有动画效果,代码与以下类似:
public PointCollection GetCurrentValue( PointCollection defaultOriginValue, PointCollection defaultDestinationValue, AnimationClock animationClock) { return GetCurrentValueCore(defaultOriginValue, defaultDestinationValue, animationClock); }
然后,GetCurrentValueCore 方法被定义为受保护和抽象:
protected abstract PointCollection GetCurrentValueCore( PointCollection defaultOriginValue, PointCollection defaultDestinationValue, AnimationClock animationClock);
现在从 PointCollectionAnimationBase 派生的任何非抽象类均需要重写此 GetCurrentValueCore 方法并提供实际代码以便插入。
此栏的可下载代码包含一个带有五个项目的 Visual Studio® 解决方案。其中一个项目是名为 Petzold.AnimationExtensions 的 DLL,包含本文中所述的使 PointCollection 和 Point3DCollection 类型的对象具有动画效果所需的类。此 Visual Studio 解决方案中的其他四个项目是利用该 DLL 的演示程序。PointCollectionAnimationBase 类是 DLL 的组成部分,并且在名为 PointCollectionAnimationBase.cs 的文件中实现。该 DLL 中的其他所有类和接口也在以该类或接口命名的文件中。
冻结
所有 <Type>AnimationBase 类均从 AnimationTimeline 继承,AnimationTimeline 通过 Animatable 和 Timeline 从 Freezable 类派生。事实上,在一定程度上,Petzold.AnimationExtensions DLL 中的所有类均从 Freezable 派生而来,此事实将产生非常重要的结果。
从 Freezable 派生的类都具有一些非常特殊的要求。忽略这些要求,后果请自负。如果继续操作,您可能会遇到某些问题:不能正常工作而且不知道原因,或者遇到奇怪的异常但找不到解决实际问题的线索。
从 Freezable 派生的每个非抽象类必须重写 CreateInstanceCore 方法。一般来说,此方法需要做的是调用类的构造函数。Freezable 将此方法定义为抽象,因此如果您未包括该方法,编译器将提醒您。但是,仅在您的非抽象类是 Freezable 层次结构中的第一个非抽象类时,编译器才会提醒您未包括该方法。如果您要从非抽象类派生,则很容易会忘记 CreateInstanceCore。
此外,从 Freezable 派生的类应支持其所有具有相关属性的属性。这不是一项要求,但如果不这样做,则还需要重写其他五种方法并了解有关克隆和冻结的所有信息。
要执行常见的线性插入动画,您应创建一个从 <Type>AnimationBase 派生的 <Type>Animation 形式的类。(对于此示例,该类是 PointCollectionAnimation,并且它在两个 PointCollection 对象之间插入相应点。)此类必须重写 CreateInstanceCore 方法,并且应定义相关属性支持的名为 From、To 和 By 的属性。您还应定义 IsAdditive 和 IsCumulative 属性,这两个属性的相关属性已由 AnimationTimeline 定义。
<Type>Animation 的最大任务在于重写 GetCurrentValueCore。最后一个参数是 AnimationClock 类型的对象,并且 CurrentProgress 属性是一个介于 0 到 1 之间的数字,指示动画的进度。此值将涉及到许多属性,如由 Timeline 类定义和 <Type>Animation 继承的 AccelerationRatio、DecelerationRatio、AutoReverse 和 SpeedRatio。不过,GetCurrentValueCore 必须首先确定要在哪些属性之间插入值。例如,如果已设置 From 属性,但尚未设置 To 和 By 属性,则插入将发生在该方法的 From 和 defaultDestinationValue 参数之间。
因此,了解何时在代码或标记中设置 From、To 和 By 以及何时未设置这些属性对 GetCurrentValueCore 来说很重要。大多数现有的 Windows Presentation Foundation 动画基于值类型,如 Double 或 Point 结构。但是,DoubleAnimation 类如何确定 From 属性是采用默认值 0,还是已专门将其设置为 0 呢?
简单方法:在 <Type> 为值类型的所有 <Type>Animation 类中,From、To 和 By 属性均定义为空类型。(我之前从未注意到这些方面,因为除了设置这些属性外,我从未想到去做其他事。)相关属性定义会将默认值设置为空,从而使 GetCurrentValueCore 可以轻松确定是否已设置该属性。
对于 PointCollectionAnimation 等类,具有动画效果的对象是引用类型,所以如果尚未设置 From、To 和 By 属性,它们也将为空。不过,使引用类型具有动画效果还会涉及到其他更加杂乱的问题。尤其是,在每次调用 GetCurrentValueCore 过程中您要做的最后一件事是进行内存分配。因此,您的类应创建一个具有动画效果的类型的对象作为字段,并从 GetCurrentValueCore 方法返回该对象。
实际上,我发现处理 PointCollectionAnimation 时,在后续调用 GetCurrentValueCore 过程中仅返回相同的对象是不够的。无论谁位于 GetCurrentValue wants 的接收端,都希望接收到一个完全不同的对象。因此,我创建了两个 PointCollection 对象作为字段(命名为 ptsDst1 和 ptsDst2,因为它们都是要插入的目标),并基于名为 flip 的布尔字段在后续调用 GetCurrentValueCore 过程中在两个字段之间来回切换。此外,我还创建了一个名为 ptsTo 的 PointCollection 对象作为字段,来存储该方法将插入到的点的集合。由于 By 属性会导致此集合成为两个集合的总和,所以此独立的集合是必要的。
图 2 中显示了 PointCollectionAnimation 中 GetCurrentValueCore 的实现。
对于任何特定的动画,对 GetCurrentValueCore 的前两次调用可能会产生一些内存分配,因为已为 PointCollection 字段分配了足够的空间来存储所有的点。不过,一旦它们达到了适当大小,该动画应会继续而不再进一步分配内存。
仅使用 PointCollectionAnimation 来转换、缩放或旋转一组点毫无意义可言,因为这些操作可通过正常的转换类来执行。此类在该规则之外的非线性转换中非常有用。例如,图 3 中的 SquaringTheCircle.xaml 文件使 PointCollection 具有动态效果,以便图形从正方形转换为相等面积的圆形,然后再转换回正方形,因此解决了自欧氏时代以来一直使西方文明感到困惑的几何问题。
在我的 WPF 书籍中,功能完全相当的程序实际上需要 13 个单独的 Point 动画。我当然更喜欢雅致的新版本。
文本变形
TextMorphCheckBox 项目为 CheckBox 控件定义了一个模板,它使用复合线元素引用定义为模板资源的两个 PointCollection 对象之一覆盖 CheckBox 内容(如果存在)。单击 CheckBox 时,它将在两个 PointCollection 对象之间播放动画。图 4 中显示了大量 ControlTemplate 标记。
此动画中所用的 2 个 PointCollection 对象包含对脚本字体中词语“否”和“是”的定义。通过在这两个集合之间实现动画,文本可在一秒钟内从一个词语转换为另一个词语,如图 5 所示。
图 5a 变形文本复选框
图 5b
图 5c
图 5d
图 5e
图 5f
在文本和动画变形过程中可以单击 CheckBox 来逆转自身,这正好说明 PointCollectionAnimation 类正在正确处理传送到 GetCurrentValueCore 方法的参数。
下面我来说明 TextMorphCheckBox 模板中的两条复合线从何而来。在“Rock Me Amadeus”曾火爆的遥远年代里使用 Windows 的某些人可能会记得 Windows 中曾使用过最初级的矢量字体。在 Windows Vista™ 中,这些字体依然存在,并可在文件 Modern.fon、Roman.fon 和 Script.fon 中找到。这些字体中的字符仅通过直线段来定义,并且通常与绘图仪一起使用。就像在 TrueType 字体中一样,没有曲线、没有提示,而且没有填充。您可以在记事本中选择这些早期的矢量字体,但 Microsoft Word 可能会拒绝将这些过时的东西插入其精美的文档中。
如果您对字体文件格式稍有了解,便可以轻松打开这些字体文件,并提取出定义每个字符的复合线。执行该操作后,我手动调整了单个字符的坐标以便为词语 No 和 Yes 创建单条连续的复合线,然后再加以调整以确保两个 PointCollection 对象拥有相同数量的点。我曾想通过插入某些控制点将这些坐标转换为一系列的二次 Bezier 曲线,但无需这样做。只需合适的粗粗一笔,这些复合线便看起来实现了这个古怪的目的。
关键帧动画
使用 Windows Presentation Foundation 动画的编程人员都明白,相对于用途更加丰富的 <Type>AnimationUsingKeyFrames 类来说,<Type>Animation 类的确只是一个简单的备选项。关键帧动画使您能够将一系列离散的跳转、线条动画和基于 Bezier 曲线段减慢或加速的动画混合在一起。每个关键帧均包含时间和值。动画在基值(通常为上一个关键帧的结束值)和关键帧值之间插入。
我预想实现一个 PointCollectionAnimationUsingKeyFrames 类将颇富挑战性,但却并不会因此沮丧。第一步是派生一个从 Freezable 继承的抽象 PointCollectionKeyFrame 类,并实现 IKeyFrame 接口。这需要定义您希望相关属性支持的 KeyTime 和 Value 属性。我通过在 PointCollectionKeyFrame 中定义名为 InterpolateValue 的公共方法模拟了现有的关键帧动画类:
public PointCollection InterpolateValue( PointCollection baseValue, double keyFrameProgress) { return InterpolateValueCore(baseValue, keyFrameProgress); }
此外,还定义了受保护和抽象的 InterpolateValueCore 方法。请注意第二个参数是介于 0 和 1 之间的进度值,但它是特定关键帧的进度值,而不是整个动画的进度值。
下一步是从 PointCollectionKeyFrame 派生三个类,分别为 DiscretePointCollectionKeyFrame、LinearPointCollectionKeyFrame 和 SplinePointCollectionKeyFrame。这些类将重写 InterpolateValueCore 方法以在基值参数和 Value 属性之间执行插入。SplinePointCollectionKeyFrame 还定义了名为 KeySpline 的属性。再一次,我为结果定义了两个名为 ptsDst1 和 ptsDst2 的字段和一个名为 flip 的布尔字段。我发现如果首先从包含图 6 中所示的 InterpolateValueCore 方法的 PointCollectionKeyFrame 派生一个抽象的 NonDiscretePointCollectionKeyFrame 类,则可以避免某些重复性代码。
请注意该方法将调用名为 GetProgress 的方法,NonDiscretePointCollectionKeyFrame 将该方法定义为抽象。LinearPointCollectionKeyFrame 和 SplinePointCollectionKeyFrame 均将重写此方法。LinearPointCollectionKeyFrame 仅返回参数;SplinePointCollectionKeyFrame 返回一个由 KeySpline 结构定义的方法方便提供的值,KeySpline 结构可将线性进度值转换为基于 Bezier 曲线的进度。
接下来,您需要一个 <Type>KeyFrameCollection 形式的类,它是 <Type>KeyFrame 对象的可冻结集合。在我所举的示例中,此集合类有一个相当易混淆的名称 PointCollectionKeyFrameCollection,它是 PointCollectionKeyFrame 对象的集合。通过将此集合类定义为从泛型 FreezableCollection<T> 类继承,您可以节省大量的工作。不要忘记:从 FreezableCollection<T> 派生时,您必须重写 CreateInstanceCore。
public class PointCollectionKeyFrameCollection : FreezableCollection<PointCollectionKeyFrame> { // CreateInstanceCore override required when // deriving from Freezable protected override Freezable CreateInstanceCore() { return new PointCollectionKeyFrameCollection(); } }
最后亦即最难的一步是从 <Type>AnimationBase 派生 <Type>AnimationUsingKeyFrames 类,以及通过定义 <Type>KeyFrameCollection 类型的 KeyFrame 属性(及相关属性)实现 IKeyFrameAnimation 接口。<Type>AnimationUsingKeyFrames 类将重写 GetCurrentValueCore 方法,但必须确定实际上关键帧集合中的哪些对象应执行插入。
在处理此类时,我发现 IsCumulative 属性用途不是很大,但在基于单个点累积动画方面可能具有一定意义的实用性。我定义了一个名为 AccumulationPoint 的新属性(及其相关属性),它是在每次迭代过程中添加到该集合的单个点。这样,我就能够创建一个称为 StickFigureWalking 的动画。该动画基于 5 个 PointCollection 对象和 5 个具有累积点的关键帧,使简笔画可以在屏幕上走动,如图 7 所示。
图 7 走动
制作 3D 点动画
创建您自己的动画类时,您可以模拟现有的类(就像我制作 PointCollection 动画一样),也可以放弃该范例,生成自己的类。这就是我使用 Point3DCollection 类型的对象制作动画所做的操作,Point3DCollection 类型是 MeshGeometry3D 的 Positions 属性的数据类型。我定义了一个普通的 Point3DCollectionAnimationBase 类,但接下来还要制定一个使用名为 Point3DCollectionAnimationUsingDeformer 的类的不同策略。Point3DCollectionAnimationUsingDeformer 仅定义一个 IDeformer 类型的名为 Deform 的属性,它是只定义一种方法的接口:
Point3DCollection Deform(Point3DCollection points, double progress);
Deform 方法接受 3D 点的集合和介于 0 到 1 之间的进度值,并返回一个 3D 点的集合。目的是允许执行标准类无法完成的非线性转换。Deform 如何精确地对一组点执行转换仅受限于您的想象,或许还受限于您的数学技能。
我提供了一个示例:使 3D 对象扭曲变形的名为 Twister 的类。扭曲与 3D 编程人员使用的标准 AxisAngleRotation3D 转换类似,它涉及绕轴旋转,不过特定点的旋转角度随该点到中心点的距离不同而变化。
Twister 类实现 IDeformer 接口,并自己定义若干属性。Axis 和 Center 属性指示以矢量表示的旋转轴以及旋转中心。MinAngle 和 MaxAngle 属性分别指定进度值为 0 和 1 时的旋转角度。这些角度是沿旋转轴的每单位距离的角度。例如,假设 Axis 矢量穿过中心点。水平点(包括中心点和与轴垂直相交的点)根本不会旋转。距中心点一个单位距离的水平点在 MinAngle 和 MaxAngle 之间旋转。距中心点两个单位距离的水平点在两倍 MinAngle 和两倍 MaxAngle 之间旋转。在中心点的另一方,点将反向旋转。
图 8 显示了 Twister 类中的 Deform 方法。请注意,该方法将在目标集合的两个早期创建的 Point3DCollection 类之间翻转,还使用早期创建的名为 xform 的 RotateTransform3D 对象以及名为 axisRotate 的 AxisAngleRotation3D 对象。
真正的乐趣在于寻找要扭曲的对象。TeapotTwister 项目包括一个 TeapotMesh.cs 文件,它会为无数 3D 演示中使用的著名 Iowa Teapot 生成一个 MeshGeometry3D 对象。我根据 DirectX® 9.0 中静态 Mesh.Teapot 方法中提供的数据创建了此文件。图 9 显示了引用 TeapotMesh 类、Point3DCollectionAnimationUsingDeformer 类和 Twister 的完整 XAML 文件。扭曲的茶壶如图 10 所示。
图 10 扭曲的茶壶
XAML 文件显示了 Point3DCollectionAnimationUsingDeformer 的属性元素中引用的 Twister 类,但也可以将 Twister 类定义为资源:
<pae:Twister x:Key=”twister” Axis=”1 0 0” />
然后,可以使用 Point3DCollectionAnimationUsingDeformer 标记中的属性引用它:
Deformer=”{StaticResource twister}”
虽然动态修改 MeshGeometry3D 的 Positions 集合是一项强大的技术,但理论上并非完全足够。无论对 MeshGeometry3D 的 Positions 集合中的 Point3D 对象进行哪些非线性转换,还应将这些转换应用到 Normals 集合中的 Vector3D 对象。虽然在 Point3D 和 Vector3D 之间定义了显式转换,但这些对象组成的集合之间却不存在转换。这似乎暗示需要创建类的完整的平行结构才能为 Vector3DCollection 对象实现动画效果。
即使没有该增强功能,这些新的动画类也已满足了我在编程方面的基本目的之一:在计算机上进行可视化观看(如变形文本和扭曲茶壶),这些都是我以前从未看到过的。
图2
protected override PointCollection GetCurrentValueCore( PointCollection defaultOriginValue, PointCollection defaultDestinationValue, AnimationClock animationClock) { // Let’s hope this doesn’t happen too often if (animationClock.CurrentProgress == null) return null; double progress = animationClock.CurrentProgress.Value; int count; // Set ptsFrom from From or defaultOriginValue PointCollection ptsFrom = From ?? defaultOriginValue; // Set ptsTo from To, By, or defaultOriginValue ptsTo.Clear(); if (To != null) { foreach (Point pt in To) ptsTo.Add(pt); } else if (By != null) { count = Math.Min(ptsFrom.Count, By.Count); for (int i = 0; i < count; i++) ptsTo.Add(new Point(ptsFrom[i].X + By[i].X, ptsFrom[i].Y + By[i].Y)); } else { foreach (Point pt in defaultDestinationValue) ptsTo.Add(pt); } // Choose which destination collection to use PointCollection ptsDst = flip ? ptsDst1 : ptsDst2; flip = !flip; ptsDst.Clear(); // Interpolate the points count = Math.Min(ptsFrom.Count, ptsTo.Count); for (int i = 0; i < count; i++) { ptsDst.Add(new Point((1 - progress) * ptsFrom[i].X + progress * ptsTo[i].X, (1 - progress) * ptsFrom[i].Y + progress * ptsTo[i].Y)); } // If IsAdditive, add the base values to ptsDst if (IsAdditive && From != null && (To != null || By != null)) { count = Math.Min(ptsDst.Count, defaultOriginValue.Count); for (int i = 0; i < count; i++) { Point pt = ptsDst[i]; pt.X += defaultOriginValue[i].X; pt.Y += defaultOriginValue[i].Y; ptsDst[i] = pt; } } // Take account of IsCumulative if (IsCumulative && animationClock.CurrentIteration != null) { int iter = animationClock.CurrentIteration.Value; for (int i = 0; i < ptsDst.Count; i++) { Point pt = ptsDst[i]; pt.X += (iter - 1) * (ptsTo[i].X - ptsFrom[i].X); pt.Y += (iter - 1) * (ptsTo[i].Y - ptsFrom[i].Y); ptsDst[i] = pt; } } // Return the PointCollection return ptsDst; }
Figure 3 SquaringTheCircle.xaml
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:pae=”clr-namespace:Petzold.AnimationExtensions; assembly=Petzold.AnimationExtensions” x:Class=”SquaringTheCircle.SquaringTheCircle” Title=”Squaring the Circle”> <Canvas RenderTransform=”2 0 0 -2 300 300”> <Path StrokeThickness=”3” Stroke=”Blue” Fill=”AliceBlue”> <Path.Data> <PathGeometry> <PathFigure x:Name=”fig” IsClosed=”True”> <PolyBezierSegment x:Name=”seg” /> </PathFigure> <PathGeometry.Transform> <RotateTransform Angle=”45” /> </PathGeometry.Transform> </PathGeometry> </Path.Data> </Path> </Canvas> <Window.Triggers> <EventTrigger RoutedEvent=”Window.Loaded”> <BeginStoryboard> <Storyboard RepeatBehavior=”Forever” AutoReverse=”True”> <PointAnimation Storyboard.TargetName=”fig” Storyboard.TargetProperty=”StartPoint” From=”0 100” To=”0 125” /> <pae:PointCollectionAnimation Storyboard.TargetName=”seg” Storyboard.TargetProperty=”Points” From=” 55 100, 100 55, 100 0, 100 -55, 55 -100, 0 -100, -55 -100, -100 -55, -100 0, -100 55, -55 100, 0 100” To=” 62.5 62.5, 62.5 62.5, 125 0, 62.5 -62.5, 62.5 -62.5, 0 -125, -62.5 -62.5, -62.5 -62.5, -125 0, -62.5 62.5, -62.5 62.5, 0 125” /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> </Window>
Figure 4 TextMorphCheckBox.xaml 摘录
<ControlTemplate x:Key=”templateTextMorph” TargetType=”{x:Type CheckBox}”> <ControlTemplate.Resources> <PointCollection x:Key=”textYes”> 5 11, 3 10, 2 8, 2 7, 3 5, 5 4, 6 4, 8 5, 9 7, 9 9, 8 13, 7 16, 6 20, 6 22, 7 24, 8 25, 10 25, 12 24, 14 22, 16 19, 17 17, 19 11, 21 4, 19 11, 16 21, 14 27, 12 32, 10 36, 8 37, 7 36, 7 34, 8 31, 10 28, 13 26, 17 25, 23 23, 25 22, 26 21, 27 19, 27 17, 26 16, 25 16, 23 17, 22 19, 22 22, 23 24, 25 25, 27 25, 29 24, 30 23, 32 20, 34 17, 35 15, 35 17, 37 20, 38 22, 38 24, 36 25, 32 24, 34 25, 38 25, 40 24, 41 23, 43 20 </PointCollection> <PointCollection x:Key=”textNo”> 5 11, 3 10, 2 8, 2 7, 3 5, 5 4, 6 4, 8 5, 9 7, 9 8, 9 9, 8 14, 7 18, 5 25, 7 18, 10 10, 11 8, 12 6, 13 5, 15 4, 17 4, 19 5, 20 7, 20 8, 20 9, 19 14, 17 21, 17 24, 18 25, 19 25, 21 24, 22 23, 24 20, 25 18, 26 17, 28 16, 29 16, 30 16, 29 16, 28 16, 26 17, 25 18, 24 20, 24 21, 24 22, 25 24, 27 25, 28 25, 29 25, 31 24, 32 23, 33 21, 33 20, 33 19, 32 17, 30 16, 29 17, 29 18, 29 19, 30 21, 32 22, 35 22, 37 21, 38 20 </PointCollection> </ControlTemplate.Resources> ... <!-- This Border displays the text --> <Border> <Border.Background> <VisualBrush Stretch=”Uniform”> <VisualBrush.Visual> <Polyline Name=”polyline” Stroke=”{TemplateBinding Foreground}” StrokeThickness=”2” StrokeStartLineCap=”Round” StrokeEndLineCap=”Round” StrokeLineJoin=”Round” Points=”{StaticResource textNo}” /> </VisualBrush.Visual> </VisualBrush> </Border.Background> </Border> ... <!-- Triggers convert text from No to Yes and back --> <ControlTemplate.Triggers> <EventTrigger RoutedEvent=”CheckBox.Checked”> <BeginStoryboard> <Storyboard TargetName=”polyline” TargetProperty=”Points”> <pae:PointCollectionAnimation To=”{StaticResource textYes}” /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent=”CheckBox.Unchecked”> <BeginStoryboard> <Storyboard TargetName=”polyline” TargetProperty=”Points”> <pae:PointCollectionAnimation To=”{StaticResource textNo}” /> </Storyboard> </BeginStoryboard> </EventTrigger> ... </ControlTemplate.Triggers> </ControlTemplate>
图6
Figure 6 InterpolateValueCore 方法
protected override PointCollection InterpolateValueCore( PointCollection baseValue, double keyFrameProgress) { // Choose which destination collection to use. PointCollection ptsDst = flip ? ptsDst1 : ptsDst2; flip = !flip; ptsDst.Clear(); int num = Math.Min(baseValue.Count, Value.Count); double progress = GetProgress(keyFrameProgress); for (int i = 0; i < num; i++) ptsDst.Add(new Point( (1 - progress) * baseValue[i].X + progress * Value[i].X, (1 - progress) * baseValue[i].Y + progress * Value[i].Y)); return ptsDst; }
Figure 8 Twister 中的 Deform 方法
public Point3DCollection Deform(Point3DCollection pointsSrc, double progress) { // Clear destination collection Point3DCollection pointsDst = flip ? pointsDst1 : pointsDst2; flip = !flip; pointsDst.Clear(); // Prepare RotateTransform3D object using AxisAngleRotation3D xform.CenterX = Center.X; xform.CenterY = Center.Y; xform.CenterZ = Center.Z; // Prepare AxisAngleRotation3D object axisRotate.Axis = Axis; Vector3D axisNormalized = Axis; axisNormalized.Normalize(); double angleAttenuated = MinAngle + progress * (MaxAngle - MinAngle); // Transform each point based on its distance from the center foreach (Point3D point in pointsSrc) { axisRotate.Angle = angleAttenuated * Vector3D.DotProduct(axisNormalized, point - Center); pointsDst.Add(xform.Transform(point)); } // Return the points return pointsDst; }
Figure 9 TeapotTwister.xaml
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:pae=”clr-namespace:Petzold.AnimationExtensions; assembly=Petzold.AnimationExtensions” xmlns:src=”clr-namespace:TeapotTwister” x:Class=”TeapotTwister.TeapotTwister” Title=”Teapot Twister (3D Deformation)”> <Window.Resources> <src:TeapotMesh x:Key=”teapot” /> </Window.Resources> <Viewport3D> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup> <!-- 3D teapot geometry and materials --> <GeometryModel3D x:Name=”geomod” Geometry=”{Binding Source={StaticResource teapot}, Path=Geometry}”> <GeometryModel3D.Material> <DiffuseMaterial Brush=”Cyan” /> </GeometryModel3D.Material> <GeometryModel3D.BackMaterial> <DiffuseMaterial Brush=”Blue” /> </GeometryModel3D.BackMaterial> </GeometryModel3D> <!-- Light sources --> <AmbientLight Color=”#404040” /> <DirectionalLight Color=”#C0C0C0” Direction=”2 -3 1” /> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> <!-- Camera --> <Viewport3D.Camera> <PerspectiveCamera Position=”0 0 6” LookDirection=”0 0 -1” UpDirection=”0 1 0” FieldOfView=”45” /> </Viewport3D.Camera> </Viewport3D> <!-- Animation using Twister class --> <Window.Triggers> <EventTrigger RoutedEvent=”Window.Loaded”> <BeginStoryboard> <Storyboard TargetName=”geomod” TargetProperty=”Geometry.Positions”> <pae:Point3DCollectionAnimationUsingDeformer Duration=”0:0:5” AutoReverse=”true” RepeatBehavior=”Forever”> <pae:Point3DCollectionAnimationUsingDeformer.Deformer> <pae:Twister Axis=”1 0 0” /> </pae:Point3DCollectionAnimationUsingDeformer.Deformer> </pae:Point3DCollectionAnimationUsingDeformer> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> </Window>