Click here to Skip to main content
Click here to Skip to main content

Stick Figure Animation Using C# and WPF

By , 14 Oct 2010
 
Prize winner in Competition "Best C# article of September 2010"
Prize winner in Competition "Best overall article of September 2010"

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.

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:

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.

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:

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.

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:

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:

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:

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)

About the Author

Marcelo Ricardo de Oliveira
Software Developer
Brazil Brazil
Member
Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, and works for Curso de Ingles Online.
 
He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
 
Published Windows Phone apps:
 
 
Awards:
 
CodeProject MVP 2012
CodeProject MVP 2011
 
Best Web Dev article of May 2012
Best Mobile article of January 2012
Best Mobile article of December 2011
Best Mobile article of October 2011
Best Web Dev article of September 2011
Best Web Dev article of August 2011
HTML5 / CSS3 Competition - Second Prize
Best ASP.NET article of June 2011
Best ASP.NET article of May 2011
Best ASP.NET article of April 2011
Best C# article of November 2010
Best overall article of November 2010
Best C# article of October 2010
Best C# article of September 2010
Best overall article of September 2010
Best overall article of February 2010
Best C# article of November 2009

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5mvpMika Wendelius25 Sep '12 - 18:58 
QuestionVery nicememberCIDev19 Sep '12 - 20:00 
AnswerRe: Very nicemvpMarcelo Ricardo de Oliveira22 Sep '12 - 12:50 
GeneralMy Vote of 5memberRaviRanjankr18 Jul '11 - 4:49 
GeneralRe: My Vote of 5mvpMarcelo Ricardo de Oliveira18 Jul '11 - 6:19 
GeneralMy vote of 5memberpophelix2 Apr '11 - 20:55 
GeneralRe: My vote of 5mvpMarcelo Ricardo de Oliveira7 Apr '11 - 4:44 
GeneralMy vote of 5memberMSHAO9 Dec '10 - 13:30 
GeneralRe: My vote of 5memberMarcelo Ricardo de Oliveira11 Dec '10 - 6:49 
GeneralMy vote of 5 alsomemberRichard Weselny25 Nov '10 - 2:31 
GeneralRe: My vote of 5 alsomemberMarcelo Ricardo de Oliveira25 Nov '10 - 9:48 
GeneralMy vote of 5memberRichard Weselny25 Nov '10 - 2:31 
GeneralMy vote of 5memberBaesky27 Oct '10 - 21:10 
GeneralRe: My vote of 5memberMarcelo Ricardo de Oliveira28 Oct '10 - 2:02 
GeneralMy vote of 5memberAndre' Gardiner27 Oct '10 - 12:11 
GeneralRe: My vote of 5memberMarcelo Ricardo de Oliveira27 Oct '10 - 21:08 
GeneralMy vote of 5mvpAbhijit Jana26 Oct '10 - 7:49 
GeneralRe: My vote of 5memberMarcelo Ricardo de Oliveira26 Oct '10 - 13:09 
GeneralCongrats!mvpAl-Farooque Shubho26 Oct '10 - 0:52 
GeneralRe: Congrats!memberMarcelo Ricardo de Oliveira26 Oct '10 - 3:52 
GeneralMuito bom (very good)memberantoniohlopes25 Oct '10 - 15:13 
GeneralRe: Muito bom (very good)memberMarcelo Ricardo de Oliveira26 Oct '10 - 0:23 
GeneralThe whole Brazilian coders community is proud of you!!memberGerson Freire25 Oct '10 - 11:13 
GeneralRe: The whole Brazilian coders community is proud of you!!memberMarcelo Ricardo de Oliveira25 Oct '10 - 12:46 
GeneralMy vote of 5mvpJosh Fischer20 Oct '10 - 6:09 

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 14 Oct 2010
Article Copyright 2010 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid