From b1e6ee7f569b2ff47d26993061f674c9617e43b9 Mon Sep 17 00:00:00 2001 From: SleepyDebug <171101568+SleepyDebug@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:58:54 -0400 Subject: [PATCH] Release SleepStrap 6.9.6 --- SleepStrap/Locale.cs | 2 +- SleepStrap/Models/Persistable/Settings.cs | 1 + SleepStrap/Services/MacroAutomationService.cs | 244 ++++++++++-------- SleepStrap/Services/VisualModService.cs | 16 +- SleepStrap/SleepStrap.csproj | 4 +- SleepStrap/UI/Elements/Base/WpfUiWindow.cs | 4 +- .../UI/Elements/Settings/MainWindow.xaml | 119 +++++++-- .../UI/Elements/Settings/MainWindow.xaml.cs | 161 +++++++++++- .../Elements/Settings/Pages/ClippingPage.xaml | 28 +- .../UI/Elements/Settings/Pages/FontsPage.xaml | 12 +- .../UI/Elements/Settings/Pages/MacroPage.xaml | 61 +++-- .../UI/Elements/Settings/Pages/OtherPage.xaml | 21 +- .../Elements/Settings/Pages/RivalsPage.xaml | 20 +- .../Elements/Settings/Pages/SkyboxPage.xaml | 69 ++--- .../Elements/Settings/Pages/TexturesPage.xaml | 111 +++----- .../Settings/Pages/VisualModsPage.xaml | 20 +- SleepStrap/UI/Style/Dark.xaml | 16 +- SleepStrap/UI/Style/Light.xaml | 14 +- .../UI/ViewModels/Settings/MacroViewModel.cs | 16 +- .../UI/ViewModels/Settings/SkyboxViewModel.cs | 16 +- 20 files changed, 575 insertions(+), 380 deletions(-) diff --git a/SleepStrap/Locale.cs b/SleepStrap/Locale.cs index bf0b06a..5ec2967 100644 --- a/SleepStrap/Locale.cs +++ b/SleepStrap/Locale.cs @@ -131,7 +131,7 @@ public static void Initialize() } #if QA_BUILD - window.BorderBrush = System.Windows.Media.Brushes.Red; + window.BorderBrush = System.Windows.Media.Brushes.Black; window.BorderThickness = new Thickness(4); #endif })); diff --git a/SleepStrap/Models/Persistable/Settings.cs b/SleepStrap/Models/Persistable/Settings.cs index d9216c7..ae71bc7 100644 --- a/SleepStrap/Models/Persistable/Settings.cs +++ b/SleepStrap/Models/Persistable/Settings.cs @@ -89,6 +89,7 @@ public class Settings public string MacroSecondaryWeapon { get; set; } = "Warper"; public string MacroMeleeWeapon { get; set; } = "Maul"; public string MacroUtilityWeapon { get; set; } = "Grappler"; + public bool MacroUseListLayout { get; set; } = false; public bool MacroQuickRespawn { get; set; } = false; public bool MacroAutoUtility { get; set; } = false; public bool MacroAutoInspect { get; set; } = false; diff --git a/SleepStrap/Services/MacroAutomationService.cs b/SleepStrap/Services/MacroAutomationService.cs index 14dae69..432f1aa 100644 --- a/SleepStrap/Services/MacroAutomationService.cs +++ b/SleepStrap/Services/MacroAutomationService.cs @@ -11,6 +11,12 @@ public enum MacroWeaponCategory Utility } + public enum MacroWeaponLayout + { + Grid, + List + } + public readonly record struct MacroPoint(int X, int Y); public static class MacroAutomationService @@ -22,74 +28,105 @@ public static class MacroAutomationService private const int RecordedScreenTop = 0; private const int RecordedScreenWidth = 1920; private const int RecordedScreenHeight = 1080; - private const int TargetClientWidth = 1920; - private const int TargetClientHeight = 1080; private const int SwRestore = 9; - private const uint SwpNoZOrder = 0x0004; - private const uint SwpNoActivate = 0x0010; - private const uint MonitorDefaultToNearest = 0x00000002; - private const int GwlStyle = -16; - private const int GwlExStyle = -20; private const uint MouseEventMove = 0x0001; private const uint MouseEventLeftDown = 0x0002; private const uint MouseEventLeftUp = 0x0004; + private const uint MouseEventWheel = 0x0800; private const uint MouseEventVirtualDesk = 0x4000; private const uint MouseEventAbsolute = 0x8000; - private static readonly IReadOnlyDictionary Slots = + private static readonly IReadOnlyDictionary GridSlots = + new Dictionary + { + [MacroWeaponCategory.Primary] = new[] + { + new MacroPoint(-1115, 571), new MacroPoint(-970, 566), new MacroPoint(-824, 572), + new MacroPoint(-682, 576), new MacroPoint(-1267, 726), new MacroPoint(-1114, 725), + new MacroPoint(-965, 720), new MacroPoint(-817, 712), new MacroPoint(-657, 725), + new MacroPoint(-1269, 863), new MacroPoint(-1111, 865), new MacroPoint(-964, 869), + new MacroPoint(-817, 862), new MacroPoint(-680, 860), new MacroPoint(-1268, 1013) + }, + [MacroWeaponCategory.Secondary] = new[] + { + new MacroPoint(-1116, 574), new MacroPoint(-966, 575), new MacroPoint(-821, 569), + new MacroPoint(-673, 566), new MacroPoint(-1263, 719), new MacroPoint(-1111, 717), + new MacroPoint(-966, 718), new MacroPoint(-821, 724), new MacroPoint(-666, 714), + new MacroPoint(-1264, 876), new MacroPoint(-1105, 875) + }, + [MacroWeaponCategory.Melee] = new[] + { + new MacroPoint(-1115, 575), new MacroPoint(-968, 581), new MacroPoint(-832, 581), + new MacroPoint(-672, 571), new MacroPoint(-1266, 721), new MacroPoint(-1109, 719), + new MacroPoint(-970, 719), new MacroPoint(-812, 716), new MacroPoint(-676, 718), + new MacroPoint(-1244, 867) + }, + [MacroWeaponCategory.Utility] = new[] + { + new MacroPoint(-1100, 569), new MacroPoint(-974, 577), new MacroPoint(-814, 587), + new MacroPoint(-687, 587), new MacroPoint(-1261, 734), new MacroPoint(-1101, 719), + new MacroPoint(-977, 732), new MacroPoint(-793, 731), new MacroPoint(-679, 722), + new MacroPoint(-1265, 859), new MacroPoint(-1106, 857), new MacroPoint(-965, 858) + } + }; + + private static readonly IReadOnlyDictionary ListSlots = new Dictionary { [MacroWeaponCategory.Primary] = new[] { - new MacroPoint(-1119, 577), new MacroPoint(-961, 569), new MacroPoint(-820, 574), - new MacroPoint(-665, 574), new MacroPoint(-1259, 729), new MacroPoint(-1108, 715), - new MacroPoint(-960, 721), new MacroPoint(-832, 718), new MacroPoint(-667, 722), - new MacroPoint(-1267, 882), new MacroPoint(-1116, 856), new MacroPoint(-970, 862), - new MacroPoint(-836, 861), new MacroPoint(-648, 860), new MacroPoint(-1261, 1032) + new MacroPoint(-968, 492), new MacroPoint(-958, 577), new MacroPoint(-969, 671), + new MacroPoint(-965, 740), new MacroPoint(-984, 830), new MacroPoint(-980, 914), + new MacroPoint(-974, 997), new MacroPoint(-969, 437), new MacroPoint(-963, 531), + new MacroPoint(-960, 602), new MacroPoint(-953, 691), new MacroPoint(-950, 788), + new MacroPoint(-952, 877), new MacroPoint(-949, 950), new MacroPoint(-957, 1030) }, [MacroWeaponCategory.Secondary] = new[] { - new MacroPoint(-1119, 598), new MacroPoint(-945, 578), new MacroPoint(-816, 589), - new MacroPoint(-669, 583), new MacroPoint(-1276, 738), new MacroPoint(-1077, 713), - new MacroPoint(-959, 719), new MacroPoint(-804, 716), new MacroPoint(-665, 721), - new MacroPoint(-1271, 895), new MacroPoint(-1110, 890) + new MacroPoint(-962, 477), new MacroPoint(-957, 571), new MacroPoint(-965, 657), + new MacroPoint(-956, 741), new MacroPoint(-972, 827), new MacroPoint(-989, 902), + new MacroPoint(-955, 992), new MacroPoint(-969, 774), new MacroPoint(-969, 858), + new MacroPoint(-972, 949), new MacroPoint(-953, 1022) }, [MacroWeaponCategory.Melee] = new[] { - new MacroPoint(-1121, 587), new MacroPoint(-994, 578), new MacroPoint(-814, 578), - new MacroPoint(-663, 581), new MacroPoint(-1277, 726), new MacroPoint(-1117, 733), - new MacroPoint(-978, 727), new MacroPoint(-803, 730), new MacroPoint(-662, 730), - new MacroPoint(-1260, 865) + new MacroPoint(-962, 489), new MacroPoint(-955, 570), new MacroPoint(-954, 655), + new MacroPoint(-954, 740), new MacroPoint(-968, 826), new MacroPoint(-960, 901), + new MacroPoint(-962, 1005), new MacroPoint(-970, 850), new MacroPoint(-958, 932), + new MacroPoint(-959, 1024) }, [MacroWeaponCategory.Utility] = new[] { - new MacroPoint(-1109, 582), new MacroPoint(-991, 576), new MacroPoint(-806, 572), - new MacroPoint(-666, 571), new MacroPoint(-1261, 735), new MacroPoint(-1135, 724), - new MacroPoint(-959, 735), new MacroPoint(-760, 737), new MacroPoint(-684, 747), - new MacroPoint(-1259, 880), new MacroPoint(-1143, 877), new MacroPoint(-951, 870) + new MacroPoint(-953, 480), new MacroPoint(-968, 570), new MacroPoint(-960, 643), + new MacroPoint(-974, 738), new MacroPoint(-968, 828), new MacroPoint(-952, 914), + new MacroPoint(-990, 1009), new MacroPoint(-973, 680), new MacroPoint(-975, 774), + new MacroPoint(-966, 855), new MacroPoint(-971, 942), new MacroPoint(-990, 1043) } }; public static MacroPoint ResolveSlot( MacroWeaponCategory category, int originalIndex, - IEnumerable missingIndices) + IEnumerable missingIndices, + MacroWeaponLayout layout) { + IReadOnlyDictionary slots = + layout == MacroWeaponLayout.List ? ListSlots : GridSlots; int shift = missingIndices.Count(index => index < originalIndex); - int effectiveIndex = Math.Clamp(originalIndex - shift, 0, Slots[category].Length - 1); - return Slots[category][effectiveIndex]; + int effectiveIndex = Math.Clamp(originalIndex - shift, 0, slots[category].Length - 1); + int positionIndex = effectiveIndex; + return slots[category][positionIndex]; } public static async Task RunLoadoutAsync( IReadOnlyList<(MacroWeaponCategory Category, int OriginalIndex, IReadOnlyList MissingIndices)> selections, + MacroWeaponLayout layout, CancellationToken cancellationToken) { IntPtr robloxWindow = FindRobloxWindow(); if (robloxWindow == IntPtr.Zero) throw new InvalidOperationException("Roblox is not running."); - await NormalizeRobloxWindowAsync(robloxWindow, cancellationToken); - // Do not send SW_RESTORE to a fullscreen or maximized Roblox window. Windows can // interpret that as a request to leave its current presentation state. if (IsIconic(robloxWindow)) @@ -121,8 +158,14 @@ public static async Task RunLoadoutAsync( await Task.Delay(650, cancellationToken); var selection = selections[index]; - MacroPoint point = ResolveSlot(selection.Category, selection.OriginalIndex, selection.MissingIndices); + MacroPoint point = ResolveSlot(selection.Category, selection.OriginalIndex, selection.MissingIndices, layout); point = MapRecordedPointToWindow(point, robloxWindow); + if (layout == MacroWeaponLayout.List) + { + int missingBefore = selection.MissingIndices.Count(missing => missing < selection.OriginalIndex); + int effectiveIndex = Math.Max(0, selection.OriginalIndex - missingBefore); + await PrepareListScrollAsync(point, effectiveIndex >= 7, cancellationToken); + } await MoveAndClickExactPointAsync(point, cancellationToken); await Task.Delay(index < 3 ? 180 : 250, cancellationToken); } @@ -225,6 +268,41 @@ private static async Task MoveAndClickExactPointAsync(MacroPoint point, Cancella } } + private static async Task PrepareListScrollAsync(MacroPoint point, bool scrollToBottom, CancellationToken cancellationToken) + { + App.Logger.WriteLine( + "MacroAutomationService", + $"Normalizing List layout at {point.X}, {point.Y}; target edge: {(scrollToBottom ? "bottom" : "top")}"); + if (!SetCursorPos(point.X, point.Y)) + throw new InvalidOperationException("SleepStrap could not position the cursor over the List layout."); + + // Normalize every category to the top first. RIVALS preserves some List + // scroll positions between openings, so relying on the current offset can + // select the wrong weapon even when the recorded point is correct. + await Task.Delay(90, cancellationToken); + for (int notch = 0; notch < 24; notch++) + { + SendMouseWheel(120); + await Task.Delay(24, cancellationToken); + } + + await Task.Delay(220, cancellationToken); + if (!scrollToBottom) + return; + + // Every recorded weapon from slot eight onward was captured with its + // category at the bottom edge. Deliberately overscroll so no wheel input + // dropped by RIVALS can leave the list between pages. + for (int notch = 0; notch < 30; notch++) + { + SendMouseWheel(-120); + await Task.Delay(28, cancellationToken); + } + + // Let the animated list settle before the exact recorded click. + await Task.Delay(420, cancellationToken); + } + private static async Task ClickRecordedPointAsync(IntPtr window, MacroPoint recordedPoint, int waitAfter, CancellationToken cancellationToken) { await MoveAndClickExactPointAsync(MapRecordedPointToWindow(recordedPoint, window), cancellationToken); @@ -278,67 +356,25 @@ private static MacroPoint MapRecordedPointToWindow(MacroPoint recordedPoint, Int return new MacroPoint(mappedX, mappedY); } - private static async Task NormalizeRobloxWindowAsync(IntPtr window, CancellationToken cancellationToken) + private static void SendMouseButton(uint flags) { - if (!GetClientRect(window, out RECT currentClient)) - return; - - int currentWidth = currentClient.Right - currentClient.Left; - int currentHeight = currentClient.Bottom - currentClient.Top; - if (currentWidth == TargetClientWidth && currentHeight == TargetClientHeight) - return; - - // Restore down rather than minimize to the taskbar: a minimized window cannot - // receive the selector hotkey or clicks. The coordinate mapper below still - // scales to the final client size if the monitor cannot fit full 1080p. - ShowWindowAsync(window, SwRestore); - await Task.Delay(150, cancellationToken); - - IntPtr monitor = MonitorFromWindow(window, MonitorDefaultToNearest); - MONITORINFO monitorInfo = new() { Size = Marshal.SizeOf() }; - if (monitor == IntPtr.Zero || !GetMonitorInfo(monitor, ref monitorInfo)) - return; - - int style = GetWindowLong(window, GwlStyle); - int extendedStyle = GetWindowLong(window, GwlExStyle); - RECT targetFrame = new() { Right = TargetClientWidth, Bottom = TargetClientHeight }; - if (!AdjustWindowRectEx(ref targetFrame, style, false, extendedStyle)) - return; - - int chromeWidth = (targetFrame.Right - targetFrame.Left) - TargetClientWidth; - int chromeHeight = (targetFrame.Bottom - targetFrame.Top) - TargetClientHeight; - int workWidth = monitorInfo.Work.Right - monitorInfo.Work.Left; - int workHeight = monitorInfo.Work.Bottom - monitorInfo.Work.Top; - int availableClientWidth = Math.Max(640, workWidth - Math.Max(0, chromeWidth)); - int availableClientHeight = Math.Max(360, workHeight - Math.Max(0, chromeHeight)); - double scale = Math.Min( - 1d, - Math.Min(availableClientWidth / (double)TargetClientWidth, availableClientHeight / (double)TargetClientHeight)); - int targetClientWidth = Math.Max(640, (int)Math.Floor(TargetClientWidth * scale / 2d) * 2); - int targetClientHeight = Math.Max(360, (int)Math.Floor(TargetClientHeight * scale / 2d) * 2); - - targetFrame = new RECT { Right = targetClientWidth, Bottom = targetClientHeight }; - if (!AdjustWindowRectEx(ref targetFrame, style, false, extendedStyle)) - return; - - int outerWidth = targetFrame.Right - targetFrame.Left; - int outerHeight = targetFrame.Bottom - targetFrame.Top; - int x = monitorInfo.Work.Left + Math.Max(0, (workWidth - outerWidth) / 2); - int y = monitorInfo.Work.Top + Math.Max(0, (workHeight - outerHeight) / 2); - if (!SetWindowPos(window, IntPtr.Zero, x, y, outerWidth, outerHeight, SwpNoZOrder | SwpNoActivate)) - return; - - await Task.Delay(150, cancellationToken); - if (GetClientRect(window, out RECT resizedClient)) + INPUT[] inputs = { - App.Logger.WriteLine( - "MacroAutomationService", - $"Restored and scaled Roblox client from {currentWidth}x{currentHeight} to " + - $"{resizedClient.Right - resizedClient.Left}x{resizedClient.Bottom - resizedClient.Top}"); - } + new INPUT + { + Type = 0, + Data = new INPUTUNION + { + Mouse = new MOUSEINPUT { Flags = flags } + } + } + }; + + if (SendInput(1, inputs, Marshal.SizeOf()) != 1) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows rejected the simulated mouse click."); } - private static void SendMouseButton(uint flags) + private static void SendMouseWheel(int delta) { INPUT[] inputs = { @@ -347,13 +383,17 @@ private static void SendMouseButton(uint flags) Type = 0, Data = new INPUTUNION { - Mouse = new MOUSEINPUT { Flags = flags } + Mouse = new MOUSEINPUT + { + MouseData = unchecked((uint)delta), + Flags = MouseEventWheel + } } } }; if (SendInput(1, inputs, Marshal.SizeOf()) != 1) - throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows rejected the simulated mouse click."); + throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows rejected the simulated List scroll."); } private static void SendMouseAtPoint(MacroPoint point, uint buttonFlags) @@ -444,15 +484,6 @@ private struct RECT public int Bottom; } - [StructLayout(LayoutKind.Sequential)] - private struct MONITORINFO - { - public int Size; - public RECT Monitor; - public RECT Work; - public uint Flags; - } - [StructLayout(LayoutKind.Sequential)] private struct INPUT { @@ -514,21 +545,6 @@ private struct MOUSEINPUT [DllImport("user32.dll", SetLastError = true)] private static extern bool ClientToScreen(IntPtr window, ref POINT point); - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SetWindowPos(IntPtr window, IntPtr insertAfter, int x, int y, int width, int height, uint flags); - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool AdjustWindowRectEx(ref RECT rectangle, int style, bool hasMenu, int extendedStyle); - - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetWindowLong(IntPtr window, int index); - - [DllImport("user32.dll")] - private static extern IntPtr MonitorFromWindow(IntPtr window, uint flags); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern bool GetMonitorInfo(IntPtr monitor, ref MONITORINFO monitorInfo); - [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint inputCount, INPUT[] inputs, int inputSize); diff --git a/SleepStrap/Services/VisualModService.cs b/SleepStrap/Services/VisualModService.cs index 9aa35a3..0b22f11 100644 --- a/SleepStrap/Services/VisualModService.cs +++ b/SleepStrap/Services/VisualModService.cs @@ -137,14 +137,18 @@ public static void ApplyEmbeddedSkybox(string resourceFolder) public static void EnsureSelectedSkyboxReady(bool applyRivalsFix = true) { string selected = App.Settings.Prop.CustomSkyboxSourceName; - if (!App.Settings.Prop.CustomSkyboxEnabled || !SkyboxGalleryService.IsPreset(selected)) + if (!App.Settings.Prop.CustomSkyboxEnabled) + { + RemoveCustomSkybox(); return; + } - bool cacheIsReady = HasCachedSkybox && SkyboxFiles.All(file => IsDdsTexture(Path.Combine(SkyboxCacheRoot, file))); - if (cacheIsReady) - ApplyCachedSkybox(); - else - ApplyEmbeddedSkybox(SkyboxGalleryService.GetResourceFolder(selected), false); + if (!SkyboxGalleryService.IsPreset(selected)) + return; + + // This runs during launch after Roblox has been closed. Rebuilding the + // chosen preset here prevents a previous sky from surviving in cache. + ApplyEmbeddedSkybox(SkyboxGalleryService.GetResourceFolder(selected), true); if (applyRivalsFix) ApplyRivalsSkyboxCompatibilityFix(); diff --git a/SleepStrap/SleepStrap.csproj b/SleepStrap/SleepStrap.csproj index 13132a8..63d344c 100644 --- a/SleepStrap/SleepStrap.csproj +++ b/SleepStrap/SleepStrap.csproj @@ -7,8 +7,8 @@ true True SleepStrap.ico - 6.9.5.0 - 6.9.5.0 + 6.9.6.0 + 6.9.6.0 app.manifest true x64 diff --git a/SleepStrap/UI/Elements/Base/WpfUiWindow.cs b/SleepStrap/UI/Elements/Base/WpfUiWindow.cs index b697a99..5bf3b6a 100644 --- a/SleepStrap/UI/Elements/Base/WpfUiWindow.cs +++ b/SleepStrap/UI/Elements/Base/WpfUiWindow.cs @@ -21,14 +21,14 @@ public void ApplyTheme() const int customThemeIndex = 2; // index for CustomTheme merged dictionary _themeService.SetTheme(App.Settings.Prop.Theme.GetFinal() == Enums.Theme.Dark ? ThemeType.Dark : ThemeType.Light); - _themeService.SetAccent(System.Windows.Media.Color.FromRgb(139, 92, 246)); + _themeService.SetAccent(System.Windows.Media.Color.FromRgb(255, 255, 255)); // there doesn't seem to be a way to query the name for merged dictionaries var dict = new ResourceDictionary { Source = new Uri($"pack://application:,,,/UI/Style/{Enum.GetName(App.Settings.Prop.Theme.GetFinal())}.xaml") }; Application.Current.Resources.MergedDictionaries[customThemeIndex] = dict; #if QA_BUILD - this.BorderBrush = System.Windows.Media.Brushes.Red; + this.BorderBrush = System.Windows.Media.Brushes.Black; this.BorderThickness = new Thickness(4); #endif } diff --git a/SleepStrap/UI/Elements/Settings/MainWindow.xaml b/SleepStrap/UI/Elements/Settings/MainWindow.xaml index 2914eb4..baea557 100644 --- a/SleepStrap/UI/Elements/Settings/MainWindow.xaml +++ b/SleepStrap/UI/Elements/Settings/MainWindow.xaml @@ -11,16 +11,87 @@ mc:Ignorable="d" d:DataContext="{d:DesignInstance dmodels:MainWindowViewModel, IsDesignTimeCreatable=True}" Title="{Binding WindowTitle}" - MinWidth="920" - Width="920" - Height="600" - Background="{DynamicResource SleepStrapWindowBackgroundBrush}" + MinWidth="940" + Width="980" + Height="640" + Background="Transparent" ExtendsContentIntoTitleBar="True" WindowBackdropType="Mica" WindowStartupLocation="CenterScreen" Closing="WpfUiWindow_Closing" Closed="WpfUiWindow_Closed"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -82,7 +153,8 @@ - + @@ -93,12 +165,13 @@ - + - + + @@ -131,25 +204,36 @@ - + + + + + + + - - + - + + - - - + + + + + - + @@ -165,6 +249,9 @@ + diff --git a/SleepStrap/UI/Elements/Settings/MainWindow.xaml.cs b/SleepStrap/UI/Elements/Settings/MainWindow.xaml.cs index 4c5d7a2..48ba609 100644 --- a/SleepStrap/UI/Elements/Settings/MainWindow.xaml.cs +++ b/SleepStrap/UI/Elements/Settings/MainWindow.xaml.cs @@ -78,6 +78,12 @@ private async void SettingsIntroOverlay_Loaded(object sender, RoutedEventArgs e) EasingFunction = new SineEase { EasingMode = EasingMode.EaseInOut } }); + SettingsIntroSubtitle.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 0.9, TimeSpan.FromMilliseconds(320)) + { + BeginTime = TimeSpan.FromMilliseconds(760), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }); + int index = 0; foreach (TextBlock letter in SettingsBrandLetters.Children.OfType()) { @@ -122,8 +128,8 @@ private async void SettingsIntroOverlay_Loaded(object sender, RoutedEventArgs e) }); } - await Task.Delay(1700); - var fade = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(320)) + await Task.Delay(1500); + var fade = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(280)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } }; @@ -143,6 +149,157 @@ private static void AnimatePageNavigation(object sender, NavigationEventArgs e) var easing = new CubicEase { EasingMode = EasingMode.EaseOut }; page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260)) { EasingFunction = easing }); offset.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(12, 0, TimeSpan.FromMilliseconds(330)) { EasingFunction = easing }); + + page.Dispatcher.BeginInvoke(() => AnimatePageContents(page), System.Windows.Threading.DispatcherPriority.Loaded); + } + + private static void AnimatePageContents(FrameworkElement page) + { + StackPanel? existingLetters = FindVisualChildren(page) + .FirstOrDefault(panel => Equals(panel.Tag, "AnimatedPageTitleLetters")); + if (existingLetters is not null) + { + ReplayPageContents(existingLetters); + return; + } + + TextBlock? title = FindVisualChildren(page) + .FirstOrDefault(text => text.FontSize >= 27 && !String.IsNullOrWhiteSpace(text.Text)); + + if (title is null || VisualTreeHelper.GetParent(title) is not Panel titleParent) + return; + + var letters = new StackPanel + { + Orientation = Orientation.Horizontal, + VerticalAlignment = title.VerticalAlignment, + HorizontalAlignment = title.HorizontalAlignment, + Tag = "AnimatedPageTitleLetters" + }; + + foreach (char character in title.Text) + { + var letter = new TextBlock + { + Text = character == ' ' ? "\u00A0" : character.ToString(), + FontFamily = title.FontFamily, + FontSize = title.FontSize, + FontWeight = title.FontWeight, + FontStyle = title.FontStyle, + Foreground = title.Foreground, + Opacity = 0, + RenderTransform = new TranslateTransform(0, 9) + }; + letters.Children.Add(letter); + } + + if (titleParent is Grid) + { + Grid.SetRow(letters, Grid.GetRow(title)); + Grid.SetColumn(letters, Grid.GetColumn(title)); + Grid.SetRowSpan(letters, Grid.GetRowSpan(title)); + Grid.SetColumnSpan(letters, Grid.GetColumnSpan(title)); + title.Visibility = Visibility.Collapsed; + titleParent.Children.Add(letters); + } + else if (titleParent is StackPanel stack) + { + int index = stack.Children.IndexOf(title); + stack.Children.RemoveAt(index); + stack.Children.Insert(index, letters); + } + + ReplayPageContents(letters); + } + + private static void ReplayPageContents(StackPanel letters) + { + var popEase = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.28 }; + for (int index = 0; index < letters.Children.Count; index++) + { + if (letters.Children[index] is not TextBlock letter || letter.RenderTransform is not TranslateTransform rise) + continue; + + letter.BeginAnimation(OpacityProperty, null); + rise.BeginAnimation(TranslateTransform.YProperty, null); + letter.Opacity = 0; + rise.Y = 9; + TimeSpan delay = TimeSpan.FromMilliseconds(65 + (index * 42)); + letter.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)) { BeginTime = delay }); + rise.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(9, 0, TimeSpan.FromMilliseconds(300)) + { + BeginTime = delay, + EasingFunction = popEase + }); + } + + Grid? headerGrid = FindAncestor(letters, grid => grid.Children.OfType().Any(border => border.Height == 1)); + Border? titleLine = headerGrid?.Children.OfType().FirstOrDefault(border => border.Height == 1); + if (titleLine is not null) + { + titleLine.RenderTransformOrigin = new Point(0.5, 0.5); + var lineScale = titleLine.RenderTransform as ScaleTransform ?? new ScaleTransform(); + lineScale.BeginAnimation(ScaleTransform.ScaleXProperty, null); + lineScale.ScaleX = 0; + lineScale.ScaleY = 1; + titleLine.RenderTransform = lineScale; + lineScale.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(520)) + { + BeginTime = TimeSpan.FromMilliseconds(150), + EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseOut } + }); + } + + Border? header = headerGrid is null ? null : FindAncestor(headerGrid); + if (header is null || VisualTreeHelper.GetParent(header) is not StackPanel pageSections) + return; + + int sectionIndex = 0; + foreach (UIElement section in pageSections.Children.OfType().Skip(1)) + { + section.BeginAnimation(OpacityProperty, null); + section.Opacity = 0; + var sectionOffset = section.RenderTransform as TranslateTransform ?? new TranslateTransform(); + sectionOffset.BeginAnimation(TranslateTransform.YProperty, null); + sectionOffset.Y = 10; + section.RenderTransform = sectionOffset; + TimeSpan delay = TimeSpan.FromMilliseconds(210 + (sectionIndex++ * 65)); + section.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260)) + { + BeginTime = delay, + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }); + sectionOffset.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(10, 0, TimeSpan.FromMilliseconds(360)) + { + BeginTime = delay, + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }); + } + } + + private static IEnumerable FindVisualChildren(DependencyObject root) where T : DependencyObject + { + for (int index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++) + { + DependencyObject child = VisualTreeHelper.GetChild(root, index); + if (child is T match) + yield return match; + + foreach (T descendant in FindVisualChildren(child)) + yield return descendant; + } + } + + private static T? FindAncestor(DependencyObject start, Func? predicate = null) where T : DependencyObject + { + DependencyObject? current = VisualTreeHelper.GetParent(start); + while (current is not null) + { + if (current is T match && (predicate is null || predicate(match))) + return match; + current = VisualTreeHelper.GetParent(current); + } + return null; } public void LoadState() diff --git a/SleepStrap/UI/Elements/Settings/Pages/ClippingPage.xaml b/SleepStrap/UI/Elements/Settings/Pages/ClippingPage.xaml index 27497aa..f09a696 100644 --- a/SleepStrap/UI/Elements/Settings/Pages/ClippingPage.xaml +++ b/SleepStrap/UI/Elements/Settings/Pages/ClippingPage.xaml @@ -14,7 +14,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -37,18 +37,14 @@ - - - - - - - - + - - - + + + + + + @@ -163,7 +159,7 @@ @@ -175,7 +171,7 @@ MediaOpened="PreviewMedia_MediaOpened" MediaEnded="PreviewMedia_MediaEnded" Unloaded="PreviewMedia_Unloaded" /> + CornerRadius="8" Background="#D9101012" Opacity="0">