Introduction
In WPF most animations are time-based, using storyboards to animate a objects properties over a certain period of time. That is great and works well, until an artist hands you a stack of images that are to be used as individual frames of an animation. This is my current situation, so I spent an hour or two trying to figure out a way to accomplish frame-based animations in WPF. This is what I came up with…
ImageSourceHelper Class
The first problem I encountered was with how to actually set an Image object’s Source property. At first you’d think that you could just say:
Image myImage = new Image() { Source = "Images\MyImage.png" };
But then you’ll soon realize that this is WPF, and the developers spared no expense to make things as hard and non-intuitive as possible. The easiest way I could find to set an Image’s Source property through code-behind is as such:
ImageSourceConverter imageSourceConverter = new ImageSourceConverter();
ImageSource imageSource = (ImageSource)imageSourceConverter.ConvertFromString("pack://application:,,/Images/MyImage.png");
Image myImage = new Image() { Source = imageSource };
To me that’s a lot of code for something so simple, so I decided to wrap it up in a static class called ImageSourceHelper.
#region Using Directives
using System.Windows.Media;
#endregion Using Directives
namespace FrameBasedAnimationTest
{
public static class ImageSourceHelper
{
#region Static Fields
public static ImageSourceConverter _imageSourceConverter = new ImageSourceConverter();
#endregion Static Fields
#region Static Methods
// |===========================================================================================================|
// | GetImageSource
// |===========================================================================================================|
public static ImageSource GetImageSource(string path)
{
return (ImageSource)_imageSourceConverter.ConvertFromString(
string.Format("pack://application:,,/{0}", path));
}
#endregion Static Methods
}
}
With the help of our new static class, ImageSourceHelper, we can now do the following:
Image myImage = new Image() { Source = ImageSourceHelper.GetImageSource("Images/MyImage.png") };
That was a good enough solution for me and it seems to work well.
FrameBasedAnimation Class (First Draft)
The next step I took was to actually start creating a FrameBasedAnimation class that inherits from the Image class. The intial class looked something like the following:
#region Using Directives
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endregion Using Directives
namespace FrameBasedAnimationTest
{
public partial class FrameBasedAnimation : Image
{
#region Dependency Properties
// |===========================================================================================================|
// | ActiveFrameIndexProperty
// |===========================================================================================================|
public static readonly DependencyProperty ActiveFrameIndexProperty = DependencyProperty
.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | IsActiveProperty
// |===========================================================================================================|
public static readonly DependencyProperty IsActiveProperty = DependencyProperty
.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));
#endregion Dependency Properties
#region Properties
// |===========================================================================================================|
// | ActiveFrame
// |===========================================================================================================|
public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }
// |===========================================================================================================|
// | ActiveFrameIndex
// |===========================================================================================================|
public int ActiveFrameIndex
{
get { return (int)GetValue(ActiveFrameIndexProperty); }
set
{
if (value < 0)
{
throw new Exception("The ActiveFrameIndex can not be negative.");
}
if (value > MaximumFrameIndex)
{
throw new Exception("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");
}
SetValue(ActiveFrameIndexProperty, value);
Source = ActiveFrame;
}
}
// |===========================================================================================================|
// | Frames
// |===========================================================================================================|
public List<ImageSource> Frames { get; private set; }
// |===========================================================================================================|
// | IsActive
// |===========================================================================================================|
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set
{
// Activate.
if (!IsActive && value)
{
CompositionTarget.Rendering += new System.EventHandler(CompositionTarget_Rendering);
}
// Deactivate.
if (IsActive && !value)
{
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
SetValue(IsActiveProperty, value);
}
}
// |===========================================================================================================|
// | MaximumFrameIndex
// |===========================================================================================================|
public int MaximumFrameIndex { get { return Frames.Count - 1; } }
// |===========================================================================================================|
// | TotalFrames
// |===========================================================================================================|
public int TotalFrames { get { return Frames.Count; } }
#endregion Properties
#region Constructors
// |===========================================================================================================|
// | FrameBasedAnimation
// |===========================================================================================================|
public FrameBasedAnimation()
{
InitializeComponent();
Frames = new List<ImageSource>();
}
#endregion Constructors
#region Event Handlers
// |===========================================================================================================|
// | CompositionTarget_Rendering
// |===========================================================================================================|
private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
// Set ActiveFrameIndex accordingly.
if (ActiveFrameIndex < MaximumFrameIndex)
{
ActiveFrameIndex++;
}
else
{
Stop();
}
}
#endregion Event Handlers
#region Methods
// |===========================================================================================================|
// | Resume
// |===========================================================================================================|
public void Resume()
{
if (TotalFrames < 0)
{
throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");
}
IsActive = true;
}
// |===========================================================================================================|
// | Start
// |===========================================================================================================|
public void Start()
{
Resume();
ActiveFrameIndex = 0;
}
// |===========================================================================================================|
// | Stop
// |===========================================================================================================|
public void Stop()
{
IsActive = false;
}
#endregion Methods
}
}
Its a fairly simple class. It has a collection of frames represented by a List of ImageSource objects. The ActiveFrameIndex keeps track of what frame the animation is currently displaying and when set updates the class’s base property, Source. There are three other properties, ActiveFrame, MaximumFrameIndex, and TotalFrames, for convenience purposes.
In addition, there are three methods that you’d expect to see on a animation class: Start, Stop, and Resume. As you can see I ended up skipping a Pause method because I found that it wasn’t neccessary.
- The Start method starts the animation AND sets the current frame, ActiveFrameIndex, back the beginning.
- The Stop method simply stops animation where it is.
- The Resume method is an alternative to Start, which DOES NOT set the ActiveFrameIndex back to zero.
Now the only interesting part of this class so far is how it actually increments the ActiveFrameIndex as the animation is playing. You’d probably expect to see some sort of timer with a Tick event handler, but I found something that I think is even better in this situation: registering an event handler with CompositionTarget.Rendering.
This event gets triggered before every frame WPF renders, which is very convenient for us because now we can actually be in sync with the application’s\WPF’s underlying rendering engine. Now the old timer method of doing things should still work, but I liked this way better.
Now we actually register the CompositionTarget.Rendering event handler in the set of our IsActive property, depending on certain conditions which you can see in the code shown previously. You’ll also notice that we “de-register” the event handler when we don’t need it anymore, this is an optimization which will cut out unncessary calls, since we don’t need to update the ActiveFrameIndex property when the animation is stopped.
The last thing you’ll see in that class is the dependency properties. Those are simply so our class can participate in WPF’s dependency system. Which means those properties can support binding and be accessed through XAML markup code.
FrameBasedAnimation Class (Second Draft)
Our intial version of the FrameBasedAnimation class should “work”, but it doesn’t have any concept of two things: Wrapping the animation and more importantly, frame-rate. The final version of the class that incorpiates both of those two things is below:
#region Using Directives
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endregion Using Directives
namespace FrameBasedAnimationTest
{
public partial class FrameBasedAnimation : Image
{
#region Dependency Properties
// |===========================================================================================================|
// | ActiveFrameIndexProperty
// |===========================================================================================================|
public static readonly DependencyProperty ActiveFrameIndexProperty = DependencyProperty
.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | IsActiveProperty
// |===========================================================================================================|
public static readonly DependencyProperty IsActiveProperty = DependencyProperty
.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | FramesPerSecondProperty
// |===========================================================================================================|
public static readonly DependencyProperty FramesPerSecondProperty = DependencyProperty
.Register("FramesPerSecond", typeof(double), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | WrapAroundProperty
// |===========================================================================================================|
public static readonly DependencyProperty WrapAroundProperty = DependencyProperty
.Register("WrapAround", typeof(bool), typeof(FrameBasedAnimation));
#endregion Dependency Properties
#region Properties
// |===========================================================================================================|
// | ActiveFrame
// |===========================================================================================================|
public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }
// |===========================================================================================================|
// | ActiveFrameIndex
// |===========================================================================================================|
public int ActiveFrameIndex
{
get { return (int)GetValue(ActiveFrameIndexProperty); }
set
{
if (value < 0)
{
throw new Exception("The ActiveFrameIndex can not be negative.");
}
if (value > MaximumFrameIndex)
{
throw new Exception("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");
}
SetValue(ActiveFrameIndexProperty, value);
Source = ActiveFrame;
}
}
// |===========================================================================================================|
// | BypassFramesPerSecond
// |===========================================================================================================|
public bool BypassFramesPerSecond { get; set; }
// |===========================================================================================================|
// | Frames
// |===========================================================================================================|
public List<ImageSource> Frames { get; private set; }
// |===========================================================================================================|
// | FramesPerSecond
// |===========================================================================================================|
public double FramesPerSecond
{
get { return (double)GetValue(FramesPerSecondProperty); }
set
{
if (value <= 0)
{
throw new Exception("FramesPerSecond must be greater than 0.");
}
SetValue(FramesPerSecondProperty, value);
}
}
// |===========================================================================================================|
// | IsActive
// |===========================================================================================================|
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set
{
// Activate.
if (!IsActive && value)
{
CompositionTarget.Rendering += new System.EventHandler(CompositionTarget_Rendering);
}
// Deactivate.
if (IsActive && !value)
{
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
SetValue(IsActiveProperty, value);
}
}
// |===========================================================================================================|
// | LastRenderTime
// |===========================================================================================================|
private TimeSpan LastRenderTime { get; set; }
// |===========================================================================================================|
// | MaximumFrameIndex
// |===========================================================================================================|
public int MaximumFrameIndex { get { return Frames.Count - 1; } }
// |===========================================================================================================|
// | TotalFrames
// |===========================================================================================================|
public int TotalFrames { get { return Frames.Count; } }
// |===========================================================================================================|
// | WrapAround
// |===========================================================================================================|
public bool WrapAround
{
get { return (bool)GetValue(WrapAroundProperty); }
set { SetValue(WrapAroundProperty, value); }
}
#endregion Properties
#region Constructors
// |===========================================================================================================|
// | FrameBasedAnimation
// |===========================================================================================================|
public FrameBasedAnimation()
{
InitializeComponent();
Frames = new List<ImageSource>();
FramesPerSecond = 30;
}
#endregion Constructors
#region Event Handlers
// |===========================================================================================================|
// | CompositionTarget_Rendering
// |===========================================================================================================|
private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
TimeSpan timeSinceLastRender;
// Enforce FramesPerSecond if BypassFramesPerSecond is false.
if (!BypassFramesPerSecond)
{
timeSinceLastRender = (DateTime.Now.TimeOfDay - LastRenderTime);
if (timeSinceLastRender.TotalSeconds < (1 / FramesPerSecond))
{
return;
}
LastRenderTime = DateTime.Now.TimeOfDay;
}
// Set ActiveFrameIndex accordingly.
if (ActiveFrameIndex < MaximumFrameIndex)
{
ActiveFrameIndex++;
}
else
{
if (WrapAround)
{
ActiveFrameIndex = 0;
}
else
{
Stop();
}
}
}
#endregion Event Handlers
#region Methods
// |===========================================================================================================|
// | Resume
// |===========================================================================================================|
public void Resume()
{
if (TotalFrames < 0)
{
throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");
}
IsActive = true;
}
// |===========================================================================================================|
// | Start
// |===========================================================================================================|
public void Start()
{
Resume();
ActiveFrameIndex = 0;
}
// |===========================================================================================================|
// | Stop
// |===========================================================================================================|
public void Stop()
{
IsActive = false;
}
#endregion Methods
}
}
The first thing I did was create a new property called WrapAround, which will simply wrap the ActiveFrameIndex when it goes over the MaximumFrameIndex, but only when set to true. Then I added it’s corresponding dependency property.
To make the WrapAround property actually do work we have to modify the CompostionTarget.Rendering event handler to the following:
#region Event Handlers
// |===========================================================================================================|
// | CompositionTarget_Rendering
// |===========================================================================================================|
private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
// Set ActiveFrameIndex accordingly.
if (ActiveFrameIndex < MaximumFrameIndex)
{
ActiveFrameIndex++;
}
else
{
if (WrapAround)
{
ActiveFrameIndex = 0;
}
else
{
Stop();
}
}
}
#endregion Event Handlers
As you can see that was a fairly simple change: if we try to increment the ActiveFrameIndex above MaximumFrameIndex, then check the WrapAround property to see if we should set the ActiveFrameIndex back to zero, or simply stop the animation.
Now comes implementing the frame-rate which is slightly a little more complicated, but not too much. The first thing I did is add a FramesPerSecond CLR property and it’s corresponding Dependency property.
The rest of the work will be done in the CompositionTarget.Rendering event handler. What we need to do now is only perform the ActiveFrameIndex incrementing logic if the time elapsed since our last update is less than (1 / FramesPerSecond). Which looks like the following:
#region Event Handlers
// |===========================================================================================================|
// | CompositionTarget_Rendering
// |===========================================================================================================|
private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
TimeSpan timeSinceLastRender;
// Enforce FramesPerSecond if BypassFramesPerSecond is false.
timeSinceLastRender = (DateTime.Now.TimeOfDay - LastRenderTime);
if (timeSinceLastRender.TotalSeconds < (1 / FramesPerSecond))
{
return;
}
LastRenderTime = DateTime.Now.TimeOfDay;
// Set ActiveFrameIndex accordingly.
if (ActiveFrameIndex < MaximumFrameIndex)
{
ActiveFrameIndex++;
}
else
{
if (WrapAround)
{
ActiveFrameIndex = 0;
}
else
{
Stop();
}
}
}
#endregion Event Handlers
Notice that we are also using a new property that must be added to the class called LastRenderTime, which is a TimeSpan object. The next step is to get the elasped time since our last update which means we subtract LastRenderTime from the current time, and store it in a new TimeSpan object called timeSinceLastRender.
Then finally we can check to see if timeSinceLastRender.TotalSeconds is less than (1 / FramesPerSecond), and only update if that statement is true.
So how did I arrive at that if statement? Its simple:
Let’s say we have 4 frames in our animation and we want 1 frame to show per second. In that case we need to only update if timeSinceLastRender.TotalSeconds is less than 1.0.
Let’s say we have 4 frames in our animation and we want 2 frames to show per second. In that case we need to only update if timeSinceLastRender.TotalSeconds is less than 0.5.
Let’s say we have 4 frames in our animation and we want 3 frames to show per second. In that case we need to only update if timeSinceLastRender.TotalSeconds is less than 0.33.
As you can see there is a pattern here which is to take 1 and divide it by our FramesPerSecond:
1 / 1 = 1
1 / 2 = .5
1 / 3 = .33
etc…
FrameBasedAnimation Class (Final Version)
The last thing we need to do is provide a way of omitting or bypassing frame-rate if we want to because sometimes we will want things to just run as fast as possible. Keep in mind however that since we are using CompositionTarget.Rendering instead of the old timer method, we will be limited to the application’s default frame-rate which I believe is at 60 frames-per-second. In reality this really isn’t a limitation though because we should be obidding by the application’s settings to keep our frame-based animation consistinent with any other time-based animations in our application.
The final version of the FrameBasedAnimation class is as follows:
#region Using Directives
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endregion Using Directives
namespace FrameBasedAnimationTest
{
public partial class FrameBasedAnimation : Image
{
#region Dependency Properties
// |===========================================================================================================|
// | ActiveFrameIndexProperty
// |===========================================================================================================|
public static readonly DependencyProperty ActiveFrameIndexProperty = DependencyProperty
.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | IsActiveProperty
// |===========================================================================================================|
public static readonly DependencyProperty IsActiveProperty = DependencyProperty
.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | FramesPerSecondProperty
// |===========================================================================================================|
public static readonly DependencyProperty FramesPerSecondProperty = DependencyProperty
.Register("FramesPerSecond", typeof(double), typeof(FrameBasedAnimation));
// |===========================================================================================================|
// | WrapAroundProperty
// |===========================================================================================================|
public static readonly DependencyProperty WrapAroundProperty = DependencyProperty
.Register("WrapAround", typeof(bool), typeof(FrameBasedAnimation));
#endregion Dependency Properties
#region Properties
// |===========================================================================================================|
// | ActiveFrame
// |===========================================================================================================|
public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }
// |===========================================================================================================|
// | ActiveFrameIndex
// |===========================================================================================================|
public int ActiveFrameIndex
{
get { return (int)GetValue(ActiveFrameIndexProperty); }
set
{
if (value < 0)
{
throw new Exception("The ActiveFrameIndex can not be negative.");
}
if (value > MaximumFrameIndex)
{
throw new Exception("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");
}
SetValue(ActiveFrameIndexProperty, value);
Source = ActiveFrame;
}
}
// |===========================================================================================================|
// | BypassFramesPerSecond
// |===========================================================================================================|
public bool BypassFramesPerSecond { get; set; }
// |===========================================================================================================|
// | Frames
// |===========================================================================================================|
public List<ImageSource> Frames { get; private set; }
// |===========================================================================================================|
// | FramesPerSecond
// |===========================================================================================================|
public double FramesPerSecond
{
get { return (double)GetValue(FramesPerSecondProperty); }
set
{
if (value <= 0)
{
throw new Exception("FramesPerSecond must be greater than 0.");
}
SetValue(FramesPerSecondProperty, value);
}
}
// |===========================================================================================================|
// | IsActive
// |===========================================================================================================|
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set
{
// Activate.
if (!IsActive && value)
{
CompositionTarget.Rendering += new System.EventHandler(CompositionTarget_Rendering);
}
// Deactivate.
if (IsActive && !value)
{
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
SetValue(IsActiveProperty, value);
}
}
// |===========================================================================================================|
// | LastRenderTime
// |===========================================================================================================|
private TimeSpan LastRenderTime { get; set; }
// |===========================================================================================================|
// | MaximumFrameIndex
// |===========================================================================================================|
public int MaximumFrameIndex { get { return Frames.Count - 1; } }
// |===========================================================================================================|
// | TotalFrames
// |===========================================================================================================|
public int TotalFrames { get { return Frames.Count; } }
// |===========================================================================================================|
// | WrapAround
// |===========================================================================================================|
public bool WrapAround
{
get { return (bool)GetValue(WrapAroundProperty); }
set { SetValue(WrapAroundProperty, value); }
}
#endregion Properties
#region Constructors
// |===========================================================================================================|
// | FrameBasedAnimation
// |===========================================================================================================|
public FrameBasedAnimation()
{
InitializeComponent();
Frames = new List<ImageSource>();
FramesPerSecond = 30;
}
#endregion Constructors
#region Event Handlers
// |===========================================================================================================|
// | CompositionTarget_Rendering
// |===========================================================================================================|
private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
TimeSpan timeSinceLastRender;
// Enforce FramesPerSecond if BypassFramesPerSecond is false.
timeSinceLastRender = (DateTime.Now.TimeOfDay - LastRenderTime);
if (timeSinceLastRender.TotalSeconds < (1 / FramesPerSecond))
{
return;
}
LastRenderTime = DateTime.Now.TimeOfDay;
// Set ActiveFrameIndex accordingly.
if (ActiveFrameIndex < MaximumFrameIndex)
{
ActiveFrameIndex++;
}
else
{
if (WrapAround)
{
ActiveFrameIndex = 0;
}
else
{
Stop();
}
}
}
#endregion Event Handlers
#region Methods
// |===========================================================================================================|
// | Resume
// |===========================================================================================================|
public void Resume()
{
if (TotalFrames < 0)
{
throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");
}
IsActive = true;
}
// |===========================================================================================================|
// | Start
// |===========================================================================================================|
public void Start()
{
Resume();
ActiveFrameIndex = 0;
}
// |===========================================================================================================|
// | Stop
// |===========================================================================================================|
public void Stop()
{
IsActive = false;
}
#endregion Methods
}
}
Source Files
Below is the link to the actual source files for a working sample of the code shown in this post. Feel free to use it in any of your projects.
http://freewebs.com/thrash505/framebasedanimationtest.zip