Chapter 30. Animation


Suppose you have a program with a button, and when the user clicks that button, you want the button to increase in size. You would prefer that the button not just jump from one size to a larger size. You want the button to increase in size smoothly. In other words, you want the increase in size to be animated. I'm sure there are some who would insist that buttons really shouldn't do such things, but their protests will be ignored in this chapter as the rest of us explore the animation facilities of the Microsoft Windows Presentation Foundation.

In the early chapters of this book, I showed you how to use DispatcherTimer to implement animation. Increasing the size of a button based on timer ticks is fairly easy.

EnlargeButtonWithTimer.cs

[View full width]

//--------- ---------------------------------------------- // EnlargeButtonWithTimer.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.EnlargeButtonWithTimer { public class EnlargeButtonWithTimer : Window { const double initFontSize = 12; const double maxFontSize = 48; Button btn; [STAThread] public static void Main() { Application app = new Application(); app.Run(new EnlargeButtonWithTimer()); } public EnlargeButtonWithTimer() { Title = "Enlarge Button with Timer"; btn = new Button(); btn.Content = "Expanding Button"; btn.FontSize = initFontSize; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(0.1); tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender, EventArgs args) { btn.FontSize += 2; if (btn.FontSize >= maxFontSize) { btn.FontSize = initFontSize; (sender as DispatcherTimer).Stop(); } } } }



The button has an initial FontSize of 12 device-independent units. When clicked, the event handler creates a DispatcherTimer that generates Tick events every tenth second. The TimerOnTick method increases the FontSize by two units every tenth second until the size reaches 48 units, at which point the button is restored to its original size and the timer is stopped. It's easy to calculate that the whole process takes about 1.8 seconds: just multiply the timer period by the difference in the initial and final FontSize values, divided by the increment to the FontSize property.

What happens if you click the button a second time while it's expanding? The Click event handler creates a second timer, so the button's size increases at twice the speed. When one of the two timers increases the size to 48 units, that Tick handler restores the button's size and stops that timer, but the other timer increases the button to 48 units again. Of course, such behavior is easy to avoid if you set a Boolean variable when the timer is going and don't start a second timer when that variable is set.

A couple of features of the WPF help make this animation a success. The retained graphics system is designed so that visual objects can change size or position without flickering or causing excessive redrawing. The dependency property system ensures that changing just the FontSize property of the button will make the button larger to accommodate the new size.

Although you can always fall back on using DispatcherTimer if a particular animation requires a lot of custom code, for many animations you can use the animation facilities built into the WPF. Most of the animation-related classes are defined in the System.Windows.Media.Animation namespace. Although this namespace contains almost 200 classes, very many of them fall into just a few categories.

WPF animations always target dependency properties. The System.Windows.Media.Animation namespace has many classes because different classes exist for animating properties of various types. In alphabetical order, the 22 animatable types are Boolean, Byte, Char, Color, Decimal, Double, Int16, Int32, Int64, Matrix, Object, Point, Point3D, Quaternion, Rect, Rotation3D, Single, Size, String, Thickness, Vector, and Vector3D.

The FontSize property is of type double, so to animate the FontSize property, you can use a double animationan animation that changes the size of a dependency property of type double. Properties of type double are very common throughout the WPF, and consequently, the double is probably the most common animated type.

There are actually three classes that implement double animations, as shown in the following partial class hierarchy:

Object

     DispatcherObject (abstract)

          DependencyObject

               Freezable (abstract)

                    Animatable (abstract)

                         Timeline (abstract)

                              AnimationTimeline (abstract)

                                   DoubleAnimationBase (abstract)

                                        DoubleAnimation

                                        DoubleAnimationUsingKeyFrames

                                        DoubleAnimationUsingPath

For each of the 22 data types that you can animate, there exists an abstract class that begins with the data type followed by AnimationBase, such as DoubleAnimationBase. I'll refer to these classes collectively as <Type>AnimationBase. (But don't assume from my syntax that these classes are implemented as generics; they are not.) For all 22 data types, there also exists a <Type>AnimationUsingKeyFrames class. All but five types have a <Type>Animation class, and only three types have a <Type>AnimationUsingPath class. In total, there are 64 classes of <Type>AnimationBase and its derivatives.

The most straightforward of the classes that derive from DoubleAnimationBase is the DoubleAnimation class, which simply changes a property of type double linearly from one value to another. Here's a program that uses a DoubleAnimation object to mimic the behavior of the EnlargeButtonWithTimer program.

EnlargeButtonWithAnimation.cs

[View full width]

//--------- -------------------------------------------------- // EnlargeButtonWithAnimation.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; namespace Petzold.EnlargeButtonWithAnimation { public class EnlargeButtonWithAnimation : Window { const double initFontSize = 12; const double maxFontSize = 48; Button btn; [STAThread] public static void Main() { Application app = new Application(); app.Run(new EnlargeButtonWithAnimation()); } public EnlargeButtonWithAnimation() { Title = "Enlarge Button with Animation"; btn = new Button(); btn.Content = "Expanding Button"; btn.FontSize = initFontSize; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { DoubleAnimation anima = new DoubleAnimation(); anima.Duration = new Duration(TimeSpan .FromSeconds(2)); anima.From = initFontSize; anima.To = maxFontSize; anima.FillBehavior = FillBehavior.Stop; btn.BeginAnimation(Button .FontSizeProperty, anima); } } }



The constructor creates the Button object in the same way as the previous program, but the Click handler creates an object of type DoubleAnimation. Every animation has a duration, which you specify with a Duration structure, generally based on a TimeSpan object. In this case, the duration of the animation is set to two seconds.

The ButtonOnClick handler then sets the From and To properties of the DoubleAnimation, indicating that this animation will change a double property from an initial value of initFontSize (or 12) to a final value of maxFontSize (or 48). The animation essentially performs a linear interpolation between the two values to determine how large the FontSize of the button should be at any particular point in time during the animation.

Setting the From property isn't required in this example because the button's FontSize is initially 12 anyway. But if the From property indicated a value different from the value already set on the element being animated, the animation would begin by jumping to the value specified by the From property.

The FillBehavior property indicates what happens when the double reaches its value of 48 and the animation ends. The default value is FillBehavior.HoldEnd, which means that the value remains at 48. The FillBehavior.Stop option causes the double property to go back to its initial pre-animation value.

The BeginAnimation method is defined by UIElement (and a few other classes) and, of course, inherited by Button. The first argument is the dependency property being animated, and the second argument is the DoubleAnimation object to animate that property. Rather than calling the BeginAnimation method on the Button object, you could call BeginAnimation for the window:

BeginAnimation(Window.FontSizeProperty, anima); 


This animation would change the FontSize of the Window, and the Button would then inherit that animated FontSize through normal property inheritance. Any other child of the window would also inherit that FontSize. In the complete scheme of dependency properties, animations have the highest priority in setting a property.

Rather than calling BeginAnimation, the program could kick off the animation with a call to ApplyAnimationClock, another method defined by UIElement:

btn.ApplyAnimationClock(Button.FontSizeProperty, anima.CreateClock()); 


Notice that the second argument is an object of type AnimationClock returned from the CreateClock method that is defined by AnimationTimeline and inherited by DoubleAnimation.

An object of type Timeline represents a period of time. This is the class that defines the Duration and FillBehavior properties, and some other time-related properties that I'll discuss shortly. The AnimationTimeline class derives from Timeline and defines the CreateClock method. The clock is the mechanism that paces the animation. The DoubleAnimation class itself defines the From and To properties, which are of type double.

The two C# programs shown so far begin the animation in response to a Click event from a Button. However, the animation could be started based on other criteria. For example, you could set a timer that checks the time of day and start an animation at the stroke of midnight. Or a program might use the FileSystemWatcher class to monitor the file system and start an animation whenever a new directory is created. Or a program might display an animation based on information obtained from a second thread of execution in the program.

Animations defined in XAML, however, are always associated with triggers. You are familiar with triggers and trigger collections from Chapters 24 and 25. The Style, ControlTemplate, and DataTemplate classes all define properties named Triggers that are collections of TriggerBase objects. In Chapter 24, I discussed four of the classes that derive from TriggerBase. These are Trigger, MultiTrigger, DataTrigger, and MultiDataTrigger. As you'll recall, these triggers have the power to change properties of elements based on changes in other properties or changes in data bindings.

Chapter 24 did not discuss the fifth class that derives from TriggerBase, which is EventTrigger, because this trigger is used mostly in connection with animations. (It can also trigger sounds.) This is the class you generally use in XAML in connection with animations, although you can also start an animation based on other types of triggers.

You might have discovered on your own that FrameworkElement defines a property named Triggers that is a collection of Trigger objects. I didn't discuss this Triggers collection in earlier chapters because it is very special. The Triggers collection defined by FrameworkElement can contain only EventTrigger objects, and you can't do much with EventTrigger objects except to trigger animations or play sounds.

EventTrigger defines three properties. The SourceName property is a string that refers to an element's Name or x:Name attribute. This is the element with the event that triggers the animation. (In common practice, the SourceName property is often implicitly defined by the context of EventTrigger and doesn't need to be explicitly specified.) The RoutedEvent property of EventTrigger is the particular triggering event. EventTrigger also defines an Actions property that indicates what happens when that event occurs. The Actions property is a collection of TriggerAction objects, and the most important class that derives from TriggerAction is BeginStoryboard.

A Triggers section of a Button control might look something like this:

<Button.Triggers>     <EventTrigger RoutedEvent="Button.Click">         <BeginStoryboard ... >             ...         </BeginStoryboard>     </EventTrigger> </Button.Triggers> 


Animations in XAML always involve storyboards, which are objects that extend Timeline to provide information about property targeting. (You can also use storyboards in code, but as you've seen, you're not required to use them.) The child of BeginStoryboard is often a Storyboard element. Like DoubleAnimation, Storyboard derives from Timeline, as the following class hierarchy indicates:

Object

     DispatcherObject (abstract)

          Dependencyobject

               Freezable (abstract)

                    Animatable (abstract)

                         Timeline (abstract)

                              AnimationTimeline (abstract)

                                   DoubleAnimationBase (abstract)

                                        DoubleAnimation

                                        DoubleAnimationUsingKeyFrames

                                        DoubleAnimationUsingPath

                                   ...

                              TimelineGroup (abstract)

                                   ParallelTimeline

                                        Storyboard

                              MediaTimeline

The hierarchy is complete from Timeline on down, except for the ellipsis, which encompasses all of the various animation classes for all 22 animatable data types.

The Storyboard class has the ability to consolidate multiple timelines and also has attached properties to indicate the name and property of the element to which the animation applies. (Sometimes the element name is implied from the context.) The TimelineGroup class defines a Children property for a collection of timelines so that a DoubleAnimation can be a child of a Storyboard. Here's typical and more complete animation markup that goes all the way down to the DoubleAnimation object:

<Button.Triggers>     <EventTrigger RoutedEvent="Button.Click">         <BeginStoryboard ... >             <Storyboard ... >                 <DoubleAnimation ... />             </Storyboard>         </BeginStoryboard>     </EventTrigger> </Button.Triggers> 


These layers of markup might seem to form an excessively deep hole for the DoubleAnimation to reside in, but nothing is superfluous. The Button.Triggers section can have multiple EventTrigger elements. Often the trigger action is a BeginStoryboard object, but it can also be a SoundPlayerAction or a ControllableStoryboardAction. BeginStoryboard always has a Storyboard child, but that Storyboard can have multiple animations as children.

Here is the XAML equivalent of the animation that enlarges the button when you click it.

EnlargeButtonInXaml.xaml

[View full width]

<!-- === =================================================== EnlargeButtonInXaml.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Button FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"> Expanding Button <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard TargetProperty="FontSize"> <DoubleAnimation From="12" To="48" Duration="0:0:2" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> </Page>



Notice that the Triggers section is part of the Button control, so the source element for the EventTrigger is implicitly the Button itself. The triggering event is Click. The Storyboard element refers to the TargetProperty of the animation. This is the property that the animation animates, and it must be a dependency property.

TargetProperty is more than just a property of Storyboard. It is an attached property of Storyboard, and the property alternatively can be moved to the DoubleAnimation element where it appears as Storyboard.TargetProperty:

<Storyboard>     <DoubleAnimation Storyboard.TargetProperty="FontSize"                      ... /> </Storyboard> 


Storyboard also defines an attached property of TargetName so that different animations can target different elements in the XAML file. Both the Storyboard.TargetName and Storyboard.TargetProperty attached properties are usually defined in children of the Storyboard element, but if all the child animations within a Storyboard have the same TargetName or TargetProperty, you can transfer the attribute to the Storyboard element, as I've done in the EnlargeButtonInXaml.xaml file.

Here's a variation of the previous XAML file that has two buttons in a StackPanel. They are given Name attributes of "btn1" and "btn2."

EnlargeButtonsInXaml.xaml

[View full width]

<!-- === ==================================================== EnlargeButtonsInXaml.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <Button Name="btn1" FontSize="12" Margin="12" HorizontalAlignment="Center"> Expand Other Button </Button> <Button Name="btn2" FontSize="12" Margin="12" HorizontalAlignment="Center"> Expand Other Button </Button> </StackPanel> <Page.Triggers> <EventTrigger SourceName="btn1" RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="btn2" Storyboard .TargetProperty="FontSize" From="12" To="48" Duration="0:0:2" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger SourceName="btn2" RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="btn1" Storyboard .TargetProperty="FontSize" From="12" To="48" Duration="0:0:2" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



Two EventTrigger elements are consolidated in the Triggers section of the Page rather than the Triggers section of the Button. Because these EventTrigger elements are actually concerned with Button events, they require a SourceName attribute to indicate the element generating the event. Each DoubleAnimation also has a Storyboard.TargetName attribute to indicate the control being animated. In an early version of this program, clicking a button caused that button to be enlarged, but I decided it was more interesting to switch the targets to that clicking a button causes the other button to expand.

The animation occurs over a period of time indicated by Duration, which commonly has a format of hours:minutes:seconds. Fractional seconds are allowed. For example, here's how you set the duration to 2.5 seconds:

Duration="0:0:2.5" 


If you have long animations, you can simplify this syntax. The following attribute indicates three minutes:

Duration="0:3" 


This next one is seven hours:

Duration="7" 


Try it! (You don't have to let the animation continue to the end if you don't want to.) For durations of 24 hours or longer, you specify a number of days followed by a period, followed by the hours. The duration that follows is 4 days, 12 hours, and 7 minutes:

Duration="4.12:7" 


If you leave out the Duration attribute or set it to the string "Automatic", the animation will run for one second. (In other contexts, it is also possible to set Duration to the string "Forever", but that option doesn't work in this particular context.)

The two XAML animations I've shown you change the FontSize property of the Button from a starting value to an ending value. The From and To attributes of DoubleAnimation indicate these values. If you leave out the From attribute, the animation will work pretty much the same because the FontSize of the Button is initially set to 12. (The difference occurs when you click the button in the middle of an animation. With the From attribute present, the animation begins again at 12; without this attribute, the animation continues from the value at the time you click the button.)

You can set the ending value to something less than the starting value:

From="48" To="12" 


In this case, the animation begins by giving the button a FontSize of 48, which then decreases to 12.

An alternative to To is the By attribute:

By="100" 


With By, the ending value is the starting value plus the By amount. If you use By by itself, the animation changes the button's font size from 12 to 112. The To and By attributes are mutually exclusive; if you include both, the By attribute is ignored.

This combination of attributes causes the FontSize to change from 50 to 100:

From="50" To="100" 


With the following attributes, FontSize changes from 50 to 150:

From="50" By="100" 


You can have a From attribute by itself with no To or By attributes:

From="50" 


Now the animation changes the FontSize property from 50 to 12, which is the value specified in the Button element. If the Button element had no explicit FontSize attribute, the animation would change the property from 50 to the default FontSize value for the Button (typically 11).

If the animation includes From and To attributes, or From and By attributes, you can also include the IsAdditive attribute. Its default value is false; if you set it to true, the values specified in the From, To, and By attributes are added to the initial value of the target property. Here's an example:

From="50" To="100" IsAdditive="True" 


If the initial value of FontSize is 12, the animation changes FontSize from 62 to 112. With the following markup with a By attribute instead of To, FontSize changes from 62 to 162:

From="50" By="100" IsAdditive="True" 


So far, as you've seen, the FontSize snaps back to its starting value after the animation has completed. That's a result of setting the FillBehavior property to the enumeration value FillBehavior.Stop. The default value is FillBehavior.HoldEnd, which causes the FontSize to remain at the final value of the animation.

If you've been experimenting with either EnlargeButtonInXaml.xaml or EnlargeButtonsInXaml.xaml, return to the file in its pristine state so that the DoubleAnimation element causes the FontSize to change from 12 to 48 over two seconds. You can set the AutoReverse property of DoubleAnimation to play the timeline in reverse after it's played forward:

AutoReverse="True" 


Now the animation increases the FontSize from 12 to 48 over two seconds and then decreases FontSize from 48 back to 12 over two seconds. The Duration attribute indicates the time for just a single forward play of the Timeline.

So far, all the animations have run just once. You can change that by setting the RepeatBehavior attribute. Here's how you make the timeline play three times:

RepeatBehavior="3x" 


This is called an iteration count. It indicates the total number of times the timeline is to play. (A value of 1x is the default.) The Duration attribute indicates the time for playing the timeline once. If you have the Duration attribute set to 0:0:2, AutoReverse set to true, and RepeatBehavior set to 3x, the entire animation lasts 12 seconds and the final FontSize value is 12. If you leave out the AutoReverse attribute or set it to false, the animation runs for six seconds, and the final value is 48 before the FillBehavior setting causes it to snap back to 12.

You can set RepeatBehavior to fractional iteration counts:

RepeatBehavior="1.5x" 


If AutoReverse is false, the animation last three seconds, and the value when the animation ends is 30. (First the FontSize increases from 12 to 48, and then it begins increasing from 12 again but stops halfway through.) If AutoReverse is set to true, the animation lasts six seconds. The FontSize goes from 12 to 48 and then back to 12 (that's one iteration) and then from 12 to 48 (that's half of the second iteration).

Another approach is setting RepeatBehavior to a duration of time:

RepeatBehavior="0:0:10" 


If AutoReverse is false, the animation changes the FontSize from 12 to 48 five times. If AutoReverse is true, the animation changes the FontSize from 12 to 48 and back to 12 twice, and then from 12 to 48. The duration specified in RepeatBehavior can be less than that in the Duration attribute, in which case the timeline doesn't finish playing once.

And then there is the ever-popular animation that continues forever or until boredom kicks in:

RepeatBehavior="Forever" 


FillBehavior has no effect if RepeatBehavior is Forever.

When you set RepeatBehavior to play the timeline more than once, you can also set IsCumulative to true. Each iteration causes the values to accumulate. For example, consider the following markup:

<DoubleAnimation From="12" To="24"                  Duration="0:0:2"                  AutoReverse="true"                  RepeatBehavior="3x"                  IsCumulative="true" /> 


The first iteration goes from 12 to 24 and back to 12. The second iteration is from 24 to 36 and back to 24. The third iteration takes the values from 36 to 48 and back to 36.

If you want a delay between the event that triggers the animation and the time at which the timeline begins, you can specify that as the BeginTime attribute:

BeginTime="0:0:2" 


Now there's a delay of two seconds before the timeline begins. After that, the BeginTime has no effect on repetitions and reversals. You can set BeginTime to a negative value to start the animation at a point beyond its actual beginning. For example, in the current examples, if you set BeginTime to 0:0:1, the animation begins with a FontSize of 30. You can also set CutoffTime to end an animation at a particular time. CutoffTime encompasses the duration you set in BeginTime.

Because an animation can last longer (or shorter) than the time you specify in the Duration property, I'll be using the term "total duration" to indicate the overall length of the animation as it's affected by BeginTime, CutoffTime, RepeatBehavior, and AutoReverse.

Storyboard derives from TimelineGroup, which defines a property named Children of type Timeline. This means that Storyboard can host multiple animations. All animations that share the same Storyboard are triggered by the same event.

The following stand-alone XAML file uses Ellipse to display a ball that is initially in the upper-left corner of a Canvas panel. The EventTrigger is based on the MouseDown event of the Ellipse. The Storyboard contains two DoubleAnimation objects. One animation changes the Canvas.Left attached property to make the ball move back and forth horizontally. The second animation changes the Canvas.Top attached property to make the ball move down and then back up. Notice that both DoubleAnimation elements include attributes for the Storyboard.TargetProperty attached property, and that the target properties of these two animations are themselves attached properties (Canvas.Left and Canvas.Top), so they must be enclosed in parentheses.

TwoAnimations.xaml

[View full width]

<!-- ================================================ TwoAnimations.xaml (c) 2006 by Charles Petzold ============================================= === --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Ellipse Width="48" Height="48" Fill="Red" Canvas.Left="0" Canvas.Top="0"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .MouseDown"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" From="0" To="288" Duration="0:0:1" AutoReverse="True" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" From="0" To="480" Duration="0:0:5" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



Click the Ellipse to start the animation. The horizontal back-and-forth motion has a Duration setting of one second, whereas the up-and-down motion has a Duration of five seconds. Both have AutoReverse set to true. The total duration of the first animation is two seconds; the second is 10 seconds. These two animations begin at the same time. The composite animation has a total duration that is equal to the longest total duration of its components. That's 10 seconds.

What you'll see is the ball begin moving both down and to the right. After one second, the ball stops moving right and reverses itself to start moving left. When it reaches a horizontal position of 0, that first animation has concluded. The second animation continues moving the ball down and then back up.

If you want these two animations to have different Duration settings but to end at the same time, you need to set the RepeatBehavior property of the shorter animation. Here's a similar program, except that the horizontal motion has a Duration setting of one-fourth of a second. The vertical motion is five seconds. Both have AutoReverse set to true. For both animations to end at the same time, the first animation must have a RepeatBehavior setting of 20x.

WiggleWaggle.xaml

[View full width]

<!-- =============================================== WiggleWaggle.xaml (c) 2006 by Charles Petzold ============================================= == --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Ellipse Width="48" Height="48" Fill="Red" Canvas.Left="0" Canvas.Top="0"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .MouseDown"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" From="0" To="288" Duration="0:0:0.25" AutoReverse="True" RepeatBehavior="20x" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" From="0" To="480" Duration="0:0:5" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



Alternatively, you can set the RepeatBehavior of the first animation to a period of time. Because the second animation runs a total of 10 seconds, this would be as follows:

RepeatBehavior="0:0:10" 


In either case, after the ball completes its 20 back-and-forth trips and its single up-and-down trip, the animation stops. If you want that show to run three times in a row, you can set the RepeatBehavior of the first animation to 60x and the RepeatBehavior of the second animation to 3x, or you can include a RepeatBehavior attribute right in the Storyboard element that applies to the two DoubleAnimation elements:

<Storyboard RepeatBehavior="3x"> 


You can also set that RepeatBehavior to Forever. Or you can leave out the RepeatBehavior in the Storyboard and set the RepeatBehavior to Forever in both DoubleAnimation elements.

If you decide that you'd like to speed up the animation by 50 percent, you don't need to change the Duration settings in the individual DoubleAnimation elements. You can just set the SpeedRatio property in the Storyboard:

<Storyboard SpeedRatio="1.5"> 


This feature is ideal for debugging animations, particularly when you are dealing with parallel animations. You can initially set Duration values so the animations run very slowly for debugging, and then you can eventually speed everything up by setting a SpeedRatio to apply to the composite animation.

All sibling animations begin at the same time. If that's not what you want, you need to use the BeginTime property to delay the start of an animation. In some cases, you might have a group of parallel animations that, when concluded, should be followed by another group of parallel animations. You can use the same BeginTime property on all the animations of the second group so that they follow the completion of the first group, or you can enclose the second group of animations in a ParallelTimeline element and set the BeginTime just for that element. Here's an example.

WiggleWaggleAndExplode.xaml

[View full width]

<!-- === ====================================================== WiggleWaggleAndExplode.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Ellipse Width="48" Height="48" Fill="Red" Canvas.Left="0" Canvas.Top="0"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .MouseDown"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" From="0" To="288" Duration="0:0:0.25" AutoReverse="True" RepeatBehavior="20x" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" From="0" To="480" Duration="0:0:5" AutoReverse="True" /> <ParallelTimeline BeginTime="0:0:10" FillBehavior="Stop"> <DoubleAnimation Storyboard .TargetProperty="Width" From="48" To="480" Duration="0:0:1" /> <DoubleAnimation Storyboard .TargetProperty="Height" From="48" To="480" Duration="0:0:1" /> </ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



The idea here is that after the ball returns to its original position, two DoubleAnimation elements quickly increase the Width and Height of the ball to 5 inches. Then the ball "explodes" and returns to its original size.

The two DoubleAnimation objects that increase the size are grouped in a ParallelTimeline element. This ParallelTimeline has a BeginTime of 10 seconds, which is the total duration of the first two animations. The ParallelTimeline element also sets the FillBehavior to Stop. Both the BeginTime and the FillBehavior apply to the two children of the ParallelTimeline. These two properties could have been included in both DoubleAnimation children, but consolidating them in the ParallelTimeline element makes the program easier to understand and change.

So far, most of the programs I've shown have altered an element or control in response to an event on that element or control. One element's events can also trigger animations in other elements. The following stand-alone XAML file displays a Page with three radio buttons labeled red, green, and blue. Clicking any button changes the background color of the Page.

This program uses ColorAnimation elements, which work pretty much the same as DoubleAnimation except that the From, To, and By properties are of type Color, and the TargetProperty must be of type Color.

ColorRadioButtons.xaml

[View full width]

<!-- === ================================================= ColorRadioButtons.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" Background="{x:Static SystemColors.WindowBrush}" Name="page"> <Page.Resources> <Style TargetType="{x:Type RadioButton}"> <Setter Property="Margin" Value="6" /> </Style> </Page.Resources> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> <RadioButton Content="Red"> <RadioButton.Triggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard .TargetName="page" Storyboard .TargetProperty="Background.Color" To="Red" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> </RadioButton.Triggers> </RadioButton> <RadioButton Content="Green"> <RadioButton.Triggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard .TargetName="page" Storyboard .TargetProperty="Background.Color" To="Green" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> </RadioButton.Triggers> </RadioButton> <RadioButton Content="Blue"> <RadioButton.Triggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard .TargetName="page" Storyboard .TargetProperty="Background.Color" To="Blue" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> </RadioButton.Triggers> </RadioButton> </StackPanel> </Page>



The Page root element is assigned a Name property of page, and each of the ColorAnimation elements refers to that target. The TargetProperty is the Color property of the Background property of the Page, under the assumption that this Background property is a SolidColorBrush object, which is not normally the case. For this markup to work, the Page needs to be initialized with a SolidColorBrush, as it is in the root element. The animations have no From property, so the background color changes from the current color to the color specified as To over a period of one second.

The quantity of repetitious markup here will probably convince you to implement these radio buttons in code if you want to have more than just a couple of them.

Why didn't I use a Style to reduce the repetitious markup in ColorRadiusButtons.xaml? It is certainly possible to include animations in a Triggers section of a Style definition, but the animations in the Style cannot target an element other than the one being styled. Here's a stand-alone XAML file that includes a Style for Button elements. The Style includes a couple of Setter elements and, within the Triggers section, two EventTrigger elements. The first is triggered by the MouseEnter event, and the second by the MouseLeave event. Both start a DoubleAnimation that changes the FontSize of the Button.

FishEyeButtons1.xaml

[View full width]

<!-- === =============================================== FishEyeButtons1.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="FontSize" Value="12" /> <Style.Triggers> <EventTrigger RoutedEvent="Button. MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="FontSize" To="36" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent="Button. MouseLeave"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="FontSize" To="12" Duration="0:0:0.25" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </StackPanel.Resources> <Button>Button No. 1</Button> <Button>Button No. 2</Button> <Button>Button No. 3</Button> <Button>Button No. 4</Button> <Button>Button No. 5</Button> <Button>Button No. 6</Button> <Button>Button No. 7</Button> <Button>Button No. 8</Button> <Button>Button No. 9</Button> </StackPanel>



These two EventTrigger elements cause the buttons to exhibit a "fish-eye" effect when the pointer passes over them. As the mouse pointer rests on a button, the button gradually grows to three times its normal size. When the pointer leaves, the button quickly returns to its normal size.

As you'll note, if you move the pointer over a button and then pull it away quickly, the button won't go to the full size of 36 specified in the first DoubleAnimation. This is a result of the default HandoffBehavior property defined by BeginStoryboard. This property governs the interaction of two different animations that affect the same property. The default setting for HandoffBehavior is the enumeration value HandoffBehavior.SnapshotAndReplace, which stops the previous animation, takes a "snapshot" of its current value, and then replaces the animation with the new one.

To see an alternative approach, change the BeginStoryboard element in the MouseLeave section to have a HandoffBehavior setting of HandoffBehavior.Compose:

<BeginStoryboard HandoffBehavior="Compose"> 


Also, change the Duration to one second. Now when you move the pointer over a button and then pull it away, the button continues to expand a bit before the second animation kicks in.

When you define an animation in an element, it needs to be triggered by an EventTrigger in the Triggers section of that element. In a Style, however, you don't need to trigger the animation with EventTrigger. Here's an alternative approach to the fish-eye buttons that triggers the animation with a regular Trigger for the property IsMouseOver.

FishEyeButtons2.xaml

[View full width]

<!-- === =============================================== FishEyeButtons2.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="FontSize" Value="12" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="FontSize" To="36" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetProperty="FontSize" To="12" Duration="0:0:0.25" /> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </StackPanel.Resources> <Button>Button No. 1</Button> <Button>Button No. 2</Button> <Button>Button No. 3</Button> <Button>Button No. 4</Button> <Button>Button No. 5</Button> <Button>Button No. 6</Button> <Button>Button No. 7</Button> <Button>Button No. 8</Button> <Button>Button No. 9</Button> </StackPanel>



In this case, it's necessary for BeginStoryboard to be a child of the EnterActions and ExitActions collections that are defined by TriggerBase.

Changing the size of a Button by changing its FontSize is convenient, but it's not a general solution to changing the size of elements. In the general case, you'll probably want to set a transform for the element and then animate that transform. Animating transforms is particularly handy when you are implementing animated rotation. You may wonder about the propriety of a Button that rotates, but consider a program in which it might be useful to deliver a little feedback after a button click to assure the user that the program has recognized the click. You could, for example, use rotation to "shake" the button a bit.

When animating transforms, you can make the TargetName either the element that contains the transform, or the transform itself. Generally, you animate RenderTransform rather than LayoutTransform. The TargetProperty of the animation refers to a property of the RenderTransform of the element being animated. By default, the RenderTransform property is a MatrixTransform object, so if you don't want to animate individual properties of a Matrix structure, you must first set RenderTransform to a particular type of transform (for example, RotateTransform) and then animate a property of that transform.

The following XAML file defines a Style for a Button, where the RenderTransform property of the Button is set to a RotateTransform object with default settings (that is, no rotation). In addition, the RenderTransformOrigin is set to the value (0.5, 0.5), so any rotation occurs around the center of the button.

ShakingButton.xaml

[View full width]

<!-- === =============================================== ShakingButton.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> <Setter Property="RenderTransformOrigin" Value="0.5 0.5" /> <Setter Property="RenderTransform"> <Setter.Value> <RotateTransform /> </Setter.Value> </Setter> <Style.Triggers> <EventTrigger RoutedEvent="Button. Click"> <BeginStoryboard> <Storyboard TargetProperty="RenderTransform.Angle"> <DoubleAnimation From="-5" To="5" Duration="0:0:0.05" AutoReverse="True" RepeatBehavior="3x" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </StackPanel.Resources> <Button>Button No. 1</Button> <Button>Button No. 2</Button> <Button>Button No. 3</Button> <Button>Button No. 4</Button> <Button>Button No. 5</Button> </StackPanel>



The TargetProperty of the animation is RenderTransform.Angle, so obviously the animation is assuming that RenderTransform has already been set to an object of type RotateTransform. Over the course of 0.05 seconds, the rotation angle changes from 5 degrees to 5 degrees. It's then reversed and repeated twice more.

If you slowed down this animation, you would see that the button initially jumps from its normal position to a rotation of 5 degrees, and at the end jumps back. At high speeds, this is imperceptible, but in other cases, you might want to first animate the button from 0 degrees to 5 degrees, then move it repeatedly between 5 degrees and 5 degrees, and conclude with a third animation that moves it gracefully from 5 degrees back to 0 degrees. This is a job for a key-frame animation, which I'll discuss later in this chapter.

An animation can be controlled while in progress by making use of classes that derive from ControllableStoryboardAction. The following class hierarchy shows how these classes fit in with the other derivatives of TriggerAction.

Object

     DispatcherObject (abstract)
          DependencyObject

               TriggerAction (abstract)

                    SoundPlayerAction

                    BeginStoryboard

                    ControllableStoryboardAction (abstract)

                         PauseStoryboard

                         RemoveStoryboard

                         ResumeStoryboard

                         SeekStoryboard

                         SetStoryboardSpeedRatio

                         SkipStoryboardToFill

                         StopStoryboard

To use these classes, you set up an animation normally with BeginStoryboard, and you assign BeginStoryboard a Name attribute. ControllableStoryboardAction defines a property named BeginStoryboardName that you use to refer to this name. Like BeginStoryboard, these derivatives of ControllableStoryboardAction are defined as part of a TriggerAction collection.

Here's a stand-alone XAML file with a Rectangle that has two RotateTransform objects definedone centered around its lower-left corner and the other centered around its lower-right corner. The storyboard (defined normally except that the BeginStoryboard has its Name attribute defined) uses a DoubleAnimation to change the angle of the first RotateTransform from 90 degrees to 0 degrees, and then a second DoubleAnimation changes the angle of the second RotateTransform from 0 degrees to 90 degrees, making it appear as if the rectangle stands up, and then falls down in the other direction.

ControllingTheStoryboard.xaml

[View full width]

<!-- === =========== ============================================= ControllingTheStoryboard.xaml (c) 2006 by Charles Petzold ============================================= ============== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <StackPanel> <!-- Canvas displaying animated rectangle. --> <Canvas Width="350" Height="200"> <Rectangle Canvas.Left="150" Canvas .Top="50" Stroke="Black" StrokeThickness="4" Fill="Aqua" Width="50" Height="150"> <Rectangle.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform1" Angle="-90" CenterX="0" CenterY="150" /> <RotateTransform x :Name="xform2" CenterX="50" CenterY="150" /> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> </Canvas> <!-- StackPanel with buttons to control animation. --> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <Button Name="btnBegin" Content="Begin" Margin="12" /> <Button Name="btnPause" Content="Pause" Margin="12" /> <Button Name="btnResume" Content="Resume" Margin="12" /> <Button Name="btnStop" Content="Stop" Margin="12" /> <Button Name="btnSkip" Content="Skip to End" Margin="12" /> <Button Name="btnCenter" Content="Skip to Center" Margin="12" /> </StackPanel> <!-- Triggers section for button clicks. --> <StackPanel.Triggers> <EventTrigger SourceName="btnBegin" RoutedEvent="Button.Click"> <BeginStoryboard Name="storybrd"> <Storyboard > <DoubleAnimation Storyboard .TargetName="xform1" Storyboard .TargetProperty="Angle" From="-90" To="0" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="xform2" Storyboard .TargetProperty="Angle" BeginTime="0:0:5" From="0" To="90" Duration="0:0:5" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger SourceName="btnPause" RoutedEvent="Button.Click"> <PauseStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnResume" RoutedEvent="Button.Click"> <ResumeStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnStop" RoutedEvent="Button.Click"> <StopStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnSkip" RoutedEvent="Button.Click"> <SkipStoryboardToFill BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnCenter" RoutedEvent="Button.Click"> <SeekStoryboard BeginStoryboardName="storybrd" Offset="0:0:5" /> </EventTrigger> </StackPanel.Triggers> </StackPanel> </Page>



Six buttons sit under the rectangle, each of which is given a Name. The StackPanel.Triggers section contains EventTrigger elements associated with the Click events for each of these buttons. The Begin button starts the animation like normal. The Pause button triggers the PauseStoryboard action, which pauses the animation. The Resume button resumes it. The Stop button returns the rectangle to its pre-animated state. The button labeled Skip To End triggers the SkipStoryboardToFill action, which skips to the end of the animation and whatever the FillBehavior has in store. The SeekStoryboard action can go to an arbitrary point in the animation. I chose to make it go to the middle.

I originally defined a Triggers section for each of the buttons and put the individual EventTrigger elements in these Triggers sections, but I couldn't get the program to work that way. Apparently, all the EventTrigger objects that begin and control the storyboard must be consolidated in the same Triggers section.

In many XAML demonstration programs, an animation begins as soon as the program is loaded and continues until the program is terminated. In these files, the Loaded event triggers the animation. This trigger can be the Loaded event for the element being animated or the Loaded event for another element, perhaps a Panel or the Page.

The following stand-alone XAML file is an animated version of the RenderTransformAndLayoutTransform.xaml file from the previous chapter. It demonstrates why you should usually animate RenderTransform to cause as little disruption to the element's environment as possible, unless you actually want the animated element to affect layout, in which case you should definitely animate LayoutTransform.

RenderTransformVersusLayoutTransform.xaml

[View full width]

<!-- == =========== =========== =============================================== RenderTransformVersusLayoutTransform.xaml (c) 2006 by Charles Petzold ============================================ =========================== --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" TextBlock.FontSize="18pt" > <!-- RenderTransform section. --> <TextBlock TextAlignment="Center" Margin="24"> Animate <Italic>RenderTransform</Italic> </TextBlock> <UniformGrid Rows="3" Columns="3"> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button"> <Button.RenderTransform> <RotateTransform x:Name="xform1" /> </Button.RenderTransform> </Button> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> <!-- LayoutTransform section. --> <TextBlock TextAlignment="Center" Margin="24"> Animate <Italic>LayoutTransform</Italic> </TextBlock> <UniformGrid Rows="3" Columns="3"> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" > <Button.LayoutTransform> <RotateTransform x:Name="xform2" /> </Button.LayoutTransform> </Button> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> <!-- Animations. --> <StackPanel.Triggers> <EventTrigger RoutedEvent="StackPanel.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="xform2" Storyboard .TargetProperty="Angle" Duration="0:0:10" From="0" To="360" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xform1" Storyboard .TargetProperty="Angle" Duration="0:0:10" From="0" To="360" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </StackPanel.Triggers> </StackPanel>



As you might recall from the nonanimated version of this program, two 3-by-3 UniformGrid panels are filled with buttons, and the middle button of each is rotated, the first with RenderTransform and the second with LayoutTransform. In this version, the two angles of rotation are animated.

The root element is a StackPanel, and the two animations are included in the Triggers section for the StackPanel, with a triggering event of Loaded. The target properties of the animations are the transforms themselves, which have both been given x:Name attributes. The TargetProperty is just Angle. The animations continue forever.

When you're using a program like XAML Cruncher to experiment with an animation triggered by a Loaded event, keep in mind that the animation begins anew when the file passes through the XamlReader.Load method and the elements are recreated. In XAML Cruncher, this happens with each editor keystroke that alters the text of the file. You might instead want to suspend automatic parsing and press F6 to parse manually and begin the animation.

Animations that start automatically and continue forever can be fun and educational as well. The following stand-alone XAML file defines a single Path based on two EllipseGeometry objects, the second of which is rotated by the animation. As the first circle meets up with the second circle, it provides a dramatic visual reminder of the default FillRule of EvenOdd.

TotalEclipseOfTheSun.xaml

[View full width]

<!-- === ==================================================== TotalEclipseOfTheSun.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Gray" Stroke="Black" StrokeThickness="3"> <Path.Data> <GeometryGroup> <EllipseGeometry Center="96 288" RadiusX="48" RadiusY="48" /> <EllipseGeometry Center="288 96" RadiusX="48" RadiusY="48"> <EllipseGeometry.Transform> <RotateTransform x :Name="rotate" CenterX="288" CenterY="288" /> </EllipseGeometry.Transform> </EllipseGeometry> </GeometryGroup> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="rotate" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The following XAML program has a TextBlock with both a ScaleTransform and a RotateTransform defined in a TransformGroup. The ScaleX property of the ScaleTransform is animated between 1 and 1 to flip the text around the vertical axis, almost simulating text rotating in three dimensions around a vertical axis. The RotateTransform rotates the text around its center. The two animations have different durations and repeat forever for a mix of effects.

AnimatedTextTransform.xaml

[View full width]

<!-- === ===================================================== AnimatedTextTransform.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <TextBlock Text="XAML" FontSize="144pt" FontFamily="Arial Black" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5"> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform x:Name="xformScale" /> <RotateTransform x :Name="xformRotate" /> </TransformGroup> </TextBlock.RenderTransform> <TextBlock.Triggers> <EventTrigger RoutedEvent="TextBlock. Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="xformScale" Storyboard.TargetProperty="ScaleX" From="1" To="-1" Duration="0:0:3" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetName="xformRotate" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </TextBlock.Triggers> </TextBlock> </Page>



So far I've shown programs that use DoubleAnimation and ColorAnimation. Another common type of animation is PointAnimation, which is an animation between two Point objects. The following stand-alone XAML program defines a Path made of four Bezier splines.

SquaringTheCircle.xaml

[View full width]

<!-- === ================================================= SquaringTheCircle.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" RenderTransform="2 0 0 -2 300 300"> <Path StrokeThickness="3" Stroke="Blue" Fill="AliceBlue"> <Path.Data> <PathGeometry> <PathFigure x:Name="bez1" IsClosed="True"> <BezierSegment x:Name="bez2" /> <BezierSegment x:Name="bez3" /> <BezierSegment x:Name="bez4" /> <BezierSegment x:Name="bez5" /> </PathFigure> <PathGeometry.Transform> <RotateTransform Angle="45" /> </PathGeometry.Transform> </PathGeometry> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard RepeatBehavior="Forever" AutoReverse="True" > <PointAnimation Storyboard .TargetName="bez1" Storyboard .TargetProperty="StartPoint" From="0 100" To="0 125" /> <PointAnimation Storyboard .TargetName="bez2" Storyboard .TargetProperty="Point1" From="55 100" To="62.5 62.5" /> <PointAnimation Storyboard .TargetName="bez2" Storyboard .TargetProperty="Point2" From="100 55" To="62.5 62.5" /> <PointAnimation Storyboard .TargetName="bez2" Storyboard .TargetProperty="Point3" From="100 0" To="125 0" /> <PointAnimation Storyboard .TargetName="bez3" Storyboard .TargetProperty="Point1" From="100 -55" To="62.5 -62.5" /> <PointAnimation Storyboard .TargetName="bez3" Storyboard .TargetProperty="Point2" From="55 -100" To="62.5 -62.5" /> <PointAnimation Storyboard .TargetName="bez3" Storyboard .TargetProperty="Point3" From="0 -100" To="0 -125" /> <PointAnimation Storyboard .TargetName="bez4" Storyboard .TargetProperty="Point1" From="-55 -100" To="-62.5 -62.5" /> <PointAnimation Storyboard .TargetName="bez4" Storyboard .TargetProperty="Point2" From="-100 -55" To="-62.5 -62.5" /> <PointAnimation Storyboard .TargetName="bez4" Storyboard .TargetProperty="Point3" From="-100 0" To="-125 0" /> <PointAnimation Storyboard .TargetName="bez5" Storyboard .TargetProperty="Point1" From="-100 55" To="-62.5 62.5" /> <PointAnimation Storyboard .TargetName="bez5" Storyboard .TargetProperty="Point2" From="-55 100" To="-62.5 62.5" /> <PointAnimation Storyboard .TargetName="bez5" Storyboard .TargetProperty="Point3" From="0 100" To="0 125" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The 13 PointAnimation objects change the 13 points that make up the figure. (The last point is the same as the first.) The From values define a circle centered on the point (0, 0), with a radius of 100. The To values define a square with sides of 177 units, so that the square occupies approximately the same area as the circle. The animation shifts between the circle and the square.

As defined by the To values, the corners of the square are at the left, top, right, and bottom, so the figure is more like a diamond shape. I decided I wanted the sides horizontal and vertical, but instead of changing all the coordinates, I simply added a RotateTransform to the Path to rotate the figure 45 degrees. The RenderTransform for the Canvas moves the origin to the point (300, 300) and doubles the size of the figure.

DrawingContext is the class you use for drawing when you override the OnRender method defined by UIElement and also when you create objects of type DrawingVisual. Many of the drawing methods defined by DrawingContext have overloads that include AnimationClock arguments. In this way you can define elements that intrinsically animate themselves, potentially forever. Here's an example of a class that derives from FrameworkElement and overrides OnRender.

AnimatedCircle.cs

[View full width]

//----------------------------------------------- // AnimatedCircle.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; namespace Petzold.RenderTheAnimation { class AnimatedCircle : FrameworkElement { protected override void OnRender (DrawingContext dc) { DoubleAnimation anima = new DoubleAnimation(); anima.From = 0; anima.To = 100; anima.Duration = new Duration(TimeSpan .FromSeconds(1)); anima.AutoReverse = true; anima.RepeatBehavior = RepeatBehavior .Forever; AnimationClock clock = anima .CreateClock(); dc.DrawEllipse(Brushes.Blue, new Pen (Brushes.Red, 3), new Point(125, 125), null, 0, clock, 0, clock); } } }



You could put an instance of such a class in a XAML file or in code. I made it part of the following two-file project named RenderTheAnimation that also includes this C# file.

RenderTheAnimation.cs

//--------------------------------------------------- // RenderTheAnimation.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows; namespace Petzold.RenderTheAnimation {     class RenderTheAnimation : Window     {         [STAThread]         public static void Main()         {             Application app = new Application();             app.Run(new RenderTheAnimation());         }         public RenderTheAnimation()         {             Title = "Render the Animation";             Content = new AnimatedCircle();         }     } } 



The next stand-alone XAML file is an animation of an Ellipse that is triggered by the Loaded event of the Ellipse itself. The animation has a TargetProperty of Width, but the Height property is a Binding to the Width property, so both properties change in the same way.

Pulse.xaml

[View full width]

<!-- ========================================== Pulsing.xaml (c) 2006 by Charles Petzold ========================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Ellipse HorizontalAlignment="Center" VerticalAlignment="Center" Width="48" Fill="Red" Height="{Binding RelativeSource={RelativeSource self}, Path=Width}"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .Loaded"> <BeginStoryboard> <Storyboard TargetProperty="Width" RepeatBehavior="Forever"> <DoubleAnimation From="48" To="288" Duration="0:0:0.25" BeginTime="0:0:1" RepeatBehavior="2x" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Page>



The DoubleAnimation has a BeginTime of one second, so the animation doesn't do anything during that time. The animation then increases the Width (and, through the binding, also the Height) quickly from one-half inch to three inches, snapping back to one-half inch. The RepeatBehavior setting makes this happen twice.

The Storyboard itself also has a RepeatBehavior setting of Forever so that the DoubleAnimation is repeated indefinitely. For each repetition, the BeginTime delays the two pulses for one second, perhaps giving an overall effect of a heartbeat.

Here's an irritating little visual of a Rectangle that's animated in several ways. Two DoubleAnimation objects increase the Width and Height of the Rectangle. At the same time, two other DoubleAnimation objects decrease the Canvas.Left and Canvas.Top attached properties so that the Rectangle seems to stay in place. Two PointAnimation objects change the StartPoint and EndPoint properties of the LinearGradientBrush. These two points are initially the upper-left and lower-right corners of the rectangle, but they move to the upper-right and lower-left. Finally, two ColorAnimation objects swap the two colors used in the gradient brush.

PulsatingRectangle.xaml

[View full width]

<!-- === ================================================== PulsatingRectangle.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Rectangle Name="rect" Canvas.Left="96" Canvas.Top="96" Width="192" Height="192" Stroke="Black"> <Rectangle.Fill> <LinearGradientBrush x:Name="brush"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0" Color="Red" /> <GradientStop Offset="1" Color="Blue" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="rect" Storyboard .TargetProperty="Width" From="192" To="204" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="rect" Storyboard .TargetProperty="Height" From="192" To="204" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="rect" Storyboard .TargetProperty="(Canvas.Left)" From="96" To="90" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="rect" Storyboard .TargetProperty="(Canvas.Top)" From="96" To="90" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <PointAnimation Storyboard .TargetName="brush" Storyboard .TargetProperty="StartPoint" From="0 0" To="1 0" Duration="0:0:5" AutoReverse="True" RepeatBehavior="Forever" /> <PointAnimation Storyboard .TargetName="brush" Storyboard .TargetProperty="EndPoint" From="1 1" To="0 1" Duration="0:0:5" AutoReverse="True" RepeatBehavior="Forever" /> <ColorAnimation Storyboard.TargetName="brush" Storyboard .TargetProperty="GradientStops[0].Color" From="Red" To="Blue" Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever" /> <Coloranimation Storyboard.TargetName="brush" Storyboard .TargetProperty="GradientStops[1].Color" From="Blue" To="Red" Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



These eight animation objects have three different Duration settings, and all have RepeatBehavior set to Forever. You might be tempted to set the RepeatBehavior for the Storyboard to Forever and to remove the individual RepeatBehavior attributes, but that wouldn't give the same effect. What would be repeated would be an animation that lasted 10 seconds (the total duration of the longest animation object taking AutoReverse into account). Animations with durations shorter than 10 seconds would occur only at the beginning of each 10-second period.

If you want all multiple animations to go on forever, you can move the RepeatBehavior setting of Forever to the Storyboard only if all the individual animations have the same total durations.

AutoReverse is very handy in animations that repeat forever because it helps avoid discontinuities when the animation repeats. But it's not the only way to make animations smooth. You can make nonreversing continuous animations smooth by making the ending position (or size or whatever) of one animated object coincide with the beginning position (or whatever) of another animated object.

The following XAML program uses five EllipseGeometry objects to define five concentric circles. (I used EllipseGeometry rather than Ellipse because it's easier to keep EllipseGeometry objects centered on the same point.) Ten DoubleAnimation objects increase the RadiusX and RadiusY of these five circles in a uniform way. All 10 DoubleAnimation objects have the IsAdditive property set to true, and all increase the dimension from 0 to 25. Because the radii of the five circles are 0, 25, 50, 75, and 100 units, each circle ends up at the initial size of the next-larger circle, making it seem as if all the circles are uniformly increasing in size.

To complete the illusion, special handling is required for the smallest circle and the largest circle. The smallest circle begins with a RadiusX and RadiusY of 0, and the first DoubleAnimation animates the StrokeThickness from 0 to the value specified in the Path element (12.5). Consequently, the smallest circle seems to arise out of nothingness. The final DoubleAnimation in the long list decreases the Opacity property of the largest circle, making it seem to fade away.

Because all the animations have the same Duration, the RepeatBehavior can be moved to the parent Storyboard in this program.

ExpandingCircles.xaml

[View full width]

<!-- === ================================================ ExpandingCircles.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" WindowTitle="Expanding Circles"> <Canvas Width="400" Height="400" HorizontalAlignment="Center" VerticalAlignment="Center" > <!-- The inner circle. --> <Path Name="pathInner" Stroke="Red" StrokeThickness="12.5"> <Path.Data> <EllipseGeometry x:Name="elips1" Center="200 200" RadiusX="0" RadiusY="0" /> </Path.Data> </Path> <!-- All circles except the inner and outer. circles --> <Path Stroke="Red" StrokeThickness="12.5"> <Path.Data> <GeometryGroup> <EllipseGeometry x:Name="elips2" Center="200 200" RadiusX="25" RadiusY="25" /> <EllipseGeometry x:Name="elips3" Center="200 200" RadiusX="50" RadiusY="50" /> <EllipseGeometry x:Name="elips4" Center="200 200" RadiusX="75" RadiusY="75" /> </GeometryGroup> </Path.Data> </Path> <!-- The outer circle. --> <Path Name="pathOuter" Stroke="Red" StrokeThickness="12.5"> <Path.Data> <EllipseGeometry x:Name="elips5" Center="200 200" RadiusX="100" RadiusY="100" /> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard RepeatBehavior="Forever"> <DoubleAnimation Storyboard .TargetName="pathInner" Storyboard .TargetProperty="StrokeThickness" From="0" Duration="0 :0:5" /> <DoubleAnimation Storyboard .TargetName="elips1" Storyboard .TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips1" Storyboard .TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips2" Storyboard .TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips2" Storyboard .TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips3" Storyboard .TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips3" Storyboard .TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips4" Storyboard .TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips4" Storyboard .TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips5" Storyboard .TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="elips5" Storyboard .TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard .TargetName="pathOuter" Storyboard .TargetProperty="Opacity" From="1" To="0" Duration="0:0:5" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> </Page>



Of course, I could have eliminated some of these DoubleAnimation objects with bindings between the RadiusX and RadiusY properties of the individual EllipseGeometry elements.

Sometimes you can replace a bunch of simultaneous animations with a single animation, but doing so might take some thought and perhaps even mathematics. The following program began with the Infinity.xaml file that I created in Chapter 28. In that file I used Bezier splines to draw an infinity sign colored with a LinearGradientBrush using the seven colors of the rainbow. For this chapter I decided I wanted to animate that brush by shifting to the right the colors that make up the gradient.

The following file has seven DoubleAnimation objects that animate the Offset properties of the seven GradientStop elements of the Brush. Each of these DoubleAnimation objects has the same Duration (seven seconds, but tripled in speed with a SpeedRatio setting of 3) and the same From and To values of 0 and 1, but each has a different BeginTime, from zero seconds to six seconds.

InfinityAnimation1.xaml

[View full width]

<!-- === ================================================== InfinityAnimation1.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" StrokeThickness="25"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0 -100"> <PolyBezierSegment Points=" -55 -100, -100 -55, -100 0, -100 55, -55 100, 0 100, 55 100, 100 50, 150 0, 200 -50, 245 -100, 300 -100, 355 -100, 400 -55, 400 0, 400 55, 355 100, 300 100, 245 100, 200 50, 150 0, 100 -50, 55 -100, 0 -100" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> <Path.Stroke> <LinearGradientBrush x:Name="brush"> <LinearGradientBrush.GradientStops> <GradientStop Color="Red" /> <GradientStop Color="Orange" /> <GradientStop Color="Yellow" /> <GradientStop Color="Green" /> <GradientStop Color="Blue" /> <GradientStop Color="Indigo" /> <GradientStop Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Path.Stroke> <Path.Triggers> <EventTrigger RoutedEvent="Path.Loaded"> <BeginStoryboard> <Storyboard TargetName="brush" SpeedRatio="3"> <DoubleAnimation Storyboard .TargetProperty="GradientStops[0].Offset" From="0" To="1" Duration="0:0:7" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[1].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:1" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[2].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:2" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[3].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:3" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[4].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:4" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[5].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:5" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetProperty="GradientStops[6].Offset" From="0" To="1" Duration="0:0:7" BeginTime="0:0:6" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Path.Triggers> </Path> </Canvas>



When the program begins, all GradientStop objects have a default Offset property of 0. Normally that would result in the entire infinity sign being colored with the last GradientStop in the list, which is violet. However, the first DoubleAnimation begins immediately, assigning the first GradientStop a nonzero Offset, and the infinity sign turns red. A second later (actually a third of a second because of the SpeedRatio setting), the Offset of the first GradientStop is approximately 0.14, and the second DoubleAnimation kicks in, which colors the beginning of the infinity sign with orange. Each color progressively becomes active. Once all the DoubleAnimation objects have started, the cycling colors continue smoothly.

The program works, but I later realized that the LinearGradientBrush itself has a Transform property, and this property might serve to simplify the program considerably.

Sure enough. The following revised version of the program defines the Offset properties of the GradientStop objects explicitly but also includes a named TranslateTransform object for the brush.

InfinityAnimation2.xaml

[View full width]

<!-- === ================================================== InfinityAnimation2.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" StrokeThickness="25" Data="M 0 -100 C -55 -100, -100 -55, -100 0 S -55 100, 0 100 S 100 50, 150 0 S 245 -100, 300 -100 S 400 -55, 400 0 S 355 100, 300 100 S 200 50, 150 0 S 55 -100, 0 -100"> <Path.Stroke> <LinearGradientBrush SpreadMethod="Repeat"> <LinearGradientBrush.Transform> <TranslateTransform x :Name="xform" /> </LinearGradientBrush.Transform> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.14" Color="Orange" /> <GradientStop Offset="0.28" Color="Yellow" /> <GradientStop Offset="0.42" Color="Green" /> <GradientStop Offset="0.56" Color="Blue" /> <GradientStop Offset="0.70" Color="Indigo" /> <GradientStop Offset="0.85" Color="Violet" /> <GradientStop Offset="1.00" Color="Red" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Path.Stroke> <Path.Triggers> <EventTrigger RoutedEvent="Path.Loaded"> <BeginStoryboard> <Storyboard TargetName="xform" TargetProperty="X"> <DoubleAnimation From="0" To="621" Duration="0:0:2" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Path.Triggers> </Path> </Canvas>



The single DoubleAnimation changes the X property of the TranslateTransform from 0 to 621, with a Duration of two seconds, repeated forever. By default the brush has a WrapMode of Tile, so the brush is essentially repeated beyond its bounds, and the transform shifts the brush with the same effect as in the earlier program.

So where does the 621 come from? You'll recall from diagrams in Chapter 2 that the default gradient of a LinearGradientBrush is from the upper-left to the lower-right of an element. Lines perpendicular to that diagonal line are colored the same. The InfinityAnimation2.xaml program defines eight GradientStop objects. The first and last are both red, and they have an offset of 0 and 1.0, respectively. The dashed lines in the following diagram symbolize the areas colored red.

The element being colored is the infinity sign. It has a height of H and a width of W. Looking at the coordinates, one might conclude that H is 200 and W is 500, but the stroke thickness is 25, which extends the dimensions of the image 12.5 units on all four sides. H is actually 225, and W is 525.

The TranslateTransform shifts the brush right. When it shifts W plus X units, the brush appears the same as the unshifted version. What is X? Based on similar triangles, the ratio of X to H is the same as the ratio of H to W, so X can be easily calculated as approximately 96, so that W plus X equals 621.

And now I will confess that I originally derived this number empirically in XAML Cruncher by commenting out the animation and experimenting with the X property in the TranslateTransform element. I kept trying values until the brush remained the same with and without the translation factor.

One advantage of using the WPF animation facilities instead of DispatcherTimer is that animated values are calculated based on elapsed time. When using a timer, often animations are based solely on timer ticks, and if the program misses a few timer ticks because the system is overloaded, the animation could be slowed down. That would be disastrous for a clock application, and that's why most clock applications obtain the actual time on each timer tick and update the clock hands from that.

The WPF animation facilities are designed to be accurate enough that you can use them to pace the hands of a clock, as in the XamlClock.xaml program that follows.

The following XAML file draws a clock. It uses a Canvas within a Viewbox, so the clock is as large or as small as the window that displays it. Take note of the two Path elements that draw the tick marks. These two elements display dotted lines in a circle with a radius of 90 units. The first element defines a StrokeDashArray of 0 and 3.14159 and a StrokeThickness of 3, which means that the dots are 3p units apart. The circumference of the circle is 180p, so the dotted line displays 60 small tick marks. The second element defines a StrokeDashArray of 0 and 7.854 (which is actually 2.5p) and a StrokeThickness of 6, so the dots are 15p units apart, which means that 12 large dots are displayed in the circle.

Following the definition of the tick marks, three additional Path elements draw the hands of the clock. All three clock hands point straight up. Each hand is associated with a RotateTransform that has an xName attribute set. The three DoubleAnimation elements change the angles of the transforms from 0 degrees to 360 degrees, with a Duration of 12 hours for the hour hand, one hour for the minute hand, and one minute for the second hand.

XamlClock.xaml

[View full width]

<!-- ============================================ XamlClock.xaml (c) 2006 by Charles Petzold ============================================ --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: Title="XAML Clock"> <Window.Resources> <!-- Every drawn object is a Path, so this style affects all of them. --> <Style TargetType="{x:Type Path}"> <Setter Property="Stroke" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" /> <Setter Property="StrokeThickness" Value="2" /> <Setter Property="StrokeStartLineCap" Value="Round" /> <Setter Property="StrokeEndLineCap" Value="Round" /> <Setter Property="StrokeLineJoin" Value="Round" /> <Setter Property="StrokeDashCap" Value="Round" /> </Style> </Window.Resources> <Viewbox> <!-- Draw clock on canvas, with center at (0, 0). --> <Canvas Width="200" Height="200"> <Canvas.RenderTransform> <TranslateTransform X="100" Y="100" /> </Canvas.RenderTransform> <!-- Tick marks (small and large). --> <Path Data="M 0 -90 A 90 90 0 1 1 -0. 01 -90" StrokeDashArray="0 3.14159" StrokeThickness="3" /> <Path Data="M 0 -90 A 90 90 0 1 1 -0. 01 -90" StrokeDashArray="0 7.854" StrokeThickness="6" /> <!-- Hour hand pointing up. --> <Path Data="M 0 15 L 10 0, 0 -60, -10 0 Z" Fill="{DynamicResource {x:Static SystemColors .ControlDarkBrushKey}}"> <Path.RenderTransform> <RotateTransform x :Name="xformHour" /> </Path.RenderTransform> </Path> <!-- Minute hand pointing up. --> <Path Data="M 0 20 L 5 0 0 -80 -5 0 Z" Fill="{DynamicResource {x:Static SystemColors .ControlLightBrushKey}}"> <Path.RenderTransform> <RotateTransform x :Name="xformMinute" /> </Path.RenderTransform> </Path> <!-- Second hand pointing up. --> <Path Data="M 0 10 L 0 -80"> <Path.RenderTransform> <RotateTransform x :Name="xformSecond" /> </Path.RenderTransform> </Path> </Canvas> </Viewbox> <!-- All animations. --> <Window.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard Name="storyboard"> <DoubleAnimation Storyboard .TargetName="xformHour" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="12:0:0" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xformMinute" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="1:0:0" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xformSecond" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="0:1:0" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Window.Triggers> </Window>



You can load this XAML file into XAML Cruncher, remove the x:Class attribute near the top, and precisely at noon or the stroke of midnight, press F7 to open the window. You then have a clock that will keep the correct time (until daylight-saving time starts or ends, of course).

Are you not quite happy about the inconvenience of starting the program at noon or midnight? Okay then, just before you press F7, check the current time. Perhaps it's 4:38. In the Storyboard element, set the BeginTime property to the negative of that time:

<Storyboard Name="storyboard" BeginTime="-4:38"> 


Now if you launch the window, you will see that the clock is set correctly. You're essentially saying, "I want the animation to begin 4 hours and 38 minutes ago, which means I want all hands pointing straight up 4 hours and 38 minutes ago."

As you've probably figured out, XamlClock.xaml is part of a project named XamlClock, and the Storyboard element has a Name attribute set for a reason. A little bit of C# code is all that's necessary to initialize the BeginTime property to the negative of the current time.

XamlClock.cs

[View full width]

//------------------------------------------ // XamlClock.cs (c) 2006 by Charles Petzold //------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.XamlClock { public partial class XamlClock : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new XamlClock()); } public XamlClock() { InitializeComponent(); // Initialize Storyboard to display current time. storyboard.BeginTime = -DateTime.Now .TimeOfDay; } } }



The TimeOfDay property of DateTime returns an object of type TimeSpan, which is the type of the BeginTime property.

In the previous chapter, I developed a technique that put transforms to work performing calculations in XAML. Might it be possible to use this technique to create a clock entirely in XAML? Yes, it is possible, and the following stand-alone XAML file demonstrates it. The Resources section of the Page begins with a FrameworkElement whose Tag is set to the current DateTime. I thought it would be wise to obtain the current DateTime just once rather than several times and avoid the risk of the time changing between accesses.

Three TransformGroup objects perform all the necessary calculations. For example, the initial angle of the hour hand must be set to the sum of 30 degrees times the hour and 0.5 degrees times the minute. (For 3:30, that's 105 degrees, halfway between the 3 and 4.) That's accomplished by setting the X and Y offsets of a TranslateTransform to the hour and minute values, and multiplying by a matrix with M11 set to 30 and M21 set to 0.5. The product has an OffsetX property equal to 30 times the hour plus 0.5 times the minute. The RotateTransform for the hour hand initializes the Angle property to this value. The DoubleAnimation elements all have the IsAdditive property set to true to add to these initial values the animated values from 0 degrees to 360 degrees.

In addition, I've used Bezier splines to make the hour hand and minute hand just a little fancier.

AllXamlClock.xaml

[View full width]

<!-- =============================================== AllXamlClock.xaml (c) 2006 by Charles Petzold ============================================= == --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <Page.Resources> <!-- Get the current DateTime just once and stash it in a Tag property of an arbitrary FrameworkElement. --> <FrameworkElement x:Key="dt" Tag="{x :Static s:DateTime.Now}" /> <!-- Multiply Hour by 30 degrees and Minute by 0.5 degrees and add. Result is stored in angleHour.Value.OffsetX. --> <TransformGroup x:Key="angleHour"> <TranslateTransform X="{Binding Source={StaticResource dt}, Path=Tag.Hour}" Y="{Binding Source={StaticResource dt}, Path=Tag.Minute}" /> <MatrixTransform Matrix="30 0 0.5 1 0 0" /> </TransformGroup> <!-- Multiply Minute by 6 degrees and Second by 0.1 degrees and add. Result is stored in angleMinute.Value.OffsetX. --> <TransformGroup x:Key="angleMinute"> <TranslateTransform X="{Binding Source={StaticResource dt}, Path=Tag.Minute}" Y="{Binding Source={StaticResource dt}, Path=Tag.Second}" /> <MatrixTransform Matrix="6 0 0.1 1 0 0" /> </TransformGroup> <!-- Multiply Second by 6 degrees. Result is angleSecond.Value.M11. --> <TransformGroup x:Key="angleSecond"> <ScaleTransform ScaleX="{Binding Source={StaticResource dt}, Path=Tag.Second}" /> <ScaleTransform ScaleX="6" /> </TransformGroup> <!-- Every drawn object is a Path, so this style affects all of them. --> <Style TargetType="{x:Type Path}"> <Setter Property="Stroke" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" /> <Setter Property="StrokeThickness" Value="2" /> <Setter Property="StrokeStartLineCap" Value="Round" /> <Setter Property="StrokeEndLineCap" Value="Round" /> <Setter Property="StrokeLineJoin" Value="Round" /> <Setter Property="StrokeDashCap" Value="Round" /> </Style> </Page.Resources> <Viewbox> <!-- Draw clock on canvas, with center at (0, 0). --> <Canvas Width="200" Height="200"> <Canvas.RenderTransform> <TranslateTransform X="100" Y="100" /> </Canvas.RenderTransform> <!-- Tick marks (small and large). --> <Path Data="M 0 -90 A 90 90 0 1 1 -0. 01 -90" StrokeDashArray="0 3.14159" StrokeThickness="3" /> <Path Data="M 0 -90 A 90 90 0 1 1 -0. 01 -90" StrokeDashArray="0 7.854" StrokeThickness="6" /> <!-- Hour hand pointing up. --> <Path Data="M 0 -60 C 0 -30, 20 -30, 5 -20 L 5 0 C 5 7.5, -5 7.5, -5 0 L -5 -20 C -20 -30, 0 -30 0 -60" Fill="{DynamicResource {x:Static SystemColors .ControlDarkBrushKey}}"> <Path.RenderTransform> <RotateTransform x :Name="xformHour" Angle="{Binding Source={StaticResource angleHour}, Path=Value .OffsetX}" /> </Path.RenderTransform> </Path> <!-- Minute hand pointing up. --> <Path Data="M 0 -80 C 0 -75, 0 -70, 2. 5 -60 L 2.5 0 C 2.5 5, -2.5 5, -2.5 0 L -2.5 -60 C 0 -70, 0 -75, 0 -80" Fill="{DynamicResource {x:Static SystemColors .ControlLightBrushKey}}"> <Path.RenderTransform> <RotateTransform x :Name="xformMinute" Angle="{Binding Source={StaticResource angleMinute}, Path=Value .OffsetX}" /> </Path.RenderTransform> </Path> <!-- Second hand pointing up. --> <Path Data="M 0 10 L 0 -80"> <Path.RenderTransform> <RotateTransform x :Name="xformSecond" Angle="{Binding Source={StaticResource angleSecond}, Path=Value .M11}" /> </Path.RenderTransform> </Path> </Canvas> </Viewbox> <!-- All animations. --> <Page.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="xformHour" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="12:0:0" IsAdditive="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xformMinute" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="1:0:0" IsAdditive="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xformSecond" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="0:1:0" IsAdditive="True" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



I wanted to have a ball bounce between the midpoints of the four sides of its container. Notice that the animation in the following stand-alone XAML file is triggered on the SizeChanged event of the Canvas, so it starts anew whenever the window holding the Canvas changes size.

FourSidedBounce1.xaml

[View full width]

<!-- === =============================================== FourSidedBounce1.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" Name="canv"> <Ellipse Name="elips" Fill="Blue" Width="48" Height="48" /> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas .SizeChanged"> <BeginStoryboard> <Storyboard TargetName="elips"> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" BeginTime="-0:0:1" Duration="0:0:2" RepeatBehavior="Forever" AutoReverse="True" From="0" To="{Binding ElementName=canv, Path=ActualWidth}" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" Duration="0:0:2" RepeatBehavior="Forever" AutoReverse="True" From="0" To="{Binding ElementName=canv, Path=ActualHeight}" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The first DoubleAnimation changes the horizontal location of the ball between 0 and the ActualWidth of the canvas, whereas the second DoubleAnimation changes the vertical location between 0 and ActualHeight, both with AutoReverse turned on. Normally, this would cause the ball to bounce between the upper-left corner and the lower-right corner, but the first DoubleAnimation has a BeginTime set to the negative of half the Duration, so the initial position of the ball is at the middle of the top of the canvas.

Everything works fine except that the ball goes beyond the right side and bottom of the container. The Canvas.Left property can't change from 0 to the ActualWidth of the canvas; it has to change from 0 to ActualWidth minus the diameter of the ball. I could have used a similar technique as in AllXamlClock to perform this calculation, but I figured it was easier to put the ball in the upper-left quadrant of a four-cell Grid, where the rightmost column and bottom row were given the same dimensions as the ball, as shown in the following program.

FourSidedBounce2.xaml

[View full width]

<!-- === ================================================ FourSidedBounce1.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="48" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="48" /> </Grid.ColumnDefinitions> <Canvas Name="canv"> <Ellipse Name="elips" Fill="Blue" Width="48" Height="48" /> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas. SizeChanged"> <BeginStoryboard> <Storyboard TargetName="elips"> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" BeginTime="-0:0:1" Duration="0:0:2" RepeatBehavior="Forever" AutoReverse="True" From="0" To="{Binding ElementName=canv, Path=ActualWidth}" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" Duration="0:0:2" RepeatBehavior="Forever" AutoReverse="True" From="0" To="{Binding ElementName=canv, Path=ActualHeight}" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> </Grid> </Page>



The following stand-alone XAML file is intended to simulate a ball bouncing on a floor. The floor is a horizontal Line element. The animation varies the Canvas.Top property of the ball between 96 and 480 units over the course of a second, with AutoReverse turned on, of course, and repeating forever. Because the maximum Canvas.Top property is 480, the Height of the ellipse is 24, and StrokeThickness of the floor is 5, I set the Y coordinate of the Line element that defines the floor to 480 plus 24 plus 2 (about half the StrokeThickess).

BouncingBall.xaml

[View full width]

<!-- =============================================== BouncingBall.xaml (c) 2006 by Charles Petzold ============================================= == --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <Line X1="0" Y1="506" X2="1000" Y2="506" Stroke="Black" StrokeThickness="5" /> <Ellipse Name="elips" Width="24" Height="24" Fill="Red" Canvas.Left="96"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" RepeatBehavior="Forever"> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" From="96" To="480" Duration="0:0:1" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



This animation has many problems that prevent it from looking realistic. Of course, by repeating forever, it violates physical laws, but even that might be forgiven if only the speed of the ball weren't quite so consistent. The ball should speed up as it drops and slow down as it rises.

You can vary the speed of an animation with the AccelerationRatio and DecelerationRatio attributes. By default these are both 0. You can set them to values between 0 and 1, but the sum must not exceed 1. An AccelerationRatio of 0.25 means that the animation speeds up during the first 25 percent of its Duration time. The DecelerationRatio similarly slows down the animation at the end of the Duration.

To more closely simulate a bouncing ball, insert the following attribute into the DoubleAnimation element:

AccelerationRatio="1" 


This setting speeds up the animation during its entire duration so it's moving fastest as the ball hits the floor. When the animation reverses, the same AccelerationRatio causes the ball to slow down as it reaches its peak.

To see a different effect, set the DecelerationRatio:

DecelerationRatio="1" 


Now it looks like a ball suspended from a spring and bouncing up and down. Set both attributes to 0.5 to achieve an effect where the ball travels fastest midway in its journey up or down, much like a pendulum.

The following BetterBouncingBall.xaml file has an AccelerationRatio set and also attempts to make the ball flatten out a bit as it strikes the floor.

BetterBouncingBall.xaml

[View full width]

<!-- === ================================================== BetterBouncingBall.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <Line X1="0" Y1="506" X2="1000" Y2="506" Stroke="Black" StrokeThickness="5" /> <Ellipse Name="elips" Width="24" Height="24" Fill="Red" Canvas.Left="96"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" RepeatBehavior="Forever"> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Top)" From="96" To="490" Duration="0:0:1" AutoReverse="True" AccelerationRatio="1" /> <ParallelTimeline BeginTime="0:0:0.98" AutoReverse="True"> <DoubleAnimation Storyboard.TargetProperty="Width" To="32" Duration="0:0:0.02" /> <DoubleAnimation Storyboard.TargetProperty="Height" To="16" Duration="0:0:0.02" /> <DoubleAnimation Storyboard .TargetProperty="(Canvas.Left)" From="0" To="-4" Duration="0:0:0.02" IsAdditive="True" /> </ParallelTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



A pendulum should accelerate as it approaches the midpoint of its arc, and then it should decelerate, so both the acceleration and the deceleration ratios can be set to 0.5. The following stand-alone XAML file constructs a pendulum from a Line element and a Button that (rather perversely) displays the date and time at which the program was loaded. To keep both the Line and Button swinging together, I assembled them on a StackPanel, gave the panel a RotateTransform, and animated the Angle property between 30 degrees and 30 degrees.

PendulumButton.xaml

[View full width]

<!-- ================================================= PendulumButton.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <StackPanel Width="200"> <Line X1="100" X2="100" Y2="200" StrokeThickness="3" Stroke="Black" /> <Button Content="{x:Static s:DateTime.Now}" HorizontalAlignment="Center" /> <StackPanel.RenderTransform> <RotateTransform x:Name="xform" CenterX="100" /> </StackPanel.RenderTransform> <StackPanel.Triggers> <EventTrigger RoutedEvent="StackPanel. Loaded"> <BeginStoryboard> <Storyboard TargetName="xform" TargetProperty="Angle"> <DoubleAnimation From="-30" To="30" Duration="0:0:1" AccelerationRatio="0 .5" DecelerationRatio="0.5" AutoReverse="True" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </StackPanel.Triggers> </StackPanel> </Page>



Although AccelerationRatio and DecelerationRatio are handy, you have no control over the rate at which the animation speeds up or slows down. If you need that controlor if you want to create an animation that is more complex than a linear change from one value to anotheryou'll want to explore the key-frame animation.

To animate a property of type double by using key frames, you use a DoubleAnimationUsingKeyFrames element rather than DoubleAnimation. DoubleAnimationUsingKeyFrames contains a property named KeyFrames, of type DoubleKeyFrameCollection. This collection consists of elements of type DiscreteDoubleKeyFrame (to jump to discrete values), LinearDoubleKeyFrame (to linearly change a value), and SplineDoubleKeyFrame (to change a value at a nonlinear rate based on a Bezier spline). These children of DoubleAnimationUsingKeyFrames are all derived from the abstract DoubleKeyFrame class, as shown in the following class hierarchy:

Object

     DispatcherObject (abstract)

          DependencyObject

               Freezable (abstract)

                    DoubleKeyFrame (abstract)

                         DiscreteDoubleKeyFrame

                         LinearDoubleKeyFrame

                         SplineDoubleKeyFrame

                    DoubleKeyFrameCollection

A <Type>KeyFrame class and a <Type>KeyFrameCollection class exist for all 22 animatable types. All 22 types have a Discrete<Type>KeyFrame class; all but five types have Linear<Type> KeyFrame and Spline<Type>KeyFrame classes. The total number of classes involved here is 100.

The following table summarizes the types of animations supported for the 22 animatable types.

   

<Type>AnimationUsingKeyFrames

Type

<Type>Animation

<Type>AnimationUsingPath

Discrete

Linear

Spline

Boolean

  

  

Byte

 

Char

  

  

Color

 

Decimal

 

Double

Int16

 

Int32

 

Int64

 

Matrix

 

  

Object

  

  

Point

Point3D

 

Quaternion

 

Rect

 

Rotation3D

 

Single

 

Size

 

String

  

  

Thickness

 

Vector

 

Vector3D

 


Five of these typesspecifically Boolean, Char, Matrix, Object, and Stringcan only take on discrete values. You can't interpolate between two values of these types. (Although Char can be interpolated in theory, it probably doesn't make much sense for an animation to animate from one Unicode character to another. Objects of type Matrix can be interpolated in theory, but the results might not be quite meaningful.) I'll discuss <Type>AnimationUsingPath toward the end of this chapter.

The abstract DoubleKeyFrame class defines two properties named KeyTime (of type KeyTime) and Value. Neither DiscreteDoubleKeyFrame nor LinearDoubleKeyFrame defines any additional properties. Each key frame element essentially indicates the value of the animated property at a particular time. The key frame elements must be specified in the order in which they take effect.

Here's a simple example using two elements of type LinearDoubleKeyFrame and two elements of type DiscreteDoubleKeyFrame.

SimpleKeyFrameAnimation.xaml

[View full width]

<!-- === ======================================================= SimpleKeyFrameAnimation.xaml (c) 2006 by Charles Petzold ============================================= ============= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Ellipse Name="elips" Width="48" Height="48" Fill="Red" Canvas.Left="480" Canvas.Top="96" /> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty="(Canvas.Left)"> <DoubleAnimationUsingKeyFrames RepeatBehavior="Forever" Duration="0:0:10"> <LinearDoubleKeyFrame KeyTime="0:0:5" Value="0" /> <LinearDoubleKeyFrame KeyTime="0:0:5.5" Value="48" /> <DiscreteDoubleKeyFrame KeyTime="0:0:6" Value="144" /> <DiscreteDoubleKeyFrame KeyTime="0:0:7" Value="240" /> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The ball begins at the location (480, 96), as specified with the Canvas.Left and Canvas.Top attached properties in the Ellipse element. The animation changes the Canvas.Left property. For the first five seconds of the animation, that property changes from 480 to 0. As the first LinearDoubleKeyFrame indicates, at time 0:0:5, the value of Canvas.Left should equal 0. The second LinearDoubleKeyFrame element indicates that at time 0:0:5.5, the value of Canvas.Left should equal 48. Therefore, over the next half second, the ball moves right because Canvas.Left changes from 0 to 48.

A half second laterwhen the time from the beginning of the animation is six secondsthe ball jumps to a position of 144. A second later it jumps to 240.

The key frames are finished, but the DoubleAnimationUsingKeyFrames element indicates that the total duration of the animation is 10 seconds, so the ball sits at the position (240, 96) for three seconds. The DoubleAnimationUsingKeyFrames element also indicates a RepeatBehavior of Forever, so at the end of the 10 seconds, the ball jumps back to its initial position of (480, 96), and the animation begins again.

With both DiscreteDoubleKeyFrame and LinearDoubleKeyFrame, the value of the animated property at the time indicated by KeyTime is the value given as Value. With DiscreteDoubleKeyFrame, the animated value jumps to Value at KeyTime. With LinearDoubleKeyFrame, the animated value changes linearly from its previous value to the Value at KeyTime. How fast it appears to change depends on the previous Value and the previous KeyTime.

Here's a program that uses four elements of type DiscretePointKeyFrame.

DiscretePointJumps.xaml

[View full width]

<!-- === ================================================== DiscretePointJumps.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Red"> <Path.Data> <EllipseGeometry x:Name="elips" RadiusX="24" RadiusY="24" /> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty="Center"> <PointAnimationUsingKeyFrames Duration="0:0:4" RepeatBehavior="Forever"> <DiscretePointKeyFrame KeyTime="0:0:0" Value="288 96" /> <DiscretePointKeyFrame KeyTime="0:0:1" Value="480 288" /> <DiscretePointKeyFrame KeyTime="0:0:2" Value="288 480" /> <DiscretePointKeyFrame KeyTime="0:0:3" Value="96 288" /> </PointAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



Without the animation, the ellipse would be centered at the point (0, 0), but the first DiscretePointKeyFrame element indicates it should be located at the point (288, 96) at the key time of 0. If you look closely, you might be able to see the ellipse jump from (0, 0) to (288, 96) as the program loads. The ellipse remains at (288, 96) for one second until the key time of one second in the second DiscretePointKeyFrame element.

When three seconds have elapsed from the time the program was loaded, the ellipse is moved to point (96, 288). That's the last DiscretePointKeyFrame element, but the PointAnimationUsingKeyFrames element indicates that the total Duration of the animation is four seconds, so the ellipse sits at the point (96, 288) for the last of those four seconds.

If you leave out the Duration attribute, the last key time governs the total length of the animation. In this example, that's three seconds, so the animation ends at the same time the last DiscretePointKeyFrame is supposed to take effect. You won't even see it. The ellipse will seem to jump directly from the bottom position at (288, 480) to the top position at (288, 96).

The following file uses LinearPointKeyFrame to move a ball between four points, seemingly bouncing between the four sides of a rectangle.

KeyFramePointAnimation.xaml

[View full width]

<!-- === ====================================================== KeyFramePointAnimation.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Rectangle Stroke="Black" Width="480" Height="480" /> <Path Fill="Aqua" Stroke="Chocolate" StrokeThickness="3"> <Path.Data> <EllipseGeometry x:Name="elips" Center="240 50" RadiusX="48" RadiusY="48" /> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty="Center"> <PointAnimationUsingKeyFrames Duration="0:0:4" RepeatBehavior="Forever"> <LinearPointKeyFrame Value="430 240" KeyTime="0:0:1" /> <LinearPointKeyFrame Value="240 430" KeyTime="0:0:2" /> <LinearPointKeyFrame Value="50 240" KeyTime="0:0:3" /> <LinearPointKeyFrame Value="240 50" KeyTime="0:0:4" /> </PointAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



If the KeyTime properties are not present, the default is the static property KeyTime.Uniform, which means that every key frame has the same length of time. The Duration indicated for the overall animation is apportioned equally. If there is no Duration attribute, a default of one second is assumed.

If you remove all the KeyTime attributes from the previous XAML file, it will work the same way.

Another option is to specify a KeyTime as a percentage of the Duration. Such percentages must consecutively increase. If you replace the LinearPointKeyFrame elements in the previous program with the following markup, the program runs the same way:

<LinearPointKeyFrame Value="430 240" KeyTime="25%" /> <LinearPointKeyFrame Value="240 430" KeyTime="50%" /> <LinearPointKeyFrame Value="50 240" KeyTime="75%" /> <LinearPointKeyFrame Value="240 50" KeyTime="100%" /> 


The final option is KeyTime.Paced, which allocates the time among the key frames so that the overall rate of change is constant. For example, if one LinearDoubleKeyFrame changes a double from 0 to 500, and another changes a double from 500 to 750, twice as much time is allocated to the first than to the second. Here's an example.

PacedAnimation.xaml

[View full width]

<!-- ================================================= PacedAnimation.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Chocolate" StrokeThickness="3"> <Path.Data> <EllipseGeometry x:Name="elips" RadiusX="24" RadiusY="24" /> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty="Center"> <PointAnimationUsingKeyFrames Duration="0:0:5" RepeatBehavior="Forever"> <LinearPointKeyFrame Value="48 48" KeyTime="Paced" /> <LinearPointKeyFrame Value="480 240" KeyTime="Paced" /> <LinearPointKeyFrame Value="480 48" KeyTime="Paced" /> <LinearPointKeyFrame Value="48 240" KeyTime="Paced" /> <LinearPointKeyFrame Value="48 48" KeyTime="Paced" /> </PointAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The ball moves in a bow tie pattern, but each leg of the trip gets enough time so that the overall speed is constant.

The clock programs I showed earlier moved the second hand continuously in a smooth motion. You might prefer a second hand that makes discrete jumps between the seconds. (Or, better yet, you might prefer giving your users the option to change the clock's second hand to suit their own preference.)

Here's a little program that shows two second hands without anything else. The first has the Angle property of its RotateTransform animated with DoubleAnimation from 0 to 360 degrees, with a Duration of one minute, just as in the earlier clock programs.

The right-hand second hand uses DoubleAnimationUsingKeyFrames with a single child of DiscreteDoubleKeyFrame. The KeyTime for the key frame is one second and the Value is 6, meaning 6 degrees. However, the IsCumulative property on the DoubleAnimationUsingKeyFrames element is set to true, so those 6-degree values are accumulated.

SecondHandSteps.xaml

[View full width]

<!-- === =============================================== SecondHandSteps.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Line Stroke="Black" StrokeThickness="3" X1="0" Y1="0" X2="0" Y2="-100" Canvas.Left="150" Canvas.Top="150"> <Line.RenderTransform> <RotateTransform x:Name="xform1" /> </Line.RenderTransform> </Line> <Line Stroke="Black" StrokeThickness="3" X1="0" Y1="0" X2="0" Y2="-100" Canvas.Left="450" Canvas.Top="150"> <Line.RenderTransform> <RotateTransform x:Name="xform2" /> </Line.RenderTransform> </Line> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard .TargetName="xform1" Storyboard .TargetProperty="Angle" From="0" To="360" Duration="0:1" RepeatBehavior="Forever" /> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform2" Storyboard .TargetProperty="Angle" RepeatBehavior="Forever" IsCumulative="True"> <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="6" /> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



The following XAML file has two simultaneous DoubleAnimationUsingKeyFrames elements, each containing two DiscreteDoubleKeyFrame elements that alternate between values of 0 and 1. The TargetProperty is the Opacity property of a TextBlock. The two TextBlock elements display the words "EAT" and "HERE", and as the Opacity properties change values, the two words alternate on the screen, much like the neon sign on a diner.

Diner1.xaml

[View full width]

<!-- ========================================= Diner1.xaml (c) 2006 by Charles Petzold ========================================= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Grid TextBlock.FontSize="192"> <TextBlock Name="eat" Text="EAT" Foreground="Red" HorizontalAlignment="Center" VerticalAlignment="Center" /> <TextBlock Name="here" Text="HERE" Foreground="Blue" Opacity="0" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <Page.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetProperty="Opacity" RepeatBehavior="Forever"> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="eat"> <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="0" /> <DiscreteDoubleKeyFrame KeyTime="0:0:2" Value="1" /> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="here"> <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="1" /> <DiscreteDoubleKeyFrame KeyTime="0:0:2" Value="0" /> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



The highest value of KeyTime in this example is two seconds, which is the length of the animation. The two TextBlock elements have initial Opacity values of 1 and 0. At the KeyTime of one second, they are changed to values of 0 and 1, respectively. The second DiscreteDoubleKeyFrame in each group changes the values back to their initial values at the end of the animation. The animation then repeats. If you set a Duration property of 0:0:2 in the Storyboard element, you can remove the second DiscreteDoubleKeyFrame in each collection.

As usual with XAML animations (or programming in general), you can build the diner sign in several different ways. Here's a version with just one TextBlock element.

Diner2.xaml

[View full width]

<!-- ======================================== Diner2.xaml (c) 2006 by Charles Petzold ======================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Grid TextBlock.FontSize="192"> <TextBlock Name="txtblk" Foreground="Black" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> <Page.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="txtblk" Duration="0:0:2" RepeatBehavior="Forever"> <StringAnimationUsingKeyFrames Storyboard .TargetProperty="Text"> <DiscreteStringKeyFrame KeyTime="0:0:0" Value="EAT" /> <DiscreteStringKeyFrame KeyTime="0:0:1" Value="HERE" /> </StringAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames Storyboard .TargetProperty="Foreground.Color"> <DiscreteColorKeyFrame KeyTime="0:0:0" Value="Red" /> <DiscreteColorKeyFrame KeyTime="0:0:1" Value="Blue" /> </ColorAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



This single TextBlock element has no Text property set, and the Foreground property exists solely to ensure that a SolidColorBrush exists. The StringAnimationUsingKeyFrames alternates the Text property between EAT and HERE. The ColorAnimationUsingKeyFrames alternates the Color property of Foreground between Red and Blue. In each case, the first key frame has a KeyTime of zero seconds to specify the initial values (the word "EAT" in red), and the second key frame has a KeyTime of one second to change the values of the word "HERE" in blue. The Storyboard indicates a Duration of two seconds.

The following XAML file has eight LinearColorKeyFrame elements that change the background of the Canvas to each of the colors of the rainbow. The animation lasts as long as the highest KeyTime, which is seven seconds. At the beginning of the animation, the color is set to red and immediately starts changing to orange. Between second six and second seven, the color changes from violet to red, so when the animation begins again after those seven seconds have elapsed, there's no discontinuity as the color starts again at red.

ColorAnimation.xaml

[View full width]

<!-- ================================================= ColorAnimation.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" Background="Red"> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetProperty="Background.Color"> <ColorAnimationUsingKeyFrames RepeatBehavior="Forever"> <LinearColorKeyFrame KeyTime="0:0:0" Value="Red" /> <LinearColorKeyFrame KeyTime="0:0:1" Value="Orange" /> <LinearColorKeyFrame KeyTime="0:0:2" Value="Yellow" /> <LinearColorKeyFrame KeyTime="0:0:3" Value="Green" /> <LinearColorKeyFrame KeyTime="0:0:4" Value="Blue" /> <LinearColorKeyFrame KeyTime="0:0:5" Value="Indigo" /> <LinearColorKeyFrame KeyTime="0:0:6" Value="Violet" /> <LinearColorKeyFrame KeyTime="0:0:7" Value="Red" /> </ColorAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



I've discussed Discrete<Type>KeyFrame and Linear<Type>KeyFrame but not yet the third possibility for key frame elements, which is Spline<Type>KeyFrame. The Spline<Type>KeyFrame interpolates between the starting and ending values based on a spline rather than a straight line. Using this key frame, you can get effects similar to the Acceleration and Deceleration properties but with much more control.

The Spline<Type>KeyFrame classes inherit the KeyTime and Value properties and define just one additional property named KeySpline, which consists of just two control points of a Bezier curve. The Bezier curve is assumed to begin at the point (0, 0) and end at the point (1, 1). The two points specified in the KeySpline must have X and Y coordinates not less than 0 or greater than 1. With the values of the control points restricted in this way, it's not possible for the Bezier curve to loop. The resultant curve defines a relationship between time (the X axis) and the value of the animation (the Y axis).

For example, suppose you're animating a double between the values of 100 and 200 over a period of 10 seconds. What is the value of the double at five seconds? With a plain DoubleAnimation or LinearDoubleKeyFrame, the value is obviously 150. With a SplineDoubleKeyFrame, the value depends on the spline you've defined. The time of five seconds is the halfway point between 0 and 10 seconds, so it corresponds to an X coordinate of 0.5. If the Y coordinate of the spline at that point is 0.75, the value of the double is 75 percent of the amount between 100 and 200, or 175.

Here's an example of applying SplineDoubleKeyFrame to the bouncing ball problem.

AnotherBouncingBall.xaml

[View full width]

<!-- === =================================================== AnotherBouncingBall.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <Line X1="0" Y1="506" X2="1000" Y2="506" Stroke="Black" StrokeThickness="5" /> <Ellipse Name="elips" Width="24" Height="24" Fill="Red" Canvas.Left="96"> <Ellipse.Triggers> <EventTrigger RoutedEvent="Ellipse .Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty=" (Canvas.Top)" RepeatBehavior="Forever"> <DoubleAnimationUsingKeyFrames> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="96" /> <SplineDoubleKeyFrame KeyTime="0:0:1" Value="480" KeySpline="0.25 0, 0.6 0.2" /> <SplineDoubleKeyFrame KeyTime="0:0:2" Value="96" KeySpline="0.75 1, 0.4 0.8" /> < /DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Canvas>



The Canvas.Top attached property is set to 96 by the initial DiscreteDoubleKeyFrame element. It is animated to 480 by the first SplineDoubleKeyFrame and back to 96 by the second. Each SplineDoubleKeyFrame element sets the KeySpline property to a string consisting of two points.

How did I derive the points I used for the KeySpline values? I created a program, named SplineKeyFrameExperiment, that allows me to experiment with the control points. The program consists of a XAML file that lays out the window as well as the following C# file that defines several properties and event handlers.

SplineKeyFrameExperiment.cs

[View full width]

//--------- ------------------------------------------------ // SplineKeyFrameExperiment.cs (c) 2006 by Charles Petzold //------------------------------------------------ --------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.SplineKeyFrameExperiment { public partial class SplineKeyFrameExperiment : Window { // Two dependency properties for ControlPoint1 and ControlPoint2 public static DependencyProperty ControlPoint1Property = DependencyProperty.Register ("ControlPoint1", typeof(Point), typeof(SplineKeyFrameExperiment), new PropertyMetadata(new Point(0, 0), ControlPointOnChanged)); public static DependencyProperty ControlPoint2Property = DependencyProperty.Register ("ControlPoint2", typeof(Point), typeof(SplineKeyFrameExperiment), new PropertyMetadata(new Point(1, 1), ControlPointOnChanged)); [STAThread] public static void Main() { Application app = new Application(); app.Run(new SplineKeyFrameExperiment()); } // Constructor: Mostly draws tick marks too numerous for XAML public SplineKeyFrameExperiment() { InitializeComponent(); for (int i = 0; i <= 10; i++) { // Horizontal text and lines TextBlock txtblk = new TextBlock(); txtblk.Text = (i / 10m).ToString ("N1"); canvMain.Children.Add(txtblk); Canvas.SetLeft(txtblk, 40 + 48 * i); Canvas.SetTop(txtblk, 14); Line line = new Line(); line.X1 = 48 * (i + 1); line.Y1 = 30; line.X2 = line.X1; line.Y2 = 528; line.Stroke = Brushes.Black; canvMain.Children.Add(line); // Vertical text and lines txtblk = new TextBlock(); txtblk.Text = (i / 10m).ToString ("N1"); canvMain.Children.Add(txtblk); Canvas.SetLeft(txtblk, 5); Canvas.SetTop(txtblk, 40 + 48 * i); line = new Line(); line.X1 = 30; line.Y1 = 48 * (i + 1); line.X2 = 528; line.Y2 = line.Y1; line.Stroke = Brushes.Black; canvMain.Children.Add(line); } UpdateLabel(); } // ControlPoint1 and ControlPoint2 properties. public Point ControlPoint1 { set { SetValue(ControlPoint1Property, value); } get { return (Point)GetValue (ControlPoint1Property); } } public Point ControlPoint2 { set { SetValue(ControlPoint2Property, value); } get { return (Point)GetValue (ControlPoint2Property); } } // Called whenever one of the ControlPoint properties changes. static void ControlPointOnChanged (DependencyObject obj, DependencyPropertyChangedEventArgs args) { SplineKeyFrameExperiment win = obj as SplineKeyFrameExperiment; // Set KeySpline element in XAML animation. if (args.Property == ControlPoint1Property) win.spline.ControlPoint1 = (Point)args.NewValue; else if (args.Property == ControlPoint2Property) win.spline.ControlPoint2 = (Point)args.NewValue; } // Handles MouseDown and MouseMove events. void CanvasOnMouse(object sender, MouseEventArgs args) { Canvas canv = sender as Canvas; Point ptMouse = args.GetPosition(canv); ptMouse.X = Math.Min(1, Math.Max(0, ptMouse.X / canv.ActualWidth)); ptMouse.Y = Math.Min(1, Math.Max(0, ptMouse.Y / canv.ActualHeight)); // Set ControlPoint properties. if (args.LeftButton == MouseButtonState.Pressed) ControlPoint1 = ptMouse; if (args.RightButton == MouseButtonState.Pressed) ControlPoint2 = ptMouse; // Update the label showing ControlPoint properties. if (args.LeftButton == MouseButtonState.Pressed || args.RightButton == MouseButtonState.Pressed) UpdateLabel(); } // Set content of XAML Label. void UpdateLabel() { lblInfo.Content = String.Format( "Left mouse button changes ControlPoint1 = ({0:F2})\n" + "Right mouse button changes ControlPoint2 = ({1:F2})", ControlPoint1, ControlPoint2); } } }



This file defines two properties named ControlPoint1 and ControlPoint2 that are backed by dependency properties. These are the two control points for the Bezier curve used in the animation. Whenever one of these properties changes, the static ControlPointOnChanged method in the class is called, setting the properties of a KeySpline object defined in the XAML portion of the window (coming up).

The C# code also has an event handler for MouseDown and MouseMove events that sets ControlPoint1 and ControlPoint2 based on mouse coordinates. The last method of the file sets the content of a Label (also defined in the XAML file) with the values of ControlPoint1 and ControlPoint2.

Here's the XAML file. The light-gray Canvas displays a surface whose upper-left corner is the point (0, 0) and lower-right corner is the point (1, 1). The window constructor in the C# portion of the program draws lines marking every 0.1 units. This Canvas also displays a Bezier curve between the two corners whose two control points are the ControlPoint1 and ControlPoint2 properties of the Window object. You change these points by clicking and dragging on the surface of the Canvas.

SplineKeyFrameExperiment.xaml

[View full width]

<!-- === =========== ============================================= SplineKeyFrameExperiment.xaml (c) 2006 by Charles Petzold ============================================= ============== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .SplineKeyFrameExperiment" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.SplineKeyFrameExperiment" Title="SplineKeyFrame Experiment" Name="window"> <Canvas> <!-- Canvas with gray box and axes with tick marks. --> <Canvas Name="canvMain" Canvas.Left="24" Canvas.Top="24" > <!-- Horizontal axis header. --> <Line X1="0.5in" Y1="0.08in" X2="2 .75in" Y2="0.08in" Stroke="{DynamicResource {x:Static SystemColors .WindowTextBrushKey}}" /> <TextBlock Text="Time" Canvas.Left="2. 85in" Canvas.Top="0in" /> <Line X1="3.25in" Y1="0.08in" X2="5 .5in" Y2="0.08in" Stroke="{DynamicResource {x:Static SystemColors .WindowTextBrushKey}}" /> <!-- Gray box for displaying grid. --> <Canvas Canvas.Left="48" Canvas .Top="48" Width="480" Height="480" Background="LightGray" MouseDown="CanvasOnMouse" MouseMove="CanvasOnMouse"> <!-- The Bezier curve formed by the control points. --> <Path Stroke="Black" StrokeThickness="0.005"> <Path.Data> <PathGeometry> <PathFigure StartPoint="0 0"> <BezierSegment Point1="{Binding ElementName=window, Path=ControlPoint1}" Point2="{Binding ElementName=window, Path=ControlPoint2}" Point3="1 1" /> </PathFigure> </PathGeometry> </Path.Data> <Path.RenderTransform> <ScaleTransform ScaleX="480" ScaleY="480" /> </Path.RenderTransform> </Path> <!-- Line between (0, 0) and first control point. --> <Line Stroke="DarkGray" StrokeThickness="0.005" X1="0" Y1="0" X2="{Binding ElementName=window, Path=ControlPoint1.X}" Y2="{Binding ElementName=window, Path=ControlPoint1.Y}"> <Line.RenderTransform> <ScaleTransform ScaleX="480" ScaleY="480" /> </Line.RenderTransform> </Line> <!-- Line between second control point and (1, 1). --> <Line Stroke="DarkGray" StrokeThickness="0.005" X1="1" Y1="1" X2="{Binding ElementName=window, Path=ControlPoint2.X}" Y2="{Binding ElementName=window, Path=ControlPoint2.Y}"> <Line.RenderTransform> <ScaleTransform ScaleX="480" ScaleY="480" /> </Line.RenderTransform> </Line> </Canvas> </Canvas> <!-- Ball showing elapsing time (changed by animation). --> <Path Name="time" Fill="Blue"> <Path.Data> <EllipseGeometry Center="72 556" RadiusX="6" RadiusY="6" /> </Path.Data> </Path> <!-- Ball showing changing value (changed by animation). --> <Path Name="value" Fill="Blue"> <Path.Data> <EllipseGeometry Center="556 72" RadiusX="6" RadiusY="6" /> </Path.Data> </Path> <!-- Line showing elapsing time. --> <Line Stroke="Blue" X1="{Binding ElementName=time, Path=Data.Center.X}" Y1="{Binding ElementName=value, Path=Data.Center.Y}" X2="{Binding ElementName=time, Path=Data.Center.X}" Y2="556" /> <!-- Line showing changing value. --> <Line Stroke="Blue" X1="{Binding ElementName=time, Path=Data.Center.X}" Y1="{Binding ElementName=value, Path=Data.Center.Y}" X2="556" Y2="{Binding ElementName=value, Path=Data.Center.Y}" /> <!-- Label showing control points (set from code). --> <Label Name="lblInfo" Canvas.Left="38" Canvas.Top="580" /> <!-- Go button. --> <Button Canvas.Left="450" Canvas.Top="580" MinWidth="72" Content="Go!"> <Button.Triggers> <EventTrigger RoutedEvent="Button. Click"> <BeginStoryboard> <Storyboard> <!-- Show time elapsing linearly. --> <PointAnimation Storyboard .TargetName="time" Storyboard .TargetProperty="Data.Center" From="72 556" To="552 556" Duration="0:0:5" /> <!-- Show value changing by spline. --> <PointAnimationUsingKeyFrames Storyboard .TargetName="value" Storyboard .TargetProperty="Data.Center"> <DiscretePointKeyFrame KeyTime="0:0:0" Value="556 72" /> <SplinePointKeyFrame KeyTime="0:0:5" Value="556 552"> <!-- KeySpline set from code. --> <SplinePointKeyFrame.KeySpline> <KeySpline x:Name="spline" /> < /SplinePointKeyFrame.KeySpline> </SplinePointKeyFrame> < /PointAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> </Canvas> </Window>



At the lower-right corner of the window is a Button labeled "Go!" When you click this Button, two animations are triggered. A PointAnimation moves a little ball with a target name of "time" at a linear rate across the bottom of the grid to represent the passing of time. A PointAnimationUsingKeyFrames moves a little ball with a target name of "value" down the right side of the grid to represent changing values based on a SplinePointKeyFrame with a KeySpline set from the Bezier spline you've specified.

In this way you can experiment with control points to get the effect you want. If both control points are values of (0.1, 0.9), the animated property changes very quickly at first and then slows down. If both control points are (0.9, 0.1), the opposite happens: the animation starts off slow but then speeds up.

If the first control point is (0.1, 0.9) and the second is (0.9, 0.1), the animation starts up fast, then slows down, then speeds up again. Switch the two to get the opposite effect: the animation starts slowly, then speeds up, and then slows down again. If you need something more elaborate, use multiple Spline<Type>KeyFrame elements in a row.

An object in free fall covers a distance proportional to the square of the time. To mimic this effect with a KeySpline, you'll want a curve that maps a time of 0.1 to a value of 0.01, a time of 0.2 to a value of 0.04, a time of 0.3 to a value of 0.09, and so forth. You won't be able to get the beginning of the curve quite right, but the rest of it is well approximated with a first control point of (0.25, 0) and a second of (0.6, 0.2). That's what I used in the AnotherBouncingBall.xaml program for the fall. I used the mirror image of the curvethe points (0.75, 1) and (0.4, 0.8)for the bounce back up.

I use those same values for the movement of the five suspended balls in the physics apparatus (and executive toy) known as Newton's Cradle.

NewtonsCradle.xaml

[View full width]

<!-- ================================================ NewtonsCradle.xaml (c) 2006 by Charles Petzold ============================================= === --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" Title="Newton's Cradle" WindowTitle="Newton's Cradle by Charles Petzold"> <Canvas> <Canvas.Resources> <Style TargetType="{x:Type Path}"> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeThickness" Value="3" /> <Setter Property="Fill" Value="Silver" /> <Setter Property="Data" Value="M 0 0 V 300 A 25 25 0 1 1 0 350 A 25 25 0 1 1 0 300" /> </Style> </Canvas.Resources> <Path> <Path.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform1" Angle="30" /> <TranslateTransform X="200" /> </TransformGroup> </Path.RenderTransform> </Path> <Path> <Path.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform2" Angle="30" /> <TranslateTransform X="252" /> </TransformGroup> </Path.RenderTransform> </Path> <Path> <Path.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform3" Angle="30" /> <TranslateTransform X="304" /> </TransformGroup> </Path.RenderTransform> </Path> <Path> <Path.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform4" /> <TranslateTransform X="356" /> </TransformGroup> </Path.RenderTransform> </Path> <Path> <Path.RenderTransform> <TransformGroup> <RotateTransform x :Name="xform5" /> <TranslateTransform X="408" /> </TransformGroup> </Path.RenderTransform> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Page.Loaded"> <BeginStoryboard> <Storyboard TargetProperty="Angle" RepeatBehavior="Forever"> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform1"> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="30"/> <SplineDoubleKeyFrame KeyTime="0:0:1" Value="0" KeySpline="0.25 0, 0.6 0.2" /> <DiscreteDoubleKeyFrame KeyTime="0:0:3" Value="0" /> <SplineDoubleKeyFrame KeyTime="0:0:4" Value="30" KeySpline="0.75 1, 0.4 0.8" /> < /DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform2"> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="30"/> <SplineDoubleKeyFrame KeyTime="0:0:1" Value="0" KeySpline="0.25 0, 0.6 0.2" /> <DiscreteDoubleKeyFrame KeyTime="0:0:3" Value="0" /> <SplineDoubleKeyFrame KeyTime="0:0:4" Value="30" KeySpline="0.75 1, 0.4 0.8" /> < /DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform3"> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="30"/> <SplineDoubleKeyFrame KeyTime="0:0:1" Value="0" KeySpline="0.25 0, 0.6 0.2" /> <SplineDoubleKeyFrame KeyTime="0:0:2" Value="-30" KeySpline="0.75 1, 0.4 0.8" /> <SplineDoubleKeyFrame KeyTime="0:0:3" Value="0" KeySpline="0.25 0, 0.6 0.2" /> <SplineDoubleKeyFrame KeyTime="0:0:4" Value="30" KeySpline="0.75 1, 0.4 0.8" /> < /DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform4"> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" /> <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="0" /> <SplineDoubleKeyFrame KeyTime="0:0:2" Value="-30" KeySpline="0.75 1, 0.4 0.8" /> <SplineDoubleKeyFrame KeyTime="0:0:3" Value="0" KeySpline="0.25 0, 0.6 0.2" /> < /DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="xform5"> <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" /> <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="0" /> <SplineDoubleKeyFrame KeyTime="0:0:2" Value="-30" KeySpline="0.75 1, 0.4 0.8" /> <SplineDoubleKeyFrame KeyTime="0:0:3" Value="0" KeySpline="0.25 0, 0.6 0.2" /> < /DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> </Page>



A DoubleAnimationUsingKeyFrames element is associated with each of the five suspended balls. Each collection of key frames is a combination of DiscreteDoubleKeyFrame and SplineDoubleKeyFrame. The two balls on each side stop in the center; the one in the middle does not.

All animations described so far either set a property to a specified value (Discrete<Type>KeyFrame) or perform an interpolation between two values. The interpolated value is always somewhere along the straight line that connects the starting value and the ending value. The difference between Linear<Type>KeyFrame and Spline<Type>KeyFrame is how the interpolation calculation treats timelinearly or as an X coordinate of a spline. But in neither case does the interpolated value deviate from that straight line between the starting and ending values.

This does not imply that you can't move an animated object in anything other than a straight line. You've seen how to animate the Angle property of a Rotate transform to make an object move in circles. But how would you like a general approach to moving an object along an arbitrary path? That's the purpose of the final category of animation classes, which is restricted to just three classes: DoubleAnimationUsingPath, PointAnimationUsingPath, and MatrixAnimationUsingPath. Each of these classes defines IsAdditive and IsCumulative properties and, most importantly, a PathGeometry property that you set to a graphics path.

Here's an example of a PointAnimationUsingPath that sets the PathGeometry property to a Bezier curve that makes a little loop. The target of the animation is the center of a little ball (what else?), such that the ball moves back and forth along the Bezier curve.

SimplePathAnimation.xaml

[View full width]

<!-- === =================================================== SimplePathAnimation.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Blue"> <Path.Data> <EllipseGeometry x:Name="elips" RadiusX="12" RadiusY="12" /> </Path.Data> <Path.Triggers> <EventTrigger RoutedEvent="Path.Loaded"> <BeginStoryboard> <Storyboard TargetName="elips" TargetProperty="Center"> <PointAnimationUsingPath Duration="0:0:2.5" AutoReverse="True" RepeatBehavior="Forever"> <PointAnimationUsingPath.PathGeometry> <PathGeometry Figures="M 96 288 C 576 0, 0 0, 480 288" /> < /PointAnimationUsingPath.PathGeometry> </PointAnimationUsingPath> </Storyboard> </BeginStoryboard> </EventTrigger> </Path.Triggers> </Path> </Canvas>



To actually see the PathGeometry that's controlling the movement of the ball, you can include another Path element right after the Canvas start tag:

<Path Stroke="Black" Data="M 96 288 C 576 0, 0 0, 480 288" /> 


The center of the ball tracks this path exactly. PointAnimationUsingPath animates a property of type Point by forcing the property to take on values defined by a graphics path, in this case a Bezier curve. The path can alternatively be a rectangle, ellipse, arc, or any combination of those figures.

PointAnimationUsingPath is very different from SplinePointKeyFrame, which it might be confused with because Bezier splines are often (but not exclusively) used with PointAnimationUsingPath and Bezier splines are always (but in a restricted way) used with SplinePointKeyFrame. It's important to distinguish between these two classes.

With SplinePointKeyFrame, the interpolated point always lies on the straight line between a starting point and an ending point. Elapsed time is used to calculate an X coordinate of the spline (between 0 and 1), and the Y coordinate of the spline (also between 0 and 1) is then used to calculate the interpolated value.

With PointAnimationUsingPath, time is allocated based on the total length of the path you've set to the PathGeometry property. For example, if the animation has a Duration of four seconds, and one second has elapsed, the animation determines the X and Y coordinates of the path one-quarter of the distance from the beginning to the end. These become the Point value of the animation.

What if you want to move an object along a path and the element does not have a Point property? Perhaps you want to move a Button along a path. You position a Button object on a Canvas panel by specifying the Left and Right attached properties. To move the Button, you must animate these properties.

To move any arbitrary object (such as a Button) along a path, you can mimic PointAnimationUsingPath with two DoubleAnimationUsingPath objects. One animation controls the X coordinate, and the other controls the Y coordinate. DoubleAnimationUsingPath defines a Source property specifically for this purpose. You set this property to a member of the PathAnimationSource enumerationX, Y, or Angle. (More on the Angle option shortly.)

Here's a XAML file that defines a Bezier curve in a PathGeometry as a resource. It uses this resource both to draw the Bezier curve with the Path element and also to set the PathGeometry in two DoubleAnimationUsingPath elements, the first controlling the Canvas.Left property of the Button by setting Source to X, and the second controlling the Canvas.Top property by setting Source to Y.

PathAnimatedButton.xaml

[View full width]

<!-- === ================================================== PathAnimatedButton.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Canvas.Resources> <PathGeometry x:Key="path" Figures="M 96 192 C 288 0, 384 384, 576 192" /> </Canvas.Resources> <Path Stroke="Black" Data="{StaticResource path}" /> <Button Name="btn"> Button </Button> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard TargetName="btn"> <DoubleAnimationUsingPath Storyboard .TargetProperty="(Canvas.Left)" Duration="0:0:2.5" AutoReverse="True" RepeatBehavior="Forever" PathGeometry="{StaticResource path}" Source="X" /> <DoubleAnimationUsingPath Storyboard .TargetProperty="(Canvas.Top)" Duration="0:0:2.5" AutoReverse="True" RepeatBehavior="Forever" PathGeometry="{StaticResource path}" Source="Y" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



You can also set the Source property of DoubleAnimationUsingPath to PathAnimationSource .Angle, and when you specify that member, DoubleAnimationUsingPath animates a property based on the slope of the path in degrees. Typically, you would then use this DoubleAnimationUsingPath to animate the Angle property of a RotateTransform.

The following program demonstrates this technique. It is very similar to the previous XAML file, except that the PathGeometry is a bit more elaborate and defines a continuous curved line based on two Bezier curves. The Button element now includes a RotateTransform that is set to its RenderTransform property. A third DoubleAnimationUsingPath animates the Angle property of this transform with the same Bezier curve and Source set to Angle.

PathAngleAnimatedButton.xaml

[View full width]

<!-- === ======================================================= PathAngleAnimatedButton.xaml (c) 2006 by Charles Petzold ============================================= ============= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Canvas.Resources> <PathGeometry x:Key="path" Figures="M 96 192 C 288 0, 384 384, 576 192 S 662 192 576 576 S 384 576 96 192" /> </Canvas.Resources> <Path Stroke="Black" Data="{StaticResource path}" /> <Button Name="btn"> Button <Button.RenderTransform> <RotateTransform x:Name="xform" /> </Button.RenderTransform> </Button> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard RepeatBehavior="Forever"> <DoubleAnimationUsingPath Storyboard.TargetName="btn" Storyboard .TargetProperty="(Canvas.Left)" Duration="0:0:10" PathGeometry="{StaticResource path}" Source="X" /> <DoubleAnimationUsingPath Storyboard.TargetName="btn" Storyboard .TargetProperty="(Canvas.Top)" Duration="0:0:10" PathGeometry="{StaticResource path}" Source="Y" /> <DoubleAnimationUsingPath Storyboard.TargetName="xform" Storyboard .TargetProperty="Angle" Duration="0:0:10" PathGeometry="{StaticResource path}" Source="Angle" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



Run this program and you'll see the upper-left corner of the button tracing the Bezier curve as you might expect, but the rotation of the button also traces the curve so that the top of the button is always parallel to the curve.

Keep in mind that although it seems natural to animate the horizontal position of an element by setting Source to X, to animate the vertical position by setting Source to Y, and to animate the Angle of rotation by setting Source to Angle, you're not restricted to those options. For example, the following program animates the Opacity property of a TextBlock by using a path constructed from three straight lines that if graphed form a rectangle.

RectangularOpacity.xaml

[View full width]

<!-- === ================================================== RectangularOpacity.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <TextBlock x:Name="txtblk" Text="XAML" FontSize="144pt" FontFamily="Arial Black" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock.Triggers> <EventTrigger RoutedEvent="TextBlock. Loaded"> <BeginStoryboard> <Storyboard TargetName="txtblk" TargetProperty="Opacity" RepeatBehavior="Forever"> <DoubleAnimationUsingPath Duration="0:0:4" Source="Y"> <DoubleAnimationUsingPath.PathGeometry> <PathGeometry> <PathGeometry. Figures> <PathFigure StartPoint="0 0"> <LineSegment Point="0 1" /> <LineSegment Point="2 1" /> <LineSegment Point="2 0" /> </PathFigure> </PathGeometry .Figures> </PathGeometry> < /DoubleAnimationUsingPath.PathGeometry> </DoubleAnimationUsingPath> </Storyboard> </BeginStoryboard> </EventTrigger> </TextBlock.Triggers> </TextBlock> </Page>



The DoubleAnimationUsingPath element has a Source property set to Y, which means the Y values of the path will be used to animate the Opacity property of the TextBlock. (These Y values should be between 0 and 1.) The path is a series of straight lines from (0, 0) to (0, 1) to (2, 1) to (2, 0). The total length of this path is 4 units. The first line is from the point (0, 0) to (0, 1). That's 1 unit in length, so it will govern the first quarter of the duration of the animation. The animation is conveniently four seconds in duration, so over the first second of the animation, the Y coordinate of the path (and the Opacity of the TextBlock) changes from 0 to 1. The next line in the path is (0, 1) to (2, 1), a distance of two units, so over the next two seconds, the Y coordinate (and the Opacity) remains at 1. The final line is from (2, 1) to (2, 0), a distance of 1 unit, which brings the Y coordinate and Opacity back to 0 over the final second. (Of course, you can easily do something similar with a combination of LinearDoubleKeyFrame and DiscreteDoubleKeyFrame.)

If you use three DoubleAnimationUsingPath elements (or one PointAnimationUsingPath and one DoubleAnimationUsingPath) to animate horizontal and vertical position and rotation angle, you can replace those elements with a single MatrixAnimationUsingPath element. This element animates a Matrix object. The X and Y coordinates of the path become the OffsetX and OffsetY properties of the transform matrix; the remainder of the matrix optionally defines a rotation based on the tangent angle of the path. (You need to set DoesRotateWithTangent to true to get the rotation.)

The visuals of the MatrixAnimatedButton.xaml file are identical to those of the previous PathAngleAnimatedButton.xaml..

MatrixAnimatedButton.xaml

[View full width]

<!-- === ==================================================== MatrixAnimatedButton.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Canvas.Resources> <PathGeometry x:Key="path" Figures="M 96 192 C 288 0, 384 384, 576 192 S 662 192 576 576 S 384 576 96 192" /> </Canvas.Resources> <Path Stroke="Black" Data="{StaticResource path}" /> <Button> Button <Button.RenderTransform> <MatrixTransform x:Name="xform" /> </Button.RenderTransform> </Button> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard RepeatBehavior="Forever"> <MatrixAnimationUsingPath Storyboard.TargetName="xform" Storyboard .TargetProperty="Matrix" Duration="0:0:10" PathGeometry="{StaticResource path}" DoesRotateWithTangent="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



I will freely admit that applications involving moving and rotating a button along a path don't arise very frequently. However, one common application of path animations is the unicycle-man cyborg, who must navigate a treacherous path over the hills of Bezier Boulevard.

UnicycleMan.xaml

[View full width]

<!-- ============================================== UnicycleMan.xaml (c) 2006 by Charles Petzold ============================================= = --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Canvas.Resources> <PathGeometry x:Key="path" Figures="M 200 200 C 300 0, 500 400, 700 200 C 900 0, 1000 200, 900 300 C 100 1100, 1200 800, 400 500 C 100 400, 100 400, 200 200" /> <Style TargetType="{x:Type Path}"> <Setter Property="Stroke" Value="{DynamicResource {x:Static SystemColors .WindowTextBrushKey}}" /> </Style> </Canvas.Resources> <!-- Draw the path. --> <Path Data="{StaticResource path}" /> <!-- Draw the unicycle-man. --> <Path> <Path.Data> <GeometryGroup> <!-- Wheel. --> <EllipseGeometry Center="0 -25" RadiusX="25" RadiusY="25" /> <!-- Spokes --> <GeometryGroup> <LineGeometry StartPoint="0 0" EndPoint="0 -50" /> <LineGeometry StartPoint="-25 -25" EndPoint="25 -25" /> <LineGeometry StartPoint="-18 -7" EndPoint="18 -43" /> <LineGeometry StartPoint="18 -7" EndPoint="-18 -43" /> <GeometryGroup.Transform> <RotateTransform x :Name="xformSpokes" CenterX="0" CenterY="-25" /> </GeometryGroup.Transform> </GeometryGroup> <!-- Body, head, and arms. --> <LineGeometry StartPoint="0 -25" EndPoint="0 -80" /> <EllipseGeometry Center="0 -90" RadiusX="10" RadiusY="10" /> <LineGeometry StartPoint="9 -85" EndPoint="0 -90" /> <LineGeometry StartPoint="-35 -70" EndPoint="35 -70"> <LineGeometry.Transform> <RotateTransform x :Name="xformArm" CenterX="0" CenterY="-70" /> </LineGeometry.Transform> </LineGeometry> </GeometryGroup> </Path.Data> <Path.RenderTransform> <MatrixTransform x :Name="xformUnicycleMan" /> </Path.RenderTransform> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard SpeedRatio="0.5"> <!-- Move the unicycle-man along the path. --> <MatrixAnimationUsingPath Storyboard .TargetName="xformUnicycleMan" Storyboard .TargetProperty="Matrix" Duration="0:0:12" PathGeometry="{StaticResource path}" DoesRotateWithTangent="True" RepeatBehavior="Forever" /> <!-- Rotate the spokes of the wheel. --> <DoubleAnimation Storyboard .TargetName="xformSpokes" Storyboard .TargetProperty="Angle" Duration="0:0:1" RepeatBehavior="Forever" From="0" To="360" /> <!-- Move the arms for balance . --> <DoubleAnimation Storyboard .TargetName="xformArm" Storyboard .TargetProperty="Angle" Duration="0:0:0.2" RepeatBehavior="Forever" AutoReverse="True" From="-20" To="20" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas>



Notice that the unicycle-man is a single Path object that has its RenderTransform set to a MatrixTransform with a name of "xformUnicycleMan." This is the transform subjected to the MatrixAnimationUsingPath element.

The point (0, 0) of the object animated with MatrixAnimationUsingPath (or PointAnimationUsingPath) traces the path exactly. For paths drawn from left to right on the screen, positive Y coordinates of the object appear below the path, and negative Y coordinates appear above the path. That's why the unicycle-man is drawn with negative Y coordinates (and why the Button in previous programs hangs below the initial portion of the path).

Two other components of the unicycle-man are also animated. The spokes of the wheel are a series of Line elements in a GeometryGroup, and this GeometryGroup has its own animated RotateTransform named "xformSpokes." The arms are a single Line element with an animated RotateTransform named "xformArm." These animations occur within the context of the complete unicycle-man, who is moved and rotated along the path.

The TextGeometryDemo program in Chapter 28 demonstrates how to obtain a Geometry object that describes the outlines of text characters and how to put that Geometry to use in graphics programming. It's also possible to use those Geometry objects in animations. The companion content for this chapter includes a project named AnimatedTextGeometry that demonstrates animations based on the outlines of text characters.

A program in which unicycle-man travels up, down, across, and through text characters is an exercise left to the reader.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net