Rick Barraza
Silverlight UX Development
Thursday December 13, 2007
DD#03: Custom Animation Loops
Sorry for the delay in posts, we were offline competing in the Microsoft PhizzPop L.A. challenge all last week. The good news is we won, so we're off to SXSW in March to compete in the Finals!
I'm a big fan of creating your own custom animations and have been pretty vocal about it in both the Flex space and now in Silverlight/WPF. So today we'll look at a basic custom animation process I've been using and explore a little bit the 3D logic in the animation loop.
Here’s what we’re building today:
Instructions: No interaction required. Just sit back and enjoy :)
We'll start off as we normally do, with an empty canvas in our in main Page.XAML to hold the elements we'll be attaching in code:
<Canvas x:Name="nodes" Width="480" Height="480" Canvas.Left="11" Canvas.Top="11" >
<Canvas.Clip>
<RectangleGeometry Rect="0, 0, 480, 480"/>
</Canvas.Clip>
</Canvas>
We'll also have a custom user control, named nodeName.xaml, that looks like this:
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100"
Height="20"
>
<TextBlock Foreground="#FFFFFF" Width="100" Height="20" Text="silverlight" FontFamily="Arial"
FontSize="9" RenderTransformOrigin="0,0">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="scale" ScaleX="1" ScaleY="1"/>
<RotateTransform x:Name="rotate" Angle="-60"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Canvas>
And has a code behind file that looks like this:
namespace SilverlightDirtyDozen
{
public class nodeName : Control
{
private double _FibX;
private double _FibY;
private double _objDepth;
private double _perspective_ratio;
private double _rotation;
public double FibX
{
get { return _FibX; }
set { _FibX = value; }
}
public double FibY
{
get { return _FibY; }
set { _FibY = value; }
}
public double objDepth
{
get { return _objDepth; }
set { _objDepth = value; }
}
public double perspective_ratio
{
get { return _perspective_ratio; }
set { _perspective_ratio = value; }
}
private ScaleTransform st;
private RotateTransform rt;
FrameworkElement lroot;
public double rotation
{
get { return rt.Angle; }
set { rt.Angle = value; }
}
public double scale
{
get { return st.ScaleX; }
set { st.ScaleX = value; st.ScaleY = st.ScaleX; }
}
public nodeName()
{
System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream("SilverlightDirtyDozen.nodeName.xaml");
lroot = this.InitializeFromXaml(new System.IO.StreamReader(s).ReadToEnd());
st = lroot.FindName("scale") as ScaleTransform;
rt = lroot.FindName("rotate") as RotateTransform;
}
}
}
The most important thing to note is that I'm using two named transforms ('scale' and 'rotate') in the XAML and grabbing a reference to them in the code behind file to manipulate them through code.
Those are the only additional elements we need. Everything else will be handled in the main code behind file of Page.xaml.
I should probably also apologize right now for getting a little math geeky on the actual animation engine since I was bored and wanted to port over a basic ActionScript 1.0 3D engine to C#/Silverlight. So, lets just jump into it...
We'll break this into three parts; the initialization, the setup, and the looper. Here is the initialization stuff:
public partial class dd03 : Canvas
{
double gRatio = 1 / 1.618033989;
double gAngle;
double rad = 50;
double rGrowth = 1.01;
double toRadians = Math.PI / 180;
double depth = 1000;
Point origin = new Point(240, 240);
int totalNames = 50;
int screenDepth = 150;
int z_axis_rotation = 1;
int y_axis_rotation = 0;
int x_axis_rotation = 0;
double convertToRadians = Math.PI / 180;
double rot = 0;
Storyboard looper;
int counter = 0;
public void Page_Loaded(object o, EventArgs e)
{
// Required to initialize variables
InitializeComponent();
looper = new Storyboard();
looper.SetValue<string>(Storyboard.NameProperty, "looper");
this.Resources.Add(looper);
looper.Completed += new EventHandler(looper_Completed);
setupNodes();
}
Again, creating an animation loop is very easy, faking 3D is the messy part. Most of the variables are for creating a 3D environment and placing nodes in a Fibonacci sunflower pattern, not creating a dynamic animation engine. To create a basic engine, you just need to create a Storyboard object (named looper in this code), initialize it, name it, and add it to the page resources. Once we call looper.Begin(), it will play once (at the established framerate, which is why I use a Storyboard instead of a Timer event) and then call it's Completed event Handler. All you gotta do is tell the looper_Completed event handler to looper.Begin() again, and you have yourself an infinite loop that will play at the correct frame rate. We'll talk about kill switches in a subsequent Dirty Dozen, if you want to not leak processor so egregiously or use a custom drag drop animation with drag or elastic..
So before telling our Storyboard object named looper to begin, you'll notice we first setup our elements on our empty node canvas in the setupNodes() function:
private void setupNodes()
{
gAngle = 360 - (360 * gRatio);
for (var x = 0; x < totalNames; x++)
{
nodeName temp = new nodeName();
temp.SetValue<string>(NameProperty, "node_" + x.ToString());
getLoc(temp);
temp.objDepth = -x;
temp.perspective_ratio = screenDepth / (screenDepth - x);
nodes.Children.Add(temp);
}
looper.Begin();
}
private void getLoc(nodeName passObj)
{
rot = rot + gAngle;
rad *= rGrowth;
passObj.FibX = Math.Cos(rot*toRadians)*rad;
passObj.FibY = Math.Sin(rot*toRadians)*rad;
passObj.rotation = rot;
}
Note that these two functions only create the user controls but don't set any rendering values on them, only logical properties we will use to calculate and render later (such as an objDepth, perspective_ratio, FibX, FibY, rotation, etc.) All the rendering changes and placement occur in the looper_completed function, which finally gets started as the last line of setupNodes().
Here is what that function looks like. Remember, it's the Storyboard's completed event handler, so by telling itself to begin again as the last line of code in its function, the function will always loop at the established framerate of your Silverlight movie:
void looper_Completed(object sender, EventArgs e)
{
// this is some fast, ugly code to change the rotations
// after a couple seconds.
if (counter > -1)
{
if (counter > 400)
{
z_axis_rotation = 2;
x_axis_rotation = 1;
counter = -2;
}
counter++;
}
// Here is all the trig for 3D. Never changes between projects...
double sin_x = Math.Sin(x_axis_rotation * convertToRadians);
double cos_x = Math.Cos(x_axis_rotation * convertToRadians);
double sin_y = Math.Sin(y_axis_rotation * convertToRadians);
double cos_y = Math.Cos(y_axis_rotation * convertToRadians);
double sin_z = Math.Sin(z_axis_rotation * convertToRadians);
double cos_z = Math.Cos(z_axis_rotation * convertToRadians);
double left = 0;
double top = 0;
// loop through every node and set their values based on their
// logical position using the trig values from above.
foreach (nodeName n in nodes.Children)
{
// these equations never change between projects either...
double rotatedY = (n.FibY * cos_x) - (n.objDepth * sin_x);
double rotatedDepth = (n.objDepth * cos_x) + (n.FibY * sin_x);
n.FibY = rotatedY;
n.objDepth = rotatedDepth;
double rotatedX = (n.FibX * cos_y) - (n.objDepth * sin_y);
rotatedDepth = (n.objDepth * cos_y) + (n.FibX * sin_y);
n.FibX = rotatedX;
n.objDepth = rotatedDepth;
rotatedX = (n.FibX * cos_z) - (n.FibY * sin_z);
rotatedY = (n.FibY * cos_z) + (n.FibX * sin_z);
// Now we start applying the calculated values with our objects
n.FibX = rotatedX;
n.FibY = rotatedY;
n.perspective_ratio = screenDepth / (screenDepth + n.objDepth);
n.scale = n.perspective_ratio;
n.Opacity = n.perspective_ratio;
n.SetValue<double>(Canvas.ZIndexProperty, n.objDepth);
left = (n.FibX * n.perspective_ratio) + origin.X;
top = (n.FibY * n.perspective_ratio) + origin.Y;
n.SetValue<double>(Canvas.TopProperty, top);
n.SetValue<double>(Canvas.LeftProperty, left);
}
looper.Begin();
}
I'm not going to lie to you. If you haven't played with Trig since High School or been tinkering in Flash, this may be a bit of math to take in, but once you do it a couple times it starts sinking in no problem. I could have just as easily only done this in the loop:
void looper_Completed(object sender, EventArgs e)
{
foreach (nodeName n in nodes.Children)
{
double left = Convert.toDouble(n.getValue(Canvas.LeftProperty)) + 1;
n.setValue<double>(Canvas.LeftProperty, left);
}
looper.Begin();
}
If I just wanted each node to move off the screen to the right, but that experience would have been hilariously boring. The 3D Fibonacci ring is a lot more interesting, so I thought I would share the whole shebang with you.
In summary then, ( after taking all the 3D and Fibonacci math out) to create a custom animation loop:
1. Create a Storyboard variable.
2. In your startup code, initialize it
3. Then name it
4. Add a Completed Event Handler to it (with all your goodie code in there)
5. Add the storyboard to your this.Resources()
6. Call storyboardname.Begin() when you want the engine to start running.
7. To keep the storyboard looping, add another storyboardname.Begin() at
the end of the Completed event handler.
We'll take a little break from animations in the next Dirty Dozen and look at importing and working with custom fonts. Until then, SLapp happy.






