diff --git a/LightlessSync/UI/Style/AnimatedHeader.cs b/LightlessSync/UI/Style/AnimatedHeader.cs
new file mode 100644
index 0000000..0037b53
--- /dev/null
+++ b/LightlessSync/UI/Style/AnimatedHeader.cs
@@ -0,0 +1,463 @@
+using Dalamud.Bindings.ImGui;
+using Dalamud.Interface;
+using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using System.Numerics;
+
+namespace LightlessSync.UI.Style;
+
+///
+/// A reusable animated header component with a gradient background, some funny stars, and shooting star effects to match the lightless void theme a bit.
+///
+public class AnimatedHeader
+{
+ private struct Particle
+ {
+ public Vector2 Position;
+ public Vector2 Velocity;
+ public float Life;
+ public float MaxLife;
+ public float Size;
+ public ParticleType Type;
+ public List? Trail;
+ public float Twinkle;
+ public float Depth;
+ public float Hue;
+ }
+
+ private enum ParticleType
+ {
+ TwinklingStar,
+ ShootingStar
+ }
+
+ private readonly List _particles = [];
+ private float _particleSpawnTimer;
+ private readonly Random _random = new();
+
+ private const float _particleSpawnInterval = 0.2f;
+ private const int _maxParticles = 50;
+ private const int _maxTrailLength = 50;
+ private const float _edgeFadeDistance = 30f;
+ private const float _extendedParticleHeight = 40f;
+
+ public float Height { get; set; } = 150f;
+ public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
+ public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
+ public bool EnableParticles { get; set; } = true;
+ public bool EnableBottomGradient { get; set; } = true;
+
+ ///
+ /// Draws the animated header with some customizable content
+ ///
+ /// Width of the header
+ /// Action to draw custom content inside the header
+ public void Draw(float width, Action drawContent)
+ {
+ var windowPos = ImGui.GetWindowPos();
+ var windowPadding = ImGui.GetStyle().WindowPadding;
+
+ var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
+ var headerEnd = headerStart + new Vector2(width, Height);
+ var extendedParticleSize = new Vector2(width, Height + _extendedParticleHeight);
+
+ DrawGradientBackground(headerStart, headerEnd);
+
+ if (EnableParticles)
+ {
+ DrawParticleEffects(headerStart, extendedParticleSize);
+ }
+
+ drawContent(headerStart, headerEnd);
+
+ if (EnableBottomGradient)
+ {
+ DrawBottomGradient(headerStart, headerEnd, width);
+ }
+ }
+
+ ///
+ /// Draws a simple animated header with title and subtitle.
+ ///
+ public void DrawSimple(float width, string title, string subtitle, IFontHandle? titleFont = null, Vector4? titleColor = null, Vector4? subtitleColor = null)
+ {
+ Draw(width, (headerStart, headerEnd) =>
+ {
+ var textX = 20f;
+ var textY = 30f;
+
+ ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
+
+ if (titleFont != null)
+ {
+ using (titleFont.Push())
+ {
+ ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
+ }
+ }
+ else
+ {
+ ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
+ }
+
+ ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
+ ImGui.TextColored(subtitleColor ?? UIColors.Get("LightlessBlue"), subtitle);
+ });
+ }
+
+ ///
+ /// Draws a header with title, subtitle, and action buttons in the top-right corner.
+ ///
+ public void DrawWithButtons(float width, string title, string subtitle, List buttons, IFontHandle? titleFont = null)
+ {
+ Draw(width, (headerStart, headerEnd) =>
+ {
+ // Draw title and subtitle
+ var textX = 20f;
+ var textY = 30f;
+
+ ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
+
+ if (titleFont != null)
+ {
+ using (titleFont.Push())
+ {
+ ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
+ }
+ }
+ else
+ {
+ ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title);
+ }
+
+ ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
+ ImGui.TextColored(UIColors.Get("LightlessBlue"), subtitle);
+
+ // Draw buttons
+ if (buttons.Count > 0)
+ {
+ DrawHeaderButtons(headerStart, width, buttons);
+ }
+ });
+ }
+
+ private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
+ {
+ var drawList = ImGui.GetWindowDrawList();
+
+ drawList.AddRectFilledMultiColor(
+ headerStart,
+ headerEnd,
+ ImGui.GetColorU32(TopColor),
+ ImGui.GetColorU32(TopColor),
+ ImGui.GetColorU32(BottomColor),
+ ImGui.GetColorU32(BottomColor)
+ );
+
+ // Draw static background stars
+ var random = new Random(42);
+ for (int i = 0; i < 50; i++)
+ {
+ var starPos = headerStart + new Vector2(
+ (float)random.NextDouble() * (headerEnd.X - headerStart.X),
+ (float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
+ );
+ var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
+ drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
+ }
+ }
+
+ private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
+ {
+ var drawList = ImGui.GetWindowDrawList();
+ var gradientHeight = 60f;
+
+ for (int i = 0; i < gradientHeight; i++)
+ {
+ var progress = i / gradientHeight;
+ var smoothProgress = progress * progress;
+ var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress;
+ var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress;
+ var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress;
+ var alpha = 1f - smoothProgress;
+ var gradientColor = new Vector4(r, g, b, alpha);
+ drawList.AddLine(
+ new Vector2(headerStart.X, headerEnd.Y + i),
+ new Vector2(headerStart.X + width, headerEnd.Y + i),
+ ImGui.GetColorU32(gradientColor),
+ 1f
+ );
+ }
+ }
+
+ private void DrawHeaderButtons(Vector2 headerStart, float headerWidth, List buttons)
+ {
+ var spacing = 8f * ImGuiHelpers.GlobalScale;
+ var rightPadding = 15f * ImGuiHelpers.GlobalScale;
+ var topPadding = 15f * ImGuiHelpers.GlobalScale;
+ var buttonY = headerStart.Y + topPadding;
+
+ using (ImRaii.PushFont(UiBuilder.IconFont))
+ {
+ // Calculate button size (assuming all buttons are the same size)
+ var buttonSize = ImGui.CalcTextSize(FontAwesomeIcon.Globe.ToIconString());
+ buttonSize += ImGui.GetStyle().FramePadding * 2;
+
+ float currentX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
+
+ using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)))
+ using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f }))
+ using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f }))
+ {
+ for (int i = buttons.Count - 1; i >= 0; i--)
+ {
+ var button = buttons[i];
+ ImGui.SetCursorScreenPos(new Vector2(currentX, buttonY));
+
+ if (ImGui.Button(button.Icon.ToIconString()))
+ {
+ button.OnClick?.Invoke();
+ }
+
+ if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
+ {
+ ImGui.SetTooltip(button.Tooltip);
+ }
+
+ currentX -= buttonSize.X + spacing;
+ }
+ }
+ }
+ }
+
+ private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
+ {
+ var deltaTime = ImGui.GetIO().DeltaTime;
+ _particleSpawnTimer += deltaTime;
+
+ if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles)
+ {
+ SpawnParticle(bannerSize);
+ _particleSpawnTimer = 0f;
+ }
+
+ if (_random.NextDouble() < 0.003)
+ {
+ SpawnShootingStar(bannerSize);
+ }
+
+ var drawList = ImGui.GetWindowDrawList();
+
+ for (int i = _particles.Count - 1; i >= 0; i--)
+ {
+ var particle = _particles[i];
+
+ var screenPos = bannerStart + particle.Position;
+
+ if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
+ {
+ particle.Trail.Insert(0, particle.Position);
+ if (particle.Trail.Count > _maxTrailLength)
+ particle.Trail.RemoveAt(particle.Trail.Count - 1);
+ }
+
+ if (particle.Type == ParticleType.TwinklingStar)
+ {
+ particle.Twinkle += 0.005f * particle.Depth;
+ }
+
+ particle.Position += particle.Velocity * deltaTime;
+ particle.Life -= deltaTime;
+
+ var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
+ particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
+
+ if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
+ {
+ _particles.RemoveAt(i);
+ continue;
+ }
+
+ if (particle.Type == ParticleType.TwinklingStar)
+ {
+ if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
+ particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
+ if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
+ particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
+ }
+
+ var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
+ var fadeOut = Math.Min(1f, particle.Life / 20f);
+ var lifeFade = Math.Min(fadeIn, fadeOut);
+
+ var edgeFadeX = Math.Min(
+ Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance),
+ Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance)
+ );
+ var edgeFadeY = Math.Min(
+ Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance),
+ Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance)
+ );
+ var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
+
+ var baseAlpha = lifeFade * edgeFade;
+ var finalAlpha = particle.Type == ParticleType.TwinklingStar
+ ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
+ : baseAlpha;
+
+ if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
+ {
+ var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
+
+ for (int t = 1; t < particle.Trail.Count; t++)
+ {
+ var trailProgress = (float)t / particle.Trail.Count;
+ var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
+ var trailWidth = (1f - trailProgress) * 3f + 1f;
+
+ var glowAlpha = trailAlpha * 0.4f;
+ drawList.AddLine(
+ bannerStart + particle.Trail[t - 1],
+ bannerStart + particle.Trail[t],
+ ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
+ trailWidth + 4f
+ );
+
+ drawList.AddLine(
+ bannerStart + particle.Trail[t - 1],
+ bannerStart + particle.Trail[t],
+ ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
+ trailWidth
+ );
+ }
+ }
+ else if (particle.Type == ParticleType.TwinklingStar)
+ {
+ DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
+ }
+
+ _particles[i] = particle;
+ }
+ }
+
+ private static void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth)
+ {
+ var color = HslToRgb(hue, 1.0f, 0.85f);
+ color.W = alpha;
+
+ drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
+
+ var glowColor = color with { W = alpha * 0.3f };
+ drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
+ }
+
+ private static Vector4 HslToRgb(float h, float s, float l)
+ {
+ h = h / 360f;
+ float c = (1 - MathF.Abs(2 * l - 1)) * s;
+ float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
+ float m = l - c / 2;
+
+ float r, g, b;
+ if (h < 1f / 6f)
+ {
+ r = c; g = x; b = 0;
+ }
+ else if (h < 2f / 6f)
+ {
+ r = x; g = c; b = 0;
+ }
+ else if (h < 3f / 6f)
+ {
+ r = 0; g = c; b = x;
+ }
+ else if (h < 4f / 6f)
+ {
+ r = 0; g = x; b = c;
+ }
+ else if (h < 5f / 6f)
+ {
+ r = x; g = 0; b = c;
+ }
+ else
+ {
+ r = c; g = 0; b = x;
+ }
+
+ return new Vector4(r + m, g + m, b + m, 1.0f);
+ }
+
+ private void SpawnParticle(Vector2 bannerSize)
+ {
+ var position = new Vector2(
+ (float)_random.NextDouble() * bannerSize.X,
+ (float)_random.NextDouble() * bannerSize.Y
+ );
+
+ var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
+ var depth = depthLayers[_random.Next(depthLayers.Length)];
+
+ var velocity = new Vector2(
+ ((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
+ ((float)_random.NextDouble() - 0.5f) * 0.05f * depth
+ );
+
+ var isBlue = _random.NextDouble() < 0.5;
+ var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
+ var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
+ var maxLife = 120f + (float)_random.NextDouble() * 60f;
+
+ _particles.Add(new Particle
+ {
+ Position = position,
+ Velocity = velocity,
+ Life = maxLife,
+ MaxLife = maxLife,
+ Size = size,
+ Type = ParticleType.TwinklingStar,
+ Trail = null,
+ Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
+ Depth = depth,
+ Hue = hue
+ });
+ }
+
+ private void SpawnShootingStar(Vector2 bannerSize)
+ {
+ var maxLife = 80f + (float)_random.NextDouble() * 40f;
+ var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
+ var startY = -10f;
+
+ _particles.Add(new Particle
+ {
+ Position = new Vector2(startX, startY),
+ Velocity = new Vector2(
+ -50f - (float)_random.NextDouble() * 40f,
+ 30f + (float)_random.NextDouble() * 40f
+ ),
+ Life = maxLife,
+ MaxLife = maxLife,
+ Size = 2.5f,
+ Type = ParticleType.ShootingStar,
+ Trail = new List(),
+ Twinkle = 0,
+ Depth = 1.0f,
+ Hue = 270f
+ });
+ }
+
+ ///
+ /// Clears all active particles. Useful when closing or hiding a window with an animated header.
+ ///
+ public void ClearParticles()
+ {
+ _particles.Clear();
+ _particleSpawnTimer = 0f;
+ }
+}
+
+///
+/// Represents a button in the animated header.
+///
+public record HeaderButton(FontAwesomeIcon Icon, string Tooltip, Action? OnClick = null);
diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs
index c5331a0..340253f 100644
--- a/LightlessSync/UI/UpdateNotesUi.cs
+++ b/LightlessSync/UI/UpdateNotesUi.cs
@@ -13,6 +13,7 @@ using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Dalamud.Interface;
using LightlessSync.UI.Models;
+using LightlessSync.UI.Style;
using LightlessSync.Utils;
namespace LightlessSync.UI;
@@ -27,37 +28,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private CreditsFile _credits = new();
private bool _scrollToTop;
private bool _hasInitializedCollapsingHeaders;
-
- private struct Particle
- {
- public Vector2 Position;
- public Vector2 Velocity;
- public float Life;
- public float MaxLife;
- public float Size;
- public ParticleType Type;
- public List? Trail;
- public float Twinkle;
- public float Depth;
- public float Hue;
- }
-
- private enum ParticleType
- {
- TwinklingStar,
- ShootingStar
- }
-
- private readonly List _particles = [];
- private float _particleSpawnTimer;
- private readonly Random _random = new();
-
- private const float _headerHeight = 150f;
- private const float _particleSpawnInterval = 0.2f;
- private const int _maxParticles = 50;
- private const int _maxTrailLength = 50;
- private const float _edgeFadeDistance = 30f;
- private const float _extendedParticleHeight = 40f;
+ private readonly AnimatedHeader _animatedHeader = new();
public UpdateNotesUi(ILogger logger,
LightlessMediator mediator,
@@ -94,6 +65,11 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
_hasInitializedCollapsingHeaders = false;
}
+ public override void OnClose()
+ {
+ _animatedHeader.ClearParticles();
+ }
+
private void CenterWindow()
{
var viewport = ImGui.GetMainViewport();
@@ -116,21 +92,18 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private void DrawHeader()
{
- var windowPos = ImGui.GetWindowPos();
var windowPadding = ImGui.GetStyle().WindowPadding;
var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2);
- var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
- var headerEnd = headerStart + new Vector2(headerWidth, _headerHeight);
+ var buttons = new List
+ {
+ new(FontAwesomeIcon.Comments, "Join our Discord", () => Util.OpenLink("https://discord.gg/dsbjcXMnhA")),
+ new(FontAwesomeIcon.Code, "View on Git", () => Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync"))
+ };
- var extendedParticleSize = new Vector2(headerWidth, _headerHeight + _extendedParticleHeight);
+ _animatedHeader.DrawWithButtons(headerWidth, "Lightless Sync", "Update Notes", buttons, _uiShared.UidFont);
- DrawGradientBackground(headerStart, headerEnd);
- DrawHeaderText(headerStart);
- DrawHeaderButtons(headerStart, headerWidth);
- DrawBottomGradient(headerStart, headerEnd, headerWidth);
-
- ImGui.SetCursorPosY(windowPadding.Y + _headerHeight + 5);
+ ImGui.SetCursorPosY(windowPadding.Y + _animatedHeader.Height + 5);
ImGui.SetCursorPosX(20);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
@@ -155,347 +128,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
ImGuiHelpers.ScaledDummy(3);
-
- DrawParticleEffects(headerStart, extendedParticleSize);
}
- private static void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
- {
- var drawList = ImGui.GetWindowDrawList();
-
- var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
- var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f);
-
- drawList.AddRectFilledMultiColor(
- headerStart,
- headerEnd,
- ImGui.GetColorU32(darkPurple),
- ImGui.GetColorU32(darkPurple),
- ImGui.GetColorU32(deepPurple),
- ImGui.GetColorU32(deepPurple)
- );
-
- var random = new Random(42);
- for (int i = 0; i < 50; i++)
- {
- var starPos = headerStart + new Vector2(
- (float)random.NextDouble() * (headerEnd.X - headerStart.X),
- (float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
- );
- var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
- drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
- }
- }
-
- private static void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
- {
- var drawList = ImGui.GetWindowDrawList();
- var gradientHeight = 60f;
-
- for (int i = 0; i < gradientHeight; i++)
- {
- var progress = i / gradientHeight;
- var smoothProgress = progress * progress;
- var r = 0.12f + (0.0f - 0.12f) * smoothProgress;
- var g = 0.08f + (0.0f - 0.08f) * smoothProgress;
- var b = 0.20f + (0.0f - 0.20f) * smoothProgress;
- var alpha = 1f - smoothProgress;
- var gradientColor = new Vector4(r, g, b, alpha);
- drawList.AddLine(
- new Vector2(headerStart.X, headerEnd.Y + i),
- new Vector2(headerStart.X + width, headerEnd.Y + i),
- ImGui.GetColorU32(gradientColor),
- 1f
- );
- }
- }
-
- private void DrawHeaderText(Vector2 headerStart)
- {
- var textX = 20f;
- var textY = 30f;
-
- ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
-
- using (_uiShared.UidFont.Push())
- {
- ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync");
- }
-
- ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
- ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes");
- }
-
- private void DrawHeaderButtons(Vector2 headerStart, float headerWidth)
- {
- var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe);
- var spacing = 8f * ImGuiHelpers.GlobalScale;
- var rightPadding = 15f * ImGuiHelpers.GlobalScale;
- var topPadding = 15f * ImGuiHelpers.GlobalScale;
- var buttonY = headerStart.Y + topPadding;
- var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
- var discordButtonX = gitButtonX - buttonSize.X - spacing;
-
- ImGui.SetCursorScreenPos(new Vector2(discordButtonX, buttonY));
-
- using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)))
- using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f }))
- using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f }))
- {
- if (_uiShared.IconButton(FontAwesomeIcon.Comments))
- {
- Util.OpenLink("https://discord.gg/dsbjcXMnhA");
- }
-
- if (ImGui.IsItemHovered())
- {
- ImGui.SetTooltip("Join our Discord");
- }
-
- ImGui.SetCursorScreenPos(new Vector2(gitButtonX, buttonY));
- if (_uiShared.IconButton(FontAwesomeIcon.Code))
- {
- Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync");
- }
-
- if (ImGui.IsItemHovered())
- {
- ImGui.SetTooltip("View on Git");
- }
- }
- }
-
- private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
- {
- var deltaTime = ImGui.GetIO().DeltaTime;
- _particleSpawnTimer += deltaTime;
-
- if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles)
- {
- SpawnParticle(bannerSize);
- _particleSpawnTimer = 0f;
- }
-
- if (_random.NextDouble() < 0.003)
- {
- SpawnShootingStar(bannerSize);
- }
-
- var drawList = ImGui.GetWindowDrawList();
-
- for (int i = _particles.Count - 1; i >= 0; i--)
- {
- var particle = _particles[i];
-
- var screenPos = bannerStart + particle.Position;
-
- if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
- {
- particle.Trail.Insert(0, particle.Position);
- if (particle.Trail.Count > _maxTrailLength)
- particle.Trail.RemoveAt(particle.Trail.Count - 1);
- }
-
- if (particle.Type == ParticleType.TwinklingStar)
- {
- particle.Twinkle += 0.005f * particle.Depth;
- }
-
- particle.Position += particle.Velocity * deltaTime;
- particle.Life -= deltaTime;
-
- var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
- particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
-
- if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
- {
- _particles.RemoveAt(i);
- continue;
- }
-
- if (particle.Type == ParticleType.TwinklingStar)
- {
- if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
- particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
- if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
- particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
- }
-
- var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
- var fadeOut = Math.Min(1f, particle.Life / 20f);
- var lifeFade = Math.Min(fadeIn, fadeOut);
-
- var edgeFadeX = Math.Min(
- Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance),
- Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance)
- );
- var edgeFadeY = Math.Min(
- Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance),
- Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance)
- );
- var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
-
- var baseAlpha = lifeFade * edgeFade;
- var finalAlpha = particle.Type == ParticleType.TwinklingStar
- ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
- : baseAlpha;
-
- if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
- {
- var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
-
- for (int t = 1; t < particle.Trail.Count; t++)
- {
- var trailProgress = (float)t / particle.Trail.Count;
- var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
- var trailWidth = (1f - trailProgress) * 3f + 1f;
-
- var glowAlpha = trailAlpha * 0.4f;
- drawList.AddLine(
- bannerStart + particle.Trail[t - 1],
- bannerStart + particle.Trail[t],
- ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
- trailWidth + 4f
- );
-
- drawList.AddLine(
- bannerStart + particle.Trail[t - 1],
- bannerStart + particle.Trail[t],
- ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
- trailWidth
- );
- }
- }
- else if (particle.Type == ParticleType.TwinklingStar)
- {
- DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
- }
-
- _particles[i] = particle;
- }
- }
-
- private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha,
- float depth)
- {
- var color = HslToRgb(hue, 1.0f, 0.85f);
- color.W = alpha;
-
- drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
-
- var glowColor = color with { W = alpha * 0.3f };
- drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
- }
-
- private static Vector4 HslToRgb(float h, float s, float l)
- {
- h = h / 360f;
- float c = (1 - MathF.Abs(2 * l - 1)) * s;
- float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
- float m = l - c / 2;
-
- float r, g, b;
- if (h < 1f / 6f)
- {
- r = c;
- g = x;
- b = 0;
- }
- else if (h < 2f / 6f)
- {
- r = x;
- g = c;
- b = 0;
- }
- else if (h < 3f / 6f)
- {
- r = 0;
- g = c;
- b = x;
- }
- else if (h < 4f / 6f)
- {
- r = 0;
- g = x;
- b = c;
- }
- else if (h < 5f / 6f)
- {
- r = x;
- g = 0;
- b = c;
- }
- else
- {
- r = c;
- g = 0;
- b = x;
- }
-
- return new Vector4(r + m, g + m, b + m, 1.0f);
- }
-
-
- private void SpawnParticle(Vector2 bannerSize)
- {
- var position = new Vector2(
- (float)_random.NextDouble() * bannerSize.X,
- (float)_random.NextDouble() * bannerSize.Y
- );
-
- var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
- var depth = depthLayers[_random.Next(depthLayers.Length)];
-
- var velocity = new Vector2(
- ((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
- ((float)_random.NextDouble() - 0.5f) * 0.05f * depth
- );
-
- var isBlue = _random.NextDouble() < 0.5;
- var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
- var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
- var maxLife = 120f + (float)_random.NextDouble() * 60f;
-
- _particles.Add(new Particle
- {
- Position = position,
- Velocity = velocity,
- Life = maxLife,
- MaxLife = maxLife,
- Size = size,
- Type = ParticleType.TwinklingStar,
- Trail = null,
- Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
- Depth = depth,
- Hue = hue
- });
- }
-
- private void SpawnShootingStar(Vector2 bannerSize)
- {
- var maxLife = 80f + (float)_random.NextDouble() * 40f;
- var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
- var startY = -10f;
-
- _particles.Add(new Particle
- {
- Position = new Vector2(startX, startY),
- Velocity = new Vector2(
- -50f - (float)_random.NextDouble() * 40f,
- 30f + (float)_random.NextDouble() * 40f
- ),
- Life = maxLife,
- MaxLife = maxLife,
- Size = 2.5f,
- Type = ParticleType.ShootingStar,
- Trail = new List(),
- Twinkle = 0,
- Depth = 1.0f,
- Hue = 270f
- });
- }
-
-
private void DrawTabs()
{
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))