diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index e6e62f86ed..823a0fbbef 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -39,6 +39,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/SpringAnimationsPage.xaml b/samples/RenderDemo/Pages/SpringAnimationsPage.xaml
new file mode 100644
index 0000000000..26d572bb3f
--- /dev/null
+++ b/samples/RenderDemo/Pages/SpringAnimationsPage.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs b/samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs
new file mode 100644
index 0000000000..78d3df2233
--- /dev/null
+++ b/samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace RenderDemo.Pages;
+
+public class SpringAnimationsPage : UserControl
+{
+ public SpringAnimationsPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/src/Avalonia.Base/Animation/Easings/SpringEasing.cs b/src/Avalonia.Base/Animation/Easings/SpringEasing.cs
new file mode 100644
index 0000000000..70e74c639f
--- /dev/null
+++ b/src/Avalonia.Base/Animation/Easings/SpringEasing.cs
@@ -0,0 +1,80 @@
+namespace Avalonia.Animation.Easings;
+
+///
+/// Eases a value using a user-defined spring formula.
+///
+public class SpringEasing : Easing
+{
+ private readonly Spring _internalSpring;
+
+ ///
+ /// The spring mass.
+ ///
+ public double Mass
+ {
+ get => _internalSpring.Mass;
+ set
+ {
+ _internalSpring.Mass = value;
+ }
+ }
+
+ ///
+ /// The spring stiffness.
+ ///
+ public double Stiffness
+ {
+ get => _internalSpring.Stiffness;
+ set
+ {
+ _internalSpring.Stiffness = value;
+ }
+ }
+
+ ///
+ /// The spring damping.
+ ///
+ public double Damping
+ {
+ get => _internalSpring.Damping;
+ set
+ {
+ _internalSpring.Damping = value;
+ }
+ }
+
+ ///
+ /// The spring initial velocity.
+ ///
+ public double InitialVelocity
+ {
+ get => _internalSpring.InitialVelocity;
+ set
+ {
+ _internalSpring.InitialVelocity = value;
+ }
+ }
+
+ public SpringEasing(double mass = 0d, double stiffness = 0d, double damping = 0d, double initialVelocity = 0d)
+ {
+ _internalSpring = new Spring();
+
+ Mass = mass;
+ Stiffness = stiffness;
+ Damping = damping;
+ InitialVelocity = initialVelocity;
+ }
+
+ public SpringEasing(Spring keySpline)
+ {
+ _internalSpring = keySpline;
+ }
+
+ public SpringEasing()
+ {
+ _internalSpring = new Spring();
+ }
+
+ ///
+ public override double Ease(double progress) => _internalSpring.GetSpringProgress(progress);
+}
diff --git a/src/Avalonia.Base/Animation/Spring.cs b/src/Avalonia.Base/Animation/Spring.cs
new file mode 100644
index 0000000000..03ada9196d
--- /dev/null
+++ b/src/Avalonia.Base/Animation/Spring.cs
@@ -0,0 +1,143 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Utilities;
+
+namespace Avalonia.Animation;
+
+///
+/// Determines how an animation is used based on spring formula.
+///
+[TypeConverter(typeof(SpringTypeConverter))]
+public class Spring
+{
+ private SpringSolver _springSolver;
+ private double _mass;
+ private double _stiffness;
+ private double _damping;
+ private double _initialVelocity;
+ private bool _isDirty;
+
+ ///
+ /// Create a .
+ ///
+ public Spring()
+ {
+ _mass = 0.0;
+ _stiffness = 0.0;
+ _damping = 0.0;
+ _initialVelocity = 0.0;
+ _isDirty = true;
+ }
+
+ ///
+ /// Create a with the given parameters.
+ ///
+ /// The spring mass.
+ /// The spring stiffness.
+ /// The spring damping.
+ /// The spring initial velocity.
+ public Spring(double mass, double stiffness, double damping, double initialVelocity)
+ {
+ _mass = mass;
+ _stiffness = stiffness;
+ _damping = damping;
+ _initialVelocity = initialVelocity;
+ _isDirty = true;
+ }
+
+ ///
+ /// Parse a from a string. The string needs to contain 4 values in it.
+ ///
+ /// string with 4 values in it
+ /// culture of the string
+ /// Thrown if the string does not have 4 values
+ /// A with the appropriate values set
+ public static Spring Parse(string value, CultureInfo? culture)
+ {
+ if (culture is null)
+ {
+ culture = CultureInfo.InvariantCulture;
+ }
+
+ using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
+ return new Spring(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
+ }
+
+ ///
+ /// The spring mass.
+ ///
+ public double Mass
+ {
+ get => _mass;
+ set
+ {
+ _mass = value;
+ _isDirty = true;
+ }
+ }
+
+ ///
+ /// The spring stiffness.
+ ///
+ public double Stiffness
+ {
+ get => _stiffness;
+ set
+ {
+ _stiffness = value;
+ _isDirty = true;
+ }
+ }
+
+ ///
+ /// The spring damping.
+ ///
+ public double Damping
+ {
+ get => _damping;
+ set
+ {
+ _damping = value;
+ _isDirty = true;
+ }
+ }
+
+ ///
+ /// The spring initial velocity.
+ ///
+ public double InitialVelocity
+ {
+ get => _initialVelocity;
+ set
+ {
+ _initialVelocity = value;
+ _isDirty = true;
+ }
+ }
+
+ ///
+ /// Calculates spring progress from a linear progress.
+ ///
+ /// the linear progress
+ /// The spring progress
+ public double GetSpringProgress(double linearProgress)
+ {
+ if (_isDirty)
+ {
+ Build();
+ }
+
+ return _springSolver.Solve(linearProgress);
+ }
+
+ ///
+ /// Create cached spring solver.
+ ///
+ private void Build()
+ {
+ _springSolver = new SpringSolver(_mass, _stiffness, _damping, _initialVelocity);
+ _isDirty = false;
+ }
+}
diff --git a/src/Avalonia.Base/Animation/SpringTypeConverter.cs b/src/Avalonia.Base/Animation/SpringTypeConverter.cs
new file mode 100644
index 0000000000..cfcf4c4a14
--- /dev/null
+++ b/src/Avalonia.Base/Animation/SpringTypeConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Avalonia.Animation;
+
+///
+/// Converts string values to values.
+///
+public class SpringTypeConverter : TypeConverter
+{
+ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
+ {
+ return Spring.Parse((string)value, CultureInfo.InvariantCulture);
+ }
+}
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index d091b9072d..1cb29e4e37 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -35,8 +35,10 @@
+
+
diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
index 0288dbce91..6232fa39a1 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
@@ -42,7 +42,7 @@ namespace Avalonia.Data.Core.Plugins
if (target is IObservable
-
+
+
Loading...
+
+
+
-
+