>
Blog
Book
Portfolio
Search

11/27/2008

95505 Views // 0 Comments // Not Rated

Silverlight Circle Animation - Animate Any UIElement Over A (NOT Bezier) Curve

The Problem

Animation in Silverlight certainly gets us (“us” being .NET developers) further than we’d ever thought we’d go in terms of sexiness on our web sites. With a few lines of XAML, and a few more lines of C#, I can do what all those Flash hippies have been doing for years. Unfortunately, WPF and Silverlight animation is missing something, and of course, it’s the first thing I wanted to play with.

This missing something is animation along a curve. Now by “missing” I’m not saying that it’s not there. Oh, it’s there. The way to animate along a curve is to establish your key frames as SplineDoubleKeyFrames and specify a Bezier curve for each. Bezier curves blow; they are hard to work with (going all the way back to Paint for Windows 3.11, which had Bezier curves that were impossible to get to look how you intended) and conceptually aren’t animatably intuitive.

For Bezier curves, you define two control points that essentially bend a line. But when I’m visualizing such a scenario, I want a concrete factor to define my curve, like an angle or a radius, not an abstract “control point” that somehow defines a motion. In fact, I want to think of my curve as an arc, which is ultimately part of a circle.

Circles make sense to me. “Control Points” do not.

Anyways, want I wanted to do was, given a start point and an end point on a canvas, animate any UIElement between them – not using standard linear interpolation – but rather on a curve. The “size” of this curve would be defined by an angle. For example, if I wanted to go from the “top” of a circle to the “bottom” my angle would be 180 degrees. Or if I wanted to go around a quarter of the way, I’d have a right angle (90 degrees).

The Answer

The right way to do it is to use the Bezier curve stuff with Splines and all of that. Adam Nathan, in WPF Unleashed, (best WPF book I’ve read) suggested to use Blend to design the animation, and then port the generated XAML back into Visual Studio. But please! Defer to a design tool? Don’t get me wrong: Blend is awesome, but I’m a developer. If Microsoft wants to continue to drive a wedge between these roles, then that’s fine; don’t burden me with a dependency on Blend!

So I wrote my own animation from scratch. Allow me to introduce: CircleAnimation (up on CodePlex)! The idea behind CircleAnimation is to visualize a circle defined by two points and an angle, and animate along the arc between these points, carved out by the angle. This is all wrapped up in a user control that you can place inside your Silverlight control, wire up the UIElement to animate, and populate all the properties, which are:

  • Start Point – where the animation starts (Point)
  • End Point – where the animation ends (Point)
  • Arc Angle – the angle that defines the “size” of the arc (Double)
  • Is Start To End – true to go “forward;” false to go “backward” (Bool)
  • Is Flipped – see “Math” section (Bool)
  • Duration – the amount of time, in seconds, the animation takes (Double)
  • Target – the Id of a UIElement to animate (String)
  • StartAnimationOnLoad - weather or not to animate when the control loads (Bool)

I also expose the underlying Storyboard object, so consumers of this control can manipulate the more advanced aspects of the animation, such as accelerations, repeat behavior, etc.

The Designer

But how do we know what angle to use? Do we have to guess at the start and end points? Of course not! CircleAnimation comes with what I call a run-time designer. Okay, so it’s just a bunch of stuff that is drawn when the “ShowDesigner” property of the control is set to true. However, it alleviates the guesswork by allowing you to design your animation right inside your user control. The designer even creates a line of XAML that you can dump back into your control when you’re done.

Among this “bunch of stuff” that’s drawn are draggable start and end points, the center point, and a nice gradient arc (green-to-red shows start-to-end) that’ll define the animation path. It has a slider to define your angle, and options to show IsFlipped and IsStartToEnd, so you can tweak the motion. Here’s what it looks like:

Circle Animation

The gray rectangle is what’s being animated. The green dot is the start point, and red dot (which is currently being dragged) is the end point. The three blank lines define a triangle formed by the start point, end point, and the center of our imagined circle. (These will come into play when I go over the math).

Other than dragging the start and end points and the lines, the box in the middle is your designer, containing all the controls you need to create an animation across any arc of any size in any direction; indeed, any curved animation you want! And as you change your curve, the textbox at the bottom of the box will update with the XAML needed to perform the animation.

So here’s the design experience, in a nutshell:

  1. Add a reference to the CircleAnimation assembly to your project.
  2. Add a namespace to your control, presumably called xmlns:CircleAnimation
  3. Have a canvas be the main parent control of your user control.
  4. (Note that only direct children of this canvas can be circle-animated.)
  5. Put the following “empty” circle animation control inside the canvas:

    <CircleAnimation:CircleAnimation x:Name=”ca” Canvas.ZIndex=”999” ShowDesigner=”True” Target=”[name of UIElement to animate]” />

    (The ZIndex is just to make sure that the designer is on top of everything else, otherwise it’ll be hard to drag and see what’s going on.)

  6. Run your app.
  7. Use the designer to create your animation arc.
  8. You can click on any UIElement that is a direct child of the canvas in step #3 and begin designing the animation for that object.
  9. Copy the contents of the XAML textbox.
  10. Close the app.
  11. Paste the XMAL back over your original CircleAnimation declaration. You might need to tweak some things, but now you’re done.  The generated XAML always sets ShowDesigner to false.
  12. Switch ShowDesigner back to true and repeat to change things around, or to create an additional CircleAnimation for another object.

That’s it! Have fun circularly animating!

PS - The Math

I know no one’s going to read this, but I just needed to prettily write it all out at least one time…if only to ensure that it made sense to me. This is the culmination of over 20 pages of scribbled algebra and trigonometry, not to mention answers to the high school math homework assignments I found on the web when searching for things like “Pythagorean Theorem” and “Distance Formula.”

Here’s a diagram that the rest of this discussion references:

Circle Animation Math

Given the start point (P1), the end point (P2), and the angle that carves out the arc angle (Φ), we can determine a circle that contains both points, and based on the angle, the arc of the animation. The idea is to visualize an isosceles triangle formed by the line connecting these two points, and two radii (r) of our circle. The line connecting these points is the baseline (b) for the triangle. Therefore, the apex point of the triangle is also the center of the circle (C).

In order to calculate (C), the first step is to get the radius (r) of the circle. Using the Distance Formula from (P1) to (P2), we can get the length of (b). Once we know (b), then (b/2) is the short side of a right triangle whose other two sides are a radius (r) of the circle, and the hypotenuse (a) (the altitude of our isosceles triangle). Taking advantage of the ever-popular SOH-COA-TOA method, we can set:

(r) = (b) / [2 * sin (Φ/2)]

And solve for (r). (We get (Φ/2) since (a) is the altitude of the triangle, and thus its perpendicular bisector, slicing the apex angle in half.)

Knowing (b/2), and now (r), we use the Pythagorean Theorem to solve for (a). With the altitude (a) of the isosceles triangle known, we can now go after the apex point (C). We’ll need (M), which is the midpoint of (b), gotten via the Midpoint Formula. The following equation, taking in (M), (a), (P1), (P2), and (b), gives us the two possible values for the coordinates of the apex point (C), again, which is the center of the circle:

x4 = x3 – [(a) * (y2 – y1) / (b)]

y4 = y3 + [(a) * (x2 – x1) / (b)]

or

x4 = x3 + [(a) * (y2 –y1) /(b)]

y4 = y3 - [(a) * (x2 – x1) / (b)]

The reason we have two possible values is because the center of the triangle can point either “up” or “down.” This is due to solving the above equation (which I’ve simplified from a more complicated form) having involved taking the square root of both sides, which leaves us with a +/- in front of the result. The one you want depends on how you draw the angle, which is ultimately a parameter to the calculation.

When you work with ArcSegments in WPF / Silverlight, there is a concept of small arc vs. large arc. This is due to the fact that the input angle is generally between 0 and 180 degrees (if it’s greater than 180, you need to set an additional property, which is lame). In CircleAnimation, the angle can be between 0 and 360 (exclusive), so “large arc” and “small arc” don’t really apply; angles greater than 180 degrees will always be “large.”

Therefore, there’s just one property called “IsFlipped.” IsFlipped just means “go the other way.” The following example has IsFliped set to true (and an angle greater than 180 degrees):

Circle Animation Is Flipped True Greater Than 180 Degrees

And this one has it false:

Circle Animation Is Flipped False Greater Than 180 Degrees

But if this angle is less than 180, you’ll see this:

Circle Animation Is Flipped True Less Than 180 Degrees

Verses this:

Circle Animation Is Flipped False Less Than 180 Degrees

For the same values of IsFlipped as in the first two examples.

Back to math: now we can get (Z), or the zero point of the circle, by translating (C) down the x-axis the length of the radius.

x5 = x4 + (r)

y5 = y4

(Z) is important, since it defines the where arc angles start, i.e. where “zero degrees” is. With this orientation, for example, we now know that if (P1) was at (Z) and (P2) was at (x4, y4 + (r)), the arc we need to animate over is -90 degrees (since SilverLight goes from 0 to 360 degrees clockwise).

With (Z), we can now go after (Θ), which is our start angle. This is crucial, since we need to know the angle between (Z)’s radius and the radius between (C) and the first point of our animation. Then we “animate” over every angle from this point (P1) to the end point (P2), increasing (or decreasing, if we’re going end-to-start) this angle by one degree (or finer, for smoother animations).

To get (Θ), we first need (d), the distance between (P1) and (Z), gotten via the Distance Formula. With (d), we have another triangle, with its two other sides at (r). Knowing all three sides of the triangle, we can use the Law of Cosines to calculate (Θ). Here’s the equation:

(Θ) = arccos { [ (d)^2 – 2 * (r)^2 ] / [ -2 * (r)^2] } / π

(In all these trigonometric functions, keep in mind that .NET deals with radians, so I had to do some conversions. One degree is roughly equal to 0.0174532925 radians.)

Now that we know (Θ) and (r), we should be able to calculate what (P1) should be. (P1), the starting point of the animation, can now be represented by:

x1 = (r) * cos ((Θ)) + x4

y1 = (r) * sin ((Θ)) + y4

And then, any point along the animation for some n between (Θ) and (Φ) would be:

xn = (r) * cos ((Θ) + n) + x4

yn = (r) * sin ((Θ) + n) + y4

And so on, for (Φ) more degrees (until n equals (Φ)). In other words, if we iterated over (Θ) to (Φ), then the end point, (P2), would be:

x2 = (r) * cos (Φ) + x4

y2 = (r) * sin (Φ) + y4

We need to tack on the coordinates of the center point, (C), to respect the origin of the circle. Now there’s just one problem: what if, starting from (Z) and moving along the circle, we hit (P2) before (P1)? Well, it almost doesn’t matter. Instead of redrawing the figure above to work for both cases, I stumbled onto a short cut.

What I do, as soon as I have (Θ), is pass this to the equation above for (P1) – rounding to one decimal point. If the result of this isn’t (P1), then all I do to fix it is flip (P1) and (P2), and multiply (Θ) by -1! This basically makes everything work backwards, but conceptually is valid and equivalent. The only ramification is that, based on the different combinations of weather the angle is greater than 180 degrees or “IsFlipped” is true, change what “start-to-end” means.

In other words, if I flip my start and end points, then an animation going from start-to-end is really going from end-to-start. I know this sounds like a hack, but I don’t change anything in the UI; it’s all in the trig, and all ends up working out the same. This actually does it for the math, since we now have the means to calculate, in order, every coordinate on the circle that the object we are animating will need to hit.

All I do then is actually iterate from (Θ) to (Φ), or, more accurately, (Φ) times starting from (Θ). I build two collections of DoubleAnimationUsingKeyFrames (one for the Canvas.Top (y-coordinate) and one for the Canvas.Left (x-coordinate) property of the object to animate). The rest is straight Silverlight animation.

There are some interesting things going on, however. For example, based on the direction of travel and the size of the angle, sometimes I need to be subtracting degrees from (Θ) instead of adding. Also, for some reason, setting each key frame’s time to KeyTime.Uniform blew up. Therefore, based on the duration and the number of angles I am animating, I needed to create a time slice and increment the time span of each key frame by it.

That's it! Again: get all of the source code up on CodePlex. Have fun circularly animating!

No Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...