From 8b9e35283d233852f34a0fd42f108d0a6ff17641 Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 16 Dec 2025 11:49:56 +0100 Subject: [PATCH] reworked the animated star banner into a reusable component for reusability --- LightlessSync/UI/Style/AnimatedHeader.cs | 463 +++++++++++++++++++++++ LightlessSync/UI/UpdateNotesUi.cs | 394 +------------------ 2 files changed, 477 insertions(+), 380 deletions(-) create mode 100644 LightlessSync/UI/Style/AnimatedHeader.cs 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))