Introduction
It’s fairly easy to create and animate a graphical primitive, say by moving it from point A to point B at constant speed. But what if you need to position several graphical objects in a particular arrangement and then animate them in a non-linear fashion? Neither Silverlight nor WPF has built-in functions for this. In this article, I’ll demonstrate ways in which one can create objects and animations dynamically using lambda-delegates and higher-order functions.
By the way, you really should check out the sample project - the animation is (IMHO) quite impressive, and is an illustration of how 2D animation can appear 3D-ish. People have been doing this for ages, of course, but I for one am surprised at how easy it is to do.
Generation
Let us suppose that you have decided to create something like the following:
Theoretically, you could just use a for
loop, but seeing how there’s a possibility of doing it in a much cleaner fashion, why not make use of it? Let’s start with something simple – a set of circles is clearly a collection, so let’s create a class that will hold references to these objects:
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
public LambdaCollection(int count) { while (count --> 0) Add(new T()); }
⋮
}
So far we’re keeping things simple – all we’ve done is define a collection which is constrained in terms of what it contains (only objects that derive from DependencyObject
and have a default constructor). We have added a constructor that makes the needed number of objects. But here comes the interesting part: we now add a method which can initialize the properties of the contained T
objects using lambda expressions:
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
⋮
public LambdaCollection<T> WithProperty<U>
(DependencyProperty property, Func<int, U> generator)
{
for (int i = 0; i < Count; ++i)
this[i].SetValue(property, generator(i));
return this;
}
}
Let’s pause and take a look at what is going on. Firstly, you’ll notice this is a fluent interface, seeing how the method ends with return this
. The method itself takes two parameters. The first happens to be the property we want to change in all elements of the collection. The second is a reference to a value generator, i.e. to a function which takes the element index in the collection and returns a value of type U
. This type can be anything – the only requirement is that it matches the property being set.
Attention: There is no automatic type conversion here, so if the property is of type double
you cannot generate a value of type int
– you’ll get an exception.
So how can we use this code? It happens to be remarkably easy. For example, to create ten circles of increasing sizes, we write the following:
var circles = new LambdaCollection<Ellipse>(10)
.WithProperty(WidthProperty, i => 1.5 * (i+1))
.WithProperty(HeightProperty, i => 1.5 * (i+1));
Such an expression lets us make the diameter dependent on element position. In our case, it will be 1.5 pixels for the smallest element and 15 for the largest. And, as you can see in code, one can vary width and height independently.
Well, seeing how the manipulation of X and Y co-ordinates is such a common task, we can write a useful method that will simplify things even more:
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
⋮
public LambdaCollection<T> WithXY<U>(Func<int, U> xGenerator, Func<int, U> yGenerator)
{
for (int i = 0; i < Count; ++i)
{
this[i].SetValue(Canvas.LeftProperty, xGenerator(i));
this[i].SetValue(Canvas.TopProperty, yGenerator(i));
}
return this;
}
}
Now, let’s put it all together and create that image that we showed at the beginning of the article:
int count = 20;
var circles = new LambdaCollection<Ellipse>(count)
.WithXY(i => 100.0 + (4.0 * i * Math.Sin(i / 4.0 * (Math.PI))),
i => 100.0 + (4.0 * i * Math.Cos(i / 4.0 * (Math.PI))))
.WithProperty(WidthProperty, i => 1.5 * i)
.WithProperty(HeightProperty, i => 1.5 * i)
.WithProperty(Shape.FillProperty, i => new SolidColorBrush(
Color.FromArgb(255, 0, 0, (byte)(255 - (byte)(12.5 * i)))));
foreach (var circle in circles)
MyCanvas.Children.Add(circle);
That’s it – using a pair of methods, one can easily create various “constellations”. Now let’s look at animation.
Animation
Linear animation using DoubleAnimation
is boring. It is much more interesting when we ourselves control element values. It’s actually quite easy – by taking an existing animation class, we can redefine its animated ‘tick’ value so that it is controlled by our own generator:
public class LambdaDoubleAnimation : DoubleAnimation
{
public Func<double, double> ValueGenerator { get; set; }
protected override double GetCurrentValueCore
(double origin, double dst, AnimationClock clock)
{
return ValueGenerator(base.GetCurrentValueCore(origin, dst, clock));
}
}
Now we have a class that does linear interpolation for us, and we in turn can get a transformed value and do something with it.
Seeing how we’re working with collections, it would once again be useful to define a collection class for our purposes. Here’s such a class:
public class LambdaDoubleAnimationCollection : Collection<LambdaDoubleAnimation>
{
⋮
public LambdaDoubleAnimationCollection
(int count, Func<int, double> from, Func<int, double> to,
Func<int, Duration> duration, Func<int, Func<double, double>> valueGenerator)
{
for (int i = 0; i < count; ++i)
{
var lda = new LambdaDoubleAnimation
{
From = from(i),
To = to(i),
Duration = duration(i),
ValueGenerator = valueGenerator(i)
};
Add(lda);
}
}
public void BeginApplyAnimation(UIElement [] targets, DependencyProperty property)
{
for (int i = 0; i < Count; ++i)
targets[i].BeginAnimation(property, Items[i]);
}
}
In actual fact, it is beneficial to have several constructors here (or a constructor with many optional parameters). The parameters here are value generators, i.e., these parameters can be derived from the element position in the collection. The valueGenerator
parameter expects a 2nd-order function or a “function which is a function generator”, i.e., a generator which depends on the collection index and whose value depends on the interpolated double
value during the animation. In the C# programming language, this implies the use of a “double lambda” such as e.g. i => j => f(j)
.
Here is a small example of an animation that unrolls our spiral into a sine wave:
var c = new LambdaDoubleAnimationCollection(
circles.Count,
i => Canvas.GetLeft(circles[i]),
i => 10.0 * i,
i => new Duration(TimeSpan.FromSeconds(2)),
i => j => 100.0 / j);
c.BeginApplyAnimation(circles.Cast<UIElement>().ToArray(), Canvas.LeftProperty);
I can’t show the animation itself, but here’s a view of the end result:
Extensions
Extending this mini-framework is easy. For example, if you want elements to be animated in sequence instead of in parallel, you can just change the LambdaDoubleAnimationCollection
to the following:
public class LambdaDoubleAnimationCollection : Collection<LambdaDoubleAnimation>
{
⋮
public void BeginApplyAnimation(UIElement [] targets, DependencyProperty property)
{
for (int i = 0; i < Count; ++i)
{
Items[i].BeginTime = new TimeSpan(0);
targets[i].BeginAnimation(property, Items[i]);
}
}
public void BeginSequentialAnimation(UIElement[] targets, DependencyProperty property)
{
TimeSpan acc = new TimeSpan(0);
for (int i = 0; i < Items.Count; ++i)
{
Items[i].BeginTime = acc;
acc += Items[i].Duration.TimeSpan;
}
for (int i = 0; i < Count; ++i)
{
targets[i].BeginAnimation(property, Items[i]);
}
}
}
Same goes for any other manipulation you might need. Good luck!