Browse Source

Merge branch 'master' into fallback-null-owner-center-screen

pull/8995/head
Tiago Conceição 4 years ago
committed by GitHub
parent
commit
20e240215d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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. 31
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  8. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  9. 32
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  10. 134
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  11. 10
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  12. 11
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  13. 121
      src/Avalonia.Base/Utilities/SpringSolver.cs
  14. 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);
}
}

31
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
@ -27,8 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor
private HashSet<Visual> _dirty = new();
private HashSet<Visual> _recalculateChildren = new();
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
private Action<Task> _update;
private bool _updating;
internal CompositionTarget CompositionTarget;
@ -47,7 +47,6 @@ public class CompositingRenderer : IRendererWithCompositor
CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
_update = Update;
_invalidateScene = InvalidateScene;
}
/// <inheritdoc/>
@ -72,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor
if(_queuedUpdate)
return;
_queuedUpdate = true;
_compositor.InvokeWhenReadyForNextCommit(_update);
_compositor.InvokeBeforeNextCommit(_update);
}
/// <inheritdoc/>
@ -151,12 +150,6 @@ public class CompositingRenderer : IRendererWithCompositor
if (compositionChildren.Count == visualChildren.Count)
{
bool mismatch = false;
if (v.HasNonUniformZIndexChildren)
{
}
if (sortedChildren != null)
for (var c = 0; c < visualChildren.Count; c++)
{
@ -202,9 +195,6 @@ public class CompositingRenderer : IRendererWithCompositor
}
}
private void InvalidateScene() =>
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
private void UpdateCore()
{
_queuedUpdate = false;
@ -252,10 +242,15 @@ public class CompositingRenderer : IRendererWithCompositor
_recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene);
}
private void Update()
private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion)
{
await batchCompletion;
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
}
private void Update(Task batchCompletion)
{
if(_updating)
return;
@ -276,10 +271,10 @@ public class CompositingRenderer : IRendererWithCompositor
public void Paint(Rect rect)
{
Update();
QueueUpdate();
CompositionTarget.RequestRedraw();
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
Compositor.RequestCommitAsync().Wait();
Compositor.Commit().Wait();
else
CompositionTarget.ImmediateUIThreadRender();
}
@ -299,7 +294,7 @@ public class CompositingRenderer : IRendererWithCompositor
// Wait for the composition batch to be applied and rendered to guarantee that
// render target is not used anymore and can be safely disposed
if (Compositor.Loop.RunsInBackground)
_compositor.RequestCommitAsync().Wait();
_compositor.Commit().Wait();
}
/// <summary>

2
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -129,7 +129,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
internal void ImmediateUIThreadRender()
{
Compositor.RequestCommitAsync();
Compositor.Commit();
Compositor.Server.Render();
}
}

32
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition;
public partial class Compositor
{
/// <summary>
/// Creates a new CompositionTarget
/// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this);
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this)
{
Expression = expression
};
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
}

134
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -10,6 +10,7 @@ using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Threading;
using Avalonia.Utilities;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -24,16 +25,18 @@ namespace Avalonia.Rendering.Composition
{
internal IRenderLoop Loop { get; }
private ServerCompositor _server;
private bool _implicitBatchCommitQueued;
private Action _implicitBatchCommit;
private TaskCompletionSource<int>? _nextCommit;
private Action _commit;
private BatchStreamObjectPool<object?> _batchObjectPool = new();
private BatchStreamMemoryPool _batchMemoryPool = new();
private List<CompositionObject> _objectsForSerialization = new();
private Queue<Action<Task>> _invokeBeforeCommit = new();
internal ServerCompositor Server => _server;
private Task? _pendingBatch;
private readonly object _pendingBatchLock = new();
internal IEasing DefaultEasing { get; }
private List<Action>? _invokeOnNextCommit;
private readonly Stack<List<Action>> _invokeListPool = new();
private Task? _lastBatchCompleted;
/// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU
@ -44,21 +47,15 @@ namespace Avalonia.Rendering.Composition
{
Loop = loop;
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
_implicitBatchCommit = ImplicitBatchCommit;
_commit = () =>
{
Console.WriteLine("Dispatcher:Commit");
Commit();
};
DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f));
}
/// <summary>
/// Creates a new CompositionTarget
/// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
}
/// <summary>
/// Requests pending changes in the composition objects to be serialized and sent to the render thread
/// </summary>
@ -66,7 +63,35 @@ namespace Avalonia.Rendering.Composition
public Task RequestCommitAsync()
{
Dispatcher.UIThread.VerifyAccess();
var batch = new Batch();
if (_nextCommit == null)
{
_nextCommit = new TaskCompletionSource<int>();
var pending = _pendingBatch;
if (pending != null)
{
pending.ContinueWith(_ =>
{
Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition);
});
}
else
Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition);
}
return _nextCommit.Task;
}
internal Task Commit()
{
Dispatcher.UIThread.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use();
_nextCommit ??= new TaskCompletionSource<int>();
while (_invokeBeforeCommit.Count > 0)
_invokeBeforeCommit.Dequeue()(_nextCommit.Task);
var batch = new Batch(_nextCommit);
using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool))
{
@ -84,71 +109,36 @@ namespace Avalonia.Rendering.Composition
batch.CommitedAt = Server.Clock.Elapsed;
_server.EnqueueBatch(batch);
if (_invokeOnNextCommit != null)
ScheduleCommitCallbacks(batch.Completed);
return _lastBatchCompleted = batch.Completed;
}
async void ScheduleCommitCallbacks(Task task)
{
var list = _invokeOnNextCommit;
_invokeOnNextCommit = null;
await task;
foreach (var i in list!)
i();
list.Clear();
_invokeListPool.Push(list);
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this);
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this)
{
Expression = expression
};
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
private void QueueImplicitBatchCommit()
{
if(_implicitBatchCommitQueued)
return;
_implicitBatchCommitQueued = true;
Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch);
}
private void ImplicitBatchCommit()
{
_implicitBatchCommitQueued = false;
RequestCommitAsync();
lock (_pendingBatchLock)
{
_pendingBatch = _nextCommit.Task;
_pendingBatch.ContinueWith(t =>
{
lock (_pendingBatchLock)
{
if (_pendingBatch == t)
_pendingBatch = null;
}
}, TaskContinuationOptions.ExecuteSynchronously);
_nextCommit = null;
return _pendingBatch;
}
}
internal void RegisterForSerialization(CompositionObject compositionObject)
{
Dispatcher.UIThread.VerifyAccess();
_objectsForSerialization.Add(compositionObject);
QueueImplicitBatchCommit();
}
internal void InvokeOnNextCommit(Action action)
{
_invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new();
_invokeOnNextCommit.Add(action);
RequestCommitAsync();
}
public void InvokeWhenReadyForNextCommit(Action action)
internal void InvokeBeforeNextCommit(Action<Task> action)
{
if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted)
Dispatcher.UIThread.Post(action, DispatcherPriority.Composition);
else
_lastBatchCompleted.ContinueWith(
static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition),
action);
Dispatcher.UIThread.VerifyAccess();
_invokeBeforeCommit.Enqueue(action);
RequestCommitAsync();
}
}
}

10
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@ -15,16 +15,19 @@ namespace Avalonia.Rendering.Composition.Transport
{
private static long _nextSequenceId = 1;
private static ConcurrentBag<BatchStreamData> _pool = new();
private readonly TaskCompletionSource<int>? _tcs;
public long SequenceId { get; }
public Batch()
public Batch(TaskCompletionSource<int>? tcs)
{
_tcs = tcs;
SequenceId = Interlocked.Increment(ref _nextSequenceId);
if (!_pool.TryTake(out var lst))
lst = new BatchStreamData();
Changes = lst;
}
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
public BatchStreamData Changes { get; private set; }
public TimeSpan CommitedAt { get; set; }
@ -33,9 +36,8 @@ namespace Avalonia.Rendering.Composition.Transport
_pool.Add(Changes);
Changes = null!;
_tcs.TrySetResult(0);
_tcs?.TrySetResult(0);
}
public Task Completed => _tcs.Task;
}
}

11
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -61,21 +61,16 @@ namespace Avalonia.Threading
/// The job will be processed with the same priority as render.
/// </summary>
public static readonly DispatcherPriority Render = new(5);
/// <summary>
/// The job will be processed with the same priority as composition batch commit.
/// </summary>
public static readonly DispatcherPriority CompositionBatch = new(6);
/// <summary>
/// The job will be processed with the same priority as composition updates.
/// </summary>
public static readonly DispatcherPriority Composition = new(7);
public static readonly DispatcherPriority Composition = new(6);
/// <summary>
/// The job will be processed with the same priority as render.
/// </summary>
public static readonly DispatcherPriority Layout = new(8);
public static readonly DispatcherPriority Layout = new(7);
/// <summary>
/// The job will be processed with the same priority as data binding.
@ -85,7 +80,7 @@ namespace Avalonia.Threading
/// <summary>
/// The job will be processed before other asynchronous operations.
/// </summary>
public static readonly DispatcherPriority Send = new(9);
public static readonly DispatcherPriority Send = new(8);
/// <summary>
/// Maximum possible priority

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

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