Interesting Animation Effects with WPF

As is often the case with these posts, this one started out as something completely different :-) I was going to do a simple post on a nice animated splash screen for your WPF apps, but I got a bit carried away with the animation stuff.

In fact, I got so carried away with it, that I ended up with something that I never intended to do, and that's a rather useful bit of .NET code for creating background animations using graduated fills. Let me give you a quick look at where we are heading:

Animation1
Figure 1: Our target effect

WPF and the Positioning of Elements

Before we start, however, there's one thing we need to cover. This particular thing had me going round in circles.

It appears that when you create any enclosed shape in WPF, you cannot set its X & Y position. Well, at least you can't on an ordinary WPF window like the type that VS might create for you out of the box.

At this point, I do have to thank my good friend (and general all round WPF god) Gavin Lanata for helping me out here and explaining that, to position things the way I wanted to in code, I would have to change the root layout of my window from a grid to a canvas. It is frustrating that something so blindingly simple (and something that most devs would consider looking for) was thought not to be deemed a requirement of drawing shapes in WPF, but I digress. Nothing we can do about it now.

Let's Get Started

Okay, so fire up Visual Studio, and start a new WPF application project. You can use blend if you want, but because we'll be mostly typing code, blend's a bit of an overkill.

Once your template has loaded, and VS is ready to go, change your "MainWindow.xaml" file so that it looks like this:

<Window x:Class="WpfAnimationTest.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/
      2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="WPF Animation Testing" Height="600"
      Width="800" WindowStartupLocation="CenterScreen">
   <Canvas x:Name="Root" Background="Black">
   </Canvas>
</Window>

You'll see that, apart from changing the background to black, we've changed the default grid to a canvas and set the window size and title. Not much to it really. Unlike most WPF/XAML projects, most of what we draw here will be done in code.

For the record, you could quite easily reproduce everything in this article, by using declarative XAML, but there's going to be a massive amount of duplication of gradients, gradient stops, and other resource-hungry stuff. By using the code approach that we'll be using here, anything we use temporarily will get disposed of once it goes out of scope; and, more importantly, it's reusable code.

First, a Little Theory

If you've done much with XAML, you'll likely know that just about everything in WPF can be animated. Colours, positions, Sizes, Fills, and many other properties can easily be animated, using time lines and storyboards.

You create your animation sequences by using anyone of the many animation time line types available, generally by newing an object up, and then setting a start value, an end value, and a time length for the animation to run. You then attach these animation time lines to the properties you want them to control on your UI element before finally attaching them to a storyboard and starting things running.

One thing I didn't realise, however, that could be animated was the position of a colour gradient stop. When you create colour gradients in WPF, you create them using a series of objects called "GradientStops"

If you've ever used an art package such as Photoshop, and you've used the little square colour markers to mark places in a fill where a colour should change, you've used a similar concept. If, for example, you wanted a colour gradient that went from Red to Blue in the middle, and then to green, you might create a Stop for the colour Red at 0%, one for Blue at 50%, and one for Green at 100%. The WPF graphics engine would then fill in all the in-between colours so that you get a smooth transition from one colour to the next.

In WPF most measurements use what's called a local co-ordinate system. This means that on your form that might be, say, 800 pixels in width, a scale from 0% to 100% is represented using the values from 0 to 1. This would mean that 0 pixels is at 0.0 or 0%, 400 pixels would be at 0.5 or 50%, and 800 pixels would be at 1.0 or 100% When setting up your gradients, all of your colour stop positions are specified this way.

Let's Try Some Code

Open up the code for your main XAML window. Assuming that you've changed to a canvas as previously mentioned, you should be able to add the following code to your MainWindow constructor:

myRect = new Rectangle
{
   Width = 300,
   Height = 100,
   Stroke = Brushes.White,
   StrokeThickness = 1
};
Root.Children.Add(myRect);
Canvas.SetLeft(myRect, 100);
Canvas.SetTop(myRect, 100);

If you've done this correctly, when you press F5 you should see a white rectangle on a black background, 100 pixels from each corner, and with a size of 300 by 100 pixels. The Canvas SetLeft and SetTop calls are as previously mentioned, needed because MS decided that it was of no use to allow anyone drawing enclosed shapes to specify the position on screen where the shape was to be drawn.

If you also add a fill parameter:

myRect = new Rectangle
{
   Width = 300,
   Height = 100,
   Stroke = Brushes.White,
   StrokeThickness = 1,
   Fill = Brushes.Red
};
Root.Children.Add(myRect);
Canvas.SetLeft(myRect, 100);
Canvas.SetTop(myRect, 100);

You also can set the overall solid fill colour of the rectangle, too. Solid colours, however, are a bit boring. We're going to be changing this to something much more interesting.

First, let's make drawing our rectangle a bit easier by wrapping it up in its own little function, and we'll also build a data object for passing parameters to it. We'll add all the parameters we need into the data object now, and I'll explain what each one is for as we come to them.

Add a new class to your project, and call it "BarDescriptor.cs". Into this file, enter the following code:

namespace WpfAnimationTest
{
   public class BarDescriptor
   {
      public int RectangleX { get; set; }
      public int RectangleY { get; set; }
      public int RectangleWidth { get; set; }
      public int RectangleHeight { get; set; }
      public int AnimationTimeInSeconds { get; set; }
         // 0.0 to 1.0
      public float BarBaseRedLevel { get; set; }
      public float BarBaseGreenLevel { get; set; }
      public float BarBaseBlueLevel { get; set; }
      public float GradientStartX { get; set; }
      public float GradientStartY { get; set; }
      public float GradientEndX { get; set; }
      public float GradientEndY { get; set; }

   }
}

Make sure that you alter the namespace as required for your WPF App.

Next, open up "MainWindow.xaml.cs" (or whatever you named your main window code behind), and make sure that you have the following code in it:

using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace WpfAnimationTest
{
   public partial class MainWindow
   {
      private Rectangle _testRect;

      public MainWindow()
      {
         InitializeComponent();
         Loaded += MainWindowLoaded;
      }

      private void MainWindowLoaded(object sender,
         RoutedEventArgs e)
      {
         BarDescriptor barOne = new BarDescriptor
         {
            RectangleX = 100,
            RectangleY = 100,
            RectangleWidth = 200,
            RectangleHeight = 200,
            AnimationTimeInSeconds = 0,
            BarBaseRedLevel = 0,
            BarBaseGreenLevel = 0,
            BarBaseBlueLevel = 0,
            GradientStartX = 0,
            GradientStartY = 0,
            GradientEndX = 0,
            GradientEndY = 0
         };

         CreateRectangleAnimatedRectangle(barOne);
      }

      private void CreateRectangleAnimatedRectangle(BarDescriptor
         inputParameters)
      {
         _testRect = new Rectangle
         {
            Width = inputParameters.RectangleWidth,
            Height = inputParameters.RectangleHeight,
            Stroke = Brushes.White,
            StrokeThickness = 1,
         };

         Root.Children.Add(_testRect);
         Canvas.SetLeft(_testRect, inputParameters.RectangleX);
         Canvas.SetTop(_testRect, inputParameters.RectangleY);

      }

   }
}

Again, make sure you have the correct namespace for your application. Just as with the previous example, you should see your white rectangle again when you run it, but now you also should be able to add new rectangles easily, by creating new "BarDescriptor" objects and setting the parameters appropriately.

For now, however, assuming you set your main window to the same size as mine (800x600), set the properties to the "barOne" object as follows:

BarDescriptor barOne = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 0,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0,
   BarBaseBlueLevel = 0,
   GradientStartX = 0,
   GradientStartY = 0,
   GradientEndX = 0,
   GradientEndY = 0
};

This should create your rectangle to be exactly the same size as the main form that it sits on.

Adding a Gradient

To create a gradient colour using normal C# code, you need to use a "LinearGradientBrush" object and a "GradientStopCollection". The gradient brush will be used to fill the background of the rectangle, and the stop collection will set the colours that the gradient transitions between.

In our gradient, we'll also be making use of Opacity Levels. When you specify an opacity level (from 0.0 to 1.0 - 0% to 100%), you're setting how transparent a colour is. Setting the opacity to 0% means that you can't see through it at all; it has no opacity, whereas 100% (1) is fully transparent and shows everything behind it. Setting the opacity at a value in between allows you to control how much of the background, and how much of the colour, shows.

Just after the opening of the create rectangle method, add the following to create your colour stops:

GradientStopCollection gradientStops = new GradientStopCollection
{
   new GradientStop(Color.FromScRgb(0.0f, 1, 1, 1), 0.0),
   new GradientStop(Color.FromScRgb(0.0f, 1, 1, 1), 0.01),
   new GradientStop(Color.FromScRgb(0.5f, 1, 1, 1), 0.02),
   new GradientStop(Color.FromScRgb(1.0f, 1, 1, 1), 0.03),
   new GradientStop(Color.FromScRgb(0.5f, 1, 1, 1), 0.04),
   new GradientStop(Color.FromScRgb(0.0f, 1, 1, 1), 0.05),
   new GradientStop(Color.FromScRgb(0.0f, 1, 1, 1), 1.0),
};

This set of gradient stops creates a set of seven colour stops ALL white (the 3 R,G,B values are all set to 1), the colour stops are set at positions 0, 0.01, 0.02, 0.03, 0.04, 0.05, and 1 along the gradient fill length, and have opacity levels of 0, 0, 0.5, 1, 0.5, 0 and 0. This set of fills means we get transparent from the first stop to the second, and then a sudden increase from fully transparent to solid white, followed by a sudden decrease back to transparent; finally, 100% transparency from stop number 6 to stop number 7 (the rest of the rectangle).

Just after you defined these colour stops, add the following line:

LinearGradientBrush gradientBrush =
   new LinearGradientBrush(gradientStops, new Point(0, 0.5),
   new Point(1, 0.5));

This will create the actual gradient that will use the stop collection. It will run horizontally from 0 on the X axis to 100% (1) on the X axis across the middle of the Y axis. Once you've added the gradient, alter the rectangle creation parameters to set the gradient as its fill, and while you're at it, make the border disappear also:

_testRect = new Rectangle
{
   Width = inputParameters.RectangleWidth,
   Height = inputParameters.RectangleHeight,
   Stroke = Brushes.Transparent,
   StrokeThickness = 0,
   Fill = gradientBrush
};

All being well, when you run your app you should get something like the following:

Animation2
Figure 2: Showing the app being run

You should be able to see straight away what effect the different levels of transparency have, and how it results in a giving the fill the look of a cylindrical 3D bar. To animate this we, need to move the five gradient stops that make up the portion of the gradient that looks like the 3D bar. In the gradient stop collection above, these values are the five values that sit in the middle of the two outer ones and have the positions 0.01 through 0.05

We need to move those colour stops from the left to the right and back again. This means we need the following movements:

  • Stop 1 from 0.01 to 0.95 and back again
  • Stop 2 from 0.02 to 0.96 and back again
  • Stop 3 from 0.03 to 0.97 and back again
  • Stop 4 from 0.04 to 0.98 and back again
  • Stop 5 from 0.05 to 0.99 and back again

We achieve this by making five double value animation objects, one for each of the five colour stops. To do this, add the following into your rectangle method before the place where you create your gradient stops:

DoubleAnimation firstStopAnim =
   new DoubleAnimation(0.01, 0.95,
   new Duration(new TimeSpan(0,0,0,5)));
DoubleAnimation secondStopAnim =
   new DoubleAnimation(0.02,0.96,
   new Duration(new TimeSpan(0,0,0,5)));
DoubleAnimation thirdStopAnim =
   new DoubleAnimation(0.03, 0.97,
   new Duration(new TimeSpan(0,0,0,5)));
DoubleAnimation fourthStopAnim =
   new DoubleAnimation(0.04, 0.98,
   new Duration(new TimeSpan(0,0,0,5)));
DoubleAnimation fifthStopAnim =
   new DoubleAnimation(0.05, 0.99,
   new Duration(new TimeSpan(0,0,0,5)));

firstStopAnim.AutoReverse = true;
secondStopAnim.AutoReverse = true;
thirdStopAnim.AutoReverse = true;
fourthStopAnim.AutoReverse = true;
fifthStopAnim.AutoReverse = true;

firstStopAnim.BeginTime = new TimeSpan(0);
secondStopAnim.BeginTime = new TimeSpan(0);
thirdStopAnim.BeginTime = new TimeSpan(0);
fourthStopAnim.BeginTime = new TimeSpan(0);
fifthStopAnim.BeginTime = new TimeSpan(0);

firstStopAnim.EasingFunction = new CubicEase();
secondStopAnim.EasingFunction = new CubicEase();
thirdStopAnim.EasingFunction = new CubicEase();
fourthStopAnim.EasingFunction = new CubicEase();
fifthStopAnim.EasingFunction = new CubicEase();

Here we are adding the five stops, and then setting some default properties on them, as well as the range they go from and to. The default properties are that each one should automatically go back in the opposite direction, start at a time of 0 seconds, and use the CubicEase animation transition. Once we create the animation objects, we then need to give the inner five gradient stops unique names so that we can attach the storyboard object for controlling the animation to them.

Just after the declaration for the gradient stops collection but before the point where you create the linear gradient, add the following code:

String slotOneName = RandomName();
String slotTwoName = RandomName();
String slotThreeName = RandomName();
String slotFourName = RandomName();
String slotFiveName = RandomName();

RegisterName(slotOneName, gradientStops[1]);
RegisterName(slotTwoName, gradientStops[2]);
RegisterName(slotThreeName, gradientStops[3]);
RegisterName(slotFourName, gradientStops[4]);
RegisterName(slotFiveName, gradientStops[5]);

The function "RandomName" is a user added function that's added to your code just after the method for drawing the rectangle. You need to make random names for each of the names so that you are able to reuse the rectangle drawing method. If you attempt to reuse names that have already been assigned to previous colour stops, the rectangle function will abort BUT you won't get an exception that halts the application. Instead you'll just be left with a black window and no animation in it. It's therefore important to make sure that your stop names are unique, but not so scrambled that you form invalid property names. The function I've developed to do this uses a mixture of random and a GUID that has dangerous characters stripped out of it. To define it in your code, add the following into your MainWindow class after the rectangle function:

private string RandomName()
{
   const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
   const int nameLen = 8;
   var random = new Random();
   string temp = Guid.NewGuid().ToString().
      Replace("-", String.Empty);
   temp = Regex.Replace(temp, @"[\d-]",
      string.Empty).ToUpper();
   return new string(Enumerable.Repeat(chars, nameLen).Select
      (s => s[random.Next(s.Length)]).ToArray()) + temp;
}

With the random name routine in place, we then can continue with the animating of the gradient in our rectangle function. The next thing we need to do is to map those random property names we just created and attached to our gradient stops to our storyboard. We use static properties of the WPF Storyboard class to match everything together, and we then create our own storyboard local object and add each of the animations to its children collection.

Place the following code just after the declaration of your linear gradient, but before the point where you set up your rectangle:

Storyboard.SetTargetName(firstStopAnim, slotOneName);
Storyboard.SetTargetProperty(firstStopAnim,
   new PropertyPath(GradientStop.OffsetProperty));

Storyboard.SetTargetName(secondStopAnim, slotTwoName);
Storyboard.SetTargetProperty(secondStopAnim,
   new PropertyPath(GradientStop.OffsetProperty));

Storyboard.SetTargetName(thirdStopAnim, slotThreeName);
Storyboard.SetTargetProperty(thirdStopAnim,
   new PropertyPath(GradientStop.OffsetProperty));

Storyboard.SetTargetName(fourthStopAnim, slotFourName);
Storyboard.SetTargetProperty(fourthStopAnim,
   new PropertyPath(GradientStop.OffsetProperty));

Storyboard.SetTargetName(fifthStopAnim, slotFiveName);
Storyboard.SetTargetProperty(fifthStopAnim,
   new PropertyPath(GradientStop.OffsetProperty));

Storyboard gradientAnimation = new Storyboard
   { RepeatBehavior = RepeatBehavior.Forever };

gradientAnimation.Children.Add(firstStopAnim);
gradientAnimation.Children.Add(secondStopAnim);
gradientAnimation.Children.Add(thirdStopAnim);
gradientAnimation.Children.Add(fourthStopAnim);
gradientAnimation.Children.Add(fifthStopAnim);

The final part of the puzzle is to add the following line:

gradientAnimation.Begin(this);

Right at the end of your rectangle method, just after the point you set the left and top of your rectangle.

If everything has gone to plan, when you press F5 to run your application, you should now see your white 3D bar moving left and right. If your white bar does indeed move, we only need to make a few more tweaks to make this into something that looks exceptionally great.

Remember that, earlier on, I briefly mentioned the other parameters in our "BarDescriptor" object? Well, we're now going to use them fully.

As you already know, RectangleX, RectangleY, RegtangleWidth, and RectangleHeight specify the position and size of the rectangle you wish to draw. AnimationTimeInSeconds is how many seconds you want the storyboard to take to go from the left of the rectangle to the right. This does not contain the return time; it's only the time for one cycle of the animation itself. BarBaseRedLevel, BarBaseGreenLevel, and BarBaseBlueLevel are used to set the base colour of the bar using the 0% (0) to 100% (1) scale that everything else so far has been using. Setting these can be used to change the overall colour of the bar.

Finally, GradientStartX, GradientStartY, GradientEndX, and GradientEndY set the straight line path that the gradient will follow. These values are 0.0 to 1.0 and, like the others, represent the values 0% to 100% in the X and Y directions. For example, if you set them to 0,0 and 1,1 your 3D bar will animate at a diagonal from the upper left corner to the lower right corner. Setting them to 0,0.5 and 1,0.5 will make our animation follow the regular Left to Right we've seen so far.

Change your barOne object so that it has the following values:

BarDescriptor barOne = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 5,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0.5f,
   BarBaseBlueLevel = 0,
   GradientStartX = 0,
   GradientStartY = 0.5f,
   GradientEndX = 1,
   GradientEndY = 0.5f
};

Then, make the following changes to your "CreateRectangle" method so that the parameters are used where needed. Change the creation of the gradient stop collection so that it reads as follows:

GradientStopCollection gradientStops = new GradientStopCollection
{
   new GradientStop(Color.FromScRgb(0.5f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.0),
   new GradientStop(Color.FromScRgb(0.0f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.01),
   new GradientStop(Color.FromScRgb(0.5f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.02),
   new GradientStop(Color.FromScRgb(1.0f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.03),
   new GradientStop(Color.FromScRgb(0.5f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.04),
   new GradientStop(Color.FromScRgb(0.0f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 0.05),
   new GradientStop(Color.FromScRgb(0.5f,
      inputParameters.BarBaseRedLevel,
      inputParameters.BarBaseGreenLevel,
      inputParameters.BarBaseBlueLevel), 1.0),
};

Then, change the line where you create the linear gradient brush so that it reads:

LinearGradientBrush gradientBrush =
   new LinearGradientBrush(gradientStops,
   new Point(inputParameters.GradientStartX,
      inputParameters.GradientStartY),
   new Point(inputParameters.GradientEndX,
      inputParameters.GradientEndY));

If all has gone okay, you should have a green bar moving left and right and a slightly different gradient to previous:

Animation3
Figure 3: A successful animation

If you have, then congratulations. All you need to do now to add the rest of the bars in to create a few more "BarDescriptor" objects and pass them to the rectangle create method as follows:

BarDescriptor barOne = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 5,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0.5f,
   BarBaseBlueLevel = 0,
   GradientStartX = 0,
   GradientStartY = 0.5f,
   GradientEndX = 1,
   GradientEndY = 0.5f
};

BarDescriptor barTwo = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 4,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0.5f,
   BarBaseBlueLevel = 0,
   GradientStartX = 1,
   GradientStartY = 0,
   GradientEndX = 0,
   GradientEndY = 1
};

BarDescriptor barThree = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 3,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0.5f,
   BarBaseBlueLevel = 0,
   GradientStartX = 0,
   GradientStartY = 0,
   GradientEndX = 1,
   GradientEndY = 1
};

BarDescriptor barFour = new BarDescriptor
{
   RectangleX = 0,
   RectangleY = 0,
   RectangleWidth = 800,
   RectangleHeight = 600,
   AnimationTimeInSeconds = 6,
   BarBaseRedLevel = 0,
   BarBaseGreenLevel = 0.5f,
   BarBaseBlueLevel = 0,
   GradientStartX = 0.5f,
   GradientStartY = 0,
   GradientEndX = 0.5f,
   GradientEndY = 1
};

CreateRectangleAnimatedRectangle(barOne);
CreateRectangleAnimatedRectangle(barTwo);
CreateRectangleAnimatedRectangle(barThree);
CreateRectangleAnimatedRectangle(barFour);

The image you saw at the start of this article was the product of the above four bar descriptions all overlaid on top of each other, allowing the opacity from each one to combine with the one below. Changing the time in seconds also has the same effect as changing the speed, so different bars move at different rates.

You can play with the colour gradient and transparency maps and create all kinds of fun effects. Just remember that, for it to work correctly, you must have five gradients in the centre, in slots 1 to 5 in the collection.

The finished project, including full working code, can be found on my GitHub page at:

http://github.com/shawty

If you have any ideas you like to see explained, or an idea for a strange, wacky WPF effect of your own, or if there's just some topic you think I should cover in this column, look me up on Twitter as @shawty_ds, or come and seek me out on Linked-In in the Lidnug .NET users group that I help to run and let me know your thoughts.

Happy Animating!

Shawty



Related Articles

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date