diff --git a/WheelWizard/Views/App.axaml b/WheelWizard/Views/App.axaml index 6c44826f..c8b10f02 100644 --- a/WheelWizard/Views/App.axaml +++ b/WheelWizard/Views/App.axaml @@ -68,5 +68,6 @@ + \ No newline at end of file diff --git a/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml b/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml new file mode 100644 index 00000000..2225082a --- /dev/null +++ b/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml.cs b/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml.cs new file mode 100644 index 00000000..a98b93a8 --- /dev/null +++ b/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml.cs @@ -0,0 +1,93 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace WheelWizard.Views.Components +{ + public class WheelTrail : TemplatedControl + { + public static readonly StyledProperty AngleProperty = AvaloniaProperty.Register(nameof(Angle)); + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + public static readonly StyledProperty WheelRotationProperty = AvaloniaProperty.Register( + nameof(WheelRotation) + ); + + public double WheelRotation + { + get => GetValue(WheelRotationProperty); + set => SetValue(WheelRotationProperty, value); + } + + public static readonly StyledProperty RelativeLengthProperty = AvaloniaProperty.Register( + nameof(RelativeLength), + 6.0 + ); + public double RelativeLength + { + get => GetValue(RelativeLengthProperty); + set => SetValue(RelativeLengthProperty, value); + } + + public static readonly StyledProperty XProperty = AvaloniaProperty.Register(nameof(X)); + + public double X + { + get => GetValue(XProperty); + set => SetValue(XProperty, value); + } + + public static readonly StyledProperty YProperty = AvaloniaProperty.Register(nameof(Y)); + + public double Y + { + get => GetValue(YProperty); + set => SetValue(YProperty, value); + } + + public static readonly StyledProperty ExtendedHeightProperty = AvaloniaProperty.Register( + nameof(ExtendedHeight) + ); + + public double ExtendedHeight + { + get => GetValue(ExtendedHeightProperty); + set => SetValue(ExtendedHeightProperty, value); + } + + private readonly RotateTransform _rotateTransform = new RotateTransform { CenterX = 0.5, CenterY = 0.5 }; + private readonly TranslateTransform _translateTransform = new TranslateTransform { X = 0, Y = 0 }; + + public WheelTrail() + { + var group = new TransformGroup(); + group.Children.Add(_rotateTransform); + group.Children.Add(_translateTransform); + this.RenderTransform = group; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == AngleProperty) + { + _rotateTransform.Angle = (double)(change.NewValue ?? 0.0); + } + + if (change.Property == XProperty) + { + _translateTransform.X = (double)(change.NewValue ?? 0.0); + } + if (change.Property == YProperty) + { + _translateTransform.Y = (double)(change.NewValue ?? 0.0); + } + } + } +} diff --git a/WheelWizard/Views/Converters/BrushColorConverters.cs b/WheelWizard/Views/Converters/BrushColorConverters.cs new file mode 100644 index 00000000..120ae5f8 --- /dev/null +++ b/WheelWizard/Views/Converters/BrushColorConverters.cs @@ -0,0 +1,28 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace WheelWizard.Views.Converters; + +// Note that this is static, which means you dont have to add it as a converter +public static class BrushColorConverters +{ + public static readonly IValueConverter TransparentColor = new FuncValueConverter(x => + { + if (x is ISolidColorBrush brush) + return new Color(0, brush.Color.R, brush.Color.G, brush.Color.B); + if (x is Color c) + return new Color(0, c.R, c.G, c.B); + + return (Colors.Transparent); + }); + + public static readonly IValueConverter BrushToColor = new FuncValueConverter(x => + { + if (x is ISolidColorBrush brush) + return brush.Color; + if (x is IGradientBrush gradientBrush) + return gradientBrush.GradientStops[0].Color; + + return (Colors.Transparent); + }); +} diff --git a/WheelWizard/Views/Converters/DoubleToThicknessConverters.cs b/WheelWizard/Views/Converters/DoubleToThicknessConverters.cs new file mode 100644 index 00000000..c84853fc --- /dev/null +++ b/WheelWizard/Views/Converters/DoubleToThicknessConverters.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace WheelWizard.Views.Converters; + +public class DoubleToThicknessConverters +{ + public static readonly IValueConverter DoubleToTop = new FuncValueConverter(x => new Thickness(0, x, 0, 0)); + public static readonly IValueConverter DoubleToBottom = new FuncValueConverter(x => new Thickness(0, 0, 0, x)); + public static readonly IValueConverter DoubleToLeft = new FuncValueConverter(x => new Thickness(x, 0, 0, 0)); + public static readonly IValueConverter DoubleToRight = new FuncValueConverter(x => new Thickness(0, 0, x, 0)); +} diff --git a/WheelWizard/Views/Converters/NumberConverters.cs b/WheelWizard/Views/Converters/NumberConverters.cs index 25759437..4362aaac 100644 --- a/WheelWizard/Views/Converters/NumberConverters.cs +++ b/WheelWizard/Views/Converters/NumberConverters.cs @@ -8,6 +8,16 @@ public static class NumberConverters public static readonly IValueConverter DoubleToInt = new FuncValueConverter(x => (int)x); public static readonly IValueConverter FloatToInt = new FuncValueConverter(x => (int)x); + public static readonly IMultiValueConverter MultiplyDouble = new FuncMultiValueConverter(x => + { + double result = 1; + foreach (var brush in x) + { + result *= brush; + } + return result; + }); + // TODO, find out if you can have a third parameter so that its not always 0, but instead that you can specify the value public static readonly IValueConverter Equals0 = new FuncValueConverter(x => x == 0); @@ -15,6 +25,4 @@ public static class NumberConverters public static readonly IValueConverter SmallerThan0 = new FuncValueConverter(x => x < 0); public static readonly IValueConverter GreaterThanOrEqual0 = new FuncValueConverter(x => x >= 0); public static readonly IValueConverter SmallerThanOrEqual0 = new FuncValueConverter(x => x <= 0); - - public static readonly IValueConverter AlwaysFalse = new FuncValueConverter(x => false); } diff --git a/WheelWizard/Views/Layout.axaml b/WheelWizard/Views/Layout.axaml index 9c331224..db31cd3a 100644 --- a/WheelWizard/Views/Layout.axaml +++ b/WheelWizard/Views/Layout.axaml @@ -56,9 +56,11 @@ - - + + + + + + + + + + + + + + + + @@ -34,6 +129,18 @@ VerticalAlignment="Top" /> + + + + + @@ -72,5 +180,12 @@ Click="DolphinButton_OnClick" Width="100" Margin="0,6,0,0" /> + + + \ No newline at end of file diff --git a/WheelWizard/Views/Pages/HomePage.axaml.cs b/WheelWizard/Views/Pages/HomePage.axaml.cs index 3c04c526..674c1135 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -1,13 +1,16 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; +using Testably.Abstractions; using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services.Launcher; using WheelWizard.Services.Launcher.Helpers; using WheelWizard.Services.Settings; +using WheelWizard.Views.Components; using WheelWizard.Views.Pages.Settings; using Button = WheelWizard.Views.Components.Button; @@ -18,6 +21,9 @@ public partial class HomePage : UserControlBase private static ILauncher currentLauncher => _launcherTypes[_launcherIndex]; private static int _launcherIndex = 0; // Make sure this index never goes over the list index + private WheelTrail[] _trails; // also used as a lock + private WheelTrailState _currentTrailState = WheelTrailState.Static_None; + private static List _launcherTypes = [ new RrLauncher(), @@ -50,6 +56,11 @@ public HomePage() InitializeComponent(); PopulateGameModeDropdown(); UpdatePage(); + + _trails = [HomeTrail1, HomeTrail2, HomeTrail3, HomeTrail4, HomeTrail5]; + App.Services.GetService()?.Random.Shared.Shuffle(_trails); + // We have to do it like `App.Service.GetService`. We cant make use of `private IRandomSystem Random { get; set; } = null!;` here + // This is because this HomePage is always loaded first } private void UpdatePage() @@ -87,7 +98,7 @@ private static async void Update() private void PlayButton_Click(object? sender, RoutedEventArgs e) { currentButtonState?.OnClick?.Invoke(); - + PlayActivateAnimation(); UpdateActionButton(); DisableAllButtonsTemporarily(); } @@ -128,7 +139,11 @@ private void DisableAllButtonsTemporarily() Task.Delay(2000) .ContinueWith(_ => { - Dispatcher.UIThread.InvokeAsync(() => CompleteGrid.IsEnabled = true); + Dispatcher.UIThread.InvokeAsync(() => + { + SetButtonState(currentButtonState); + return CompleteGrid.IsEnabled = true; + }); }); } @@ -140,8 +155,223 @@ private void SetButtonState(MainButtonState state) if (Application.Current != null && Application.Current.FindResource(state.IconName) is Geometry geometry) PlayButton.IconData = geometry; DolphinButton.IsEnabled = state.SubButtonsEnabled && SettingsHelper.PathsSetupCorrectly(); + + if (_status == WheelWizardStatus.Ready) + PlayEntranceAnimation(); + } + + #region WheelTrail Animations + // -------------------------- + // IMPORTANT + // -------------------------- + // When you are changing the animation, note that you are working with locks + // Note that the enum _currentTrailState is used to determine the state of the wheel trails, and that should only be read and changed under the influence of lock(_trails) + // Also note that for NO REASON WHATSOEVER you are permitted to put other logic in these animation code other than the animation itself. + // If for whatever reason the lock gets in to a deadlock, only the animation will freeze, the rest will continue to work. + + private async void PlayEntranceAnimation() + { + // If the animations are disabled, it will never play the entrance animation + // The entrance animation is also the only one that makes the wheels visible, meaning hat if this one does not play + // all the other animations are all also impossible to play + if (!(bool)SettingsManager.ENABLE_ANIMATIONS.Get()) + return; + + var allowedToRun = WaitForWheelTrailState( + WheelTrailState.Playing_Entrance, + c => c is WheelTrailState.Static_None + // even if there are 3 waiting, only 1 will go through, since there is an default check that it cant be the same + ); + if (await allowedToRun == null) + return; + + foreach (var t in _trails) + { + t.Classes.Add("EntranceTrail"); + await Task.Delay(80); + } + + await Task.Delay(600); + foreach (var t in _trails) + { + t.Classes.Remove("EntranceTrail"); + } + + lock (_trails) + { + _currentTrailState = WheelTrailState.Static_Visible; + } + } + + private async void PlayActivateAnimation() + { + if (!(bool)SettingsManager.ENABLE_ANIMATIONS.Get()) + return; + + var allowedToRun = WaitForWheelTrailState( + WheelTrailState.Playing_Activate, + c => + c + is WheelTrailState.Static_Hover + or WheelTrailState.Static_Visible + or WheelTrailState.Playing_HoverEnter + or WheelTrailState.Playing_HoverExit, + c => c is WheelTrailState.Static_None or WheelTrailState.Playing_Activate + ); + var oldState = await allowedToRun; + if (oldState == null) + return; + + foreach (var t in _trails) + { + t.Classes.Clear(); + if (oldState == WheelTrailState.Static_Hover) + t.Classes.Add("ActivateTrailFromHover"); + else + t.Classes.Add("ActivateTrailFromIdle"); + await Task.Delay(80); + } + + await Task.Delay(1000); + foreach (var t in _trails) + { + t.Classes.Remove("ActivateTrailFromIdle"); + t.Classes.Remove("ActivateTrailFromHover"); + await Task.Delay(40); + } + + lock (_trails) + { + _currentTrailState = WheelTrailState.Static_None; + } + } + + private async void PlayButton_OnPointerEntered(object? sender, PointerEventArgs e) + { + var allowedToRun = WaitForWheelTrailState( + WheelTrailState.Playing_HoverEnter, + c => c is WheelTrailState.Static_Visible or WheelTrailState.Playing_HoverExit, + c => c is WheelTrailState.Playing_HoverExit + ); + if (await allowedToRun == null) + return; + + foreach (var t in _trails) + { + // Making sure that if after these seconds the state changed ,that it will not apply the class anymore + lock (_trails) + { + if (_currentTrailState is not WheelTrailState.Playing_HoverEnter) + return; + } + + t.Classes.Remove("HoverExitTrail"); + if (!t.Classes.Contains("HoverEnterTrail")) + t.Classes.Add("HoverEnterTrail"); + await Task.Delay(20); + } + + await Task.Delay(300); + lock (_trails) + { + if (_currentTrailState is WheelTrailState.Playing_HoverEnter) + _currentTrailState = WheelTrailState.Static_Hover; + } + } + + private async void PlayButton_OnPointerExit(object? sender, PointerEventArgs e) + { + var allowedToRun = WaitForWheelTrailState( + WheelTrailState.Playing_HoverExit, + c => c is WheelTrailState.Static_Hover or WheelTrailState.Playing_HoverEnter, + c => c is not WheelTrailState.Static_Hover and not WheelTrailState.Playing_HoverEnter + ); + if (await allowedToRun == null) + return; + + foreach (var t in _trails) + { + lock (_trails) + { + if (_currentTrailState is not WheelTrailState.Playing_HoverExit) + return; + } + t.Classes.Remove("HoverEnterTrail"); + t.Classes.Add("HoverExitTrail"); + } + + await Task.Delay(350); + lock (_trails) + { + if (_currentTrailState is WheelTrailState.Playing_HoverExit) + _currentTrailState = WheelTrailState.Static_Visible; + } } + /// + /// Easier way to wait for a specific animation state + /// + /// the state that you are trying to set it to + /// the states when it is allowed to override the state and continue the code + /// the statues when it should abort trying to set the state. it then also should not continue + /// null = aborted, WheelTrailState = the old state that it was before the swap. This means success + private async Task WaitForWheelTrailState( + WheelTrailState changeStateTo, + Func acceptWhen, + Func? abortWhen = null + ) + { + bool accepted; + WheelTrailState? oldState = null; + lock (_trails) + { + accepted = acceptWhen(_currentTrailState); + if (accepted) + { + oldState = _currentTrailState; + _currentTrailState = changeStateTo; + } + } + + while (!accepted) + { + await Task.Delay(20); + bool abort; + lock (_trails) + { + abort = (abortWhen?.Invoke(_currentTrailState) ?? false) || _currentTrailState == changeStateTo; + } + if (abort) + return null; + + lock (_trails) + { + accepted = acceptWhen(_currentTrailState); + if (accepted) + { + oldState = _currentTrailState; + _currentTrailState = changeStateTo; + } + } + } + + return oldState; + } + + enum WheelTrailState + { + Static_None, // It is not in view + Static_Visible, // It is just doing nothing + Static_Hover, // It is just doing nothing while it is being hovered + + Playing_Entrance, // Animation for entrance is playing NOTHING is allowed to interrupt Playing_Entrance + Playing_Activate, // Animation for activation is playing NOTHING is allowed to interrupt Playing_Entrance + Playing_HoverEnter, // Hover Enter animation is playing can be interrupted + Playing_HoverExit, // Hover Exit animation is exiting can be interrupted + } + + #endregion + public class MainButtonState { public MainButtonState(string text, Button.ButtonsVariantType type, string iconName, Action? onClick, bool subButtonsEnables) => diff --git a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml index bbd2df98..b610c33e 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml @@ -156,16 +156,13 @@ - SettingsManager.ENABLE_ANIMATIONS.Set(EnableAnimations.IsChecked == true); + private void EnableAnimations_OnClick(object sender, RoutedEventArgs e) => + SettingsManager.ENABLE_ANIMATIONS.Set(EnableAnimations.IsChecked == true); }