Separate progress and expiry animations

This commit is contained in:
Soreepeong 2024-02-26 13:04:17 +09:00
parent f434946137
commit e96089f8b2
6 changed files with 122 additions and 39 deletions

View file

@ -59,6 +59,9 @@ public interface IActiveNotification : INotification
/// <inheritdoc cref="INotification.Expiry"/>
new DateTime Expiry { get; set; }
/// <inheritdoc cref="INotification.ShowIndeterminateIfNoExpiry"/>
new bool ShowIndeterminateIfNoExpiry { get; set; }
/// <inheritdoc cref="INotification.Interactable"/>
new bool Interactable { get; set; }

View file

@ -37,6 +37,10 @@ public interface INotification : IDisposable
/// (sticky, indeterminate, permanent, or persistent).</remarks>
DateTime Expiry { get; }
/// <summary>Gets a value indicating whether to show an indeterminate expiration animation if <see cref="Expiry"/>
/// is set to <see cref="DateTime.MaxValue"/>.</summary>
bool ShowIndeterminateIfNoExpiry { get; }
/// <summary>Gets a value indicating whether this notification may be interacted.</summary>
/// <remarks>
/// Set this value to <c>true</c> if you want to respond to user inputs from
@ -58,8 +62,7 @@ public interface INotification : IDisposable
/// </remarks>
TimeSpan HoverExtendDuration { get; }
/// <summary>Gets the progress for the progress bar of the notification.
/// The progress should either be in the range between 0 and 1 or be a negative value.
/// Specifying a negative value will show an indeterminate progress bar.</summary>
/// <summary>Gets the progress for the background progress bar of the notification.</summary>
/// <remarks>The progress should be in the range between 0 and 1.</remarks>
float Progress { get; }
}

View file

@ -50,7 +50,7 @@ internal sealed class ActiveNotification : IActiveNotification
this.InitiatorPlugin = initiatorPlugin;
this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration);
this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration);
this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration);
this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration);
this.showEasing.Start();
this.progressEasing.Start();
@ -142,6 +142,18 @@ internal sealed class ActiveNotification : IActiveNotification
}
}
/// <inheritdoc cref="IActiveNotification.ShowIndeterminateIfNoExpiry"/>
public bool ShowIndeterminateIfNoExpiry
{
get => this.underlyingNotification.ShowIndeterminateIfNoExpiry;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.ShowIndeterminateIfNoExpiry = value;
}
}
/// <inheritdoc cref="IActiveNotification.Interactable"/>
public bool Interactable
{
@ -212,9 +224,6 @@ internal sealed class ActiveNotification : IActiveNotification
get
{
var underlyingProgress = this.underlyingNotification.Progress;
if (underlyingProgress < 0)
return 0f;
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone)
return underlyingProgress;
@ -409,6 +418,7 @@ internal sealed class ActiveNotification : IActiveNotification
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
this.DrawWindowBackgroundProgressBar();
this.DrawNotificationMainWindowContent(width);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
@ -440,6 +450,7 @@ internal sealed class ActiveNotification : IActiveNotification
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
this.DrawWindowBackgroundProgressBar();
this.DrawNotificationActionWindowContent(interfaceManager, width);
windowSize.Y += actionWindowHeight;
windowPos.Y -= actionWindowHeight;
@ -517,7 +528,7 @@ internal sealed class ActiveNotification : IActiveNotification
this.underlyingNotification.IconSource = newIconSource;
this.UpdateIcon();
}
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
public void RemoveNonDalamudInvocations()
{
@ -563,6 +574,49 @@ internal sealed class ActiveNotification : IActiveNotification
this.MaterializedIcon = null;
}
private void DrawWindowBackgroundProgressBar()
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.ProgressWaveLoopDuration) /
NotificationConstants.ProgressWaveLoopDuration);
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
var 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));
var progress = Math.Clamp(this.ProgressEased, 0f, 1f);
if (progress >= 1f)
elapsed = colorElapsed = 0f;
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var rb = windowPos + windowSize;
var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed };
var rp = windowPos + windowSize with { X = windowSize.X * progress };
ImGui.PushClipRect(windowPos, rb, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos,
midp,
ImGui.GetColorU32(
Vector4.Lerp(
NotificationConstants.BackgroundProgressColorMin,
NotificationConstants.BackgroundProgressColorMax,
colorElapsed)));
ImGui.GetWindowDrawList().AddRectFilled(
midp with { Y = 0 },
rp,
ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin));
ImGui.PopClipRect();
}
private void DrawNotificationMainWindowContent(float width)
{
var basePos = ImGui.GetCursorPos();
@ -580,62 +634,61 @@ internal sealed class ActiveNotification : IActiveNotification
// Top padding is zero, as the action window will add the padding.
ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding));
float progressL, progressR;
float barL, barR;
if (this.IsDismissed)
{
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f;
progressL = midpoint - (length * v);
progressR = midpoint + (length * v);
barL = midpoint - (length * v);
barR = midpoint + (length * v);
}
else if (this.Expiry == DateTime.MaxValue)
{
if (this.Progress >= 0)
{
progressL = 0f;
progressR = this.ProgressEased;
}
else
if (this.ShowIndeterminateIfNoExpiry)
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.IndeterminateProgressbarLoopDuration) /
NotificationConstants.IndeterminateProgressbarLoopDuration);
progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3);
progressR = Math.Min(elapsed, 2f / 3) / (2f / 3);
progressL = MathF.Pow(progressL, 3);
progressR = 1f - MathF.Pow(1f - progressR, 3);
barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3);
barR = Math.Min(elapsed, 2f / 3) / (2f / 3);
barL = MathF.Pow(barL, 3);
barR = 1f - MathF.Pow(1f - barR, 3);
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else
{
this.prevProgressL = barL = 0f;
this.prevProgressR = barR = 1f;
}
this.prevProgressL = progressL;
this.prevProgressR = progressR;
}
else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)
{
progressL = 0f;
progressR = 1f;
this.prevProgressL = progressL;
this.prevProgressR = progressR;
barL = 0f;
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else
{
progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds /
(this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds);
progressR = 1f;
this.prevProgressL = progressL;
this.prevProgressR = progressR;
barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds /
(this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds);
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
progressR = Math.Clamp(progressR, 0f, 1f);
barR = Math.Clamp(barR, 0f, 1f);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos + new Vector2(
windowSize.X * progressL,
windowSize.X * barL,
windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight),
windowPos + windowSize with { X = windowSize.X * progressR },
windowPos + windowSize with { X = windowSize.X * barR },
ImGui.GetColorU32(this.DefaultIconColor));
ImGui.PopClipRect();
}

View file

@ -20,6 +20,9 @@ public sealed record Notification : INotification
/// <inheritdoc/>
public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration;
/// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry { get; set; } = true;
/// <inheritdoc/>
public bool Interactable { get; set; } = true;

View file

@ -37,14 +37,24 @@ public static class NotificationConstants
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
internal const float IndeterminateProgressbarLoopDuration = 2000f;
/// <summary>The duration of the progress wave animation in milliseconds.</summary>
internal const float ProgressWaveLoopDuration = 2000f;
/// <summary>The time ratio of a progress wave loop where the animation is idle.</summary>
internal const float ProgressWaveIdleTimeRatio = 0.5f;
/// <summary>The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque.
/// </summary>
internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>Duration of show animation.</summary>
internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of hide animation.</summary>
internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of hide animation.</summary>
internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200);
/// <summary>Duration of progress change animation.</summary>
internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200);
/// <summary>Text color for the when.</summary>
internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f);
@ -61,6 +71,12 @@ public static class NotificationConstants
/// <summary>Text color for the body.</summary>
internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f);
/// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f);
/// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f);
/// <summary>Gets the relative time format strings.</summary>
private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings =
{
@ -94,7 +110,7 @@ public static class NotificationConstants
internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale);
/// <summary>Gets the height of the expiry progress bar.</summary>
internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale);
internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the string format of the initiator name field, if the initiator is unloaded.</summary>
internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)";

View file

@ -120,6 +120,8 @@ internal class ImGuiWidget : IDataWindowWidget
ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable);
ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry);
ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable);
ImGui.Checkbox(
@ -147,6 +149,7 @@ internal class ImGuiWidget : IDataWindowWidget
Content = text,
Title = title,
Type = type,
ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry,
Interactable = this.notificationTemplate.Interactable,
UserDismissable = this.notificationTemplate.UserDismissable,
Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration,
@ -347,6 +350,7 @@ internal class ImGuiWidget : IDataWindowWidget
public bool ManualType;
public int TypeInt;
public int DurationInt;
public bool ShowIndeterminateIfNoExpiry;
public bool Interactable;
public bool UserDismissable;
public bool ActionBar;
@ -364,6 +368,7 @@ internal class ImGuiWidget : IDataWindowWidget
this.ManualType = false;
this.TypeInt = (int)NotificationType.None;
this.DurationInt = 2;
this.ShowIndeterminateIfNoExpiry = true;
this.Interactable = true;
this.UserDismissable = true;
this.ActionBar = true;