Animations on a Controls (e.g. a DataListView)
Google Enviers' Anonymous
"I'd like to welcome you all to this weeks meeting of Google Enviers' Anonymous. Hi. My name is Phillip and I'm a Google envier. I've managed to control my condition for almost six days. But this morning, I fell off the wagon in a big way. I was working normally and then I saw it! You all know what happens next. My breathing was suddenly rapid and shallow. I reached for my paper bag to keep calm. But it was too late. Waves of jealousy swept over me. In desperation, I speed-dialed my Google Envier buddies, Steve and Bill, and they talked me through it. After a minute or so, the spasm passed and I returned to (more or less) normal, though the bitter taste of envy lingered for the rest of the day.
In my self defence, the source of my envy is nothing as crass as their billion dollar development budget, their wonderful working conditions, or their thousand plus dollar share price. All these things fall into insignificance in comparison with the true object of my obsession: their animations! The way they effortlessly add little spinning stars, glowing text or fading sparkles to their applications. My applications sit there fully functional and obedient -- but static and passive, lacking the moving eye candy that make Google apps cute and cool.
But no more! To you, fellow enviers, I present the Sparkle animation framework. With this framework, you too can put animations into your applications and free yourself from the shackles of Google envy."
Understanding Sparkle in 30 Seconds or Less
OK. A little more seriously this time. The Sparkle library's purpose is to allow (almost) any Control
to show animations.
The design goals of the Sparkle library are:
- Short-lived animations on any control. The Sparkle library is designed to draw short animations over the top of existing Controls.
- Declarative. The Sparkle library is declarative. You say what you want the animation to do, and then you run the animation. The animation is completely defined before it begins.
- Non-interactive. The Sparkle library does not do user interaction -- it does not listen to mouse moves, clicks or drags. It doesn't do collision detection or physics models. It just draws eye candy.
To use the library itself, you'll need to grasp its four major concepts:
- Animations. An animation is the canvas upon which sprite are placed. It is the white board upon which things are drawn.
- Sprites. Sprites are things that can be drawn. There are several flavours of sprites -- one for images, another for text, still another for shapes. It is normal to make your own types of sprites by subclassing
Sprite
(or implementing ISprite
).
- Effects. Effects are things that make changes to sprites over time. They are the "movers and shakers" in the library, who actually do things. Sprites sit there, completely passive, looking pretty, but the effects push them around, change their visibility, spin or size. Again, you can use existing
Effects
, or implement your own through the IEffect
interface.
- Locators. Locators are things that known how to calculate a point or a rectangle. Rather than saying "Put this sprite at (10, 20)," they allow you to say "Put this sprite at the bottom right corner of this other sprite." This idea can be tricky to get your mind around, but once you have grasped it, it is powerful.
How to Use It
Adding any sort of animation to an application is a multi-step process. With Sparkle, the workflow for creating an animation is:
- Decide where the animation will appear. That is your
Animation
.
- Think about what you want to show. They are your
Sprites
.
- Think about what you want each Sprite to do. They are your
Effects
.
- Whenever an
Effect
needs a "where
", that's when you need Locators
.
Simple Example
To get a feeling for how to use the library, there's no substitute for seeing code. So let's do a simple example of moving a word across a control.
According to our workflow (above), we first need to decide where we want our animation to appear. So let's make a new project, with a new form, and put on it a UserControl
that's docked to fill the whole form. Put a button on the form called "Run". In the click handler for that button, we'll do the work of running an animation.
The first thing we need is an Animation
on that UserControl
.
AnimationAdapter adapter = new AnimationAdapter(this.userControl1);
Animation animation = adapter.Animation;
AnimationAdapter
is the class that links an Animation
to an existing control. We've used a UserControl
, but it could be any Control
that supports the Paint
event.
Once we have an Animation
, the workflow says we need to decide what be seen: they are our Sprites
. We want to show the word "Sparkle" so we make a TextSprite
. If we had want to show an image, there is an ImageSprite
, and to show shapes, there is a ShapeSprite
. You can (of course) make your own sprites by implementing the ISprite
interface or by subclassing Sprite
:
TextSprite sparkle = new TextSprite("Sparkle!", new Font("Gill Sans", 48), Color.Blue);
OK. We have our sprite. What do we want it to do? We want to move it from the top left of animation to the bottom right. Moving (or any other sort of change over time) requires an Effect
. In this case, we need a MoveEffect
. You can create these directly – using new MoveEffect(...)
– or you can use the Effects
factory, which has lots methods to make Effect
objects:
sparkle.Add(100, 1000, Effects.Move(Corner.TopLeft, Corner.BottomRight));
This says, “Beginning 100 milliseconds after the sprite starts, and lasting for 1000 milliseconds, move this sprite from the top right to the bottom left of the animation.”
That’s all we want the sprite to do at the moment, so now we add the sprite to the animation. Not all sprites are active at the beginning of the animation, so when we add the sprite the animation we also tell it when the sprite should begin. In this case, we do want the sprite to start when the animation starts, so we give 0
as the start time for the sprite:
animation.Add(0, sparkle);
And finally, we tell the animation to run:
animation.Start();
All being well, you should see something like this:
For those who noted that the above graphic lacks one of the fundamental requirements for an animation (namely animated-ness), CodeProject doesn't support animations within a page, so click here to see the actual animation.
A Little More Interesting
Under whelmed? Admittedly, it's not that impressive, but you did only write six lines of code! But if we add a few different effects, you can easily do something more impressive. For example, if we want the text to walk around the edge of the control, spinning and fading at the same time:
sparkle.Add(0, 5000, Effects.Rotate(0, 360 * 4));
sparkle.Add(0, 5000, Effects.Blink(5));
sparkle.Add(0, 5000, Effects.Walk(Locators.AnimationBounds(-100, -50),
WalkDirection.Anticlockwise));
This gives the Sprite
three effects that run simultaneously. The first two are fairly obvious, but the third is a little trickier. It uses the Effects
factory to create an effect that will walk a sprite around a rectangle. The rectangle to be walked is a "where" so it uses a Locator
. This particular locator returns the bounds of the Animation
, inset by (100, 50).
Putting these together produces this:
Again, you have to look here to see the actual animation.
Admittedly, this is garish, but it does give an idea of what you can do with just a few more lines of code.
Animations
An animation has two distinct functions:
- It implements a timer tick based animation system. At each tick of a clock, it advances the state of the animation: it decides which sprites should become active/inactive, gives effects the opportunity to do their magic. This portion does not perform any rendering -- it simply changes the state of parts of the animation. If anything needs to be rendered, the animation triggers a
Redraw
event.
- It draws the sprites according to their current state. Normally, something would listen for the
Redraw
event on an Animation
, and in response to that event, it would redraw the animation. To do that, it calls the Animation.Draw(Graphics g)
method. This will render the animation, in its current state, onto the given Graphics
object. This operation does not change the state of the Animation
.
In addition to animating sprites and rendering them, an animation supports the basic set of commands to control its execution:
Start()
Pause()/Unpause()
Stop()
Repeat Behaviour
Animations
have a Repeat
property, which controls the animation's behaviour when it reaches the end of the animation.
Repeat.None
- The animation simply quits. All sprites disappear. This is the default.
Repeat.Pause
- The animation pauses. All sprites that were visible at the end of the animation remain visible and motionless.
Repeat.Loop
- The animation begins again.
Sprites
Sprite are the actual eye candy -- the pretty do-nothing things that the user can see. They keep whatever state information they require -- location, size, color, transparency -- and then use that state information to draw themselves when asked. They don't change their own state -- that's the responsibility of Effects
.
There are several flavours of sprites that come with the Sparkle library:
ImageSprite
. This takes an Image
and draws it according to the sprites state. If the given Image
is a frame animation itself, the Sparkle framework will animate that image automatically. I think it is only animated GIFs that Microsoft supports as frame animations.
TextSprite
. TextSprites
draw text (no prizes). But they can do a bit more formatting than just that. The text can be colored (ForeColor
property), they can be drawn with a background (BackColor
property). They can draw a border around the text (BorderWidth
and BorderColor
properties). The border can be either a rectangle (set CornerRounding
to 0) or a round cornered rectangle (set CornerRounding
to greater than 0 -- 16 is normally nice).
ShapeSprites
. These draw regular shapes (square, rectangles, round cornered rectangle, triangles, ellipses/circles). Like TextSprites
, ShapeSprites
can have a ForeColor
(color of frame of the shape), BackColor
(used for the filled part of the shape), and PenWidth
(width of the frame).
Remember, all colors can have alpha values set for them, which will allow varying levels of transparency when drawing the sprites.
Custom Sprites
It is expected that application would implement new sprites to do whatever specialized drawing it requires. To do this, you would implement the ISprite
interface or subclass Sprite
directly.
public interface ISprite : IAnimateable
{
Point Location { get; set; }
float Opacity { get; set; }
float Scale { get; set; }
Size Size { get; set; }
float Spin { get; set; }
Rectangle Bounds { get; set; }
Rectangle OuterBounds { get; }
Rectangle ReferenceBounds { get; set; }
IRectangleLocator ReferenceBoundsLocator { get; set; }
IPointLocator FixedLocation { get; set; }
IRectangleLocator FixedBounds { get; set; }
void Draw(Graphics g);
void Add(IEffect effect);
void Add(long startTick, IEffect effect);
void Add(long startTick, long duration, IEffect effect);
}
Look carefully at the existing sprites to see how they should be implemented. Pay special attention to the role of co-ordinate transformations to handle Location
and Rotation
properties.
Effects
Effects are the movers and shakers of the Sparkle library. They push Sprites
around, moving them here or there, making them visible or invisible, spinning them around. Any time you want a Sprite
to change, you need an Effect
.
Effects
are given to a Sprite
, and told when they should start and how long they will run for:
this.imageSprite.Add(100, 250, new FadeEffect(0.0f, 0.8f));
This says, "100 milliseconds after imageSprite
starts in the animation, this FadeEffect
should, during 250 milliseconds, fade the sprite from hidden (0.0 opacity) to 80% visible (0.8 opacity)."
Many Effects
work by "tweening" - they are given an initial value and a target value, and as the effect progresses, the effect gradually change a property on their Sprite
from the initial value to the end value. In the above example, the FadeEffect's
initial value is 0.0 and its end value is 0.8. As the animation progresses, the FadeEffect
would gradually change the Opacity
property of its Sprite
from 0.0 to 0.8. So, 100 milliseconds after the sprite starts, the imageSprite
will be hidden; after 225 milliseconds, it will be 40% visible; after 350 milliseconds, it will be 80% visible, and then the effect will stop.
Effects Factory
Effects
factory contains static
methods to create many commonly used effects.
-
Move(Corner to)
Move the sprite from it's current location to a corner of the animation.
-
Move(Corner from, Corner to)
Move the sprite from one corner of the animation to another. This has a zillion variations which allow different ways of saying where to start and where to end.
-
Goto(Corner to)
Go to (as in Monopoly) the given corner without any transition.
-
Fade(float from, float to)
Change the Opacity
of the sprite from the start to the end value, effectively fading it in or out.
-
Rotate(float from, float to)
Change the Spin
of the sprite from the start to the end value (both in degrees).
-
Scale(float from, float to)
Change the Scale
of the sprite, effectively making it bigger or smaller.
-
Bounds(IRectangleLocator locator)
Change the Bounds
of the sprite.
-
Walk(IRectangleLocator locator)
This is the first interesting effect. This changes the location of the sprite so that it "walks" around the perimeter of the given rectangle. This has several flavours saying which exact point of the sprite will be walked, which direction the walk should take, and where the walking should start.
-
Blink(int repetitions)
Another interesting effect. This changes the Opacity
of the sprite so that it blinks a number of times. There are a couple of variations that allow the characteristics of the "blink" to be changed: how long it takes to fade in, stay visible, fade out, stay invisible.
-
Repeater(int repetitions, IEffect effect)
This applies the given Effect
several times to the Sprite.
Custom Effects
Of course, it's again expected that applications would want to make their ways to change Sprites. You might want a move effect that bounces a sprite along an arc, or that does a checker board transition between two images. To do your own stuff, you need to implement the IEffect
interface.
public interface IEffect
{
ISprite Sprite { get; set; }
void Start();
void Apply(float fractionDone);
void Stop();
void Reset();
}
Effects need to know what Sprite they are changing, and to know when they start and stop. A sequence diagram would look something like this:
Start()
Apply()
[called 0 or more times, with any value 0.0..1.0]
Stop()
Reset()
[called 0 or once]
The only interesting bit is the Apply()
method. This is where Effect
do their actual work. Notice that the Effect
is given a "fraction done" value, not a click count (or something similar). Effects
cannot rely on Apply()
being called in any particular order: first Apply()
could have fractionDone=0.1
, the next time it could be 0.9
and then 0.5
.
Also note that Reset()
must revert the state of the Effect
and the Sprite to their original conditions before the Effect
was Start()
'ed. This means that in the Start()
method, effects normally store any state they are going to change, and then in Reset()
that state is put back.
Locators
In some ways, locators are the most difficult concept to grasp. If you can get this concept, everything else normally falls into place.
A Locator
is a point or a rectangle that can calculate itself whenever needed. A plain Point
is fixed, but a PointLocator
can be different every time it is called. By using a Locator
, "how" a point is calculated can be replaced at runtime to use any strategy it likes.
For example, the MoveEffect
changes the Location
of a Sprite
. It could be coded to move a Sprite
to the TopLeft
of an Animation
:
this.Sprite.Location = this.Animation.Bounds.Location;
This is nice and obvious solution, but not very flexible. If we then wanted to move the sprite to the centre of the Animation, we'd have to write a separate line of code, and then give some way to choose which line to execute. And another line of code for ever other possible location we could want.
But with Locators
, the MoveEffect
simply says:
this.Sprite.Location = this.Locator.GetPoint();
By using this extra layer of abstraction, the intelligence of calculating the "where
" is placed into a separate object, and becomes reusable from there.
Standard Locators
Locators
is a factory that has static
methods to produce many common locators. You can of course create the locators directly -- these are just a convenience.
-
IPointLocator At(int, int)
Create a PointLocator
for a fixed point.
-
IPointLocator SpriteAligned(Corner corner)
Create a PointLocator
which is where a Sprite must be moved to so that the given Corner is located at the corresponding corner of the Animation. So, Locators.SpriteAligned(Corner.BottomRight)
calculates where a sprite must be moved to so that its BottomRight
corner is at the BottomRight
corner of the Animation.
-
IPointLocator SpriteAligned(Corner corner, Point offset)
Same as above, but the point is offset by the given fixed amount.
-
IPointLocator SpriteAligned(Corner corner, float proportionX, float proportionY)
Create a PointLocator
which is where a Sprite must be moved to so that the given Corner is located at a point proportional across and down the bounds the Animation. So, Locators.SpriteAligned(Corner.BottomRight, 0.6f, 07.7)
calculates where a sprite must be moved to so that its BottomRight
corner is 60% across the Animation and 70% down.
-
IPointLocator SpriteBoundsPoint(Corner corner)
Create a PointLocator
which calculates the given corner of the sprite's bounds.
-
IPointLocator SpriteBoundsPoint(float proportionX, float proportionY)
Create a PointLocator
which calculates a given proportion across and down the sprite's bounds.
-
IRectangleLocator At(int, int, int, int)
Create a RectangleLocator
for a fixed rectangle.
-
IRectangleLocator AnimationBounds()
Create a RectangleLocator
for the bounds of the animation.
-
IRectangleLocator AnimationBounds(int x, int y)
Create a RectangleLocator
for the bounds of the animation inset by the given amount.
-
IRectangleLocator SpriteBounds()
Create a RectangleLocator
for the bounds of the sprite.
-
IRectangleLocator SpriteBounds(int x, int y)
Create a RectangleLocator
for the bounds of the sprite inset by the given amount.
AnimationAdaptor
The AnimationAdaptor
mentioned above gives an example of linking the Animation.Redraw
event to the Animation.Draw()
method. When the Animation
triggers a Redraw
event, the AnimationAdaptor
invalidates its Control
. This causes the Control
to repaint itself, and when the control triggers the Paint
event, the AnimationAdaptor
renders the animation through the Draw()
method. Voilá! Any Control
(with a Paint
event) can show an animation.
An AnimationAdaptor
can be used on Panels
, Buttons
, Labels
, PictureBoxes
, UserControls
, numeric spin controls, and (oddly enough) DataGridView
. It cannot be used on another control because they don't usefully support the Paint
event. There is nothing that can be done about RichTextBox
and the others, but if you want to put animations on a ListView
or a TreeView
, have a look at the ObjectListView project, which does support these animations.
One of the beautiful things about the design of Sparkle is that it can be trivially used within another framework. To use it within WPF, all that is needed is an equivalent of the AnimationAdaptor
, which listens for redraw events on the Animation
, and then do something to make the animation redraw itself.
Performance
The Sparkle library performs fairly well when used in accordance with its design goals. Animating dozens of sprites with dozens of effects has a minimal impact on performance. On my laptop, 20 or so sprites with a variety of effects uses only about 2-3% of the CPU. The limiting factor is not the animation but the redrawing of the underlying control. Currently, the whole control is redrawn every frame. For simple controls, like Buttons
or UserControls
, this is not a problem, but for complicated control, like DataGridView
, this redrawing quickly becomes taxing.
In a later version, I'll optimize to invalidate only the smallest possible area of the control.
I haven't tried using Sparkle with thousands of sprites. That really wasn't its purpose.
Status and Stability
Sparkle is a new library. It has worked well for me, but I'm sure there are bugs in it. Please report them and I will fix them.
The interfaces and major classes are stable, but not yet fixed (unchangeable). It's possible that I will add a few more properties to the ISprite
interface (I think it needs Skew
).
Conclusion
Go ahead. Dazzle your users with the glittery, snazzy animations.
Now when I look at Google's apps, I no longer suffer from envy. I still don't have their work conditions or share price, but when a little star spins and fades, I think, "Hey, I can do that too!"
To Do
- Handle updates more efficiently (calculate damaged region and only redraw that portion).
- Add fancier effects, like glow and mirror.
- Add composites (ways of putting effects onto multiple sprites at same time).
- Allow different interpolation calculations. All interpolations are currently linear. We should allow for example, acceleration, so that falling shapes in the demo accelerate as they fall.
- Allow animations to reverse.
Library License
This library is released under the LGPL v2.0.
History
v0.8 - 30 March 2010