Frame-Based Animation In WPF


Problem

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…

Solution

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.

using System.Windows.Media;

namespace FrameBasedAnimationTest
{
    public static class ImageSourceHelper
    {
        public static ImageSourceConverter _imageSourceConverter = new ImageSourceConverter();

        public static ImageSource GetImageSource(string path)
        {
            return (ImageSource)_imageSourceConverter.ConvertFromString(
            string.Format("pack://application:,,/{0}", path));
        }
    }
}

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

FrameBasedAnimation Class (First Draft)

The next step I took was to actually start creating a FrameBasedAnimation class that inherits from the Image class. The initial implementation looked something like this:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace FrameBasedAnimationTest
{
    public partial class FrameBasedAnimation : Image
    {
        public static readonly DependencyProperty ActiveFrameIndexProperty =
            DependencyProperty.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));

        public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }

        public int ActiveFrameIndex
        {
            get { return (int)GetValue(ActiveFrameIndexProperty); }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be negative.");

                if (value > MaximumFrameIndex)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");

                SetValue(ActiveFrameIndexProperty, value);
                Source = ActiveFrame;
            }
        }

        public List<ImageSource> Frames { get; private set; }

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

        public int MaximumFrameIndex { get { return Frames.Count - 1; } }
        public int TotalFrames { get { return Frames.Count; } }

        public FrameBasedAnimation()
        {
            InitializeComponent();
            Frames = new List<ImageSource>();
        }

        private void CompositionTarget_Rendering(object sender, System.EventArgs e)
        {
            // Set ActiveFrameIndex accordingly.
            if (ActiveFrameIndex < MaximumFrameIndex)
                ActiveFrameIndex++;
            else
                Stop();
        }

        public void Resume()
        {
            if (TotalFrames < 0)
                throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");
            IsActive = true;
        }

        public void Start()
        {
            Resume();
            ActiveFrameIndex = 0;
        }

        public void Stop()
        {
            IsActive = false;
        }
    }
}

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 next iteration of this class that incorpiates both concepts looks like this:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace FrameBasedAnimationTest
{
    public partial class FrameBasedAnimation : Image
    {
        public static readonly DependencyProperty ActiveFrameIndexProperty =
            DependencyProperty.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty FramesPerSecondProperty =
            DependencyProperty.Register("FramesPerSecond", typeof(double), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty WrapAroundProperty =
            DependencyProperty.Register("WrapAround", typeof(bool), typeof(FrameBasedAnimation));

        public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }

        public int ActiveFrameIndex
        {
            get { return (int)GetValue(ActiveFrameIndexProperty); }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be negative.");

                if (value > MaximumFrameIndex)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");

                SetValue(ActiveFrameIndexProperty, value);
                Source = ActiveFrame;
            }
        }

        public bool BypassFramesPerSecond { get; set; }

        public List<ImageSource> Frames { get; private set; }

        public double FramesPerSecond
        {
            get { return (double)GetValue(FramesPerSecondProperty); }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("FramesPerSecond must be greater than 0.");

                SetValue(FramesPerSecondProperty, value);
            }
        }

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

        private TimeSpan LastRenderTime { get; set; }
        public int MaximumFrameIndex { get { return Frames.Count - 1; } }
        public int TotalFrames { get { return Frames.Count; } }

        public bool WrapAround
        {
            get { return (bool)GetValue(WrapAroundProperty); }
            set { SetValue(WrapAroundProperty, value); }
        }

        public FrameBasedAnimation()
        {
            InitializeComponent();
            Frames = new List<ImageSource>();
            FramesPerSecond = 30;
        }

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

        public void Resume()
        {
            if (TotalFrames < 0)
                throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");
            IsActive = true;
        }

        public void Start()
        {
            Resume();
            ActiveFrameIndex = 0;
        }

        public void Stop()
        {
            IsActive = false;
        }
    }
}

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:

private void CompositionTarget_Rendering(object sender, System.EventArgs e)
{
    // Set ActiveFrameIndex accordingly.
    if (ActiveFrameIndex < MaximumFrameIndex)
        ActiveFrameIndex++;
    else
    {
        if (WrapAround)
            ActiveFrameIndex = 0;
        else
            Stop();
    }
}

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:

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

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:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace FrameBasedAnimationTest
{
    public partial class FrameBasedAnimation : Image
    {
        public static readonly DependencyProperty ActiveFrameIndexProperty =
            DependencyProperty.Register("ActiveFrameIndex", typeof(int), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.Register("IsActive", typeof(bool), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty FramesPerSecondProperty =
            DependencyProperty.Register("FramesPerSecond", typeof(double), typeof(FrameBasedAnimation));

        public static readonly DependencyProperty WrapAroundProperty =
            DependencyProperty.Register("WrapAround", typeof(bool), typeof(FrameBasedAnimation));

        public ImageSource ActiveFrame { get { return Frames[ActiveFrameIndex]; } }

        public int ActiveFrameIndex
        {
            get { return (int)GetValue(ActiveFrameIndexProperty); }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be negative.");

                if (value > MaximumFrameIndex)
                    throw new ArgumentOutOfRangeException("The ActiveFrameIndex can not be greater than MaximumFrameIndex.");

                SetValue(ActiveFrameIndexProperty, value);
                Source = ActiveFrame;
            }
        }

        public bool BypassFramesPerSecond { get; set; }
        public List<ImageSource> Frames { get; private set; }

        public double FramesPerSecond
        {
            get { return (double)GetValue(FramesPerSecondProperty); }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("FramesPerSecond must be greater than 0.");

                SetValue(FramesPerSecondProperty, value);
            }
        }

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

        private TimeSpan LastRenderTime { get; set; }
        public int MaximumFrameIndex { get { return Frames.Count - 1; } }
        public int TotalFrames { get { return Frames.Count; } }

        public bool WrapAround
        {
            get { return (bool)GetValue(WrapAroundProperty); }
            set { SetValue(WrapAroundProperty, value); }
        }

        public FrameBasedAnimation()
        {
            InitializeComponent();
            Frames = new List<ImageSource>();
            FramesPerSecond = 30;
        }

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

        public void Resume()
        {
            if (TotalFrames < 0)
                throw new Exception("FrameBasedAnimation can not start because it does not contain any frames.");

            IsActive = true;
        }

        public void Start()
        {
            Resume();
            ActiveFrameIndex = 0;
        }

        public void Stop()
        {
            IsActive = false;
        }
    }
}

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

, , ,

  1. #1 by Tanuj Oruganti on March 9, 2017 - 10:15 am

    Hi Tim,

    Thanks for posting this. I’m in exactly the same situation and wanted to try the project out, but the link doesn’t seem to be working anymore. Where can I download the project?

    Thanks!

  2. #2 by qford on July 21, 2013 - 9:48 am

    i put over 200 images, memory consume, how to free memory?

    • #3 by Tim Valentine on July 21, 2013 - 11:40 pm

      The code could be modified to load the images from a list of string paths as needed. Could asynchronously load them in priority order.

      • #4 by qford on July 22, 2013 - 2:36 am

        3Q. I am a newbie in WPF. I’ll try it out later. I did the same function in IOS , it’s much more easier than windows when animate lots of sequence- images , i think IOS optimizes better.

  3. #5 by Tim Valentine on November 9, 2011 - 12:59 pm

    Updated the code to make it more readable (hopefully I didn’t break anything). I’ll update the source files soon. I want to go over this solution again since it was my first post and I can probably provide a better solution or at the very least cleanup the existing one. It does work as is however.

  4. #6 by GoodApple on June 6, 2011 - 2:55 pm

    Excellent. Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: