Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

Stick Figure Animation Using C# and WPF

Rate me:
Please Sign up or sign in to vote.
4.97/5 (97 votes)
14 Oct 2010CPOL6 min read 220K   2.1K   150   89
Discover how to create complex stick figure animations using simple WPF or Silverlight elements.

StickFigure

Table of Contents

Introduction

Recently I wanted to create a kind of stick figure animation in WPF or Silverlight, but I found no material on the issue. I got frustrated and then decided to do something myself, and fortunately I was successful. This article describes, in some detail, how to create a stick figure animation in WPF, although I'm sure it could be easily ported to Silverlight. This article is intended to share some nice discoveries with the readers.

System Requirements

If you already have Visual Studio 2008 or Visual Studio 2010, that's enough to run the application. If you don't, you can download the following 100% free development tool directly from Microsoft:

Background

For some time, I wondered how to create a pivot stick animation using WPF or Silverlight. In the first attempt, I used sticks that moved independently on a canvas surface. But instead of using the standard WPF Animation classes, I had to control the animation all by myself, using timers to update both the angle and the positions of each individual stick, and also taking rotation speed into consideration. Since a stick figure is an articulated system, when one member is rotated, the dependent members must be rotated accordingly. For example, if I rotated a leg, I also had to rotate the foreleg accordingly, as well as re-calculate the foreleg coordinates based on the new position of the knee. And this was really a cumbersome task to do.

After struggling a lot with the code, in the end, it worked well, but realized that I ended up creating a little monster, a real code horror, and then decided to throw it away and start over from scratch.

The idea is that, in any articulated body, I can choose one particular segment of that body as the "root" for the whole body, and then "link" pieces successively at the edges of the first segment, creating a chain of segments. The good news is that it can be accomplished in WPF (or Silverlight if you wish) by creating a Grid element to represent each individual segment, and adding other Grid elements as child elements of the root segment. The Grid element is the most powerful visual element, and it's not without reason. The magic is done by creating three ColumnDefinitions inside the Grid: one ColumnDefinition residing in the middle of the segment and determines the extension of the segment, while the other two staying at the edges and acting as pivot joints for the child segments.

The Base Segment

The Base Segment

BaseSegment is the abstract class from which we derive the other segment classes. Notice that it does not have any appearance. Instead, it only defines the three grid columns as seen in the figure above.

C#
protected virtual void InitializeSegment()
{
    this.ShowGridLines = false;
    this.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
    this.ColumnDefinitions.Add(new ColumnDefinition() { 
              Width = GridLength.Auto, MinWidth = segmentWidth  });
    this.ColumnDefinitions.Add(new ColumnDefinition() { 
              Width = new GridLength(segmentLength) });
    this.ColumnDefinitions.Add(new ColumnDefinition() { 
              Width = GridLength.Auto, MinWidth = segmentWidth  });

    st = new ScaleTransform()
    {
    };

    tt = new TranslateTransform()
    {
        X = 0,
        Y = 0
    };

    rt = new RotateTransform()
    {
        CenterX = segmentWidth,
        CenterY = segmentWidth
    };

    TransformGroup tGroup = new TransformGroup();

    tGroup.Children.Add(st);
    tGroup.Children.Add(rt);
    tGroup.Children.Add(tt);
    this.RenderTransform = tGroup;
}

The Circle Segment

The Circle Segment

The Circle Segment is used only in the Head of the stick figure. The circle is positioned at the central column, and the two corners at the edge are used as pivot points:

C#
protected override void InitializeSkin()
{
    this.ColumnDefinitions[1].Width = new GridLength(segmentLength * 2);
    this.Height = segmentLength * 2;

    Rectangle rect = new Rectangle()
    {
        Stroke = new SolidColorBrush(Colors.White),
        StrokeThickness = 0.5,
        HorizontalAlignment = HorizontalAlignment.Stretch,
        VerticalAlignment = VerticalAlignment.Stretch,
        RadiusX = segmentWidth * 2,
        RadiusY = segmentWidth * 2
    };
    rect.SetValue(Grid.ColumnProperty, 1);
    rect.SetValue(Panel.ZIndexProperty, 1);

    this.Children.Add(rect);
}

The Axis Segment

The Axis Segment

The AxisSegment is used in almost every part in our stick figure. The code below shows that the "skin" of the axis segment is defined by a round-cornered rectangle that spans over the three columns of the BaseSegment element.

C#
protected override void InitializeSkin()
{
    rect = new Rectangle()
    {
        Stroke = new SolidColorBrush(Colors.White),
        StrokeThickness = 0.5,
        Width = segmentLength * 2,
        MaxWidth = segmentLength * 2,
        Height = segmentWidth * 2,
        RadiusX = segmentWidth,
        RadiusY = segmentWidth,
        HorizontalAlignment = HorizontalAlignment.Left,
        VerticalAlignment = VerticalAlignment.Stretch,
        Margin = new Thickness(0, 0, 0, 0)
    };

    rect.SetValue(Grid.ColumnProperty, 0);
    rect.SetValue(Grid.ColumnSpanProperty, 3);
    
    Rectangle dash1 = new Rectangle()
    {
        Stroke = new SolidColorBrush(Colors.Red),
        StrokeDashArray = new DoubleCollection(new double[]{4,4}),
        StrokeThickness = 1,
        Width = 1,
        HorizontalAlignment = HorizontalAlignment.Left,
        VerticalAlignment = VerticalAlignment.Stretch
    };

    Rectangle dash2 = new Rectangle()
    {
        Stroke = new SolidColorBrush(Colors.Green),
        StrokeDashArray = 
          new DoubleCollection(new double[] { 2, 2 }.ToList()),
        StrokeThickness = 1,
        Width = 1,
        HorizontalAlignment = HorizontalAlignment.Right,
        VerticalAlignment = VerticalAlignment.Stretch
    };

    dash1.SetValue(Grid.ColumnProperty, 1);
    dash2.SetValue(Grid.ColumnProperty, 1);

    this.Children.Add(dash1);
    this.Children.Add(dash2);

    this.Children.Add(rect);
}

Building Mr. StickMan: Head and Trunk

Head And Trunk

The Head is the first part in the stick figure. Then the Trunk is added as a child, positioned at the third column of the Head segment. Notice that, since the columns are positioned horizontally, we have to rotate the head 90 degrees so that the body can stand vertically. The head has a length of 10, while the Trunk has a length of 20. The Trunk is attached to PivotPoint P2 of the Head, that is, at the bottom of the Head segment:

C#
private void CreateStickFigure()
{
    ...

    head = new CircleSegment(10);
    head.TT.X = currentPoint.X;
    head.TT.Y = currentPoint.Y;
    head.RT.Angle = 90;
    head.VerticalAlignment = VerticalAlignment.Top;
    head.HorizontalAlignment = HorizontalAlignment.Left;

    trunk = new AxisSegment(20);

    ...

    head.AddChildElement(trunk, PivotPoint.P2, Layer.ForeGround);

    ...

    this.Children.Add(head);
}

Head, Trunk, and Arms

Head, Trunk And Arms

The arms must be positioned at the shoulder point of the stick figure; that is, the arms are children of the Trunk segment and positioned at the PivotPoint P1, that is, the first column of the grid, at the top of the Trunk.

C#
private void CreateStickFigure()
{
    ...

    head = new CircleSegment(10);
    head.TT.X = currentPoint.X;
    head.TT.Y = currentPoint.Y;
    head.RT.Angle = 90;
    head.VerticalAlignment = VerticalAlignment.Top;
    head.HorizontalAlignment = HorizontalAlignment.Left;

    trunk = new AxisSegment(20);

    ...
    
    arm1 = new AxisSegment(12);
    arm2 = new AxisSegment(12);
    
    ...

    trunk.AddChildElement(arm1, PivotPoint.P1, Layer.BackGround);
    trunk.AddChildElement(arm2, PivotPoint.P1, Layer.ForeGround);

    head.AddChildElement(trunk, PivotPoint.P2, Layer.ForeGround);

    ...

    this.Children.Add(head);
}

The Whole Body

The Whole Body

Now we have all the body segments. The segment hierarchy is defined by the tree list below:

  • Head
    • Trunk
      • Left Arm
        • Left Forearm
      • Right Arm
        • Right Forearm
      • Left Leg
        • Left Foreleg
      • Right Leg
        • Right Foreleg

Here is the code for building all parts of the body of Mr. StickMan:

C#
private void CreateStickFigure()
{
    ...

    head = new CircleSegment(10);
    head.TT.X = currentPoint.X;
    head.TT.Y = currentPoint.Y;
    head.RT.Angle = 90;
    head.VerticalAlignment = VerticalAlignment.Top;
    head.HorizontalAlignment = HorizontalAlignment.Left;

    trunk = new AxisSegment(20);

    leg1 = new AxisSegment(15);
    leg1.Margin = new Thickness(5, 0, 0, 0);

    leg2 = new AxisSegment(15);
    leg2.Margin = new Thickness(5, 0, 0, 0);

    foreleg1 = new AxisSegment(15);
    foreleg2 = new AxisSegment(15);
    arm1 = new AxisSegment(12);
    arm2 = new AxisSegment(12);
    forearm1 = new AxisSegment(12);
    forearm2 = new AxisSegment(12);

    ...

    leg1.AddChildElement(foreleg1, PivotPoint.P2, Layer.BackGround);
    leg2.AddChildElement(foreleg2, PivotPoint.P2, Layer.ForeGround);
    arm1.AddChildElement(forearm1, PivotPoint.P2, Layer.BackGround);
    arm2.AddChildElement(forearm2, PivotPoint.P2, Layer.ForeGround);

    trunk.AddChildElement(leg1, PivotPoint.P2, Layer.BackGround);
    trunk.AddChildElement(leg2, PivotPoint.P2, Layer.ForeGround);

    trunk.AddChildElement(arm1, PivotPoint.P1, Layer.BackGround);
    trunk.AddChildElement(arm2, PivotPoint.P1, Layer.ForeGround);

    head.AddChildElement(trunk, PivotPoint.P2, Layer.ForeGround);

    ...

    this.Children.Add(head);
}

Setting Up Chained Animations

Here is the heart of our stick figure animation. The SetAngleAnimations method exists inside the BaseSegment class, and defines a sequence of animations from a given array of predefined angles. All you have to do is pass to the method the name of the animation key, an array of angles (which will become the start angle and end angle for each stick member), and finally define whether the animation is continuous or not. Notice that for a given array of N angles, the method not only creates N - 1 animations, but also implements the Completed event of each animation, so that after any animation is completed, another animation is started:

C#
public void SetAngleAnimations(string key, int[] angles, bool repeatForever)
{
    List<DoubleAnimation> angleAnimationList;
    if (!angleAnimationDictionary.ContainsKey(key))
    {
        angleAnimationList = new List<DoubleAnimation>();
        angleAnimationDictionary.Add(key, angleAnimationList);
    }
    else
    {
        angleAnimationList = angleAnimationDictionary[key];
    }

    angleAnimationList.Clear();
    for (int i = 0; i < angles.Length - 1; i++)
    {
        DoubleAnimation da = new DoubleAnimation()
            {
                Name = "da" + i.ToString(),
                Duration = 
                  new Duration(new TimeSpan(0, 0, 0, 0, minAnimationDuration))
            };

        angleAnimationList.Add(da);
    }

    for (int i = 0; i < angleAnimationList.Count; i++)
    {
        angleAnimationList[i].From = angles[i];
        angleAnimationList[i].To = angles[i + 1];
        if (i < angleAnimationList.Count - 1)
        {
            angleAnimationList[i].Completed += (sender, e) =>
                {
                    var clock = sender as AnimationClock;
                    var animation = clock.Timeline as DoubleAnimation;
                    int nextIndex = Convert.ToInt32(
                       (animation.Name.Replace("da", ""))) + 1;
                    this.BeginAngleAnimation(angleAnimationList[nextIndex]);
                };
        }
        else
        {
            if (repeatForever)
            {
                angleAnimationList[i].Completed += (sender, e) =>
                {
                    var clock = sender as AnimationClock;
                    var animation = clock.Timeline as DoubleAnimation;
                    int nextIndex = 0;
                    this.BeginAngleAnimation(angleAnimationList[nextIndex]);
                };
            }
        }
    }
}

That being said, let's take a look at the SetupAngleAnimations method, which defines the animations for each member of Mr. StickMan's body:

C#
private void SetupAngleAnimations()
{
    leg1.SetAngleAnimations("walkToEast", 
       new int[] { MAX_LEG_ANGLE_WALK, MAX_LEG_ANGLE_WALK, 0, 
       -MAX_LEG_ANGLE_WALK, 0 }, false);
    leg2.SetAngleAnimations("walkToEast", 
       new int[] { -MAX_LEG_ANGLE_WALK, -MAX_LEG_ANGLE_WALK, 0, 
       MAX_LEG_ANGLE_WALK, 0 }, false);
    foreleg1.SetAngleAnimations("walkToEast", 
       new int[] { MIN_FORELEG_ANGLE_WALK, MAX_FORELEG_ANGLE_WALK, 0, 
       MIN_FORELEG_ANGLE_WALK, 0 }, false);
    foreleg2.SetAngleAnimations("walkToEast", 
       new int[] { MAX_FORELEG_ANGLE_WALK, MIN_FORELEG_ANGLE_WALK, 0, 
       MAX_FORELEG_ANGLE_WALK, 0 }, false);
    arm1.SetAngleAnimations("walkToEast", 
       new int[] { 0, -MAX_ARM_ANGLE_WALK, 0, MAX_ARM_ANGLE_WALK, 0 }, false);
    arm2.SetAngleAnimations("walkToEast", 
       new int[] { 0, MAX_ARM_ANGLE_WALK, 0, -MAX_ARM_ANGLE_WALK, 0 }, false);
    forearm1.SetAngleAnimations("walkToEast", 
       new int[] { 0, MIN_FOREARM_ANGLE_WALK, 0, MAX_FOREARM_ANGLE_WALK, 0 }, false);
    forearm2.SetAngleAnimations("walkToEast", 
       new int[] { 0, -MIN_FOREARM_ANGLE_WALK, 0, -MAX_FOREARM_ANGLE_WALK, 0 }, false);

    leg1.SetAngleAnimations("walkToWest", 
       new int[] { -MAX_LEG_ANGLE_WALK, -MAX_LEG_ANGLE_WALK, 0, 
       MAX_LEG_ANGLE_WALK, 0 }, false);
    leg2.SetAngleAnimations("walkToWest", 
       new int[] { MAX_LEG_ANGLE_WALK, MAX_LEG_ANGLE_WALK, 0, 
       -MAX_LEG_ANGLE_WALK, 0 }, false);
    foreleg1.SetAngleAnimations("walkToWest", 
       new int[] { -MIN_FORELEG_ANGLE_WALK, -MAX_FORELEG_ANGLE_WALK, 0, 
       -MIN_FORELEG_ANGLE_WALK, 0 }, false);
    foreleg2.SetAngleAnimations("walkToWest", 
      new int[] { -MAX_FORELEG_ANGLE_WALK, -MIN_FORELEG_ANGLE_WALK, 0, 
      -MAX_FORELEG_ANGLE_WALK, 0 }, false);
    arm1.SetAngleAnimations("walkToWest", 
       new int[] { 0, MAX_ARM_ANGLE_WALK, 0, -MAX_ARM_ANGLE_WALK, 0 }, false);
    arm2.SetAngleAnimations("walkToWest", 
       new int[] { 0, -MAX_ARM_ANGLE_WALK, 0, MAX_ARM_ANGLE_WALK, 0 }, false);
    forearm1.SetAngleAnimations("walkToEast", 
       new int[] { 0, -MIN_FOREARM_ANGLE_WALK, 0, -MAX_FOREARM_ANGLE_WALK, 0 }, false);
    forearm2.SetAngleAnimations("walkToEast", 
       new int[] { 0, MIN_FOREARM_ANGLE_WALK, 0, MAX_FOREARM_ANGLE_WALK, 0 }, false);

    leg2.SetAngleAnimations("kickToEast", new int[] { -15, -45, -90, -15, 0 }, false);
    foreleg2.SetAngleAnimations("kickToEast", new int[] { 0, 90, 15, 15, 0 }, false);
    arm1.SetAngleAnimations("kickToEast", new int[] { 0, 0, 0, 0, 0 }, false);
    arm2.SetAngleAnimations("kickToEast", new int[] { 0, MAX_ARM_ANGLE_WALK / 2, 
    MAX_ARM_ANGLE_WALK, MAX_ARM_ANGLE_WALK / 2, 0 }, false);
    forearm1.SetAngleAnimations("kickToEast", 
      new int[] { 0, -MAX_FOREARM_ANGLE_WALK * 2, -MAX_FOREARM_ANGLE_WALK * 2, 
      -MAX_FOREARM_ANGLE_WALK * 2, 0 }, false);
    forearm2.SetAngleAnimations("kickToEast", 
       new int[] { 0, MIN_FOREARM_ANGLE_WALK * 2, MIN_FOREARM_ANGLE_WALK * 2, 
       MIN_FOREARM_ANGLE_WALK * 2, 0 }, false);

    leg2.SetAngleAnimations("kickToWest", new int[] { 0, 0, 90, 15, 0 }, false);
    foreleg2.SetAngleAnimations("kickToWest", new int[] { 0, -90, -15, -15, 0 }, false);
    arm1.SetAngleAnimations("kickToWest", new int[] { 0, 0, 0, 0, 0 }, false);
    arm2.SetAngleAnimations("kickToWest", 
       new int[] { 0, -MAX_ARM_ANGLE_WALK / 2, -MAX_ARM_ANGLE_WALK, 
       -MAX_ARM_ANGLE_WALK / 2, 0 }, false);
    forearm1.SetAngleAnimations("kickToWest", 
       new int[] { 0, MAX_FOREARM_ANGLE_WALK * 2, MAX_FOREARM_ANGLE_WALK * 2, 
       MAX_FOREARM_ANGLE_WALK * 2, 0 }, false);
    forearm2.SetAngleAnimations("kickToWest", 
       new int[] { 0, -MIN_FOREARM_ANGLE_WALK * 2, -MIN_FOREARM_ANGLE_WALK * 2, 
       -MIN_FOREARM_ANGLE_WALK * 2, 0 }, false);
}

Notice what's being done here: The SetAngleAnimations method is doing all the boring task of defining the rotation animations for us and wiring up those animations with the corresponding stick figure members. Besides, you could create more animations just by adding more elements to the int[] array passed to the SetAngleAnimations method.

What's great about this technique is that you don't have to rotate or move each segment independently anymore - when you move or rotate a "parent" member (in the member hierarchy), all child members will move or rotate automatically! Then we no more have a bunch of pieces dropped on the screen, but a much more consistent "body". Although this is a stick figure animation, it could be easily modified to create structured body animations such as windmills, Ferris wheels, robotic arms, mechanical engines, and so on - sky is the limit!

So after we set up the classes, Mr. StickMan can easily walk around and kick with pretty little effort. I'm sure you could add more interesting movements, such as running, or even doing a "Roundhouse Kick" just like Chuck Norris.

Final Considerations

I'd like to thank you for the patience for reading the article, and I want to know what you think about the concepts presented here. I'm sure there are some improvements that can be made, so please give your feedback, especially if this article was useful for you in some way.

History

  • 2010-09-18: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer Alura Cursos Online
Brazil Brazil

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira21-Oct-10 0:25
mvaMarcelo Ricardo de Oliveira21-Oct-10 0:25 
GeneralMy vote of 5 Pin
Sherylee14-Oct-10 23:19
Sherylee14-Oct-10 23:19 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira15-Oct-10 4:20
mvaMarcelo Ricardo de Oliveira15-Oct-10 4:20 
GeneralMy vote of 5 Pin
JWhattam14-Oct-10 13:36
JWhattam14-Oct-10 13:36 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira15-Oct-10 4:18
mvaMarcelo Ricardo de Oliveira15-Oct-10 4:18 
GeneralVote of 5 Pin
Kelvin Armstrong14-Oct-10 1:07
Kelvin Armstrong14-Oct-10 1:07 
GeneralRe: Vote of 5 Pin
Marcelo Ricardo de Oliveira14-Oct-10 6:13
mvaMarcelo Ricardo de Oliveira14-Oct-10 6:13 
GeneralMy vote of 5 Pin
Leo5611-Oct-10 22:00
Leo5611-Oct-10 22:00 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira13-Oct-10 1:41
mvaMarcelo Ricardo de Oliveira13-Oct-10 1:41 
GeneralMy vote of 5 Pin
Abhinav S7-Oct-10 7:45
Abhinav S7-Oct-10 7:45 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira8-Oct-10 6:24
mvaMarcelo Ricardo de Oliveira8-Oct-10 6:24 
GeneralInteresting Pin
BillW3325-Sep-10 0:57
professionalBillW3325-Sep-10 0:57 
GeneralRe: Interesting Pin
Marcelo Ricardo de Oliveira25-Sep-10 4:51
mvaMarcelo Ricardo de Oliveira25-Sep-10 4:51 
GeneralMy vote of 5 Pin
sashidhar23-Sep-10 9:16
sashidhar23-Sep-10 9:16 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira24-Sep-10 1:37
mvaMarcelo Ricardo de Oliveira24-Sep-10 1:37 
GeneralMy vote of 5 Pin
Mass Nerder23-Sep-10 1:44
Mass Nerder23-Sep-10 1:44 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira23-Sep-10 7:26
mvaMarcelo Ricardo de Oliveira23-Sep-10 7:26 
GeneralMy vote of 5 Pin
Brij22-Sep-10 5:34
mentorBrij22-Sep-10 5:34 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira22-Sep-10 6:04
mvaMarcelo Ricardo de Oliveira22-Sep-10 6:04 
GeneralMy vote of 5 Pin
Jeroen Vonk22-Sep-10 2:36
Jeroen Vonk22-Sep-10 2:36 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira22-Sep-10 2:40
mvaMarcelo Ricardo de Oliveira22-Sep-10 2:40 
GeneralMy vote of 5 Pin
linuxjr21-Sep-10 10:44
professionallinuxjr21-Sep-10 10:44 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira21-Sep-10 10:55
mvaMarcelo Ricardo de Oliveira21-Sep-10 10:55 
GeneralNice Demo! Another 5. Pin
thompsons21-Sep-10 7:47
thompsons21-Sep-10 7:47 
GeneralRe: Nice Demo! Another 5. Pin
Marcelo Ricardo de Oliveira21-Sep-10 8:44
mvaMarcelo Ricardo de Oliveira21-Sep-10 8:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.