In WPF/SL, the very base class UIElement
of any control, implements the abstract
class Animatable
which makes any of dependency properties used in animation using BeginAnimation
. Different types of pre-defined animation timelines are available, of which, the most common is DoubleAnimation
. It can be applied to any dependency property which uses double
as value type. Now, the most important point of this post is EasingFunction
. You all must have heard about motion tween, or tweening animation. These types of animations are based on some mathematical functions which are called EasingFunctions
, these are like this y = ƒ (t)
. These determine what the value of property will be at any given point of time.
This post is on some fun stuff on how you can use expression evaluators as dynamic easing function in any animation.
This is a preview of the thing which we are going to make. (The easing function here is derived from Elastic Ease.)
Basic Requirements and Information
We'll be dealing with some good fast expression evaluators (like Flee) as this is totally on animation and timing is very important to get smooth animation. We'll do a little bit of XAML designing and C# with a pinch of math.
I'm not an expert on any programming area, just learning student and love to share my stuff, so please be kind if you find any mistakes.
The ExpressionEase Class
This class is very important for all this stuff. It inherits from EasingFunctionBase
. The implementation goes like this:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Ciloci.Flee;
namespace NS.Samples.ExpressionEase
{
public class ExpressionEase : EasingFunctionBase
{
private string expression = "t";
public ExpressionEase(string expression)
{
if (!expression.Contains("t"))
throw new ArgumentException(string.Format
("Variable '{0}' not found in expression.", "t"));
try
{
ExpressionContext context = new ExpressionContext();
context.Imports.ImportBuiltinTypes();
context.Imports.AddType(typeof(System.Math));
context.Variables.Add("t", 1.0);
var expr = ExpressionFactory.CreateDynamic(expression, context);
var rest = expr.Evaluate();
}
catch
{
throw new ArgumentException(
string.Format(
"Expression [{0}] is invalid
or contains wrong implementation or cannot be evaluated."
, expression),
"expression");
}
this.expression = expression;
}
protected override double EaseInCore(double normalizedTime)
{
ExpressionContext context = new ExpressionContext();
context.Imports.ImportBuiltinTypes();
context.Imports.AddType(typeof(System.Math));
context.Variables.Add("t", normalizedTime);
var expr = ExpressionFactory.CreateDynamic(expression, context);
var rest = expr.Evaluate();
return (double)rest;
}
public static Geometry GenerateEasingGeometry(IEasingFunction easing, int width, int height,
int time = 1000)
{
PathGeometry geometry = new PathGeometry();
double w = width;
double h = height / 2;
double timeframe = time / w;
PathGeometry sg = new PathGeometry();
PathFigure pf = new PathFigure();
bool isfigstarted = false;
for (int i = 0; i <= w; i++)
{
double normalizedtime = (timeframe * (i + 1)) / time;
Point point1 = new Point(i, h - (easing.Ease(normalizedtime) * h));
if (!isfigstarted)
{
pf.StartPoint = point1;
isfigstarted = true;
}
else
{
pf.Segments.Add(new LineSegment() { Point = point1 });
}
}
geometry.Figures.Add(pf);
return geometry;
}
}
}
The Expression Tester
In ExpressionTester
, you can see the live graph of easing function and live animation of easing function. It's a UserControl
and the implementation goes like:
<UserControl x:Class="NS.Samples.ExpressionEase.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="680"
Height="280"
Background="#C8000000"
Foreground="DimGray"
Loaded="UserControl_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250*" />
<ColumnDefinition Width="400*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2"
Margin="5,5,5,25"
FontSize="28"
TextAlignment="Center">
iFramework's Expression Ease
</TextBlock>
<Grid Grid.Row="1"
Margin="5"
Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="1">
<Grid Margin="0">
<Path x:Name="Path_Easing"
Stretch="None"
Stroke="Black"
StrokeThickness="2" />
<Line HorizontalAlignment="Center"
Stretch="Fill"
Stroke="DarkGray"
X2="1" />
<Line HorizontalAlignment="Left"
Stretch="Fill"
Stroke="DarkGray"
Y2="1" />
</Grid>
</Grid>
<TextBlock Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center">
ƒ(t)
</TextBlock>
<TextBlock Grid.Row="2"
Grid.Column="1"
TextAlignment="Center">
t
</TextBlock>
</Grid>
<Grid x:Name="Grid_ExpressionTest"
Grid.Row="1"
Grid.Column="1"
Margin="5"
Background="#37A9A9A9">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="23" />
</Grid.RowDefinitions>
<Rectangle Name="Rect"
Width="15"
Height="15"
Fill="#960076FF"
Stroke="Black"
StrokeThickness="2">
<Rectangle.RenderTransform>
<TranslateTransform x:Name="Rect_Transform" />
</Rectangle.RenderTransform>
</Rectangle>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="45" />
</Grid.ColumnDefinitions>
<TextBox Name="TextBox_Exp"
FontFamily="Trebuchet MS"
FontSize="14"
Foreground="Black"
HorizontalContentAlignment="Center"
Text="pow(2, -10 * t) * sin((t * 1 - 0.075) * (2 * pi) / 0.3) + 1"
VerticalContentAlignment="Center">
<TextBox.Background>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0" Color="#640089FF" />
<GradientStop Offset="1" Color="#C8008CFF" />
</LinearGradientBrush>
</TextBox.Background>
</TextBox>
<Button Name="Button_Apply"
Grid.Column="1"
Click="Button_Apply_Click"
Content="Apply"
FontSize="12"
Foreground="Black"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center">
<Button.Background>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0" Color="#64B1FF00" />
<GradientStop Offset="1" Color="#C8B1FF00" />
</LinearGradientBrush>
</Button.Background>
</Button>
</Grid>
</Grid>
</Grid>
</UserControl>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace NS.Samples.ExpressionEase
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
}
private void Button_Apply_Click(System.Object sender, System.Windows.RoutedEventArgs e)
{
int time = 3000;
ExpressionEase ease = new ExpressionEase(this.TextBox_Exp.Text);
var pathdata = ExpressionEase.GenerateEasingGeometry(
ease,
(int)this.Path_Easing.ActualWidth,
(int)this.Path_Easing.ActualHeight,
time)
as PathGeometry;
this.Path_Easing.Data = pathdata;
DoubleAnimation doubleAnimation_rect = new DoubleAnimation();
doubleAnimation_rect.From = 0;
doubleAnimation_rect.To =
((this.Grid_ExpressionTest.ActualWidth/2)-(this.Rect.ActualWidth / 2));
doubleAnimation_rect.Duration = new Duration(TimeSpan.FromMilliseconds(time));
doubleAnimation_rect.EasingFunction = ease;
doubleAnimation_rect.RepeatBehavior = RepeatBehavior.Forever;
doubleAnimation_rect.AutoReverse = true;
Storyboard stb = new Storyboard();
stb.Children.Add(doubleAnimation_rect);
Storyboard.SetTarget(doubleAnimation_rect, this.Rect_Transform);
Storyboard.SetTargetProperty(doubleAnimation_rect, new PropertyPath("(X)"));
stb.Begin();
}
}
}
Easing
Now, as you have made dynamic easing function class and a control to test it, let's see.
Here are some easing functions for starting:
Easing expression for linear ease. t
Easing expression for circular ease. sqrt(1-pow(t,2))
Easing expression for elastic ease. pow(2,-10*t)*sin((t*1-0.075)*(2*pi)/0.3)+1
Easing expression for cubic ease. pow(t,3)
Easing expression for quadratic ease. pow(t,2)