diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs
index 5b0dea6c60..e006459652 100644
--- a/src/Avalonia.Animation/Easing/Easing.cs
+++ b/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 the instance of the parsed type.
public static Easing Parse(string e)
{
+ if (e.Contains(','))
+ {
+ return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
+ }
+
if (_easingTypes == null)
{
_easingTypes = new Dictionary();
diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs
new file mode 100644
index 0000000000..975fcc4746
--- /dev/null
+++ b/src/Avalonia.Animation/Easing/SplineEasing.cs
@@ -0,0 +1,85 @@
+namespace Avalonia.Animation.Easings
+{
+ ///
+ /// Eases a value
+ /// using a user-defined cubic bezier curve.
+ /// Good for custom easing functions that doesn't quite
+ /// fit with the built-in ones.
+ ///
+ public class SplineEasing : Easing
+ {
+ ///
+ /// X coordinate of the first control point
+ ///
+ public double X1
+ {
+ get => _internalKeySpline.ControlPointX1;
+ set
+ {
+ _internalKeySpline.ControlPointX1 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the first control point
+ ///
+ public double Y1
+ {
+ get => _internalKeySpline.ControlPointY1;
+ set
+ {
+ _internalKeySpline.ControlPointY1 = value;
+ }
+ }
+
+ ///
+ /// X coordinate of the second control point
+ ///
+ public double X2
+ {
+ get => _internalKeySpline.ControlPointX2;
+ set
+ {
+ _internalKeySpline.ControlPointX2 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the second control point
+ ///
+ 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();
+ }
+
+ ///
+ public override double Ease(double progress) =>
+ _internalKeySpline.GetSplineProgress(progress);
+ }
+}
diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs
index 5a4f7a15a3..a6e9769186 100644
--- a/src/Avalonia.Animation/KeySpline.cs
+++ b/src/Avalonia.Animation/KeySpline.cs
@@ -81,7 +81,10 @@ namespace Avalonia.Animation
/// A with the appropriate values set
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;
+ }
}
///
@@ -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;
+ }
}
///
@@ -330,20 +343,4 @@ namespace Avalonia.Animation
}
}
}
-
- ///
- /// Converts string values to values
- ///
- 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);
- }
- }
}
diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs
new file mode 100644
index 0000000000..cd7427a37d
--- /dev/null
+++ b/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
+{
+ ///
+ /// Converts string values to values
+ ///
+ 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);
+ }
+ }
+}
diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
index df7c0693e1..fa2ed61e65 100644
--- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
+++ b/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(() => (KeySpline)conv.ConvertFrom(input));
+ }
+
[Theory]
[InlineData(0.00)]
[InlineData(0.50)]
@@ -46,6 +57,22 @@ namespace Avalonia.Animation.UnitTests
Assert.Throws(() => 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);
+ }
}
}