Avalonia
Avalonia copied to clipboard
Controlling an animation : bug with Clock.Step(TimeSpan) method
Describe the bug Here is the context : I'm using a custom Clock to control an animation (the animation is created in code behind). The custom Clock has a Step(TimeSpan) method to control the current time of the animation (I'm using the AnimationControllerClock from this repo https://github.com/wieslawsoltes/Animator/tree/main/Animator/Clocks ).
If there is a delay between the creation of the clock and the animation's RunAsync call, then the Step(TimeSpan) method doesn't work correctly : there is an offset between the required TimeSpan and the one actually played by the animation (for example I required the animation to starts at 5 seconds, but it will starts at 2 seconds, or 3, or a random value that is not what I asked.) When there is no delay, Step() method works correctly.
To Reproduce Here is the code to reproduce the bug :
On the UI we have a cursor that will be animated to translate from left to right, a slider to choose the TimeSpan at which the animation must start, and buttons to play and pause the animation.
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:AvaloniaApplication1.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="AvaloniaApplication1.Views.MainWindow" Width="1040" Height="270" WindowStartupLocation="CenterScreen" Icon="/Assets/avalonia-logo.ico" Title="AvaloniaApplication1">
<Grid Margin="20"
Name="grid"
RowDefinitions="Auto,Auto,Auto">
<!-- The cursor to animate -->
<Border Width="2"
BorderThickness="0"
Background="Green"
Name="cursor"
Height="100"
HorizontalAlignment="Left"
VerticalAlignment="Top"/>
<!-- Slider to pick the start time for the animation -->
<Slider Grid.Row="1"
Name="slider"
Margin="0 20"
Minimum="0"
Maximum="10"
TickFrequency="1"
IsSnapToTickEnabled="True"
HorizontalAlignment="Stretch" TickPlacement="TopLeft"/>
<!-- Play / stop buttons -->
<StackPanel Grid.Row="2"
Orientation="Horizontal">
<Button Content="Play"
Click="Play"/>
<Button Content="Stop"
Click="Stop"
Margin="10 0 100 0"/>
</StackPanel>
</Grid>
Code behind for the main window :
public partial class MainWindow : Window
{
private Border _cursor;
private Slider _slider;
private AnimationControllerClock _clock;
private Animation _animation;
public MainWindow()
{
InitializeComponent();
_cursor = this.FindControl<Border>("cursor");
_slider = this.FindControl<Slider>("slider");
// Slider value changed
_slider.GetObservable(Slider.ValueProperty).Skip(1).Subscribe(x =>
{
_clock?.Step(TimeSpan.FromSeconds(x));
});
CreateAnimation();
}
private async Task CreateAnimation()
{
// Create the animation
_animation = new Animation
{
Duration = TimeSpan.FromSeconds(10),
IterationCount = IterationCount.Infinite,
Children =
{
new KeyFrame
{
KeyTime = TimeSpan.FromSeconds(0),
Setters =
{
new Setter { Property = TranslateTransform.XProperty, Value = 0d },
}
},
new KeyFrame
{
KeyTime = TimeSpan.FromSeconds(10),
Setters =
{
new Setter { Property = TranslateTransform.XProperty, Value = 1000d },
}
}
}
};
// Create the clock
_clock = new AnimationControllerClock();
_clock.PlayState = PlayState.Pause;
// Delay between the creation of the clock and the call to _animation.RunAsync
await Task.Delay(0); // Case 1) : 0 ms / 2) & 3) : 3000 ms
_animation.RunAsync(_cursor, _clock);
// Add these two lines for case 3)
//_clock.PlayState = PlayState.Run;
//_clock.PlayState = PlayState.Pause;
}
/// <summary>
/// Click handler for the Play button
/// </summary>
private void Play(object? sender, RoutedEventArgs e)
{
_clock.PlayState = PlayState.Run;
}
/// <summary>
/// Click handler for the Pause button
/// </summary>
private void Stop(object? sender, RoutedEventArgs e)
{
_clock.PlayState = PlayState.Pause;
}
}
In CreateAnimation() method, just before _animation.RunAsync call, we can add a delay. Let's run the following cases :
- When the delay is 0, the application works as intended (see video 1).
- If we add a delay, say 3000 ms, then 2 sub cases : a) if we run the animation from the beginning (with no prior call to Step method), and use Step method afterwards, it works correctly. (video 2). Note : we do not need to play the animation entirely, starting and pausing it right after is enough. b) if we try to use Step method before running the animation once, Step method is then broken and we have an offset between the time asked and the one reflected in the animation (video 3)
- It seems from 2)a) that we need to run the animation once from the beginning before using the Step method. To simulate that, I toggle the Clock.Playstate to Run then to Pause, right after RunAsync call. Despite of that, we're back to case 2). (video 4).
Expected behavior I don't know if this behavior is intended, but it seems to me that the Step method should work correctly in all these cases. You might ask why I add a delay, well it is supposed to represent how my application works : the animation is created at runtime based on user inputs. RunAsync is called once the animation is done, which is deferred from the clock's creation (the clock is created when the app starts).
Screenshots If applicable, add screenshots to help explain your problem.
Desktop (please complete the following information):
- OS: Windows 11
- Version 0.10.18
Additional context Add any other context about the problem here.
https://user-images.githubusercontent.com/110585532/195808416-29ade2c1-645d-446a-9aaf-dab7dc1ca367.mp4
https://user-images.githubusercontent.com/110585532/195808425-63e4df3b-3023-4544-b16b-6a132fe399d1.mp4
https://user-images.githubusercontent.com/110585532/195808429-7136b220-84ee-4aec-a6fc-aab07a82b8a1.mp4
https://user-images.githubusercontent.com/110585532/195808432-4782d09a-6492-465a-abc2-4a4961df2c77.mp4