diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 70ed5dfde..c7b294747 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -367,6 +367,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool ShowTsm { get; set; } = true; + /// + /// Gets or sets a value indicating whether to reduce motions (animations). + /// + public bool ReduceMotions { get; set; } = false; + /// /// Gets or sets a value indicating whether or not market board data should be uploaded. /// diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index d4a08ff69..08e2817a5 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -1,5 +1,6 @@ using System.Numerics; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -20,8 +21,8 @@ internal sealed partial class ActiveNotification var opacity = Math.Clamp( (float)(this.hideEasing.IsRunning - ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) - : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + ? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.Value)), 0f, 1f); if (opacity <= 0) @@ -97,24 +98,25 @@ internal sealed partial class ActiveNotification this.lastInterestTime = DateTime.Now; this.DrawWindowBackgroundProgressBar(); - this.DrawTopBar(width, actionWindowHeight, isHovered); + this.DrawTopBar(width, actionWindowHeight, isHovered, warrantsExtension); if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) { this.DrawContentAndActions(width, actionWindowHeight); } else if (this.expandoEasing.IsRunning) { + var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.Value; if (this.underlyingNotification.Minimized) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - easedValue)); else - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * easedValue); this.DrawContentAndActions(width, actionWindowHeight); ImGui.PopStyleVar(); } if (isFocused) this.DrawFocusIndicator(); - this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); + this.DrawExpiryBar(warrantsExtension); if (ImGui.IsWindowHovered()) { @@ -184,24 +186,36 @@ internal sealed partial class ActiveNotification private void DrawWindowBackgroundProgressBar() { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.ProgressWaveLoopDuration) / - NotificationConstants.ProgressWaveLoopDuration); - elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + var elapsed = 0f; + var colorElapsed = 0f; + float progress; - var colorElapsed = - elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / - NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + if (ReducedMotions) + { + progress = this.Progress; + } + else + { + progress = Math.Clamp(this.ProgressEased, 0f, 1f); - elapsed = Math.Clamp(elapsed, 0f, 1f); - colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); - colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + elapsed = + (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; - var progress = Math.Clamp(this.ProgressEased, 0f, 1f); - if (progress >= 1f) - elapsed = colorElapsed = 0f; + colorElapsed = elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + if (progress >= 1f) + elapsed = colorElapsed = 0f; + } var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); @@ -240,7 +254,7 @@ internal sealed partial class ActiveNotification ImGui.PopClipRect(); } - private void DrawTopBar(float width, float height, bool drawActionButtons) + private void DrawTopBar(float width, float height, bool drawActionButtons, bool warrantsExtension) { var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); @@ -249,6 +263,10 @@ internal sealed partial class ActiveNotification using (Service.Get().IconFontHandle?.Push()) { ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (!drawActionButtons) + this.DrawExpiryPie(warrantsExtension, new(width - height, 0), new(height)); + if (this.UserDismissable) { if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) @@ -272,7 +290,7 @@ internal sealed partial class ActiveNotification } float relativeOpacity; - if (this.expandoEasing.IsRunning) + if (this.expandoEasing.IsRunning && !ReducedMotions) { relativeOpacity = this.underlyingNotification.Minimized @@ -297,36 +315,35 @@ internal sealed partial class ActiveNotification ImGui.TextUnformatted( ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) ? this.CreatedAt.LocAbsolute() - : this.CreatedAt.LocRelativePastLong()); + : ReducedMotions + ? this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(15)) + : this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(5))); ImGui.PopStyleColor(); ImGui.PopStyleVar(); } if (relativeOpacity < 1) { - rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); - var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); - this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); - - ltOffset.X = height; - - var agoText = this.CreatedAt.LocRelativePastShort(); + var agoText = + ReducedMotions + ? this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(15)) + : this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(5)); var agoSize = ImGui.CalcTextSize(agoText); - rtOffset.X -= agoSize.X; - ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.SetCursorPos(new(width - ((height + agoSize.X) / 2f), NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.TextUnformatted(agoText); ImGui.PopStyleColor(); - rtOffset.X -= NotificationConstants.ScaledWindowPadding; - + this.DrawIcon( + new(NotificationConstants.ScaledWindowPadding), + new(height - (2 * NotificationConstants.ScaledWindowPadding))); ImGui.PushClipRect( - windowPos + ltOffset with { Y = 0 }, - windowPos + rtOffset with { Y = height }, + windowPos + new Vector2(height, 0), + windowPos + new Vector2(width - height, height), true); - ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.SetCursorPos(new(height, NotificationConstants.ScaledWindowPadding)); ImGui.TextUnformatted(this.EffectiveMinimizedText); ImGui.PopClipRect(); @@ -437,12 +454,95 @@ internal sealed partial class ActiveNotification ImGui.PopTextWrapPos(); } - private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) + private void DrawExpiryPie(bool warrantsExtension, Vector2 offset, Vector2 size) { + if (!Service.Get().ReduceMotions) + return; + + // circle here; 0 means 0deg; 1 means 360deg + float fillStartCw, fillEndCw; + if (this.DismissReason is not null) + { + fillStartCw = fillEndCw = 0f; + } + else if (warrantsExtension) + { + fillStartCw = fillEndCw = 0f; + } + else if (this.EffectiveExpiry == DateTime.MaxValue) + { + if (this.ShowIndeterminateIfNoExpiry) + { + // draw + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminatePieLoopDuration) / + NotificationConstants.IndeterminatePieLoopDuration); + fillStartCw = elapsed; + fillEndCw = elapsed + 0.2f + (MathF.Sin(elapsed * MathF.PI) * 0.2f); + } + else + { + // do not draw + fillStartCw = fillEndCw = 0f; + } + } + else + { + fillStartCw = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds / + (this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds); + fillEndCw = 1f; + } + + if (fillStartCw > fillEndCw) + (fillStartCw, fillEndCw) = (fillEndCw, fillStartCw); + + if (fillStartCw == 0 && fillEndCw == 0) + return; + + var radius = Math.Min(size.X, size.Y) / 3f; + var ifrom = fillStartCw * MathF.PI * 2; + var ito = fillEndCw * MathF.PI * 2; + + var nseg = MathF.Ceiling(2 * MathF.PI * radius); + var step = (MathF.PI * 2) / nseg; + + var center = ImGui.GetWindowPos() + offset + (size / 2); + var color = ImGui.GetColorU32(this.Type.ToColor() * new Vector4(1, 1, 1, 0.2f)); + + var prevOff = center + (radius * new Vector2(MathF.Sin(ifrom), -MathF.Cos(ifrom))); + Span verts = stackalloc Vector2[(int)MathF.Ceiling(((ito - ifrom) / step) + 3)]; + var vertPtr = 0; + verts[vertPtr++] = center; + verts[vertPtr++] = prevOff; + + var cur = ifrom + step; + for (; cur < ito; cur += step) + { + var curOff = center + (radius * new Vector2(MathF.Sin(cur), -MathF.Cos(cur))); + if (Vector2.DistanceSquared(prevOff, curOff) >= 1) + verts[vertPtr++] = prevOff = curOff; + } + + var lastOff = center + (radius * new Vector2(MathF.Sin(ito), -MathF.Cos(ito))); + if (Vector2.DistanceSquared(prevOff, lastOff) >= 1) + verts[vertPtr++] = lastOff; + unsafe + { + var dlist = ImGui.GetWindowDrawList().NativePtr; + fixed (Vector2* pvert = verts) + ImGuiNative.ImDrawList_AddConvexPolyFilled(dlist, pvert, vertPtr, color); + } + } + + private void DrawExpiryBar(bool warrantsExtension) + { + if (Service.Get().ReduceMotions) + return; + float barL, barR; if (this.DismissReason is not null) { - var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.Value; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; var length = (this.prevProgressR - this.prevProgressL) / 2f; barL = midpoint - (length * v); @@ -455,7 +555,7 @@ internal sealed partial class ActiveNotification this.prevProgressL = barL; this.prevProgressR = barR; } - else if (effectiveExpiry == DateTime.MaxValue) + else if (this.EffectiveExpiry == DateTime.MaxValue) { if (this.ShowIndeterminateIfNoExpiry) { @@ -477,8 +577,8 @@ internal sealed partial class ActiveNotification } else { - barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.lastInterestTime).TotalMilliseconds); + barL = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds / + (this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 3bc7c3837..3cad13242 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -2,6 +2,7 @@ using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Internal; @@ -187,16 +188,18 @@ internal sealed partial class ActiveNotification : IActiveNotification set => this.newProgress = value; } + private static bool ReducedMotions => Service.Get().ReduceMotions; + /// Gets the eased progress. private float ProgressEased { get { var underlyingProgress = this.underlyingNotification.Progress; - if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone || ReducedMotions) return underlyingProgress; - var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); + var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.Value, 0f, 1f); return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index de212160c..18bb57118 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -41,6 +41,10 @@ internal static class NotificationConstants /// The duration of indeterminate progress bar loop in milliseconds. public const float IndeterminateProgressbarLoopDuration = 2000f; + /// The duration of indeterminate pie loop in milliseconds. + /// Note that this value is applicable when reduced motion configuration is on. + public const float IndeterminatePieLoopDuration = 8000f; + /// The duration of the progress wave animation in milliseconds. public const float ProgressWaveLoopDuration = 2000f; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5ccace850..53faed6ec 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -130,6 +130,12 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins, (v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins), + + new SettingsEntry( + Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"), + Loc.Localize("DalamudSettingReducedMotion", "This will suppress certain animations from Dalamud, such as the notification popup."), + c => c.ReduceMotions, + (v, c) => c.ReduceMotions = v), }; public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs index 8422a4a26..f2dcd3d55 100644 --- a/Dalamud/Utility/DateTimeSpanExtensions.cs +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -44,8 +44,9 @@ public static class DateTimeSpanExtensions /// Formats an instance of as a localized relative time. /// When. + /// The alignment unit of time span. /// The formatted string. - public static string LocRelativePastLong(this DateTime when) + public static string LocRelativePastLong(this DateTime when, TimeSpan floorBy) { var loc = Loc.Localize( "DateTimeSpanExtensions.RelativeFormatStringsLong", @@ -55,13 +56,17 @@ public static class DateTimeSpanExtensions if (relativeFormatStringLong?.FormatStringLoc != loc) relativeFormatStringLong ??= new(loc); - return relativeFormatStringLong.Format(DateTime.Now - when); + return + floorBy == default + ? relativeFormatStringLong.Format(DateTime.Now - when) + : relativeFormatStringLong.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy); } /// Formats an instance of as a localized relative time. /// When. + /// The alignment unit of time span. /// The formatted string. - public static string LocRelativePastShort(this DateTime when) + public static string LocRelativePastShort(this DateTime when, TimeSpan floorBy) { var loc = Loc.Localize( "DateTimeSpanExtensions.RelativeFormatStringsShort", @@ -71,9 +76,22 @@ public static class DateTimeSpanExtensions if (relativeFormatStringShort?.FormatStringLoc != loc) relativeFormatStringShort = new(loc); - return relativeFormatStringShort.Format(DateTime.Now - when); + return + floorBy == default + ? relativeFormatStringShort.Format(DateTime.Now - when) + : relativeFormatStringShort.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy); } + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastLong(this DateTime when) => when.LocRelativePastLong(TimeSpan.FromSeconds(1)); + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastShort(this DateTime when) => when.LocRelativePastShort(TimeSpan.FromSeconds(1)); + private sealed class ParsedRelativeFormatStrings { private readonly List<(float MinSeconds, string FormatString)> formatStrings = new();