diff --git a/src/Avalonia.Base/Animation/Spring.cs b/src/Avalonia.Base/Animation/Spring.cs
new file mode 100644
index 0000000000..6e8bd204a7
--- /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 : AvaloniaObject
+{
+ 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/SpringSolver.cs b/src/Avalonia.Base/Animation/SpringSolver.cs
new file mode 100644
index 0000000000..de58224880
--- /dev/null
+++ b/src/Avalonia.Base/Animation/SpringSolver.cs
@@ -0,0 +1,70 @@
+// Ported from:
+// https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/SpringSolver.h
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+using System;
+
+namespace Avalonia.Animation;
+
+internal struct SpringSolver
+{
+ private double m_w0;
+ private double m_zeta;
+ private double m_wd;
+ private double m_A;
+ private double m_B;
+
+ public SpringSolver(double mass, double stiffness, double damping, double initialVelocity)
+ {
+ m_w0 = Math.Sqrt(stiffness / mass);
+ m_zeta = damping / (2 * Math.Sqrt(stiffness * mass));
+
+ if (m_zeta < 1) {
+ // Under-damped.
+ m_wd = m_w0 * Math.Sqrt(1 - m_zeta * m_zeta);
+ m_A = 1;
+ m_B = (m_zeta * m_w0 + -initialVelocity) / m_wd;
+ } else {
+ // Critically damped (ignoring over-damped case for now).
+ m_A = 1;
+ m_B = -initialVelocity + m_w0;
+ m_wd = 0;
+ }
+ }
+
+ public readonly double Solve(double t)
+ {
+ if (m_zeta < 1) {
+ // Under-damped
+ t = Math.Exp(-t * m_zeta * m_w0) * (m_A * Math.Cos(m_wd * t) + m_B * Math.Sin(m_wd * t));
+ } else {
+ // Critically damped (ignoring over-damped case for now).
+ t = (m_A + m_B * t) * Math.Exp(-t * m_w0);
+ }
+
+ // Map range from [1..0] to [0..1].
+ return 1 - t;
+ }
+}
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);
+ }
+}