Browse Source

Merge remote-tracking branch 'origin/master' into ios-ime-responder

pull/8963/head
Dan Walmsley 3 years ago
parent
commit
b94cf8d920
  1. 3
      samples/RenderDemo/MainWindow.xaml
  2. 35
      samples/RenderDemo/Pages/SpringAnimationsPage.xaml
  3. 17
      samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs
  4. 80
      src/Avalonia.Base/Animation/Easings/SpringEasing.cs
  5. 143
      src/Avalonia.Base/Animation/Spring.cs
  6. 21
      src/Avalonia.Base/Animation/SpringTypeConverter.cs
  7. 121
      src/Avalonia.Base/Utilities/SpringSolver.cs
  8. 6
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
  9. 118
      tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs

3
samples/RenderDemo/MainWindow.xaml

@ -39,6 +39,9 @@
<TabItem Header="Custom Animator">
<pages:CustomAnimatorPage />
</TabItem>
<TabItem Header="Spring Animation">
<pages:SpringAnimationsPage />
</TabItem>
<TabItem Header="Clipping">
<pages:ClippingPage />
</TabItem>

35
samples/RenderDemo/Pages/SpringAnimationsPage.xaml

@ -0,0 +1,35 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.SpringAnimationsPage"
MaxWidth="600">
<UserControl.Resources>
<SpringEasing x:Key="SpringEasing" Mass="1" Stiffness="2000" Damping="20" InitialVelocity="0" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Border.spring">
<Style.Animations>
<Animation Duration="0:0:0.900"
IterationCount="Infinite"
PlaybackDirection="Normal"
Easing="{DynamicResource SpringEasing}">
<KeyFrame Cue="0%" KeySpline="">
<Setter Property="TranslateTransform.X" Value="-300"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid>
<Border Classes="spring" Background="Red" Width="50" Height="50">
<Border.RenderTransform>
<TransformGroup>
<TranslateTransform />
</TransformGroup>
</Border.RenderTransform>
</Border>
</Grid>
</UserControl>

17
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);
}
}

80
src/Avalonia.Base/Animation/Easings/SpringEasing.cs

@ -0,0 +1,80 @@
namespace Avalonia.Animation.Easings;
/// <summary>
/// Eases a <see cref="double"/> value using a user-defined spring formula.
/// </summary>
public class SpringEasing : Easing
{
private readonly Spring _internalSpring;
/// <summary>
/// The spring mass.
/// </summary>
public double Mass
{
get => _internalSpring.Mass;
set
{
_internalSpring.Mass = value;
}
}
/// <summary>
/// The spring stiffness.
/// </summary>
public double Stiffness
{
get => _internalSpring.Stiffness;
set
{
_internalSpring.Stiffness = value;
}
}
/// <summary>
/// The spring damping.
/// </summary>
public double Damping
{
get => _internalSpring.Damping;
set
{
_internalSpring.Damping = value;
}
}
/// <summary>
/// The spring initial velocity.
/// </summary>
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();
}
/// <inheritdoc/>
public override double Ease(double progress) => _internalSpring.GetSpringProgress(progress);
}

143
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;
/// <summary>
/// Determines how an animation is used based on spring formula.
/// </summary>
[TypeConverter(typeof(SpringTypeConverter))]
public class Spring
{
private SpringSolver _springSolver;
private double _mass;
private double _stiffness;
private double _damping;
private double _initialVelocity;
private bool _isDirty;
/// <summary>
/// Create a <see cref="Spring"/>.
/// </summary>
public Spring()
{
_mass = 0.0;
_stiffness = 0.0;
_damping = 0.0;
_initialVelocity = 0.0;
_isDirty = true;
}
/// <summary>
/// Create a <see cref="Spring"/> with the given parameters.
/// </summary>
/// <param name="mass">The spring mass.</param>
/// <param name="stiffness">The spring stiffness.</param>
/// <param name="damping">The spring damping.</param>
/// <param name="initialVelocity">The spring initial velocity.</param>
public Spring(double mass, double stiffness, double damping, double initialVelocity)
{
_mass = mass;
_stiffness = stiffness;
_damping = damping;
_initialVelocity = initialVelocity;
_isDirty = true;
}
/// <summary>
/// Parse a <see cref="Spring"/> from a string. The string needs to contain 4 values in it.
/// </summary>
/// <param name="value">string with 4 values in it</param>
/// <param name="culture">culture of the string</param>
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
/// <returns>A <see cref="Spring"/> with the appropriate values set</returns>
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());
}
/// <summary>
/// The spring mass.
/// </summary>
public double Mass
{
get => _mass;
set
{
_mass = value;
_isDirty = true;
}
}
/// <summary>
/// The spring stiffness.
/// </summary>
public double Stiffness
{
get => _stiffness;
set
{
_stiffness = value;
_isDirty = true;
}
}
/// <summary>
/// The spring damping.
/// </summary>
public double Damping
{
get => _damping;
set
{
_damping = value;
_isDirty = true;
}
}
/// <summary>
/// The spring initial velocity.
/// </summary>
public double InitialVelocity
{
get => _initialVelocity;
set
{
_initialVelocity = value;
_isDirty = true;
}
}
/// <summary>
/// Calculates spring progress from a linear progress.
/// </summary>
/// <param name="linearProgress">the linear progress</param>
/// <returns>The spring progress</returns>
public double GetSpringProgress(double linearProgress)
{
if (_isDirty)
{
Build();
}
return _springSolver.Solve(linearProgress);
}
/// <summary>
/// Create cached spring solver.
/// </summary>
private void Build()
{
_springSolver = new SpringSolver(_mass, _stiffness, _damping, _initialVelocity);
_isDirty = false;
}
}

21
src/Avalonia.Base/Animation/SpringTypeConverter.cs

@ -0,0 +1,21 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Animation;
/// <summary>
/// Converts string values to <see cref="Spring"/> values.
/// </summary>
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);
}
}

121
src/Avalonia.Base/Utilities/SpringSolver.cs

@ -0,0 +1,121 @@
// 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.Utilities;
internal struct SpringSolver
{
private double m_w0;
private double m_zeta;
private double m_wd;
private double m_A;
private double m_B;
/// <summary>
///
/// </summary>
/// <param name="period">The time period.</param>
/// <param name="zeta">The damping ratio.</param>
/// <param name="initialVelocity"></param>
public SpringSolver(TimeSpan period, double zeta, double initialVelocity)
: this(
2 * Math.PI / period.TotalSeconds,
zeta,
initialVelocity)
{
// T is time period [s]
// T = (2*PI / sqrt(k)) * sqrt(m)
// ωn is natural frequency of the system [Hz] [1/s]
// ωn = 2*PI / T
}
/// <summary>
///
/// </summary>
/// <param name="m">The mass of the oscillating body.</param>
/// <param name="k">The stiffness of the oscillated body (spring constant).</param>
/// <param name="c">The actual damping.</param>
/// <param name="initialVelocity">The initial velocity.</param>
public SpringSolver(double m, double k, double c, double initialVelocity)
: this(
Math.Sqrt(k / m), // ωn
c / (2 * Math.Sqrt(k * m)), // c / Cc
initialVelocity)
{
// ωn is natural frequency of the system [Hz] [1/s]
// ωn = sqrt(k / m)
// Cc is critical damping coefficient
// Cc = 2 * Sqrt(k * m)
// Cc = 2 * m * wn
// Cc = 2 * m * Sqrt(k / m)
// ζ is damping ratio (Greek letter zeta)
// ζ = m_zeta = c / Cc
}
/// <summary>
///
/// </summary>
/// <param name="ωn">The the natural frequency of the system [rad/s].</param>
/// <param name="zeta">The damping ratio.</param>
/// <param name="initialVelocity"></param>
public SpringSolver(double ωn, double zeta, double initialVelocity)
{
m_w0 = ωn;
m_zeta = zeta;
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;
}
}

6
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs

@ -120,10 +120,14 @@ namespace Avalonia.Win32.WinRT.Composition
private void RunLoop()
{
var cts = new CancellationTokenSource();
AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
cts.Cancel();
using (var act = _compositor5.RequestCommitAsync())
act.SetCompleted(new RunLoopHandler(this));
while (true)
while (!cts.IsCancellationRequested)
{
UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0);
lock (_pumpLock)

118
tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs

@ -0,0 +1,118 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Base.UnitTests.Animation;
public class SpringTests
{
[Theory]
[InlineData("1,2 3,4")]
public void Can_Parse_Spring_Via_TypeConverter(string input)
{
var conv = new SpringTypeConverter();
var spring = (Spring)conv.ConvertFrom(input);
Assert.Equal(1, spring.Mass);
Assert.Equal(2, spring.Stiffness);
Assert.Equal(3, spring.Damping);
Assert.Equal(4, spring.InitialVelocity);
}
[Theory]
[InlineData("1,2F,3,4")]
[InlineData("Foo,Bar,Fee,Buzz")]
public void Can_Handle_Invalid_String_Via_TypeConverter(string input)
{
var conv = new SpringTypeConverter();
Assert.ThrowsAny<Exception>(() => (Spring)conv.ConvertFrom(input));
}
[Fact]
public void SplineEasing_Can_Be_Mutated()
{
var easing = new SpringEasing(1, 1, 1, 0);
Assert.Equal(0, easing.Ease(0));
Assert.Equal(0.34029984660829826, easing.Ease(1));
easing.Mass = 2;
easing.Stiffness = 2;
easing.Damping = 2;
easing.InitialVelocity = 1;
Assert.NotEqual(0.05136985716812037, easing.Ease(0.5));
}
[Fact]
public void Check_SpringEasing_Handled_properly()
{
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 Avalonia.Animation.Animation()
{
Duration = TimeSpan.FromSeconds(5),
Children =
{
keyframe1,
keyframe2
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate,
Easing = new SpringEasing(1, 10, 1, 0)
};
var rotateTransform = new RotateTransform(-2.5);
var rect = new Rectangle()
{
RenderTransform = rotateTransform
};
var clock = new TestClock();
var animationRun = animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 5.522828945000075);
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));
var expected = -2.499763294237805;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:11.2655407"));
expected = -1.1011448950348934;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:12.6158773"));
expected = 2.1264981706749007;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:14.6495256"));
expected = 5.4337608446234782;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
}
Loading…
Cancel
Save