Browse Source

Merge pull request #4180 from AvaloniaUI/spline-easing

Spline easing
pull/4211/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
86c49efc96
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Animation/Easing/Easing.cs
  2. 85
      src/Avalonia.Animation/Easing/SplineEasing.cs
  3. 35
      src/Avalonia.Animation/KeySpline.cs
  4. 25
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  5. 95
      tests/Avalonia.Animation.UnitTests/KeySplineTests.cs

6
src/Avalonia.Animation/Easing/Easing.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
namespace Avalonia.Animation.Easings
@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings
/// <returns>Returns the instance of the parsed type.</returns>
public static Easing Parse(string e)
{
if (e.Contains(','))
{
return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
}
if (_easingTypes == null)
{
_easingTypes = new Dictionary<string, Type>();

85
src/Avalonia.Animation/Easing/SplineEasing.cs

@ -0,0 +1,85 @@
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a user-defined cubic bezier curve.
/// Good for custom easing functions that doesn't quite
/// fit with the built-in ones.
/// </summary>
public class SplineEasing : Easing
{
/// <summary>
/// X coordinate of the first control point
/// </summary>
public double X1
{
get => _internalKeySpline.ControlPointX1;
set
{
_internalKeySpline.ControlPointX1 = value;
}
}
/// <summary>
/// Y coordinate of the first control point
/// </summary>
public double Y1
{
get => _internalKeySpline.ControlPointY1;
set
{
_internalKeySpline.ControlPointY1 = value;
}
}
/// <summary>
/// X coordinate of the second control point
/// </summary>
public double X2
{
get => _internalKeySpline.ControlPointX2;
set
{
_internalKeySpline.ControlPointX2 = value;
}
}
/// <summary>
/// Y coordinate of the second control point
/// </summary>
public double Y2
{
get => _internalKeySpline.ControlPointY2;
set
{
_internalKeySpline.ControlPointY2 = value;
}
}
private readonly KeySpline _internalKeySpline;
public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d)
{
_internalKeySpline = new KeySpline();
this.X1 = x1;
this.Y1 = y1;
this.X2 = x2;
this.Y1 = y2;
}
public SplineEasing(KeySpline keySpline)
{
_internalKeySpline = keySpline;
}
public SplineEasing()
{
_internalKeySpline = new KeySpline();
}
/// <inheritdoc/>
public override double Ease(double progress) =>
_internalKeySpline.GetSplineProgress(progress);
}
}

35
src/Avalonia.Animation/KeySpline.cs

@ -81,7 +81,10 @@ namespace Avalonia.Animation
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
public static KeySpline Parse(string value, CultureInfo culture)
{
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
if (culture is null)
culture = CultureInfo.InvariantCulture;
using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
@ -98,6 +101,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX1 = value;
_isDirty = true;
}
else
{
@ -112,7 +116,11 @@ namespace Avalonia.Animation
public double ControlPointY1
{
get => _controlPointY1;
set => _controlPointY1 = value;
set
{
_controlPointY1 = value;
_isDirty = true;
}
}
/// <summary>
@ -126,6 +134,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX2 = value;
_isDirty = true;
}
else
{
@ -140,7 +149,11 @@ namespace Avalonia.Animation
public double ControlPointY2
{
get => _controlPointY2;
set => _controlPointY2 = value;
set
{
_controlPointY2 = value;
_isDirty = true;
}
}
/// <summary>
@ -330,20 +343,4 @@ namespace Avalonia.Animation
}
}
}
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

25
src/Avalonia.Animation/KeySplineTypeConverter.cs

@ -0,0 +1,25 @@
using System;
using System.ComponentModel;
using System.Globalization;
// Ported from WPF open-source code.
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
namespace Avalonia.Animation
{
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

95
tests/Avalonia.Animation.UnitTests/KeySplineTests.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
@ -25,6 +26,16 @@ namespace Avalonia.Animation.UnitTests
Assert.Equal(4, keySpline.ControlPointY2);
}
[Theory]
[InlineData("1,2F,3,4")]
[InlineData("Foo,Bar,Fee,Buzz")]
public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input)
{
var conv = new KeySplineTypeConverter();
Assert.ThrowsAny<Exception>(() => (KeySpline)conv.ConvertFrom(input));
}
[Theory]
[InlineData(0.00)]
[InlineData(0.50)]
@ -46,6 +57,22 @@ namespace Avalonia.Animation.UnitTests
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX2 = input);
}
[Fact]
public void SplineEasing_Can_Be_Mutated()
{
var easing = new SplineEasing();
Assert.Equal(0, easing.Ease(0));
Assert.Equal(1, easing.Ease(1));
easing.X1 = 0.25;
easing.Y1 = 0.5;
easing.X2 = 0.75;
easing.Y2 = 1.0;
Assert.NotEqual(0.5, easing.Ease(0.5));
}
/*
To get the test values for the KeySpline test, you can:
1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
@ -141,5 +168,73 @@ namespace Avalonia.Animation.UnitTests
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
[Fact]
public void Check_KeySpline_Parsing_Is_Correct()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, -2.5d),
},
KeyTime = TimeSpan.FromSeconds(0)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, 2.5d),
},
KeyTime = TimeSpan.FromSeconds(5),
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(5),
Children =
{
keyframe1,
keyframe2
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate,
Easing = Easing.Parse("0.1123555056179775,0.657303370786517,0.8370786516853934,0.499999999999999999")
};
var rotateTransform = new RotateTransform(-2.5);
var rect = new Rectangle()
{
RenderTransform = rotateTransform
};
var clock = new TestClock();
var animationRun = animation.RunAsync(rect, clock);
// position is what you'd expect at end and beginning
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 2.5);
// test some points in between end and beginning
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));
var expected = -2.4122350198982545;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:11.2655407"));
expected = -0.37153223002125113;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:12.6158773"));
expected = 0.3967885416786294;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:14.6495256"));
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
}
}

Loading…
Cancel
Save