Introduction
VisualEffects
library allows Winforms controls animation. It provides many out-of-the-box effects including control's size, location, color and opacity effects and supports easing functions. You can easily create new effects implementing IEffect
interface and provide new easing functions. Effects and easing functions are independent and can be combined as you wish. Multiple effects can be combined to work together seamlessly. A few examples:
Left anchored width manipulation effect with bounce easing:
Linear fade effect:
Linear color shift effect:
Previous effects combined:
Background
To understand how VisualEffects
works, I need to define what animators, effects, easing functions, animations and transitions are.
ANIMATORS
Animators are the engines that make animations work. At the very core, there's a timer and a stopwatch. At every tick, a new value is calculated using a provided easing function and the actual elapsed time, then control properties are manipulated by means of effects. The timer is stopped when elapsed time reaches the desired duration.
EASING FUNCTIONS
Easing functions are mathematical functions that are used to interpolate values between two endpoints usually with non-linear results. They determine the way something changes; or better, they determine how effects are applied.
Given an expected effect duration and the amount of time elapsed since an animator started to play the effect, an easing function calculates the correct value the control has to assume at a given point in time. At T(0)
value is the initial value, at T(Max)
value is the target value we want to reach.
The easing functions I'm using are taken from http://gizma.com/easing/. In my library, an easing function is a delegate function defined as follows:
public delegate double EasingDelegate( double currentTime,
double minValue, double maxValue, double duration );
If this is the first time you hear about easing, you probably have always been animating things linearly. Linear easing is a valid easing function but it's not credible since objects in real life don't just start and stop instantly, and almost never change at a constant speed. Easing functions (usually) act in a more natural and credible way.
This example shows how linear easing is implemented:
public static double Linear( double currentTime, double minHeight,
double maxHeight, double duration )
{
return maxHeight * currentTime / duration + minHeight;
}
EFFECTS
Effects are the means by which control properties are manipulated. In my library, an effect is a class that implements IEffect
interface, which is defined as follows:
public interface IEffect
{
EffectInteractions Interaction { get; }
int GetCurrentValue( Control control );
void SetValue( Control control, int originalValue, int valueToReach, int newValue );
int GetMinimumValue( Control control );
int GetMaximumValue( Control control );
}
EXAMPLE 1
A simple, perfectly working example of how to implement IEffect
to create an effect that manipulates control's Height
property is the following:
public class TopAnchoredHeightManipulationlEffect : IEffect
{
public EffectInteractions Interaction
{
get { return EffectInteractions.HEIGHT; }
}
public int GetCurrentValue( Control control )
{
return control.Height;
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
control.Height = newValue;
}
public int GetMinimumValue( Control control )
{
return control.MinimumSize.IsEmpty ? Int32.MinValue
: control.MinimumSize.Height;
}
public int GetMaximumValue( Control control )
{
return control.MaximumSize.IsEmpty ? Int32.MaxValue
: control.MaximumSize.Height;
}
}
The effect above works with Height
property only. It gets current control's Height
property in GetCurrentValue
and sets a new value for Height
in SetValue
. As a result, a control will resize from the bottom. Here, you can see it in action with Linear easing:
EXAMPLE 2
Quote: practical tips for boosting the performance of windows forms apps
Several properties dictate the size or location of a control: Width, Height, Top, Bottom, Left, Right, Size, Location, and Bounds. Setting Width and then Height causes twice the work of setting them both together via Size.
An effect working on a single property is general purpose but it is not optimized to work together with other effects. If you apply many effects together, you can get flickering or non-smooth animations. Flickering may be noticed or not depending on how you mix effects and how effects behave.
That being so, sometimes, it's better to write a custom, more specialized effect in order to optimize painting. Let's say we want our control to stay anchored on bottom and resize its height on top; we could combine two effects, one working with Height
property and one with Top
property. This approach works, but it's not the best we can do. To achieve our goal efficiently, we have to work on Height
and Top
properties at the same time:
public class BottomAnchoredHeightManipulationEffect : IEffect
{
public int GetCurrentValue( Control control )
{
return control.Height;
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
var size = new Size( control.Width, newValue );
var location = new Point( control.Left, control.Top +
( control.Height - newValue ) );
control.Bounds = new Rectangle( location, size );
}
public int GetMinimumValue( Control control )
{
if( control.MinimumSize.IsEmpty )
return Int32.MinValue;
return control.MinimumSize.Height;
}
public int GetMaximumValue( Control control )
{
if( control.MaximumSize.IsEmpty )
return Int32.MaxValue;
return control.MaximumSize.Height;
}
public EffectInteractions Interaction
{
get { return EffectInteractions.BOUNDS; }
}
}
This is how it looks like with Linear easing:
EXAMPLE 3
Now, I will show a more complex example where I try to create a color shifting effect. Since we have to work on 4 channels (A,R,G,B), it's more convenient to create an effect that manages them all together:
public class ColorShiftEffect : IEffect
{
public EffectInteractions Interaction
{
get { return EffectInteractions.COLOR; }
}
public int GetCurrentValue( Control control )
{
return control.BackColor.ToArgb();
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
int actualValueChange = Math.Abs( originalValue - valueToReach );
int currentValue = this.GetCurrentValue( control );
double absoluteChangePerc =
( (double)( ( originalValue - newValue ) * 100 ) ) / actualValueChange;
absoluteChangePerc = Math.Abs( absoluteChangePerc );
if( absoluteChangePerc > 100.0f )
return;
Color originalColor = Color.FromArgb( originalValue );
Color newColor = Color.FromArgb( valueToReach );
int newA = (int)Interpolate( originalColor.A, newColor.A, absoluteChangePerc );
int newR = (int)Interpolate( originalColor.R, newColor.R, absoluteChangePerc );
int newG = (int)Interpolate( originalColor.G, newColor.G, absoluteChangePerc );
int newB = (int)Interpolate( originalColor.B, newColor.B, absoluteChangePerc );
control.BackColor = Color.FromArgb( newA, newR, newG, newB );
}
public int GetMinimumValue( Control control )
{
return Color.Black.ToArgb();
}
public int GetMaximumValue( Control control )
{
return Color.White.ToArgb();
}
private int Interpolate( int val1, int val2, double changePerc )
{
int difference = val2 - val1;
int distance = (int)( difference * ( changePerc / 100 ) );
int result = (int)( val1 + distance );
return result;
}
}
The first thing to notice is that I had to find a way to transform Color
to an integer to make it suitable for my interface. This is not required if you worked on each channel separately, since each channel is an integer. In this case, ToArgb()
does the trick. After that in SetValue
, we cannot just cast newValue
back to Color
and assign it to achieve a color shift effect; in facts our newValue
is more likely representing a random color since the animator had no clue it was working on an integer representation of a color and thus did not take into account to change ARGB channels accordingly.
What we can do to fix that is to calculate the percentage variation of newValue
compared to the originalValue
and apply that variation on each of our original color channels.
Here's what we get (Linear easing):
ANIMATIONS AND TRANSITIONS
An Animation consists of one or more effects working together on the same control. Transition define how different animations work on different controls, and thus how different animations interact.
There is no animation or transition abstraction in my library at the moment, but since they basically apply effects on controls, they can be easily written:
EXAMPLE
Most of the times, you want an animation to be able to expand and collapse, show and hide or in general perform an effect and its opposite:
public class FoldAnimation
{
public Control Control { get; private set; }
public Size MaxSize { get; set; }
public Size MinSize { get; set; }
public int Duration { get; set; }
public int Delay { get; set; }
public FoldAnimation(Control control)
{
this.Control = control;
this.MaxSize = control.Size;
this.MinSize = control.MinimumSize;
this.Duration = 1000;
this.Delay = 0;
}
public void Show()
{
this.Control.Animate(new HorizontalFoldEffect(),
EasingFunctions.CircEaseIn, this.MaxSize.Height, this.Duration, this.Delay);
this.Control.Animate(new VerticalFoldEffect(),
EasingFunctions.CircEaseOut, this.MaxSize.Width, this.Duration, this.Delay);
}
public void Hide()
{
this.Control.Animate(new HorizontalFoldEffect(),
EasingFunctions.CircEaseOut, this.MinSize.Height, this.Duration, this.Delay);
this.Control.Animate(new VerticalFoldEffect(),
EasingFunctions.CircEaseIn, this.MinSize.Width, this.Duration, this.Delay);
}
}
The above implementation suffers two drawbacks: the first is that it does not provide a cancellation mechanism in case Hide()
method is called before Show()
method is done, or the other way round; the second is that effect duration is not adjusted so that if I call Hide()
method in the middle of Show()
animation, it will take half the desired duration time to perform.
Fortunately, Animate()
returns an AnimationStatus
object that can be used to fix those issues:
public class FoldAnimation
{
private List<StatelessAnimator.AnimationStatus> _cancellationTokens;
public Control Control { get; private set; }
public Size MaxSize { get; set; }
public Size MinSize { get; set; }
public int Duration { get; set; }
public int Delay { get; set; }
public FoldAnimation( Control control )
{
_cancellationTokens = new List<StatelessAnimator.AnimationStatus>();
this.Control = control;
this.MaxSize = control.Size;
this.MinSize = control.MinimumSize;
this.Duration = 1000;
this.Delay = 0;
}
public void Show()
{
int duration = this.Duration;
if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
{
var token = _cancellationTokens.First( aS => !aS.IsCompleted );
duration = (int)( token.ElapsedMilliseconds );
}
this.CancelAllPerformingEffects();
var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
EasingFunctions.CircEaseIn, this.MaxSize.Height, duration, this.Delay );
var cT2 = this.Control.Animate( new VerticalFoldEffect(),
EasingFunctions.CircEaseOut, this.MaxSize.Width, duration, this.Delay );
_cancellationTokens.Add( cT1 );
_cancellationTokens.Add( cT2 );
}
public void Hide()
{
int duration = this.Duration;
if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
{
var token = _cancellationTokens.First( aS => !aS.IsCompleted );
duration = (int)( token.ElapsedMilliseconds );
}
this.CancelAllPerformingEffects();
var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
EasingFunctions.CircEaseOut, this.MinSize.Height, duration, this.Delay );
var cT2 = this.Control.Animate( new VerticalFoldEffect(),
EasingFunctions.CircEaseIn, this.MinSize.Width, duration, this.Delay );
_cancellationTokens.Add( cT1 );
_cancellationTokens.Add( cT2 );
}
public void Cancel()
{
this.CancelAllPerformingEffects();
}
private void CancelAllPerformingEffects()
{
foreach (var token in _cancellationTokens)
token.CancellationToken.Cancel();
_cancellationTokens.Clear();
}
}
Using the Code
To apply an effect on your control, simply call Animator.Animate
method, or Animate
extension method on your control like this:
yourControl.Animate
(
new XLocationEffect(),
EasingFunctions.BounceEaseOut,
321,
2000,
0
);
Update 09/12/2014
I was missing the ability to reverse an animation and to perform a defined number of loops if reverse is enabled. These features are available in version 1.2.
Animator.Animate
method has two more optional parameters:
Reverse
: If set to true
, the animation will reach the target value and then, play back to the initial value. By default, this parameter is set to false
. Loops
: If you set reverse to true
, then you can define how many loops the animation has to perform. By default, this parameter is set to 1
. If you set 0
or a negative number, the animation will loop forever. You can stop the animation by using CancellationToken
as usual.
History
- 09/12/2014: Animation reverse and loops number
- 16/10/2014: Project is created