Saturday, December 4, 2010

MatrixAnimation for WPF

Windows Presentation Foundation has a powerful animation system. It comes with various classes which enable dependency properties’ values to be animated, i.e. to be automatically changed during a given period of time. WPF supports four types of animations:

  • Linear animation – a value of a property linearly changes from a starting value (referred as From) to a destination value (referred as To).
  • Key frame animation – animation is specified using key frames, each key frame specifies the time and the desired value. The animated property will be assigned once the timeline hits the give time.
  • Path-based animation – property value is given by a geometric path.
  • Frame-based animation – this is the most powerful animation approach, the CompositionTarget class is used to create custom animations based on a per-frame callback.

The first three animation types are supported by <Type>Animation, <Type>AnimationUsingKeyFrames and <Type>AnimationUsingPath classes, respectively. For example, consider the double type. Its animation is supported by DoubleAnimation, DoubleAnimationUsingKeyFrames and DoubleAnimationUsingPath classes.

WPF also supports matrix animations. However, out of the box, only key frame and path-based animations are supported for the Matrix type. This post introduces MatrixAnimation class, which performs linear, smooth animation of the Matrix type. The animation supports translation, scaling and rotation along with easing functions. The following 14 sec-length video shows an early preview of a multi-touch MDI interface whose windows are being animated using the MatrixAnimation class.

The following code snippet represents the MatrixAnimation class.
public class MatrixAnimation : MatrixAnimationBase
{
public Matrix? From
{
set { SetValue(FromProperty, value); }
get { return (Matrix)GetValue(FromProperty); }
}

public static DependencyProperty FromProperty =
DependencyProperty.Register("From", typeof(Matrix?), typeof(MatrixAnimation),
new PropertyMetadata(null));

public Matrix? To
{
set { SetValue(ToProperty, value); }
get { return (Matrix)GetValue(ToProperty); }
}

public static DependencyProperty ToProperty =
DependencyProperty.Register("To", typeof(Matrix?), typeof(MatrixAnimation),
new PropertyMetadata(null));

public IEasingFunction EasingFunction
{
get { return (IEasingFunction)GetValue(EasingFunctionProperty); }
set { SetValue(EasingFunctionProperty, value); }
}

public static readonly DependencyProperty EasingFunctionProperty =
DependencyProperty.Register("EasingFunction", typeof(IEasingFunction), typeof(MatrixAnimation),
new UIPropertyMetadata(null));

public MatrixAnimation()
{
}

public MatrixAnimation(Matrix toValue, Duration duration)
{
To = toValue;
Duration = duration;
}

public MatrixAnimation(Matrix toValue, Duration duration, FillBehavior fillBehavior)
{
To = toValue;
Duration = duration;
FillBehavior = fillBehavior;
}

public MatrixAnimation(Matrix fromValue, Matrix toValue, Duration duration)
{
From = fromValue;
To = toValue;
Duration = duration;
}

public MatrixAnimation(Matrix fromValue, Matrix toValue, Duration duration, FillBehavior fillBehavior)
{
From = fromValue;
To = toValue;
Duration = duration;
FillBehavior = fillBehavior;
}

protected override Freezable CreateInstanceCore()
{
return new MatrixAnimation();
}

protected override Matrix GetCurrentValueCore(Matrix defaultOriginValue, Matrix defaultDestinationValue, AnimationClock animationClock)
{
if (animationClock.CurrentProgress == null)
{
return Matrix.Identity;
}

var normalizedTime = animationClock.CurrentProgress.Value;
if (EasingFunction != null)
{
normalizedTime = EasingFunction.Ease(normalizedTime);
}

var from = From ?? defaultOriginValue;
var to = To ?? defaultDestinationValue;

var newMatrix = new Matrix(
((to.M11 - from.M11) * normalizedTime) + from.M11,
((to.M12 - from.M12) * normalizedTime) + from.M12,
((to.M21 - from.M21) * normalizedTime) + from.M21,
((to.M22 - from.M22) * normalizedTime) + from.M22,
((to.OffsetX - from.OffsetX) * normalizedTime) + from.OffsetX,
((to.OffsetY - from.OffsetY) * normalizedTime) + from.OffsetY);

return newMatrix;
}
}

The code is actually quite simple. The class is derived from the abstract MatrixAnimationBase class. Firstly, three dependency properties are defined, namely From, To and EasingFunction. Next comes a bunch of useful constructors. The interesting part resides in the GetCurrentValueCore method. At first, the current animation time is retrieved. Also, the animation time is eased with the easing function if it is available. Lastly, new matrix is calculated based on the From and To values. Each matrix cell is linearly scaled with the time value. And that’s it! This provides smooth animation for the matrix type!


7 comments:

kaan said...

Works like a charm. Thank you for posting this cool class.

Gerhard said...

Thank you, this saved me hours of work!

Unknown said...

Hi Piotr.
First of all this is a great animation. I'm using it to animate an image transform.
Today I found a problem that might have something to do with your animation. Is it possible that the animation doesn't work with adorners? In my code I'm adding an adorner to the image that uses your MatrixAnimation. The problem is that the animation doesn't animate the adorner (all other manipulation before and after the animation work). Do you have any idea what I have to change to make it work?
Your,
Darius

Unknown said...

Hi,

Here is how I used it:

<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<!--See: http://pwlodek.blogspot.fr/2010/12/matrixanimation-for-wpf.html-->
<!--See: https://wpf.2000things.com/tag/matrixtransform/-->
<wpfcommon:MatrixAnimation
Storyboard.TargetProperty="RenderTransform.Matrix"
Duration="0:0:1" From="1.0,0.0,0.0,1.0,0.0,0.0" To="1.0,0.0,0.0,1.0,1080.0,0.0">
</wpfcommon:MatrixAnimation>
</Storyboard>
</BeginStoryboard>

</Trigger.EnterActions>
</Trigger>
</Style.Triggers>

Best regards
Christophe

primate said...

Please can someone explain how to use this in code?

I've tried a test like this which doesn't work:

private void Button_Click(object sender, RoutedEventArgs e)
{
Matrix rectsMatrix = ((MatrixTransform)manRect.RenderTransform).Matrix;
rectsMatrix.ScaleAt(2, 2, 0.5, 0.5);
rectsMatrix.Translate(100, 100);

var t = manRect.RenderTransform as MatrixTransform;
var ani = new MatrixAnimation(rectsMatrix, new TimeSpan(0,0,1));
t.BeginAnimation(MatrixTransform.MatrixProperty, ani);
}

Thanks,
Simon

primate said...

Got it working now by adding a Matrix from value.

Tounsi said...

What about Matix3d How i can make smooth animation3D ?