64 changed files with 780 additions and 693 deletions
@ -0,0 +1,228 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation.Utils; |
|||
using Avalonia.Data; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Handles interpolatoin and time-related functions
|
|||
/// for keyframe animations.
|
|||
/// </summary>
|
|||
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T> |
|||
{ |
|||
private T _lastInterpValue; |
|||
private T _firstKFValue; |
|||
private long _repeatCount; |
|||
private double _currentIteration; |
|||
private bool _isLooping; |
|||
private bool _gotFirstKFValue; |
|||
private bool _gotFirstFrameCount; |
|||
private bool _iterationDelay; |
|||
private FillMode _fillMode; |
|||
private PlaybackDirection _animationDirection; |
|||
private Animator<T> _parent; |
|||
private Animatable _targetControl; |
|||
private T _neutralValue; |
|||
private double _speedRatio; |
|||
private TimeSpan _delay; |
|||
private TimeSpan _duration; |
|||
private TimeSpan _firstFrameCount; |
|||
private TimeSpan _internalClock; |
|||
private TimeSpan? _previousClock; |
|||
private Easings.Easing _easeFunc; |
|||
private Action _onCompleteAction; |
|||
private Func<double, T, T> _interpolator; |
|||
private IDisposable _timerSubscription; |
|||
|
|||
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, Action OnComplete, Func<double, T, T> Interpolator) |
|||
{ |
|||
if (animation.SpeedRatio <= 0) |
|||
throw new InvalidOperationException("Speed ratio cannot be negative or zero."); |
|||
|
|||
if (animation.Duration.TotalSeconds <= 0) |
|||
throw new InvalidOperationException("Duration cannot be negative or zero."); |
|||
|
|||
_parent = animator; |
|||
_easeFunc = animation.Easing; |
|||
_targetControl = control; |
|||
_neutralValue = (T)_targetControl.GetValue(_parent.Property); |
|||
|
|||
_speedRatio = animation.SpeedRatio; |
|||
|
|||
_delay = animation.Delay; |
|||
_duration = animation.Duration; |
|||
_iterationDelay = animation.DelayBetweenIterations; |
|||
|
|||
switch (animation.RepeatCount.RepeatType) |
|||
{ |
|||
case RepeatType.None: |
|||
_repeatCount = 1; |
|||
break; |
|||
case RepeatType.Loop: |
|||
_isLooping = true; |
|||
break; |
|||
case RepeatType.Repeat: |
|||
_repeatCount = (long)animation.RepeatCount.Value; |
|||
break; |
|||
} |
|||
|
|||
_animationDirection = animation.PlaybackDirection; |
|||
_fillMode = animation.FillMode; |
|||
_onCompleteAction = OnComplete; |
|||
_interpolator = Interpolator; |
|||
} |
|||
|
|||
protected override void Unsubscribed() |
|||
{ |
|||
_timerSubscription?.Dispose(); |
|||
} |
|||
|
|||
protected override void Subscribed() |
|||
{ |
|||
_timerSubscription = Timing.AnimationsTimer |
|||
.Subscribe(p => this.Step(p)); |
|||
} |
|||
|
|||
public void Step(TimeSpan frameTick) |
|||
{ |
|||
try |
|||
{ |
|||
InternalStep(frameTick); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
PublishError(e); |
|||
} |
|||
} |
|||
|
|||
private void DoComplete() |
|||
{ |
|||
if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) |
|||
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); |
|||
|
|||
_onCompleteAction?.Invoke(); |
|||
PublishCompleted(); |
|||
} |
|||
|
|||
private void DoDelay() |
|||
{ |
|||
if (_fillMode == FillMode.Backward || _fillMode == FillMode.Both) |
|||
if (_currentIteration == 0) |
|||
PublishNext(_firstKFValue); |
|||
else |
|||
PublishNext(_lastInterpValue); |
|||
} |
|||
|
|||
private void DoPlayStatesAndTime(TimeSpan systemTime) |
|||
{ |
|||
if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) |
|||
DoComplete(); |
|||
|
|||
if (!_previousClock.HasValue) |
|||
{ |
|||
_previousClock = systemTime; |
|||
_internalClock = TimeSpan.Zero; |
|||
} |
|||
else |
|||
{ |
|||
if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause) |
|||
{ |
|||
_previousClock = systemTime; |
|||
return; |
|||
} |
|||
var delta = systemTime - _previousClock; |
|||
_internalClock += delta.Value; |
|||
_previousClock = systemTime; |
|||
} |
|||
|
|||
if (!_gotFirstKFValue) |
|||
{ |
|||
_firstKFValue = (T)_parent.First().Value; |
|||
_gotFirstKFValue = true; |
|||
} |
|||
|
|||
if (!_gotFirstFrameCount) |
|||
{ |
|||
_firstFrameCount = _internalClock; |
|||
_gotFirstFrameCount = true; |
|||
} |
|||
} |
|||
|
|||
private void InternalStep(TimeSpan systemTime) |
|||
{ |
|||
DoPlayStatesAndTime(systemTime); |
|||
|
|||
var time = _internalClock - _firstFrameCount; |
|||
var delayEndpoint = _delay; |
|||
var iterationEndpoint = delayEndpoint + _duration; |
|||
|
|||
//determine if time is currently in the first iteration.
|
|||
if (time >= TimeSpan.Zero & time <= iterationEndpoint) |
|||
{ |
|||
_currentIteration = 1; |
|||
} |
|||
else if (time > iterationEndpoint) |
|||
{ |
|||
//Subtract first iteration to properly get the subsequent iteration time
|
|||
time -= iterationEndpoint; |
|||
|
|||
if (!_iterationDelay & delayEndpoint > TimeSpan.Zero) |
|||
{ |
|||
delayEndpoint = TimeSpan.Zero; |
|||
iterationEndpoint = _duration; |
|||
} |
|||
|
|||
//Calculate the current iteration number
|
|||
_currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; |
|||
} |
|||
else |
|||
{ |
|||
_previousClock = systemTime; |
|||
return; |
|||
} |
|||
|
|||
time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks); |
|||
|
|||
if (!_isLooping) |
|||
{ |
|||
if (_currentIteration > _repeatCount) |
|||
DoComplete(); |
|||
|
|||
if (time > iterationEndpoint) |
|||
DoComplete(); |
|||
} |
|||
|
|||
// Determine if the current iteration should have its normalized time inverted.
|
|||
bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false : |
|||
_animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true : |
|||
_animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false : |
|||
_animationDirection == PlaybackDirection.Reverse ? true : false; |
|||
|
|||
if (delayEndpoint > TimeSpan.Zero & time < delayEndpoint) |
|||
{ |
|||
DoDelay(); |
|||
} |
|||
else |
|||
{ |
|||
// Offset the delay time
|
|||
time -= delayEndpoint; |
|||
iterationEndpoint -= delayEndpoint; |
|||
|
|||
// Normalize time
|
|||
var interpVal = (double)time.Ticks / iterationEndpoint.Ticks; |
|||
|
|||
if (isCurIterReverse) |
|||
interpVal = 1 - interpVal; |
|||
|
|||
// Ease and interpolate
|
|||
var easedTime = _easeFunc.Ease(interpVal); |
|||
_lastInterpValue = _interpolator(easedTime, _neutralValue); |
|||
|
|||
PublishNext(_lastInterpValue); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,273 +0,0 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Provides statefulness for an iteration of a keyframe animation.
|
|||
/// </summary>
|
|||
internal class AnimatorStateMachine<T> : IObservable<object>, IDisposable |
|||
{ |
|||
object _lastInterpValue; |
|||
object _firstKFValue; |
|||
|
|||
private ulong _delayTotalFrameCount; |
|||
private ulong _durationTotalFrameCount; |
|||
private ulong _delayFrameCount; |
|||
private ulong _durationFrameCount; |
|||
private ulong _repeatCount; |
|||
private ulong _currentIteration; |
|||
|
|||
private bool _isLooping; |
|||
private bool _isRepeating; |
|||
private bool _isReversed; |
|||
private bool _checkLoopAndRepeat; |
|||
private bool _gotFirstKFValue; |
|||
|
|||
private FillMode _fillMode; |
|||
private PlaybackDirection _animationDirection; |
|||
private KeyFramesStates _currentState; |
|||
private KeyFramesStates _savedState; |
|||
private Animator<T> _parent; |
|||
private Animation _targetAnimation; |
|||
private Animatable _targetControl; |
|||
private T _neutralValue; |
|||
internal bool _unsubscribe = false; |
|||
private IObserver<object> _targetObserver; |
|||
private readonly Action _onComplete; |
|||
|
|||
[Flags] |
|||
private enum KeyFramesStates |
|||
{ |
|||
Initialize, |
|||
DoDelay, |
|||
DoRun, |
|||
RunForwards, |
|||
RunBackwards, |
|||
RunApplyValue, |
|||
RunComplete, |
|||
Pause, |
|||
Stop, |
|||
Disposed |
|||
} |
|||
|
|||
public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete) |
|||
{ |
|||
_parent = animator; |
|||
_targetAnimation = animation; |
|||
_targetControl = control; |
|||
_neutralValue = (T)_targetControl.GetValue(_parent.Property); |
|||
|
|||
_delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks); |
|||
_durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks); |
|||
|
|||
switch (animation.RepeatCount.RepeatType) |
|||
{ |
|||
case RepeatType.Loop: |
|||
_isLooping = true; |
|||
_checkLoopAndRepeat = true; |
|||
break; |
|||
case RepeatType.Repeat: |
|||
_isRepeating = true; |
|||
_checkLoopAndRepeat = true; |
|||
_repeatCount = animation.RepeatCount.Value; |
|||
break; |
|||
} |
|||
|
|||
_isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0; |
|||
_animationDirection = _targetAnimation.PlaybackDirection; |
|||
_fillMode = _targetAnimation.FillMode; |
|||
|
|||
if (_durationTotalFrameCount > 0) |
|||
_currentState = KeyFramesStates.DoDelay; |
|||
else |
|||
_currentState = KeyFramesStates.DoRun; |
|||
_onComplete = onComplete; |
|||
} |
|||
|
|||
public void Step(PlayState _playState, Func<double, T, T> Interpolator) |
|||
{ |
|||
try |
|||
{ |
|||
InternalStep(_playState, Interpolator); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_targetObserver?.OnError(e); |
|||
} |
|||
} |
|||
|
|||
private void InternalStep(PlayState _playState, Func<double, T, T> Interpolator) |
|||
{ |
|||
if (!_gotFirstKFValue) |
|||
{ |
|||
_firstKFValue = _parent.First().Value; |
|||
_gotFirstKFValue = true; |
|||
} |
|||
|
|||
if (_currentState == KeyFramesStates.Disposed) |
|||
throw new InvalidProgramException("This KeyFrames Animation is already disposed."); |
|||
|
|||
if (_playState == PlayState.Stop) |
|||
_currentState = KeyFramesStates.Stop; |
|||
|
|||
// Save state and pause the machine
|
|||
if (_playState == PlayState.Pause && _currentState != KeyFramesStates.Pause) |
|||
{ |
|||
_savedState = _currentState; |
|||
_currentState = KeyFramesStates.Pause; |
|||
} |
|||
|
|||
// Resume the previous state
|
|||
if (_playState != PlayState.Pause && _currentState == KeyFramesStates.Pause) |
|||
_currentState = _savedState; |
|||
|
|||
double _tempDuration = 0d, _easedTime; |
|||
|
|||
bool handled = false; |
|||
|
|||
while (!handled) |
|||
{ |
|||
switch (_currentState) |
|||
{ |
|||
case KeyFramesStates.DoDelay: |
|||
|
|||
if (_fillMode == FillMode.Backward |
|||
|| _fillMode == FillMode.Both) |
|||
{ |
|||
if (_currentIteration == 0) |
|||
{ |
|||
_targetObserver.OnNext(_firstKFValue); |
|||
} |
|||
else |
|||
{ |
|||
_targetObserver.OnNext(_lastInterpValue); |
|||
} |
|||
} |
|||
|
|||
if (_delayFrameCount > _delayTotalFrameCount) |
|||
{ |
|||
_currentState = KeyFramesStates.DoRun; |
|||
} |
|||
else |
|||
{ |
|||
handled = true; |
|||
_delayFrameCount++; |
|||
} |
|||
break; |
|||
|
|||
case KeyFramesStates.DoRun: |
|||
|
|||
if (_isReversed) |
|||
_currentState = KeyFramesStates.RunBackwards; |
|||
else |
|||
_currentState = KeyFramesStates.RunForwards; |
|||
|
|||
break; |
|||
|
|||
case KeyFramesStates.RunForwards: |
|||
|
|||
if (_durationFrameCount > _durationTotalFrameCount) |
|||
{ |
|||
_currentState = KeyFramesStates.RunComplete; |
|||
} |
|||
else |
|||
{ |
|||
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; |
|||
_currentState = KeyFramesStates.RunApplyValue; |
|||
|
|||
} |
|||
break; |
|||
|
|||
case KeyFramesStates.RunBackwards: |
|||
|
|||
if (_durationFrameCount > _durationTotalFrameCount) |
|||
{ |
|||
_currentState = KeyFramesStates.RunComplete; |
|||
} |
|||
else |
|||
{ |
|||
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; |
|||
_currentState = KeyFramesStates.RunApplyValue; |
|||
} |
|||
break; |
|||
|
|||
case KeyFramesStates.RunApplyValue: |
|||
|
|||
_easedTime = _targetAnimation.Easing.Ease(_tempDuration); |
|||
|
|||
_durationFrameCount++; |
|||
_lastInterpValue = Interpolator(_easedTime, _neutralValue); |
|||
_targetObserver.OnNext(_lastInterpValue); |
|||
_currentState = KeyFramesStates.DoRun; |
|||
handled = true; |
|||
break; |
|||
|
|||
case KeyFramesStates.RunComplete: |
|||
|
|||
if (_checkLoopAndRepeat) |
|||
{ |
|||
_delayFrameCount = 0; |
|||
_durationFrameCount = 0; |
|||
|
|||
if (_isLooping) |
|||
{ |
|||
_currentState = KeyFramesStates.DoRun; |
|||
} |
|||
else if (_isRepeating) |
|||
{ |
|||
if (_currentIteration >= _repeatCount) |
|||
{ |
|||
_currentState = KeyFramesStates.Stop; |
|||
} |
|||
else |
|||
{ |
|||
_currentState = KeyFramesStates.DoRun; |
|||
} |
|||
_currentIteration++; |
|||
} |
|||
|
|||
if (_animationDirection == PlaybackDirection.Alternate |
|||
|| _animationDirection == PlaybackDirection.AlternateReverse) |
|||
_isReversed = !_isReversed; |
|||
|
|||
break; |
|||
} |
|||
|
|||
_currentState = KeyFramesStates.Stop; |
|||
break; |
|||
|
|||
case KeyFramesStates.Stop: |
|||
|
|||
if (_fillMode == FillMode.Forward |
|||
|| _fillMode == FillMode.Both) |
|||
{ |
|||
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); |
|||
} |
|||
|
|||
_targetObserver.OnCompleted(); |
|||
_onComplete?.Invoke(); |
|||
Dispose(); |
|||
handled = true; |
|||
break; |
|||
default: |
|||
handled = true; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IDisposable Subscribe(IObserver<object> observer) |
|||
{ |
|||
_targetObserver = observer; |
|||
return this; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_unsubscribe = true; |
|||
_currentState = KeyFramesStates.Disposed; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Metadata; |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Animation.Utils; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
|
|||
/// </summary>
|
|||
internal class TransitionInstance : SingleSubscriberObservableBase<double> |
|||
{ |
|||
private IDisposable timerSubscription; |
|||
private TimeSpan startTime; |
|||
private TimeSpan duration; |
|||
|
|||
public TransitionInstance(TimeSpan Duration) |
|||
{ |
|||
duration = Duration; |
|||
} |
|||
|
|||
private void TimerTick(TimeSpan t) |
|||
{ |
|||
var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks; |
|||
|
|||
if (interpVal > 1d |
|||
|| interpVal < 0d) |
|||
{ |
|||
PublishCompleted(); |
|||
return; |
|||
} |
|||
|
|||
PublishNext(interpVal); |
|||
} |
|||
|
|||
protected override void Unsubscribed() |
|||
{ |
|||
timerSubscription?.Dispose(); |
|||
} |
|||
|
|||
protected override void Subscribed() |
|||
{ |
|||
startTime = Timing.GetTickCount(); |
|||
timerSubscription = Timing.AnimationsTimer |
|||
.Subscribe(t => TimerTick(t)); |
|||
PublishNext(0.0d); |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Animation.Utils |
|||
{ |
|||
internal static class DoubleUtils |
|||
{ |
|||
internal static bool AboutEqual(double x, double y) |
|||
{ |
|||
double epsilon = Math.Max(Math.Abs(x), Math.Abs(y)) * 1E-15; |
|||
return Math.Abs(x - y) <= epsilon; |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 834 B |
Loading…
Reference in new issue