From 863242b24801e883d9c1a9e66adeb8b6e9633b1c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 14 May 2024 19:00:24 +0200 Subject: [PATCH] Fix Animator for progress values less than zero --- .../Animation/Animators/Animator`1.cs | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 8a4469d0209c..6815f6d7aba7 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -28,57 +28,61 @@ protected T InterpolationHandler(double animationTime, T neutralValue) if (Count == 0) return neutralValue; - var (beforeKeyFrame, afterKeyFrame) = FindKeyFrames(animationTime); + var (from, to) = GetKeyFrames(animationTime, neutralValue); - double beforeTime, afterTime; - T beforeValue, afterValue; + var progress = (animationTime - from.Time) / (to.Time - from.Time); - if (beforeKeyFrame is null) - { - beforeTime = 0.0; - beforeValue = afterKeyFrame is { FillBefore: true, Value: T fillValue } ? fillValue : neutralValue; - } - else - { - beforeTime = beforeKeyFrame.Cue.CueValue; - beforeValue = beforeKeyFrame.Value is T value ? value : neutralValue; - } - - if (afterKeyFrame is null) - { - afterTime = 1.0; - afterValue = beforeKeyFrame is { FillAfter: true, Value: T fillValue } ? fillValue : neutralValue; - } - else - { - afterTime = afterKeyFrame.Cue.CueValue; - afterValue = afterKeyFrame.Value is T value ? value : neutralValue; - } - - var progress = (animationTime - beforeTime) / (afterTime - beforeTime); - - if (afterKeyFrame?.KeySpline is { } keySpline) + if (to.KeySpline is { } keySpline) progress = keySpline.GetSplineProgress(progress); - return Interpolate(progress, beforeValue, afterValue); + return Interpolate(progress, from.Value, to.Value); } - private (AnimatorKeyFrame? Before, AnimatorKeyFrame? After) FindKeyFrames(double time) + private (KeyFrameInfo From, KeyFrameInfo To) GetKeyFrames(double time, T neutralValue) { Debug.Assert(Count >= 1); - for (var i = 0; i < Count; i++) + // Before or right at the first frame which isn't at time 0.0: interpolate between 0.0 and the first frame. + var firstFrame = this[0]; + if (time <= firstFrame.Cue.CueValue && firstFrame.Cue.CueValue > 0.0) { - var keyFrame = this[i]; - var keyFrameTime = keyFrame.Cue.CueValue; + var beforeValue = firstFrame.FillBefore ? GetTypedValue(firstFrame.Value, neutralValue) : neutralValue; + return ( + new KeyFrameInfo(0.0, beforeValue, firstFrame.KeySpline), + KeyFrameInfo.FromKeyFrame(firstFrame, neutralValue)); + } - if (time < keyFrameTime || keyFrameTime == 1.0) - return (i > 0 ? this[i - 1] : null, keyFrame); + // Between two frames: interpolate between the previous frame and the next frame. + for (var i = 1; i < Count; ++i) + { + var frame = this[i]; + if (time <= frame.Cue.CueValue) + { + return ( + KeyFrameInfo.FromKeyFrame(this[i - 1], neutralValue), + KeyFrameInfo.FromKeyFrame(this[i], neutralValue)); + } + } + + // Past the last frame which is at time 1.0: interpolate between the last two frames. + var lastFrame = this[Count - 1]; + if (lastFrame.Cue.CueValue >= 1.0) + { + return ( + KeyFrameInfo.FromKeyFrame(this[Count - 2], neutralValue), + KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue)); } - return (this[Count - 1], null); + // Past the last frame which isn't at time 1.0: interpolate between the last frame and 1.0. + var afterValue = lastFrame.FillAfter ? GetTypedValue(lastFrame.Value, neutralValue) : neutralValue; + return ( + KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue), + new KeyFrameInfo(1.0, afterValue, lastFrame.KeySpline)); } + private static T GetTypedValue(object? untypedValue, T neutralValue) + => untypedValue is T value ? value : neutralValue; + public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { if (Property is null) @@ -107,5 +111,15 @@ internal IDisposable Run(Animation animation, Animatable control, IClock? clock, /// Interpolates in-between two key values given the desired progress time. /// public abstract T Interpolate(double progress, T oldValue, T newValue); + + private readonly struct KeyFrameInfo(double time, T value, KeySpline? keySpline) + { + public readonly double Time = time; + public readonly T Value = value; + public readonly KeySpline? KeySpline = keySpline; + + public static KeyFrameInfo FromKeyFrame(AnimatorKeyFrame source, T neutralValue) + => new(source.Cue.CueValue, GetTypedValue(source.Value, neutralValue), source.KeySpline); + } } }