Browse Source

Have each AnimationInstance and TransitionInstance use their own internal clock instead of relying on tracking the start time of the global clock. Use a binary search to find the correct keyframe instead of linear search.

Jeremy Koritzinsky 7 years ago
parent
commit
58a85c53c7

+ 1 - 1
samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

@@ -8,7 +8,7 @@ namespace RenderDemo.ViewModels
     {
         private bool _isPlaying = true;
 
-        private string _playStateText = "Pause all animations";
+        private string _playStateText = "Pause animations on this page";
 
         public void TogglePlayState()
         {

+ 0 - 5
src/Avalonia.Animation/Animation.cs

@@ -177,11 +177,6 @@ namespace Avalonia.Animation
         /// <inheritdocs/>
         public Task RunAsync(Animatable control, IClock clock = null)
         {
-            if (clock == null)
-            {
-                clock = Clock.GlobalClock;
-            }
-
             var run = new TaskCompletionSource<object>();
 
             if (this.RepeatCount == RepeatCount.Loop)

+ 13 - 19
src/Avalonia.Animation/AnimationInstance`1.cs

@@ -19,7 +19,6 @@ namespace Avalonia.Animation
         private double _currentIteration;
         private bool _isLooping;
         private bool _gotFirstKFValue;
-        private bool _gotFirstFrameCount;
         private bool _iterationDelay;
         private FillMode _fillMode;
         private PlaybackDirection _animationDirection;
@@ -29,14 +28,14 @@ namespace Avalonia.Animation
         private double _speedRatio;
         private TimeSpan _delay;
         private TimeSpan _duration;
-        private TimeSpan _firstFrameCount;
         private Easings.Easing _easeFunc;
         private Action _onCompleteAction;
         private Func<double, T, T> _interpolator;
         private IDisposable _timerSubscription;
-        private readonly IClock _clock;
+        private readonly IClock _baseClock;
+        private IClock _clock;
 
-        public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, Clock clock, Action OnComplete, Func<double, T, T> Interpolator)
+        public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
         {
             if (animation.SpeedRatio <= 0)
                 throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
@@ -72,16 +71,18 @@ namespace Avalonia.Animation
             _fillMode = animation.FillMode;
             _onCompleteAction = OnComplete;
             _interpolator = Interpolator;
-            _clock = clock;
+            _baseClock = baseClock;
         }
 
         protected override void Unsubscribed()
         {
             _timerSubscription?.Dispose();
+            _clock.PlayState = PlayState.Stop;
         }
 
         protected override void Subscribed()
         {
+            _clock = new Clock(_baseClock);
             _timerSubscription = _clock.Subscribe(Step);
         }
 
@@ -115,9 +116,9 @@ namespace Avalonia.Animation
                     PublishNext(_lastInterpValue);
         }
 
-        private void DoPlayStatesAndTime(TimeSpan systemTime)
+        private void DoPlayStates()
         {
-            if (_clock.PlayState == PlayState.Stop)
+            if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
                 DoComplete();
 
             if (!_gotFirstKFValue)
@@ -125,19 +126,12 @@ namespace Avalonia.Animation
                 _firstKFValue = (T)_parent.First().Value;
                 _gotFirstKFValue = true;
             }
-
-            if (!_gotFirstFrameCount)
-            {
-                _firstFrameCount = systemTime;
-                _gotFirstFrameCount = true;
-            }
         }
 
-        private void InternalStep(TimeSpan systemTime)
+        private void InternalStep(TimeSpan time)
         {
-            DoPlayStatesAndTime(systemTime);
- 
-            var time = systemTime - _firstFrameCount;
+            DoPlayStates();
+
             var delayEndpoint = _delay;
             var iterationEndpoint = delayEndpoint + _duration;
 
@@ -158,14 +152,14 @@ namespace Avalonia.Animation
                 }
 
                 //Calculate the current iteration number
-                _currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2;
+                _currentIteration = (int)Math.Floor((double)((double)time.Ticks / iterationEndpoint.Ticks)) + 2;
             }
             else
             {
                 return;
             }
 
-            time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks);
+            time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks));
 
             if (!_isLooping)
             {

+ 48 - 36
src/Avalonia.Animation/Animator`1.cs

@@ -52,52 +52,72 @@ namespace Avalonia.Animation
         /// (i.e., the normalized time between the selected keyframes, relative to the
         /// time parameter).
         /// </summary>
-        /// <param name="t">The time parameter, relative to the total animation time</param>
-        protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
+        /// <param name="animationTime">The time parameter, relative to the total animation time</param>
+        protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double animationTime)
         {
-            AnimatorKeyFrame firstCue, lastCue ;
+            AnimatorKeyFrame firstKeyframe, lastKeyframe ;
             int kvCount = _convertedKeyframes.Count;
             if (kvCount > 2)
             {
-                if (t <= 0.0)
+                if (animationTime <= 0.0)
                 {
-                    firstCue = _convertedKeyframes[0];
-                    lastCue = _convertedKeyframes[1];
+                    firstKeyframe = _convertedKeyframes[0];
+                    lastKeyframe = _convertedKeyframes[1];
                 }
-                else if (t >= 1.0)
+                else if (animationTime >= 1.0)
                 {
-                    firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2];
-                    lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1];
+                    firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
+                    lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
                 }
                 else
                 {
-                    (double time, int index) maxval = (0.0d, 0);
-                    for (int i = 0; i < _convertedKeyframes.Count; i++)
-                    {
-                        var comp = _convertedKeyframes[i].Cue.CueValue;
-                        if (t >= comp)
-                        {
-                            maxval = (comp, i);
-                        }
-                    }
-                    firstCue = _convertedKeyframes[maxval.index];
-                    lastCue = _convertedKeyframes[maxval.index + 1];
+                    int index = FindClosestBeforeKeyFrame(animationTime);
+                    firstKeyframe = _convertedKeyframes[index];
+                    lastKeyframe = _convertedKeyframes[index + 1];
                 }
             }
             else
             {
-                firstCue = _convertedKeyframes[0];
-                lastCue = _convertedKeyframes[1];
+                firstKeyframe = _convertedKeyframes[0];
+                lastKeyframe = _convertedKeyframes[1];
             }
 
-            double t0 = firstCue.Cue.CueValue;
-            double t1 = lastCue.Cue.CueValue;
-            var intraframeTime = (t - t0) / (t1 - t0);
-            var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral);
-            var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral);
+            double t0 = firstKeyframe.Cue.CueValue;
+            double t1 = lastKeyframe.Cue.CueValue;
+            var intraframeTime = (animationTime - t0) / (t1 - t0);
+            var firstFrameData = (firstKeyframe.GetTypedValue<T>(), firstKeyframe.isNeutral);
+            var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
             return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
         }
 
+        private int FindClosestBeforeKeyFrame(double time)
+        {
+            int FindClosestBeforeKeyFrame(int startIndex, int length)
+            {
+                if (length == 0 || length == 1)
+                {
+                    return startIndex;
+                }
+
+                int middle = startIndex + (length / 2);
+
+                if (_convertedKeyframes[middle].Cue.CueValue < time)
+                {
+                    return FindClosestBeforeKeyFrame(middle, length - middle);
+                }
+                else if (_convertedKeyframes[middle].Cue.CueValue > time)
+                {
+                    return FindClosestBeforeKeyFrame(startIndex, middle - startIndex);
+                }
+                else
+                {
+                    return middle;
+                }
+            }
+
+            return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count);
+        }
+
         /// <summary>
         /// Runs the KeyFrames Animation.
         /// </summary>
@@ -130,14 +150,6 @@ namespace Avalonia.Animation
 
             AddNeutralKeyFramesIfNeeded();
 
-            var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue);
-            _convertedKeyframes.Clear();
-
-            foreach (AnimatorKeyFrame keyframe in copy)
-            {
-                _convertedKeyframes.Add(keyframe);
-            }
-
             _isVerifiedAndConverted = true;
         }
 
@@ -167,7 +179,7 @@ namespace Avalonia.Animation
         {
             if (!hasStartKey)
             {
-                _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
+                _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
             }
 
             if (!hasEndKey)

+ 1 - 4
src/Avalonia.Animation/ClockBase.cs

@@ -21,9 +21,7 @@ namespace Avalonia.Animation
             _connectedObservable = _observable.Publish().RefCount();
         }
 
-        public bool HasSubscriptions => _observable.HasSubscriptions;
-
-        public TimeSpan CurrentTime { get; private set; }
+        protected bool HasSubscriptions => _observable.HasSubscriptions;
 
         public PlayState PlayState { get; set; }
 
@@ -47,7 +45,6 @@ namespace Avalonia.Animation
             }
 
             _observable.Pulse(_internalTime);
-            CurrentTime = _internalTime;
 
             if (PlayState == PlayState.Stop)
             {

+ 0 - 2
src/Avalonia.Animation/IClock.cs

@@ -6,8 +6,6 @@ namespace Avalonia.Animation
 {
     public interface IClock : IObservable<TimeSpan>
     {
-        bool HasSubscriptions { get; }
-        TimeSpan CurrentTime { get; }
         PlayState PlayState { get; set; }
     }
 }

+ 13 - 13
src/Avalonia.Animation/TransitionInstance.cs

@@ -15,23 +15,22 @@ namespace Avalonia.Animation
     /// </summary>
     internal class TransitionInstance : SingleSubscriberObservableBase<double>
     {
-        private IDisposable timerSubscription;
-        private TimeSpan startTime;
-        private TimeSpan duration;
-        private readonly IClock _clock;
+        private IDisposable _timerSubscription;
+        private TimeSpan _duration;
+        private readonly IClock _baseClock;
+        private IClock _clock;
 
-        public TransitionInstance(Clock clock, TimeSpan Duration)
+        public TransitionInstance(IClock clock, TimeSpan Duration)
         {
-            duration = Duration;
-            _clock = clock;
+            _duration = Duration;
+            _baseClock = clock;
         }
 
         private void TimerTick(TimeSpan t)
         {
-            var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks;
+            var interpVal = (double)t.Ticks / _duration.Ticks;
 
-            if (interpVal > 1d
-             || interpVal < 0d)
+            if (interpVal > 1d || interpVal < 0d)
             {
                 PublishCompleted();
                 return;
@@ -42,13 +41,14 @@ namespace Avalonia.Animation
 
         protected override void Unsubscribed()
         {
-            timerSubscription?.Dispose();
+            _timerSubscription?.Dispose();
+            _clock.PlayState = PlayState.Stop;
         }
 
         protected override void Subscribed()
         {
-            startTime = _clock.CurrentTime;
-            timerSubscription = _clock.Subscribe(TimerTick);
+            _clock = new Clock(_baseClock);
+            _timerSubscription = _clock.Subscribe(TimerTick);
             PublishNext(0.0d);
         }
     }

+ 0 - 3
src/Avalonia.Animation/Transition`1.cs

@@ -14,7 +14,6 @@ namespace Avalonia.Animation
     public abstract class Transition<T> : AvaloniaObject, ITransition
     {
         private AvaloniaProperty _prop;
-        private Easing _easing;
 
         /// <summary>
         /// Gets the duration of the animation.
@@ -54,7 +53,5 @@ namespace Avalonia.Animation
             var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
             return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
         }
-
-
     }
 }