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);