Tuesday, March 20, 2007

Smooth procedural animations in WPF



I was playing around with some procedural animations a little while back and decided to see just how smooth I could make them. It was a fun little endeavor with some interesting results.



I’m assuming that the built-in animation classes are not sufficient for your animation (they are not intended for interactive animations such animating an object following your mouse with some lag). If your animation can be done using the built-in classes, I’d suggest using those.



I’m just going to do a simple animation- a repeating animation of a rectangle sliding across the screen. So let’s start with the XAML:

<Grid>
<Rectangle Width='10' Height='10' Fill='Red'
RenderTransformOrigin="0.5,0.5" HorizontalAlignment='Left'>
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0" x:Name='translation'/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>

First step is to use CompositionTarget.Rendering to trigger your code. I see way too many Timer‘s using 50ms callbacks to say that this should be a given. It’s not!

public Window1() {
this.InitializeComponent();
CompositionTarget.Rendering += this.HandleRendering;
}

private void HandleRendering(object sender, EventArgs e) {
this.translation.X += 10;
if (this.translation.X > 600)
this.translation.X = 0;
}
Problem number one is that this code is frame based, but we’re in a time-based system. Different people will be running at different framerates and we’d like them all to see the animation at the same speed.
Stopwatch clock = new Stopwatch();
public Window1() {
this.InitializeComponent();
this.clock.Start();
CompositionTarget.Rendering += this.HandleRendering;
}

private void HandleRendering(object sender, EventArgs e) {
double animationTime = 1000; // 1 second
// Percent of the way through the cycle
double percent = this.clock.Elapsed.
TotalMilliseconds % animationTime / animationTime;
this.translation.X = 600 * percent;
}
Now, this is normally about how far I go with my custom animations, but we’re looking for a smooth animation, and if you’re doing a side-by-side animation with WPF, this is just not quite the same.

Why? The difference is that WPF’s animation system does the calculations not at what time it is now but what time it will be when the user actually sees the frame (when the frame is presented). To match WPF, the custom animation needs to use this time as well, and the way to find it is hidden in the event passed in the CompositionTarget.Rendering call.

TimeSpan? startTime;
public Window1() {
this.InitializeComponent();
CompositionTarget.Rendering += this.HandleRendering;
}

private void HandleRendering(object sender, EventArgs e) {
RenderingEventArgs renderingArgs = (RenderingEventArgs)e;

if (!this.startTime.HasValue)
this.startTime = renderingArgs.RenderingTime;

double animationTime = 1000; // 1 second
TimeSpan elapsed = (renderingArgs.RenderingTime - this.startTime.Value);
// Percent of the way through the cycle
double percent = elapsed.TotalMilliseconds
% animationTime / animationTime;
this.translation.X = 600 * percent;
}
This animation now matches WPF’s for smoothness, but if you’re being really discerning, there’s one more trick that you can do, which isn’t really suggested for real applications-



Overclock your animations.



The WPF animation system has an optional property on Storyboards that you can set the desired framerate. This is intended for subtle animations that don’t need to be run at 60fps (such as a pulsing button background). The fun part is that you can set the desired framerate to be higher than the default framerate and effectively overclock them.




public Window1() {
this.InitializeComponent();

DoubleAnimation framerateSetter = new DoubleAnimation();
framerateSetter.Duration = Duration.Forever;

// 200 frames per second baby!
Storyboard.SetDesiredFrameRate(framerateSetter, 200);
this.BeginAnimation(FrameworkElement.WidthProperty, framerateSetter);
CompositionTarget.Rendering += this.HandleRendering;
}

And that’s it, happy animating!