Introduction
Since Avalon is available, I am impressive by the power of the visual effects such as animations and transformations, orchestrated by storyboards. But the integration of 3-D models in our business applications fascinate myself. Until now it's often a privilege of the video games, the 3-D effects make UI more rich and provide a user experience completely inovative.
However 3-D modelisations, animations, such as rotation transformations for example, need more specific skills. I also noticed a clear increase in the ressources when I apply a visual element on a 3-D model. I think that this powerful feature cannot be used to design real and rich applications with this actual version (I'm coding with the CTP Nov. 2005). Did you see the Healthcare application ? It's awesome! If not, you'd better go to Channel9 right now!
I also noticed that with a few of tricks, it's possible to design controls presenting items with a 3-D appearance by leaning on optical effects.
To illustrate this concept, I will consider items which turn following the path of an ellipse. The item in the first plan is the current selected item.

The illusion of 3-D depth is realized by the translation of the items on the ellipse and by the scale transformation of the items during the animation.
I enjoyed developing a control where elements are ellipse objects with a different color which move according to the path of an ellipse (not visible in the case).

Let's see the code.
Defining items
First I define each element, placed on a canvas. In order to simplify the code, I decided to use simple Ellipse object to represent items on the path.
<Canvas Name="_itemsContainer">
<Ellipse Name="_item1" Width="10" Height="10" Fill="Yellow" />
...
</Canvas>
You can already notice that I don't set the initial absolute position of each element because it needs to be determined according to the number of elements with an equal distance between them.
Defining the animation using the path of the ellipse
Each element is moved by following the path of the ellipse and is animated by a storyboard. WPF provides the timeline DoubleAnimationUsingPath that enables to set an object of PathGeometry type. I set the FillBehavior property to HoldEnd in order to be able to control the execution of the storyboard, as descibes further.
<Storyboard>
<ParalleleTimeline>
<DoubleAnimationUsingPath Duration="0:0:4" FillBehavior="HoldEnd" RepeatBehavior="Forever" Storyboard.TargetProperty="(Canvas.Left)" Source="X" />
<DoubleAnimationUsingPath Duration="0:0:4" FillBehavior="HoldEnd" RepeatBehavior="Forever" Storyboard.TargetProperty="(Canvas.Top)" Source="Y" />
</ParalleleTimeline>
</Storyboard>
The first difficulty is that DoubleAnimationUsingPath provides the PathGeometry DependencyProperty which can be set with a SVG instruction corresponding to the attended shape. Unfortunatly, my memories of math are so far and I don't remember the formula to create an ellipse :)! The only solution consists in setting data to create an ellipse either using two beziers segments or two arcs (i.e. PathGeometry="M0,0 C...". Furthermore, I posted another suggestion on the Microsoft Product Feedback website, just right here.
Another solution consists in creating an EllipseGeometry object (for example in a ResourceDictionary) and applying it to the Timeline by code.
[Xaml]
<EllipseGeometry x:Key="Ellipse_Template" RadiusX="125" RadiusY="20" />
[C#]
DoubleAnimationUsingPath __animX = (DoubleAnimationUsingPath)(__storyboard.Children[0] as ParallelTimeline).Children[0];
DoubleAnimationUsingPath __animY = (DoubleAnimationUsingPath)(__storyboard.Children[0] as ParallelTimeline).Children[1];
__animX.PathGeometry = new PathGeometry();
__animY.PathGeometry = new PathGeometry();
__animX.PathGeometry.AddGeometry((EllipseGeometry)this.Resources["Ellipse_Template"]);
__animY.PathGeometry.AddGeometry((EllipseGeometry)this.Resources["Ellipse_Template"]);
Bonus: it's easy to add a RotateTransform to the ellipse rendering. It's more fun...
<EllipseGeometry x:Key="Ellipse_Template" RadiusX="125" RadiusY="20">
<EllipseGeometry.Transform>
<RotateTransform Angle="25" />
</EllipseGeometry.Transform>
</EllipseGeometry>

Defining the scale transformation
The rendering and so the scale of the elements can be easily transformed by using the DoubleAnimationUsingKeyFrames timeline object. Indeed, it enables to modify the value of a DependencyProperty by intercepting a position of time during the animation. Notice that the more number of positions will be important, the more transformation will be fluid.
<
Storyboard>
...
<ParalleleTimeline>
<DoubleAnimationUsingKeyFrames Duration="0:0:4" Storyboard.TargetProperty="Width" RepeatBehavior="Forever">
<LinearDoubleKeyFrame KeyTime="6.25%" Value="11.25" />
<LinearDoubleKeyFrame KeyTime="12.5%" Value="12.5" />
<LinearDoubleKeyFrame KeyTime="25%" Value="20" />
<LinearDoubleKeyFrame KeyTime="37.5%" Value="12.5" />
<LinearDoubleKeyFrame KeyTime="43.75%" Value="11.25" />
<LinearDoubleKeyFrame KeyTime="50%" Value="10" />
<LinearDoubleKeyFrame KeyTime="56.25%" Value="8.75" />
<LinearDoubleKeyFrame KeyTime="62.5%" Value="7.5" />
<LinearDoubleKeyFrame KeyTime="75%" Value="5" />
<LinearDoubleKeyFrame KeyTime="87.5%" Value="7.5" />
<LinearDoubleKeyFrame KeyTime="93.75%" Value="8.75" />
<LinearDoubleKeyFrame KeyTime="100%" Value="10" />
</DoubleAnimationUsingKeyFrames>
</ParalleleTimeline>
<ParalleleTimeline>
<DoubleAnimationUsingKeyFrames Duration="0:0:4" Storyboard.TargetProperty="Height" RepeatBehavior="Forever">
<LinearDoubleKeyFrame KeyTime="6.25%" Value="11.25" />
<LinearDoubleKeyFrame KeyTime="12.5%" Value="12.5" />
<LinearDoubleKeyFrame KeyTime="25%" Value="20" />
<LinearDoubleKeyFrame KeyTime="37.5%" Value="12.5" />
<LinearDoubleKeyFrame KeyTime="43.75%" Value="11.25" />
<LinearDoubleKeyFrame KeyTime="50%" Value="10" />
<LinearDoubleKeyFrame KeyTime="56.25%" Value="8.75" />
<LinearDoubleKeyFrame KeyTime="62.5%" Value="7.5" />
<LinearDoubleKeyFrame KeyTime="75%" Value="5" />
<LinearDoubleKeyFrame KeyTime="87.5%" Value="7.5" />
<LinearDoubleKeyFrame KeyTime="93.75%" Value="8.75" />
<LinearDoubleKeyFrame KeyTime="100%" Value="10" />
</DoubleAnimationUsingKeyFrames>
</ParalleleTimeline>
</Storyboard>
Another solution consists in using the RenderTransform.ScaleTransform property, however in this simple case, transform the size of elements uses less ressources.
Defining the initial position around the ellipse
Now it remains the position of each element to be defined along the path of the ellipse. I simply call the GetPointAtFractionLength method of the PathGeometry object. Thanks to Mark Grinols for this information. As you can see below, this method returns two Point objects as output parameter. The first Point will provide the x and y coordinates of the element.
[Metadata]
public void GetPointAtFractionLength (
double progress,
out Point point,
out Point tangent
)
[C#]
double __ratio = 1.0 / _itemsContainer.Children.Count;
Point
__point1 = new Point();
Point __point2 = new Point();
DoubleAnimationUsingPath __timeline = (__storyboard.Children[0] as ParallelTimeline).Children[0] as DoubleAnimationUsingPath;
__timeline.PathGeometry.GetPointAtFractionLength(__ratio, out __point1, out __point2);
Canvas
.SetLeft(_item, __point1.X);
Canvas.SetTop(_item, __point1.Y);
Refreshing the animation
To finish, I add the necessary code to start the animations when any item is clicked.
So the position of each element must be refreshed but don't forget that the animations must be paused when an item become the current, and so the selected element. To control elements during the animation, I just need to intercept the CurrentTimeInvalidated event of the storyboards. To easily retrieve items and their associated storyboard, I've created an AnimElement class that contains the definition of each item (this class will be replaced by an embedded Definiton class).
__storyboard.CurrentTimeInvalidated += delegate(object sender, EventArgs e)
{
if (_selectedItem == null)
{
AnimElement __element = _transforms[((sender as ClockGroup).Timeline as Storyboard)];
if (_selectedIndex != __element.Index && Math.Round(__element.ObjectContext.Width) == 20 && Math.Round(__element.ObjectContext.Height) == 20)
{
_selectedIndex = __element.Index;
Dictionary<Storyboard, AnimElement>.ValueCollection.Enumerator __enumerator = _transforms.Values.GetEnumerator();
while (__enumerator.MoveNext())
__enumerator.Current.Storyboard.Pause(__enumerator.Current.ObjectContext);
}
}
else if (Math.Round(_selectedItem.Width) == 20 && Math.Round(_selectedItem.Height) == 20)
{
Dictionary<Storyboard, AnimElement>.ValueCollection.Enumerator __enumerator = _transforms.Values.GetEnumerator();
while (__enumerator.MoveNext())
__enumerator.Current.Storyboard.Pause(__enumerator.Current.ObjectContext);
}
}
};
I would like to warn you that you can see an unattended visual effect during the animation. Indeed, it would seem that when an element is moving along the path and is arriving on one extremity, the animation is speeding up. However, the elements are placed according to an equal distance.
In a next post, I will apply these principles to create a reusable and customizable control.
Download the source code (WinFX CTP Nov. 2005)