فهرست منبع

Merge branch 'master' into shape-render-optimization

Dan Walmsley 4 سال پیش
والد
کامیت
bcbccccb73
22فایلهای تغییر یافته به همراه279 افزوده شده و 150 حذف شده
  1. 20 0
      src/Avalonia.Animation/AnimatorDrivenTransition.cs
  2. 32 0
      src/Avalonia.Animation/AnimatorTransitionObservable.cs
  3. 2 7
      src/Avalonia.Animation/Clock.cs
  4. 3 9
      src/Avalonia.Animation/ClockBase.cs
  5. 2 4
      src/Avalonia.Animation/Transition.cs
  6. 52 8
      src/Avalonia.Animation/TransitionInstance.cs
  7. 58 0
      src/Avalonia.Animation/TransitionObservableBase.cs
  8. 1 12
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  9. 1 12
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  10. 1 12
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  11. 1 2
      src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
  12. 1 12
      src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs
  13. 12 0
      src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs
  14. 1 12
      src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
  15. 1 12
      src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
  16. 1 12
      src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
  17. 1 12
      src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
  18. 8 10
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  19. 1 12
      src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
  20. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  21. 1 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  22. 78 0
      tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs

+ 20 - 0
src/Avalonia.Animation/AnimatorDrivenTransition.cs

@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// <see cref="Transition{T}"/> using an <see cref="Animator{T}"/> to transition between values.
+    /// </summary>
+    /// <typeparam name="T">Type of the transitioned value.</typeparam>
+    /// <typeparam name="TAnimator">Type of the animator.</typeparam>
+    public abstract class AnimatorDrivenTransition<T, TAnimator> : Transition<T> where TAnimator : Animator<T>, new()
+    {
+        private static readonly TAnimator s_animator = new TAnimator();
+
+        public override IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue)
+        {
+            return new AnimatorTransitionObservable<T, TAnimator>(s_animator, progress, Easing, oldValue, newValue);
+        }
+    }
+}

+ 32 - 0
src/Avalonia.Animation/AnimatorTransitionObservable.cs

@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Animation.Animators;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition observable based on an <see cref="Animator{T}"/> producing a value.
+    /// </summary>
+    /// <typeparam name="T">Type of the transitioned value.</typeparam>
+    /// <typeparam name="TAnimator">Type of the animator.</typeparam>
+    public class AnimatorTransitionObservable<T, TAnimator> : TransitionObservableBase<T> where TAnimator : Animator<T>
+    {
+        private readonly TAnimator _animator;
+        private readonly Easing _easing;
+        private readonly T _oldValue;
+        private readonly T _newValue;
+
+        public AnimatorTransitionObservable(TAnimator animator, IObservable<double> progress, Easing easing, T oldValue, T newValue) : base(progress, easing)
+        {
+            _animator = animator;
+            _easing = easing;
+            _oldValue = oldValue;
+            _newValue = newValue;
+        }
+
+        protected override T ProduceValue(double progress)
+        {
+            return _animator.Interpolate(progress, _oldValue, _newValue);
+        }
+    }
+}

+ 2 - 7
src/Avalonia.Animation/Clock.cs

@@ -1,8 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Text;
-using Avalonia.Reactive;
 
 namespace Avalonia.Animation
 {
@@ -10,10 +6,9 @@ namespace Avalonia.Animation
     {
         public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
 
-        private IDisposable _parentSubscription;
+        private readonly IDisposable _parentSubscription;
 
-        public Clock()
-            :this(GlobalClock)
+        public Clock() : this(GlobalClock)
         {
         }
         

+ 3 - 9
src/Avalonia.Animation/ClockBase.cs

@@ -1,16 +1,11 @@
 using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Text;
 using Avalonia.Reactive;
 
 namespace Avalonia.Animation
 {
     public class ClockBase : IClock
     {
-        private ClockObservable _observable;
-
-        private IObservable<TimeSpan> _connectedObservable;
+        private readonly ClockObservable _observable;
 
         private TimeSpan? _previousTime;
         private TimeSpan _internalTime;
@@ -18,7 +13,6 @@ namespace Avalonia.Animation
         protected ClockBase()
         {
             _observable = new ClockObservable();
-            _connectedObservable = _observable.Publish().RefCount();
         }
 
         protected bool HasSubscriptions => _observable.HasSubscriptions;
@@ -58,10 +52,10 @@ namespace Avalonia.Animation
 
         public IDisposable Subscribe(IObserver<TimeSpan> observer)
         {
-            return _connectedObservable.Subscribe(observer);
+            return _observable.Subscribe(observer);
         }
 
-        private class ClockObservable : LightweightObservableBase<TimeSpan>
+        private sealed class ClockObservable : LightweightObservableBase<TimeSpan>
         {
             public bool HasSubscriptions { get; private set; }
             public void Pulse(TimeSpan time) => PublishNext(time);

+ 2 - 4
src/Avalonia.Animation/Transition`1.cs → src/Avalonia.Animation/Transition.cs

@@ -1,7 +1,5 @@
-using System;
-using System.Reactive.Linq;
+using System;
 using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
 
 namespace Avalonia.Animation
 {
@@ -56,4 +54,4 @@ namespace Avalonia.Animation
             return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
         }
     }
-}
+}

+ 52 - 8
src/Avalonia.Animation/TransitionInstance.cs

@@ -1,8 +1,5 @@
-using Avalonia.Metadata;
 using System;
-using System.Reactive.Linq;
-using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
+using System.Runtime.ExceptionServices;
 using Avalonia.Reactive;
 using Avalonia.Utilities;
 
@@ -11,13 +8,13 @@ namespace Avalonia.Animation
     /// <summary>
     /// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
     /// </summary>
-    internal class TransitionInstance : SingleSubscriberObservableBase<double>
+    internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
     {
         private IDisposable _timerSubscription;
         private TimeSpan _delay;
         private TimeSpan _duration;
         private readonly IClock _baseClock;
-        private IClock _clock;
+        private TransitionClock _clock;
 
         public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
         {
@@ -75,9 +72,56 @@ namespace Avalonia.Animation
 
         protected override void Subscribed()
         {
-            _clock = new Clock(_baseClock);
-            _timerSubscription = _clock.Subscribe(TimerTick);
+            _clock = new TransitionClock(_baseClock);
+            _timerSubscription = _clock.Subscribe(this);
             PublishNext(0.0d);
         }
+
+        void IObserver<TimeSpan>.OnCompleted()
+        {
+            PublishCompleted();
+        }
+
+        void IObserver<TimeSpan>.OnError(Exception error)
+        {
+            PublishError(error);
+        }
+
+        void IObserver<TimeSpan>.OnNext(TimeSpan value)
+        {
+            TimerTick(value);
+        }
+
+        /// <summary>
+        /// TODO: This clock is still fairly expensive due to <see cref="ClockBase"/> implementation.
+        /// </summary>
+        private sealed class TransitionClock : ClockBase, IObserver<TimeSpan>
+        {
+            private readonly IDisposable _parentSubscription;
+
+            public TransitionClock(IClock parent)
+            {
+                _parentSubscription = parent.Subscribe(this);
+            }
+
+            protected override void Stop()
+            {
+                _parentSubscription.Dispose();
+            }
+
+            void IObserver<TimeSpan>.OnNext(TimeSpan value)
+            {
+                Pulse(value);
+            }
+
+            void IObserver<TimeSpan>.OnCompleted()
+            {
+            }
+
+            void IObserver<TimeSpan>.OnError(Exception error)
+            {
+                ExceptionDispatchInfo.Capture(error).Throw();
+            }
+        }
     }
 }

+ 58 - 0
src/Avalonia.Animation/TransitionObservableBase.cs

@@ -0,0 +1,58 @@
+using System;
+using Avalonia.Animation.Easings;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Provides base for observables implementing transitions.
+    /// </summary>
+    /// <typeparam name="T">Type of the transitioned value.</typeparam>
+    public abstract class TransitionObservableBase<T> : SingleSubscriberObservableBase<T>, IObserver<double>
+    {
+        private readonly Easing _easing;
+        private readonly IObservable<double> _progress;
+        private IDisposable? _progressSubscription;
+
+        protected TransitionObservableBase(IObservable<double> progress, Easing easing)
+        {
+            _progress = progress;
+            _easing = easing;
+        }
+
+        /// <summary>
+        /// Produces value at given progress time point.
+        /// </summary>
+        /// <param name="progress">Transition progress.</param>
+        protected abstract T ProduceValue(double progress);
+
+        protected override void Subscribed()
+        {
+            _progressSubscription = _progress.Subscribe(this);
+        }
+
+        protected override void Unsubscribed()
+        {
+            _progressSubscription?.Dispose();
+        }
+
+        void IObserver<double>.OnCompleted()
+        {
+            PublishCompleted();
+        }
+
+        void IObserver<double>.OnError(Exception error)
+        {
+            PublishError(error);
+        }
+
+        void IObserver<double>.OnNext(double value)
+        {
+            double progress = _easing.Ease(value);
+
+            PublishNext(ProduceValue(progress));
+        }
+    }
+}

+ 1 - 12
src/Avalonia.Animation/Transitions/DoubleTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
     /// </summary>  
-    public class DoubleTransition : Transition<double>
+    public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
     {
-        private static readonly DoubleAnimator s_animator = new DoubleAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 12
src/Avalonia.Animation/Transitions/FloatTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
     /// </summary>  
-    public class FloatTransition : Transition<float>
+    public class FloatTransition : AnimatorDrivenTransition<float, FloatAnimator>
     {
-        private static readonly FloatAnimator s_animator = new FloatAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 12
src/Avalonia.Animation/Transitions/IntegerTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
     /// </summary>  
-    public class IntegerTransition : Transition<int>
+    public class IntegerTransition : AnimatorDrivenTransition<int, Int32Animator>
     {
-        private static readonly Int32Animator s_animator = new Int32Animator();
-
-        /// <inheritdocs/>
-        public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 2
src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs

@@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (InputPassThroughElement is object)
             {
-                var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value);
-                var hit = VisualRoot.GetVisualAt(p, x => x != this);
+                var hit = VisualRoot.GetVisualAt(point, x => x != this);
 
                 if (hit is object)
                 {

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 using Avalonia.Media;
 
@@ -9,15 +6,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="BoxShadows"/> type.
     /// </summary>  
-    public class BoxShadowsTransition : Transition<BoxShadows>
+    public class BoxShadowsTransition : AnimatorDrivenTransition<BoxShadows, BoxShadowsAnimator>
     {
-        private static readonly BoxShadowsAnimator s_animator = new BoxShadowsAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<BoxShadows> DoTransition(IObservable<double> progress, BoxShadows oldValue, BoxShadows newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 12 - 0
src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs

@@ -0,0 +1,12 @@
+using Avalonia.Animation.Animators;
+using Avalonia.Media;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Color"/> type.
+    /// </summary>
+    public class ColorTransition : AnimatorDrivenTransition<Color, ColorAnimator>
+    {
+    }
+}

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="CornerRadius"/> type.
     /// </summary>  
-    public class CornerRadiusTransition : Transition<CornerRadius>
+    public class CornerRadiusTransition : AnimatorDrivenTransition<CornerRadius, CornerRadiusAnimator>
     {
-        private static readonly CornerRadiusAnimator s_animator = new CornerRadiusAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<CornerRadius> DoTransition(IObservable<double> progress, CornerRadius oldValue, CornerRadius newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Point"/> type.
     /// </summary>  
-    public class PointTransition : Transition<Point>
+    public class PointTransition : AnimatorDrivenTransition<Point, PointAnimator>
     {
-        private static readonly PointAnimator s_animator = new PointAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<Point> DoTransition(IObservable<double> progress, Point oldValue, Point newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Size"/> type.
     /// </summary>  
-    public class SizeTransition : Transition<Size>
+    public class SizeTransition : AnimatorDrivenTransition<Size, SizeAnimator>
     {
-        private static readonly SizeAnimator s_animator = new SizeAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<Size> DoTransition(IObservable<double> progress, Size oldValue, Size newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Thickness"/> type.
     /// </summary>  
-    public class ThicknessTransition : Transition<Thickness>
+    public class ThicknessTransition : AnimatorDrivenTransition<Thickness, ThicknessAnimator>
     {
-        private static readonly ThicknessAnimator s_animator = new ThicknessAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<Thickness> DoTransition(IObservable<double> progress, Thickness oldValue, Thickness newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 8 - 10
src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs

@@ -1,28 +1,26 @@
 using System;
-using System.Reactive.Linq;
 using Avalonia.Animation.Animators;
 using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+#nullable enable
 
 namespace Avalonia.Animation
 {
     public class TransformOperationsTransition : Transition<ITransform>
     {
-        private static readonly TransformOperationsAnimator _operationsAnimator =  new TransformOperationsAnimator();
+        private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator();
 
-        public override IObservable<ITransform> DoTransition(IObservable<double> progress,
+        public override IObservable<ITransform> DoTransition(
+            IObservable<double> progress,
             ITransform oldValue,
             ITransform newValue)
         {
             var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
             var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
 
-            return progress
-                .Select(p =>
-                {
-                    var f = Easing.Ease(p);
-
-                    return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
-                });
+            return new AnimatorTransitionObservable<TransformOperations, TransformOperationsAnimator>(
+                s_operationsAnimator, progress, Easing, oldTransform, newTransform);
         }
     }
 }

+ 1 - 12
src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs

@@ -1,6 +1,3 @@
-using System;
-using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,15 +5,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Vector"/> type.
     /// </summary>  
-    public class VectorTransition : Transition<Vector>
+    public class VectorTransition : AnimatorDrivenTransition<Vector, VectorAnimator>
     {
-        private static readonly VectorAnimator s_animator = new VectorAnimator();
-
-        /// <inheritdocs/>
-        public override IObservable<Vector> DoTransition(IObservable<double> progress, Vector oldValue, Vector newValue)
-        {
-            return progress
-                .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
-        }
     }
 }

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -257,7 +257,7 @@ namespace Avalonia.Rendering.SceneGraph
                     if (childCount == 0 || wasVisited)
                     {
                         if ((wasVisited || FilterAndClip(node, ref clip)) &&
-                            (node.Visual is ICustomSimpleHitTest custom ? custom.HitTest(_point) : node.HitTest(_point)))
+                            (node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point)))
                         {
                             _current = node.Visual;
 

+ 1 - 1
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -900,7 +900,7 @@ namespace Avalonia.Win32
                 IntPtr.Zero,
                 rcWindow.left, rcWindow.top,
                 rcClient.Width, rcClient.Height,
-                SetWindowPosFlags.SWP_FRAMECHANGED);
+                SetWindowPosFlags.SWP_FRAMECHANGED | SetWindowPosFlags.SWP_NOACTIVATE);
 
             if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
             {

+ 78 - 0
tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Runtime.CompilerServices;
+using Avalonia.Animation;
+using Avalonia.Animation.Animators;
+using Avalonia.Layout;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Animations
+{
+    [MemoryDiagnoser]
+    public class TransitionBenchmark
+    {
+        private readonly AddValueObserver _observer;
+        private readonly List<double> _producedValues;
+        private readonly Subject<double> _timeProducer;
+        private readonly DoubleTransition _transition;
+
+        public TransitionBenchmark()
+        {
+            _transition = new DoubleTransition
+            {
+                Duration = TimeSpan.FromMilliseconds(FrameCount), Property = Layoutable.WidthProperty
+            };
+
+            _timeProducer = new Subject<double>();
+            _producedValues = new List<double>(FrameCount);
+
+            _observer = new AddValueObserver(_producedValues);
+        }
+
+        [Params(10, 100)] public int FrameCount { get; set; }
+
+        [Benchmark]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public void NewTransition()
+        {
+            var transitionObs = _transition.DoTransition(_timeProducer, 0, 1);
+
+            _producedValues.Clear();
+
+            using var transitionSub = transitionObs.Subscribe(_observer);
+
+            for (int i = 0; i < FrameCount; i++)
+            {
+                _timeProducer.OnNext(i / 1000d);
+            }
+
+            Debug.Assert(_producedValues.Count == FrameCount);
+        }
+
+        private class AddValueObserver : IObserver<double>
+        {
+            private readonly List<double> _values;
+
+            public AddValueObserver(List<double> values)
+            {
+                _values = values;
+            }
+
+            public void OnCompleted()
+            {
+            }
+
+            public void OnError(Exception error)
+            {
+            }
+
+            public void OnNext(double value)
+            {
+                _values.Add(value);
+            }
+        }
+    }
+}