2D animation is an area that I have always had a lot of interest in. I thought I would try my hand at creating a simple 2D animation program with WPF and ran into a few surprises.

Ink-A-Mator 

The sample and source is posted on the .NET 3.0 Framework site.

The first feature I wanted was the ability to create a cell of animation store that cell in memory and then advance to another cell. To do this I choose to use an InkCanvas, because it has built in drawing features.

<InkCanvas Name="Paper" />

The InkCanvas captures drawings as Strokes these strokes are stored in a StrokeCollection.

// Our Frames

List<StrokeCollection> frames = new List<StrokeCollection>();

The frames are stored in a generic list of StrokeCollections. To advance to the next frame we first save the current frame. When we save we want to make sure we are saving to the correct place in our frame collection. If we have fewer frames then the frame count we add our cell to the frames collection otherwise we update the current frame.

Next we create a new stroke collection to store the next frame and then advance our frame counter. If the next frame has already been created we want to assign its stroke collection to our InkCanvas’s Strokes property. We have similar code for viewing the previous frame, an arbitrary frame and inserting a frame between two frames.

 

private void SaveFrame()

{

if (frames.Count <= CurrentFrame)

{

    frames.Add(this.Paper.Strokes);

}

else

{

    frames[CurrentFrame] = this.Paper.Strokes;

    }

}

void OnNextFrame(object sender, RoutedEventArgs e)

{

SaveFrame();

 

this.Paper.Strokes = new StrokeCollection();

 

CurrentFrame++;

 

if (frames.Count > CurrentFrame)

{

    this.Paper.Strokes = frames[CurrentFrame];

}

 

RenderOnionLayer();

}

 

The InkCanvas class along with the Stroke and StrokeCollection class make it easy to add other features such as changing the color, size and shape of the brush. The last thing we have to do in our OnNextFrame method is to render the onion layer.

For those not familar with creating animation the ability to see what has just happened is important. We achive this by drawing the previous frames in lighter and lighter shades of gray. Here is an example of an animation that shows a bouncing red ball. The circles drawn in gray are the onion layer. They will not appear in the final animation when we play it back, they are just a tool to help the animator.

Onion

To render the onion layer we are going to use another control called an InkPresenter.

<InkPresenter Name="OnionLayer"/>

An InkPresenter is similar to an InkCanvas except that it is used for displaying ink instead of manipulating it. It too has a Strokes collection. Since we want to draw a number of different cells we are going to combine copies of their collections together in this sincle OnionLayer InkPresenter.

We can see the whole process by reading through the comments in the RenderOnionLayer method. From a high level perspective we want to change the color attribute on each of the strokes by in each of our cells. The Stroke class DrawingAttributes property exposes a large number of attributes we can change.

void RenderOnionLayer(int currentFrame)

{

// Get starting frame for the onioning

int i = CurrentFrame - onionFrameCount;

 

int count = currentFrame;

 

// Make sure the starting onion frame isn't less then zero.

if (i < 0)

{

    // update the count if it is less then zero. We only want to show up to this frame.

    count = i + onionFrameCount;

 

    i = 0;

}

 

// Create a new stroke collection for the onion layer.

this.OnionLayer.Strokes = new StrokeCollection();

 

int c = 0;

 

// iterate through the onion layers.

for (int j = i; j < count; j++)

{

    // Make a copy of the real frame.

this.OnionLayer.Strokes.Add(this.frames[j].Clone());

 

// Figure out how many stokes we have, since we are going to add all the onion strokes to the same frame.

c = this.OnionLayer.Strokes.Count - this.frames[j].Count;

 

// Make sure we have strokes to color

if (this.OnionLayer.Strokes.Count != 0)

{

    // Go through the strokes in this onion frame and change their color.

for (int k = c; k < this.OnionLayer.Strokes.Count; k++)

{

    Color color = Colors.Gray;

 

// Calculate the shade of gray dynamically so we can change based on the onion layer count.

color.A = (byte)(color.A / (count - j + 1));

 

// Apply the color.

this.OnionLayer.Strokes[k].DrawingAttributes.Color = color;

    }

    }

}

}

 

The last feature I am going to cover in this post is how we playback our animation. WPF has a great built in animation system, but it is made to interpolate between multiple values over a set period of time. For our animation we don’t want to do interpolation we want to simply display each frame for 1/24th of a second (Standard Animation Frames Per Second). To do this we are going to use an event that is fired everytime WPF renders a frame.

 

CompositionTarget.Rendering

 

If we just displayed a frame each time this event fired we wouldn’t see any change, instead we want to display frames for 1/24th of a second then show the next frame. We do this with the help of the Stopwatch class. Stopwatch is a high percision timer.

We start a new stop watch instance and then check the ElapsedTicks agaist Frequency / 24 to see if we have reached the next frame. We then restart the Stopwatch. In the middle of our loop we check to see if we have reached the end our our animation. It is important to notice that we have a bool playing that we check to make sure the animiator is playing this animation otherwise we don’t want to render the animation. This is because we have to signup for CompositionTarget.Rendering before WPF renders the first frame.

 

// The playing timer.

Stopwatch watch;

watch = Stopwatch.StartNew();

void CompositionTarget_Rendering(object sender, EventArgs e)

{

    // Only render if we are playing.

    if (playing)

    {

        long l = watch.ElapsedTicks;

      

    // Play at 24 frames a second.

if (l > Stopwatch.Frequency / 24)

{

    // We are done playing through the frames

if (playingFrame >= this.frames.Count)

{

    DonePlaying();

return;

}

 

// Advance the frame.

this.Player.Strokes = this.frames[playingFrame++];

 

// restart the stopwatch

watch.Reset();

watch.Start();

    }

}

}

In my next post I will discuss how animations are saved and loaded using the Open Packaging Format APIs that are a part of WPF and then we will look at how I created the palette controls.