Foundations

Extending the WPF Animation Classes

Charles Petzold

Code download available at: Foundations2007_07.exe(200 KB)

Contents

Interpolated Values
The Structure of the Classes
Freeze Out
Text Morphing
Keyframe Animations
Animating 3D Points

The support for animation in Microsoft® Windows® Presentation Foundation is mostly collected into the System.Windows.Media.Animation namespace. This is a large namespace that defines 164 classes devoted to animating properties of 22 specific data types. These 22 data types range from the basics like byte, int, and double to more exotic types like Matrix, Point3D, and Quaternion.

The animation facilities in Windows Presentation Foundation are surely an impressive piece of work, but only a non-programmer would consider those 22 data types to be sufficient for every application. I personally kept running into applications where I wanted to animate a whole collection of objects, specifically collections of coordinate points. The existing API gives us classes such as PointAnimation and Point3DAnimation for animating individual 2D or 3D coordinates; I wanted PointCollectionAnimation and Point3DCollectionAnimation classes to animate entire collections of points by interpolating between corresponding members of two collections.

We can probably blame the Microsoft .NET Framework in general and Windows Presentation Foundation in particular for making collections of objects almost as easy to define and use as the objects themselves. Windows Presentation Foundation already defines PointCollection and Point3DCollection classes, and in XAML the use of these collections is trivial. In XAML, the difference between assigning a property of type Point and assigning a property of type PointCollection is the difference between typing "100 50" and "100 50, 40 25, 35 80, 50 100." So why, I thought, can’t animating properties of these types be just as easy?

A hypothetical PointCollectionAnimation class would animate properties of type PointCollection. Five classes built into Windows Presentation Foundation have properties of that type: Polyline and Polygon (both derived from Shape), and PolyLineSegment, PolyBezierSegment, and PolyQuadraticBezierSegment (derived from PathSegment), and in all five classes that property is named Points. Animating that Points property lets you change a graphical figure from one shape to another with a single animation.

As far as I can tell, Point3DCollection only shows up in one class, but that’s the MeshGeometry3D class that sits at the center of the Windows Presentation Foundation 3D graphics system. The ability to animate a Point3DCollection would provide a fairly easy way to deform 3D objects—generally considered a rather advanced 3D programming task.

Visions of shifting, morphing, deforming shapes of 2D and 3D figures on the computer screen are what drove my journey to extend the Windows Presentation Foundation animation classes.

Interpolated Values

In Windows Presentation Foundation, animation is basically interpolation, and very often linear interpolation. What makes the coding of new animation classes complex is not the interpolation itself, but determining the exact values to interpolate between.

Let’s look at a simple example involving the animation of a Point value. You can draw a filled circle in XAML like this:

<Path Fill=”Blue”>
    <Path.Data>
        <EllipseGeometry x:Name=”ellipse” 
            RadiusX=”10” RadiusY=”10” />
    </Path.Data>
</Path>

Not specified in this markup is the Center property of the EllipseGeometry, which is a property of type Point. You can animate that Center property with a PointAnimation element like this:

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”10 10” To=”100 100” 
                Duration=”0:0:3” />

The Center property is animated from the point (10, 10) to the point (100, 100) over three seconds, which means that the PointAnimation class performs a linear interpolation between those two values based on the elapsed time.

You can also specify a Center property in the EllipseGeometry markup like this:

<EllipseGeometry x:Name=”ellipse” Center=”50 50” ...

This is called a base value because the animation can build on this value. You can make use of this base value by omitting the From or To attributes in the PointAnimation markup, as in this markup:

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                To=”100 100” 
                Duration=”0:0:3” />

The animation now begins at the center point (50, 50) specified in the EllipseGeometry markup and ends at the point (100, 100). This syntax is also allowed:

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”100 100” 
                Duration=”0:0:3” />

Now the animation begins at the point (100, 100) and ends at the point (50, 50). If either the From or To attributes are missing, and the EllipseGeometry markup specifies no Center attribute, then the animation involves the default value of the Center property, which is (0, 0).

It gets messier. An alternative to the To property is the By property, which is an amount added to the starting point.

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”100 100” By=”125 125”
                Duration=”0:0:3” />

This animation goes now from the point (100, 100) to (225, 225). But the From attribute doesn’t need to be present.

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                By=”125 125”
                Duration=”0:0:3” />

Now the animation begins at the point (50, 50) specified in the EllipseGeometry markup and ends with the point (175, 175).

The base value of an animation is often the value that was originally defined for the property being animated, but it doesn’t have to be. Consider the following two sequential PointAnimation objects:

<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” />

Over the first three seconds, the initial animation changes the Center property from its base value of (50, 50) to the To value of (100, 100), and there the value stays for two more seconds until the second animation kicks in. This animation includes no From, To, or By attributes! The animation begins at a base value of (100, 100) and ends at the value specified in the EllipseGeometry markup: (50, 50).

As you’ll see, a class such as PointAnimation is provided with two default values for the animation to begin and end, and it uses these defaults if the From, To, and By properties are not defined for the particular animation object. Such classes also define two Boolean properties named IsAdditive (which causes the animated values to be added to the base value) and IsCumulative (which accumulates the animated values if the animation is repeating). Neither of these properties makes the animation logic any simpler.

The Structure of the Classes

The various animation classes are often referred to collectively with names like <Type>AnimationBase, which indicates the 22 classes named DoubleAnimationBase, PointAnimationBase, and so on. But don’t assume from this convenient notation that the animation classes are implemented as generics; they really can’t be because each class needs to perform calculations specific to the particular data type. Figure 1 shows a generalized class hierarchy of the animation classes (although there are some variations for the 22 animatable types).

Figure 1 The Animation Class Hierarchy

Figure 1** The Animation Class Hierarchy **(Click the image for a larger view)

If you decide you want to write classes that animate a custom type, you’ll probably want to mimic the structure of the existing animation classes. You generally begin by defining an abstract <Type>AnimationBase class, and from that class you’ll probably derive a plain <Type>Animation class to perform animations based on linear interpolations. I want to animate properties of type PointCollection, so I began with a class named PointCollectionAnimationBase. Derive this class from AnimationTimeline and define it as abstract. You’ll need to override the read-only TargetPropertyType and return typeof(PointCollection).

In your new <Type>AnimationBase class, you’ll also want to override the GetCurrentValue method defined by AnimationTimeline:

public override object GetCurrentValue(
    object defaultOriginValue, 
    object defaultDestinationValue, 
    AnimationClock animationClock)
{
    ...
}

This is the method in which your class must calculate an interpolated value for the animation. The parameter names are those used in the documentation. The first two parameters provide the default base values where the animation is to begin and end. You use these parameters in the absence of explicit From, To, and By settings. The AnimationClock object includes a property of type double named CurrentProgress with values ranging from 0 to 1. This is the value you use for the interpolation.

Because you’ve overridden the TargetPropertyType property, WPF knows the type of the objects you wish to animate. If your class is animating objects of type PointCollection, the defaultOriginValue and defaultDestinationValue parameters will be of type PointCollection, and WPF is expecting this method to return an object of type PointCollection.

If you want to emulate the existing animation classes, then GetCurrentValue can simply cast its parameters to the proper type and call another GetCurrentValue method defined in the class, which in turn calls a method named GetCurrentValueCore. For animating PointCollection objects, the code looks like this:

public PointCollection GetCurrentValue(
    PointCollection defaultOriginValue, 
    PointCollection defaultDestinationValue, 
            AnimationClock animationClock)
{
    return GetCurrentValueCore(defaultOriginValue,
        defaultDestinationValue,
        animationClock); 
}

The GetCurrentValueCore method is then defined as protected and abstract:

protected abstract PointCollection GetCurrentValueCore(
    PointCollection defaultOriginValue, 
    PointCollection defaultDestinationValue, 
    AnimationClock animationClock);

Now any non-abstract class that derives from PointCollectionAnimationBase is required to override this GetCurrentValueCore method and provide actual code for the interpolation.

The downloadable code for this column consists of a single Visual Studio® solution with five projects. One of these projects is a DLL named Petzold.AnimationExtensions, which contains the classes described in this article to animate objects of type PointCollection and Point3DCollection. The other four projects in this Visual Studio solution are demonstration programs that make use of that DLL. The PointCollectionAnimationBase class is part of the DLL and implemented in the file named PointCollectionAnimationBase.cs. All other classes and interfaces in the DLL are likewise in files named after the class or interface.

Freeze Out

All the <Type>AnimationBase classes inherit from AnimationTimeline, which derives from the Freezable class by way of Animatable and Timeline. In fact, all the classes in the Petzold.AnimationExtensions DLL derive from Freezable one way or another, and that fact has some important consequences.

Classes that derive from Freezable have some very special requirements. Ignore these requirements at your own risk. If you do, you might encounter problems where things don’t work correctly and you don’t know why, or you might get strange exceptions that provide no clues to the real problem.

Every non-abstract class that derives from Freezable must override the CreateInstanceCore method. Generally, all this method needs to do is call the class’s constructor. Freezable defines this method as abstract, so you’ll be reminded by the compiler if you don’t include the method. But the compiler will indicate its absence only if your non-abstract class is the first non-abstract class in the hierarchy from Freezable. If you’re deriving from a non-abstract class, it’s easy to forget CreateInstanceCore.

Also, classes that derive from Freezable should back all their properties with dependency properties. It’s not a requirement, but if you don’t do it, you need to override five other methods and learn all about cloning and freezing.

To perform common linearly interpolated animations, you should create a class of the form <Type>Animation that derives from <Type>AnimationBase. (For this example, that class is PointCollectionAnimation, and it performs an interpolation of corresponding points between two PointCollection objects.) This class must override the CreateInstanceCore method, and it should define properties named From, To, and By backed by dependency properties. You should also define IsAdditive and IsCumulative properties; the dependency properties for these two properties are already defined by AnimationTimeline.

The biggest job for <Type>Animation is the override of GetCurrentValueCore. The last argument is an object of type AnimationClock, and the CurrentProgress property is a number between 0 and 1 that indicates the progress of the animation. This value takes account of properties such as AccelerationRatio, DecelerationRatio, AutoReverse, and SpeedRatio defined by the Timeline class and inherited by <Type>Animation. However, GetCurrentValueCore must first determine what it will be interpolating between. For example, if the From property has been set, but the To or By properties have not been set, then the interpolation occurs between From and the defaultDestinationValue parameter to the method.

For this reason, it’s important for GetCurrentValueCore to know when From, To, and By have been set in code or markup and when they have not been set. Most of the existing Windows Presentation Foundation animations are based on value types such as Double or the Point structure. How then can the DoubleAnimation class determine if the From property is its default value of zero, or if it has been specifically set to zero?

Easy: in all the <Type>Animation classes where <Type> is a value type, the From, To, and By properties are defined as nullable types. (This is something I never noticed before because I never had occasion to do anything except set these properties.) The dependency property definitions set the default values to null, which makes it easy for GetCurrentValueCore to determine if the property has been set.

For a class such as PointCollectionAnimation, the animated object is a reference type, so the From, To, and By properties will also be null if they haven’t been set. However, animating reference types involve other, much messier problems. In particular, the last thing you want is for a memory allocation to occur during each call to GetCurrentValueCore. For that reason, your class should create an object of the animated type as a field and return that object from the GetCurrentValueCore method.

Actually, I discovered when working on PointCollectionAnimation that simply returning the same object during successive calls to GetCurrentValueCore was insufficient. Whoever is on the receiving end of GetCurrentValue wants a different object altogether. For that reason, I created two PointCollection objects as fields (named ptsDst1 and ptsDst2 because they were destinations of the interpolation), and switched back and forth between them in successive calls to GetCurrentValueCore based on a Boolean field named flip. I also created a PointCollection object named ptsTo as a field to store the collection of points the method interpolates to. This separate collection became necessary because of the By property, which causes this collection to be a sum of two collections.

The implementation of GetCurrentValueCore in PointCollectionAnimation is shown in Figure 2.

Figure 2 GetCurrentValueCore in PointCollectionAnimation

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;
}

For any particular animation, the first couple of calls to GetCurrentValueCore might cause some memory allocations as the PointCollection fields are re-allocated to be of sufficient size to store all the points. However, once they’re the proper size, the animation should continue with no further memory allocations.

It wouldn’t make any sense to use PointCollectionAnimation for simply translating, scaling, or rotating a set of points because these operations can be performed with the normal transform classes. This class is most useful for non-linear transforms that fall outside the norm. For example, the SquaringTheCircle.xaml file in Figure 3 animates a PointCollection so a figure changes from a square to a circle with equal area and back to a square again, thus solving a geometry problem that has been baffling Western civilization since the days of Euclid.

Figure 3 SquaringTheCircle.xaml

<Window xmlns=”https://schemas.microsoft.com/winfx/2006/xaml/presentation”
        xmlns:x=”https://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>

A functionally equivalent program in my WPF book actually required 13 individual Point animations. I certainly prefer the elegance of this new version.

Text Morphing

The TextMorphCheckBox project defines a template for a CheckBox control that overlays the CheckBox content (if any) with a Polyline element that references one of two PointCollection objects defined as template resources. When you click the CheckBox, it performs an animation between that PointCollection object and the other one. A good chunk of the ControlTemplate markup is shown in Figure 4.

Figure 4 TextMorphCheckBox.xaml Excerpt

<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>

The two PointCollection objects used in this animation contain definitions for the words No and Yes in a script font. By animating between these two collections, the text morphs from one word into another over the course of a second, as shown in Figure 5.

Figure 5a The Morphing Text CheckBox

Figure 5a** The Morphing Text CheckBox **

Figure 5b

Figure 5b

Figure 5c

Figure 5c

Figure 5d

Figure 5d

Figure 5e

Figure 5e

Figure 5f

Figure 5f

The fact that you can click the CheckBox midway through the morphing text and the animation will reverse itself is a good indication that the PointCollectionAnimation class is handling the parameters passed to the GetCurrentValueCore method correctly.

Let me explain where the two polylines in the TextMorphCheckBox template came from. Some of the geezers who worked with Windows in those far distant days when "Rock Me Amadeus" was a hit might remember the existence of primitive vector fonts in Windows. These fonts are still alive in Windows Vista™ and can be found in the files Modern.fon, Roman.fon, and Script.fon. Characters in these fonts are defined simply by straight line segments and were often used with plotters. There are no curves, no hinting, and no filling as in TrueType fonts. You can select these old vector fonts in Notepad, but Microsoft Word would positively die before inserting one of these hideous things into its gorgeous documents.

If you know a little bit about font file formats, it’s easy to crack these font files and extract the polylines that define each character. After that job, I adjusted the coordinates of the individual characters by hand to create single continuous polylines for the words No and Yes, and then tweaked them some more to ensure that both PointCollection objects had the same number of points. I was mentally prepared to convert the coordinates to a series of quadratic Bezier splines by inserting some control points, but I didn’t need to. With a suitably thick stroking pen, the polylines seemed adequate for this odd purpose.

Keyframe Animations

Programmers who have worked with Windows Presentation Foundation animation know that the <Type>Animation classes are really just an easy alternative to the more versatile <Type>AnimationUsingKeyFrames classes. The keyframe animations let you mix a series of discrete jumps, linear animations, and animations that slow down and speed up based on a Bezier spline segment. Each keyframe consists of a time and a value. The animation interpolates between a base value (which is generally the ending value of the previous keyframe) and the keyframe value.

I expected that implementing a PointCollectionAnimationUsingKeyFrames class would be challenging, and I wasn’t disappointed. The first step was to derive an abstract PointCollectionKeyFrame class that inherits from Freezable and implements the IKeyFrame interface. This requires the definition of KeyTime and Value properties that you’ll want to back by dependency properties. I mimicked the existing keyframe animation classes by defining a public method named InterpolateValue in PointCollectionKeyFrame:

public PointCollection InterpolateValue(
    PointCollection baseValue,
    double keyFrameProgress)
{
    return InterpolateValueCore(baseValue, keyFrameProgress);
}

I also defined a protected and abstract InterpolateValueCore method. Notice that the second parameter is a progress value between 0 and 1, but it’s a progress value for the particular key frame and not for the animation as a whole.

The next step is to derive three classes from PointCollectionKeyFrame named DiscretePointCollectionKeyFrame, LinearPointCollectionKeyFrame, and SplinePointCollectionKeyFrame. These classes override the InterpolateValueCore method to perform an interpolation between the baseValue parameter and the Value property. SplinePointCollectionKeyFrame also defines a property named KeySpline. Once again, I defined two fields named ptsDst1 and ptsDst2 and a Boolean named flip for the result. I realized I could avoid some repetitive code by first deriving an abstract NonDiscretePointCollectionKeyFrame class from PointCollectionKeyFrame that contains the InterpolateValueCore method shown in Figure 6.

Figure 6 InterpolateValueCore Method

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;
}

Notice that the method calls a method named GetProgress, which NonDiscretePointCollectionKeyFrame defines as abstract. Both LinearPointCollectionKeyFrame and SplinePointCollectionKeyFrame override this method. LinearPointCollectionKeyFrame simply returns the parameter; SplinePointCollectionKeyFrame returns a value conveniently provided by a method defined by the KeySpline structure, which converts a linear progress value into a progress based on the Bezier spline.

Next you’ll need a class of the form <Type>KeyFrameCollection, which is a freezable collection of <Type>KeyFrame objects. In my example, this collection class has a rather confusing name, PointCollectionKeyFrameCollection, and is a collection of PointCollectionKeyFrame objects. You can save yourself a whole lot of work by defining this collection class as inheriting from the generic FreezableCollection<T> class. Don’t forget: when deriving from FreezableCollection<T>, you must override CreateInstanceCore.

public class PointCollectionKeyFrameCollection : 
    FreezableCollection<PointCollectionKeyFrame>
{
    // CreateInstanceCore override required when 
    // deriving from Freezable
    protected override Freezable CreateInstanceCore()
    {
        return new PointCollectionKeyFrameCollection();
    }
}

The final and most difficult step is the <Type>AnimationUsingKeyFrames class that derives from <Type>AnimationBase and implements the IKeyFrameAnimation interface by defining a KeyFrames property (and dependency property) of type <Type>KeyFrameCollection. The <Type>AnimationUsingKeyFrames class overrides the GetCurrentValueCore method, but must determine which of the objects in its KeyFrames collection should actually perform the interpolation.

While working on this class, I realized that the IsCumulative property was not very useful, but accumulating the animation based on a single point might provide some interesting utility. I defined a new property (and dependency property) named AccumulationPoint, which is a single point that gets added to the collection during each iteration. This let me create an animation I called StickFigureWalking. It is based on five PointCollection objects and five keyframes with an accumulation point that make a stick figure walk across the screen, as shown in Figure 7.

Figure 7 Walking

Figure 7** Walking **

Animating 3D Points

When you create your own animation classes, you can mimic the existing classes (as I did with the PointCollection animations), or you can abandon that paradigm and strike out on your own. This is what I did with animating objects of type Point3DCollection, which is the data type of the Positions property of MeshGeometry3D. I defined a normal Point3DCollectionAnimationBase class, but then pursued a different strategy with a class named Point3DCollectionAnimationUsingDeformer. The Point3DCollectionAnimationUsingDeformer defines just one property named Deform of type IDeformer, an interface that defines just one method:

Point3DCollection Deform(Point3DCollection points, double progress);

The Deform method accepts a collection of 3D points and a progress value that ranges from 0 to 1, and it returns a collection of 3D points. The intent is to allow non-linear transforms that can’t be accomplished with the standard classes. Exactly how Deform transforms one set of points to another is limited only by your imagination—and perhaps your math skills.

I provide one example: a class named Twister that twists a 3D object. Twisting is similar to the standard AxisAngleRotation3D transform available to 3D programmers and involves a rotation around an axis, except that the angle of rotation for a particular point varies with the distance of that point from a center point.

The Twister class implements the IDeformer interface and defines several properties on its own. The Axis and Center properties indicate the axis of rotation expressed as a vector and the rotation center. The MinAngle and MaxAngle properties specify the rotation angles when the progress value is 0 and 1, respectively. These angles are per unit of distance along the rotation axis. For example, imagine the Axis vector passing through the Center point. The plane of points that include that Center point and are perpendicular to Axis do not rotate at all. The plane one unit from the Center point rotates between MinAngle and MaxAngle. The plane two units from the Center point rotates between twice MinAngle and twice MaxAngle. On the other side of the Center point, points rotate in the opposite direction.

Figure 8 shows the Deform method in the Twister class. Notice that it flips between two pre-created Point3DCollection classes for the destination collection, and that it also uses a pre-created RotateTransform3D object named xform and an AxisAngleRotation3D object named axisRotate.

Figure 8 The Deform Method in Twister

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;
}

The real fun comes with finding objects to twist. The TeapotTwister project includes a TeapotMesh.cs file that generates a MeshGeometry3D object for the famous Iowa Teapot used in countless 3D demonstrations. I created this file from data available from the static Mesh.Teapot method in DirectX® 9.0. Figure 9 shows the complete XAML file that references the TeapotMesh class, the Point3DCollectionAnimationUsingDeformer class, and Twister. The twisted teapot is shown in Figure 10.

Figure 9 TeapotTwister.xaml

<Window xmlns=”https://schemas.microsoft.com/winfx/2006/xaml/presentation”
        xmlns:x=”https://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>

Figure 10 A Twisted Teapot

Figure 10** A Twisted Teapot **

The XAML file shows the Twister class referenced in a property element of Point3DCollectionAnimationUsingDeformer, but it’s also possible to define the Twister class as a resource:

   <pae:Twister x:Key=”twister” Axis=”1 0 0” />

You then reference it with an attribute in the Point3DCollectionAnimationUsingDeformer tag:

Deformer=”{StaticResource twister}”

While dynamically modifying the Positions collection of a MeshGeometry3D is a powerful technique, it is theoretically not quite adequate. Whatever non-linear transform you make to the Point3D objects in the Positions collection of MeshGeometry3D should also be applied to the Vector3D objects in the Normals collection. Although explicit casts are defined between Point3D and Vector3D, no such cast exists for collections of these objects. This seems to imply that a whole parallel structure of classes needs to be created for animating Vector3DCollection objects.

Even without that enhancement, these new animation classes have satisfied one of my basic motivations in programming: to see visuals on my computer—such as morphing text and twisting teapots—the likes of which I’ve never seen before.

Send your questions and comments to  mmnet30@microsoft.com.

Charles Petzold is a Contributing Editor to MSDN Magazine and the author of Applications=Code+Markup: A Guide to the Microsoft Windows Presentation Foundation (Microsoft Press, 2006).