From 8314eb9dddc21223af84c9bc251767d06cae430d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 19 Sep 2022 20:31:24 +0200 Subject: [PATCH] Add spring object --- src/Avalonia.Base/Animation/Spring.cs | 143 ++++++++++++++++++ src/Avalonia.Base/Animation/SpringSolver.cs | 70 +++++++++ .../Animation/SpringTypeConverter.cs | 21 +++ 3 files changed, 234 insertions(+) create mode 100644 src/Avalonia.Base/Animation/Spring.cs create mode 100644 src/Avalonia.Base/Animation/SpringSolver.cs create mode 100644 src/Avalonia.Base/Animation/SpringTypeConverter.cs 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); + } +}