Table of Contents
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.
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:
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 ColumnDefinition
s 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.
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 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 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);
}
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);
}
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);
}
Now we have all the body segments. The segment hierarchy is defined by the tree list below:
- Head
- Trunk
- Left Arm
- Right Arm
- Left Leg
- Right Leg
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);
}
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.
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.