Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Animation: Using Expression Evaluators as Dynamic Easing Functions

0.00/5 (No votes)
5 Jun 2012GPL32 min read 8K  
Animation: Using expression evaluators as dynamic easing functions

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.

Image 1

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:

C#
/*
 * The app / data alongwith all resources is licensed under
 * NS Type1 license, read more at http://www.nsapps.net/License.aspx/Type1.
 *
 */
 
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Ciloci.Flee;
 
namespace NS.Samples.ExpressionEase
{
    /// <summary>
    /// Expression Ease.
    /// </summary>
    public class ExpressionEase : EasingFunctionBase
    {
        private string expression = "t";
 
        /// <summary>
        /// Initializes a new instance of the <see cref="ExpressionEase" /> class.
        /// </summary>
        /// <param name="expression">The expression.</param>
        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;
        }
 
        /// <summary>
        /// Provides the logic portion of the easing function.
        /// </summary>
        /// <param name="normalizedTime"
        /// >Normalized time (progress) of the animation<returns>
        /// A double that represents the transformed progress.
        /// </returns>
        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;
        }
 
        /// <summary>
        /// Generates the easing geometry.
        /// </summary>
        /// <param name="easing">The easing.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="time">The time.</param>
        /// <returns></returns>
        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:

XML
<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>
C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
 
namespace NS.Samples.ExpressionEase
{
    /// <summary>
    /// MainPage.
    /// </summary>
    public partial class MainPage : UserControl
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MainPage"/> class.
        /// </summary>
        public MainPage()
        {
            InitializeComponent();
        }
 
        /// <summary>
        /// Handles the Loaded event of the UserControl control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">
        /// The <see cref="RoutedEventArgs"/> 
        /// instance containing the event data.</param>
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
        }
 
        /// <summary>
        /// Handles the Click event of the Button_Apply control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see 
        /// cref="RoutedEventArgs"/> instance containing the event data.</param>
        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)

Image 2

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)