diff --git a/build/Base.props b/build/Base.props index 5a716d20bd..9265d555c2 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,6 @@  - + diff --git a/src/Avalonia.Animation/FillMode.cs b/src/Avalonia.Animation/FillMode.cs new file mode 100644 index 0000000000..047d185db5 --- /dev/null +++ b/src/Avalonia.Animation/FillMode.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Animation +{ + public enum FillMode + { + None, + Forward, + Backward, + Both + } +} diff --git a/src/Avalonia.Animation/Keyframes/DoubleKeyFrames.cs b/src/Avalonia.Animation/Keyframes/DoubleKeyFrames.cs index 601b277069..a927863eb1 100644 --- a/src/Avalonia.Animation/Keyframes/DoubleKeyFrames.cs +++ b/src/Avalonia.Animation/Keyframes/DoubleKeyFrames.cs @@ -18,15 +18,12 @@ namespace Avalonia.Animation.Keyframes /// public override double DoInterpolation(double t) { - var pair = GetKeyFramePairByTime(t); - - double y0 = pair.FirstKeyFrame.Value; - double t0 = pair.FirstKeyFrame.Key; - double y1 = pair.SecondKeyFrame.Value; - double t1 = pair.SecondKeyFrame.Key; + var pair = GetKFPairAndIntraKFTime(t); + double y0 = pair.KFPair.FirstKeyFrame.Value; + double y1 = pair.KFPair.SecondKeyFrame.Value; // Do linear parametric interpolation - return y0 + ((t - t0) / (t1 - t0)) * (y1 - y0); + return y0 + (pair.IntraKFTime) * (y1 - y0); } } } diff --git a/src/Avalonia.Animation/Keyframes/KeyFrames.cs b/src/Avalonia.Animation/Keyframes/KeyFrames.cs index 3e3973f7c7..492f9d9972 100644 --- a/src/Avalonia.Animation/Keyframes/KeyFrames.cs +++ b/src/Avalonia.Animation/Keyframes/KeyFrames.cs @@ -48,9 +48,13 @@ namespace Avalonia.Animation.Keyframes /// /// Get the nearest pair of cue-time ordered keyframes - /// according to the given time parameter. + /// according to the given time parameter that is relative to + /// total animation time and the normalized intra-keyframe pair time + /// (i.e., the normalized time between the selected keyframes, relative to the + /// give time parameter). /// - public KeyFramePair GetKeyFramePairByTime(double t) + /// The time parameter, relative to the total animation time + public (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { KeyValuePair firstCue, lastCue; int kvCount = ConvertedKeyframes.Count(); @@ -77,7 +81,11 @@ namespace Avalonia.Animation.Keyframes firstCue = ConvertedKeyframes.First(); lastCue = ConvertedKeyframes.Last(); } - return new KeyFramePair(firstCue, lastCue); + + double t0 = firstCue.Key; + double t1 = lastCue.Key; + var intraframeTime = (t - t0) / (t1 - t0); + return (intraframeTime, new KeyFramePair(firstCue, lastCue)); } @@ -87,14 +95,14 @@ namespace Avalonia.Animation.Keyframes public IDisposable RunKeyFrames(Animation animation, Animatable control) { var _kfStateMach = new KeyFramesStateMachine(); - _kfStateMach.Start(animation); + _kfStateMach.Initialize(animation, control); Timing.AnimationStateTimer - .TakeWhile(_ => !_kfStateMach._unsubscribe) - .Subscribe(p => - { - _kfStateMach.Step(p, DoInterpolation); - }); + .TakeWhile(_ => !_kfStateMach._unsubscribe) + .Subscribe(p => + { + _kfStateMach.Step(p, DoInterpolation); + }); return control.Bind(Property, _kfStateMach, BindingPriority.Animation); } @@ -110,7 +118,7 @@ namespace Avalonia.Animation.Keyframes /// - /// Verifies and converts keyframe values according to this class's type parameter. + /// Verifies and converts keyframe values according to this class's target type. /// private void VerifyConvertKeyFrames(Animation animation, Type type) { @@ -169,7 +177,7 @@ namespace Avalonia.Animation.Keyframes throw new InvalidOperationException ($"{this.GetType().Name} must have a starting (0% cue) and ending (100% cue) keyframe."); - // Sort Cues, in case they don't order it by themselves. + // Sort Cues, in case users don't order it by themselves. ConvertedKeyframes = ConvertedKeyframes.OrderBy(p => p.Key) .ToDictionary((k) => k.Key, (v) => v.Value); } diff --git a/src/Avalonia.Animation/Keyframes/KeyFramesStateMachine.cs b/src/Avalonia.Animation/Keyframes/KeyFramesStateMachine.cs index 89faecd082..6cf91c2859 100644 --- a/src/Avalonia.Animation/Keyframes/KeyFramesStateMachine.cs +++ b/src/Avalonia.Animation/Keyframes/KeyFramesStateMachine.cs @@ -7,21 +7,24 @@ namespace Avalonia.Animation.Keyframes /// internal class KeyFramesStateMachine : IObservable, IDisposable { - ulong _delayTotalFrameCount, + + T _lastInterpValue = default(T); + + private ulong _delayTotalFrameCount, _durationTotalFrameCount, _delayFrameCount, _durationFrameCount, _repeatCount, - _currentIteration, - _totalIteration; + _currentIteration; - bool _isLooping, _isRepeating, _isReversed; + private bool _isLooping, _isRepeating, _isReversed, _checkLoopAndRepeat; private PlaybackDirection _animationDirection; KeyFramesStates _currentState, _savedState; - private Animation parentAnimation; + private Animation _parentAnimation; + private Animatable _targetControl; internal bool _unsubscribe = false; double _outputTime = 0d; - private IObserver targetObserver; + private IObserver _targetObserver; private enum KeyFramesStates { @@ -30,26 +33,28 @@ namespace Avalonia.Animation.Keyframes DO_RUN, RUN_FORWARDS, RUN_BACKWARDS, + RUN_APPLYVALUE, RUN_COMPLETE, PAUSE, STOP, DISPOSED } - public void Start(Animation animation) + public void Initialize(Animation animation, Animatable control) { - parentAnimation = animation; + _parentAnimation = animation; + _targetControl = control; _delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks); _durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks); - + switch (animation.RepeatBehavior) { case RepeatBehavior.Loop: _isLooping = true; + _checkLoopAndRepeat = true; break; case RepeatBehavior.Repeat: - _isRepeating = true; if (animation.RepeatCount != null) { if (animation.RepeatCount == 0) @@ -57,6 +62,8 @@ namespace Avalonia.Animation.Keyframes throw new InvalidOperationException ($"RepeatCount should be greater than zero when RepeatBehavior is set to Repeat."); } + _isRepeating = true; + _checkLoopAndRepeat = true; _repeatCount = (ulong)animation.RepeatCount; } else @@ -78,13 +85,29 @@ namespace Avalonia.Animation.Keyframes break; } + _animationDirection = _parentAnimation.PlaybackDirection; + if (_durationTotalFrameCount > 0) _currentState = KeyFramesStates.DO_DELAY; else _currentState = KeyFramesStates.DO_RUN; + + } public void Step(PlayState _playState, Func Interpolator) + { + try + { + InternalStep(_playState, Interpolator); + } + catch (Exception e) + { + _targetObserver?.OnError(e); + } + } + + private void InternalStep(PlayState _playState, Func Interpolator) { if (_currentState == KeyFramesStates.DISPOSED) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); @@ -103,7 +126,9 @@ namespace Avalonia.Animation.Keyframes if (_playState != PlayState.Pause && _currentState == KeyFramesStates.PAUSE) _currentState = _savedState; - checkstate: + double _tempDuration = 0d, _easedTime; + + checkstate: switch (_currentState) { case KeyFramesStates.DO_DELAY: @@ -116,32 +141,83 @@ namespace Avalonia.Animation.Keyframes break; case KeyFramesStates.DO_RUN: - // temporary stuff. - _currentState = KeyFramesStates.RUN_FORWARDS; - + if (_isReversed) + _currentState = KeyFramesStates.RUN_BACKWARDS; + else + _currentState = KeyFramesStates.RUN_FORWARDS; goto checkstate; - case KeyFramesStates.RUN_FORWARDS: - // temporary stuff. if (_durationFrameCount > _durationTotalFrameCount) + { _currentState = KeyFramesStates.RUN_COMPLETE; + goto checkstate; + } - var tmp1 = (double)_durationFrameCount / _durationTotalFrameCount; - var easedTime = parentAnimation.Easing.Ease(tmp1); - _durationFrameCount++; + _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; + _currentState = KeyFramesStates.RUN_APPLYVALUE; - targetObserver.OnNext(Interpolator(easedTime)); - break; + goto checkstate; case KeyFramesStates.RUN_BACKWARDS: - // break; + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RUN_COMPLETE; + goto checkstate; + } + + _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; + _currentState = KeyFramesStates.RUN_APPLYVALUE; + + goto checkstate; + + case KeyFramesStates.RUN_APPLYVALUE: + + _easedTime = _parentAnimation.Easing.Ease(_tempDuration); + + _durationFrameCount++; + _lastInterpValue = Interpolator(_easedTime); + _targetObserver.OnNext(_lastInterpValue); + _currentState = KeyFramesStates.DO_RUN; + + break; case KeyFramesStates.RUN_COMPLETE: - // break; + + if (_checkLoopAndRepeat) + { + _delayFrameCount = 0; + _durationFrameCount = 0; + + if (_isLooping) + { + _currentState = KeyFramesStates.DO_RUN; + } + else if (_isRepeating) + { + if (_currentIteration >= _repeatCount) + { + _currentState = KeyFramesStates.STOP; + } + else + { + _currentState = KeyFramesStates.DO_RUN; + } + _currentIteration++; + } + + if (_animationDirection == PlaybackDirection.Alternate + || _animationDirection == PlaybackDirection.AlternateReverse) + _isReversed = !_isReversed; + + break; + } + + _currentState = KeyFramesStates.STOP; + goto checkstate; case KeyFramesStates.STOP: - this.Dispose(); + _targetObserver.OnCompleted(); break; } @@ -149,16 +225,13 @@ namespace Avalonia.Animation.Keyframes public IDisposable Subscribe(IObserver observer) { - this.targetObserver = observer; + _targetObserver = observer; return this; } - public void Dispose() { - targetObserver.OnCompleted(); _unsubscribe = true; _currentState = KeyFramesStates.DISPOSED; - } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index f37d529944..a73dacc2c8 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -82,32 +82,32 @@ namespace Avalonia.Media ); } - /// - /// Creates a from a . - /// - /// The value. - /// True if the color vector is normalized floats, false if it's bytes (0-255) - /// The color. - public static Color FromVector4(Vector4 value, bool normalized) - { - if (normalized) - { - return new Color( - (byte)(value.W * 255), - (byte)(value.X * 255), - (byte)(value.Y * 255), - (byte)(value.Z * 255) - ); - } else { - return new Color( - (byte)(value.W), - (byte)(value.X), - (byte)(value.Y), - (byte)(value.Z) - ); - } - - } + // /// + // /// Creates a from a . + // /// + // /// The value. + // /// True if the color vector is normalized floats, false if it's bytes (0-255) + // /// The color. + // public static Color FromVector4(Vector4 value, bool normalized) + // { + // if (normalized) + // { + // return new Color( + // (byte)(value.W * 255), + // (byte)(value.X * 255), + // (byte)(value.Y * 255), + // (byte)(value.Z * 255) + // ); + // } else { + // return new Color( + // (byte)(value.W), + // (byte)(value.X), + // (byte)(value.Y), + // (byte)(value.Z) + // ); + // } + // } + /// /// Parses a color string. ///