reworked the animated star banner into a reusable component for reusability
This commit is contained in:
463
LightlessSync/UI/Style/AnimatedHeader.cs
Normal file
463
LightlessSync/UI/Style/AnimatedHeader.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A reusable animated header component with a gradient background, some funny stars, and shooting star effects to match the lightless void theme a bit.
|
||||
/// </summary>
|
||||
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<Vector2>? Trail;
|
||||
public float Twinkle;
|
||||
public float Depth;
|
||||
public float Hue;
|
||||
}
|
||||
|
||||
private enum ParticleType
|
||||
{
|
||||
TwinklingStar,
|
||||
ShootingStar
|
||||
}
|
||||
|
||||
private readonly List<Particle> _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;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the animated header with some customizable content
|
||||
/// </summary>
|
||||
/// <param name="width">Width of the header</param>
|
||||
/// <param name="drawContent">Action to draw custom content inside the header</param>
|
||||
public void Draw(float width, Action<Vector2, Vector2> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a simple animated header with title and subtitle.
|
||||
/// </summary>
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a header with title, subtitle, and action buttons in the top-right corner.
|
||||
/// </summary>
|
||||
public void DrawWithButtons(float width, string title, string subtitle, List<HeaderButton> 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<HeaderButton> 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<Vector2>(),
|
||||
Twinkle = 0,
|
||||
Depth = 1.0f,
|
||||
Hue = 270f
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
||||
/// </summary>
|
||||
public void ClearParticles()
|
||||
{
|
||||
_particles.Clear();
|
||||
_particleSpawnTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a button in the animated header.
|
||||
/// </summary>
|
||||
public record HeaderButton(FontAwesomeIcon Icon, string Tooltip, Action? OnClick = null);
|
||||
@@ -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<Vector2>? Trail;
|
||||
public float Twinkle;
|
||||
public float Depth;
|
||||
public float Hue;
|
||||
}
|
||||
|
||||
private enum ParticleType
|
||||
{
|
||||
TwinklingStar,
|
||||
ShootingStar
|
||||
}
|
||||
|
||||
private readonly List<Particle> _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<UpdateNotesUi> 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<HeaderButton>
|
||||
{
|
||||
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<Vector2>(),
|
||||
Twinkle = 0,
|
||||
Depth = 1.0f,
|
||||
Hue = 270f
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void DrawTabs()
|
||||
{
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))
|
||||
|
||||
Reference in New Issue
Block a user