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.

pull/1715/head
Jeremy Koritzinsky 8 years ago
parent
commit
58a85c53c7
  1. 2
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  2. 5
      src/Avalonia.Animation/Animation.cs
  3. 32
      src/Avalonia.Animation/AnimationInstance`1.cs
  4. 84
      src/Avalonia.Animation/Animator`1.cs
  5. 5
      src/Avalonia.Animation/ClockBase.cs
  6. 2
      src/Avalonia.Animation/IClock.cs
  7. 26
      src/Avalonia.Animation/TransitionInstance.cs
  8. 3
      src/Avalonia.Animation/Transition`1.cs

2
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()
{

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)

32
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)
{

84
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)

5
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)
{

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

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

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

Loading…
Cancel
Save