Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Animator for progress values less than zero #15726

Merged
merged 3 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 59 additions & 36 deletions src/Avalonia.Base/Animation/Animators/Animator`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,57 +28,70 @@ 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];
var firstTime = firstFrame.Cue.CueValue;
if (time <= firstTime && firstTime > 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)
{
if (Count == 1)
{
var beforeValue = lastFrame.FillBefore ? GetTypedValue(lastFrame.Value, neutralValue) : neutralValue;
return (
new KeyFrameInfo(0.0, beforeValue, lastFrame.KeySpline),
KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue));
}

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<T> instance)
{
if (Property is null)
Expand Down Expand Up @@ -107,5 +120,15 @@ internal IDisposable Run(Animation animation, Animatable control, IClock? clock,
/// Interpolates in-between two key values given the desired progress time.
/// </summary>
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);
}
}
}
104 changes: 104 additions & 0 deletions tests/Avalonia.Base.UnitTests/Animation/KeySplineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,109 @@ public void Check_KeySpline_Parsing_Is_Correct()
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}

// https://github.com/AvaloniaUI/Avalonia/issues/15704
[Theory]
[InlineData(nameof(BackEaseIn))]
[InlineData(nameof(BackEaseOut))]
[InlineData(nameof(BackEaseInOut))]
[InlineData(nameof(ElasticEaseIn))]
[InlineData(nameof(ElasticEaseOut))]
[InlineData(nameof(ElasticEaseInOut))]
public void KeySpline_Progress_Less_Than_Zero_Or_Greater_Than_One_Works(string easingType)
{
var easing = Easing.Parse(easingType);

var animation = new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromSeconds(1.0),
Children =
{
new KeyFrame
{
Cue = new Cue(0.0),
Setters = { new Setter(TranslateTransform.YProperty, 10.0) }
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters = { new Setter(TranslateTransform.YProperty, 20.0) }
}
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate,
Easing = easing
};

var transform = new TranslateTransform(0.0, 50.0);
var rect = new Rectangle { RenderTransform = transform };

var clock = new TestClock();

animation.RunAsync(rect, clock);

clock.Step(TimeSpan.Zero);
Assert.Equal(10.0, transform.Y, 0.0001);

for (var time = TimeSpan.FromSeconds(0.1); time < animation.Duration; time += TimeSpan.FromSeconds(0.1))
{
clock.Step(time);
Assert.True(double.IsFinite(transform.Y));
Assert.NotEqual(10.0, transform.Y);
Assert.NotEqual(20.0, transform.Y);
}

clock.Step(animation.Duration);
Assert.Equal(20.0, transform.Y, 0.0001);
}

[Theory]
[InlineData(nameof(BackEaseIn))]
[InlineData(nameof(BackEaseOut))]
[InlineData(nameof(BackEaseInOut))]
[InlineData(nameof(ElasticEaseIn))]
[InlineData(nameof(ElasticEaseOut))]
[InlineData(nameof(ElasticEaseInOut))]
public void KeySpline_Progress_Less_Than_Zero_Or_Greater_Than_One_Works_With_Single_KeyFrame(string easingType)
{
var easing = Easing.Parse(easingType);

var animation = new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromSeconds(1.0),
Children =
{
new KeyFrame
{
Cue = new Cue(1.0),
Setters = { new Setter(TranslateTransform.YProperty, 10.0) }
}
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate,
Easing = easing
};

var transform = new TranslateTransform(0.0, 50.0);
var rect = new Rectangle { RenderTransform = transform };

var clock = new TestClock();

animation.RunAsync(rect, clock);

clock.Step(TimeSpan.Zero);
Assert.Equal(50.0, transform.Y, 0.0001);

for (var time = TimeSpan.FromSeconds(0.1); time < animation.Duration; time += TimeSpan.FromSeconds(0.1))
{
clock.Step(time);
Assert.True(double.IsFinite(transform.Y));
Assert.NotEqual(50.0, transform.Y);
Assert.NotEqual(10.0, transform.Y);
}

clock.Step(animation.Duration);
Assert.Equal(10.0, transform.Y, 0.0001);
}
}
}
Loading