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/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index e47c8e7e31..98a6a3600e 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/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 _dirty = new();
private HashSet _recalculateChildren = new();
private bool _queuedUpdate;
- private Action _update;
- private Action _invalidateScene;
+ private Action _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;
}
///
@@ -72,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor
if(_queuedUpdate)
return;
_queuedUpdate = true;
- _compositor.InvokeWhenReadyForNextCommit(_update);
+ _compositor.InvokeBeforeNextCommit(_update);
}
///
@@ -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();
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
index eb499604e0..d8a608651b 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -129,7 +129,7 @@ namespace Avalonia.Rendering.Composition
///
internal void ImmediateUIThreadRender()
{
- Compositor.RequestCommitAsync();
+ Compositor.Commit();
Compositor.Server.Render();
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
new file mode 100644
index 0000000000..1a13d23acd
--- /dev/null
+++ b/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
+{
+ ///
+ /// Creates a new CompositionTarget
+ ///
+ /// A factory method to create IRenderTarget to be called from the render thread
+ ///
+ public CompositionTarget CreateCompositionTarget(Func 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);
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
index 10360f7874..21c7dd6bf9 100644
--- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs
+++ b/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 License.md
@@ -24,16 +25,18 @@ namespace Avalonia.Rendering.Composition
{
internal IRenderLoop Loop { get; }
private ServerCompositor _server;
- private bool _implicitBatchCommitQueued;
- private Action _implicitBatchCommit;
+ private TaskCompletionSource? _nextCommit;
+ private Action _commit;
private BatchStreamObjectPool