Click here to Skip to main content
Click here to Skip to main content
Go to top

Using Lambdas for WPF or Silverlight Animation

, 17 Jun 2010
Rate this:
Please Sign up or sign in to vote.
Shows how to use lambdas and higher-order functions for WPF/Silverlight graphics work

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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dmitri Nеstеruk
Founder ActiveMesa
United Kingdom United Kingdom
I work primarily with the .NET technology stack, and specialize in accelerated code production via code generation (static or dynamic), aspect-oriented programming, MDA, domain-specific languages and anything else that gets products out the door faster. My languages of choice are C# and F#, though I'm open to suggestions.
 
I'm a Microsoft MVP (Visual C#) since 2009. I run a collective tech blog at DevTalk.net. I use my own editor called TypograFix to typeset articles and blog posts.
 
Like the article and want this implemented in your product? Got a project that can benefit from Microsoft.Net goodness? Then get in touch!
Follow on   Twitter

Comments and Discussions

 
GeneralAnother great article PinmvpJosh Fischer7-Jul-10 5:07 
GeneralRe: Another great article PinmemberDmitri Nesteruk7-Jul-10 6:51 
GeneralRe: Another great article PinmvpJosh Fischer7-Jul-10 11:37 
Generalyou forget a variable Pinmemberzhushaofeng17-Jun-10 1:34 
GeneralRe: you forget a variable PinmemberDmitri Nesteruk17-Jun-10 2:09 
GeneralVery nice work PinmemberMoim Hossain17-Jun-10 1:01 
Generalvery nice Pinmemberzhushaofeng16-Jun-10 22:50 
GeneralRe: very nice PinmemberDmitri Nesteruk17-Jun-10 0:21 
GeneralAwesome ... PinmemberJammer16-Jun-10 22:37 
GeneralThanks everyone! PinmemberDmitri Nesteruk16-Jun-10 10:06 
GeneralCool! PinmemberErnest Laurentin10-Jun-10 16:54 
GeneralVery clever PinmemberhecUngravity8-Jun-10 9:22 
GeneralCool PinmemberAlan Beasley6-Jun-10 7:41 
Generalvery very nice PinmvpSacha Barber4-Jun-10 20:39 
GeneralGreat! PinmemberUserWhoCares4-Jun-10 16:57 
GeneralVery nice... PinmemberAndrew Rissing4-Jun-10 7:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140922.1 | Last Updated 17 Jun 2010
Article Copyright 2010 by Dmitri Nеstеruk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid