improved particle animations, took some inspiration from brio and character + kinda pogged at their change log

This commit is contained in:
choco
2025-10-16 01:29:38 +02:00
parent d5c12e81c3
commit 823dd39a9b
6 changed files with 191 additions and 390 deletions

View File

@@ -6,7 +6,6 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Text;
@@ -16,20 +15,15 @@ using Dalamud.Interface;
namespace LightlessSync.UI;
// Inspiration taken from Brio and Character Select+ (goats)
public class UpdateNotesUi : WindowMediatorSubscriberBase
{
private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiShared;
private readonly List<string> _contributors = [];
private readonly List<string> _credits = [];
private readonly List<string> _supporters = [];
private ChangelogFile _changelog = new();
private bool _scrollToTop;
private int _selectedTab;
// Particle system for visual effects
private struct Particle
{
public Vector2 Position;
@@ -37,27 +31,29 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
public float Life;
public float MaxLife;
public float Size;
public Vector4 Color;
public ParticleType Type;
public float Rotation;
public float RotationSpeed;
public List<Vector2>? Trail;
public bool IsLargeMoon;
public float Twinkle;
public float Depth;
public float Hue;
}
private enum ParticleType
{
Star,
Moon,
Sparkle,
FastFallingStar
TwinklingStar,
ShootingStar
}
private readonly List<Particle> _particles = [];
private float _particleTimer;
private readonly Random _particleRandom = new();
private Particle? _largeMoon;
private float _largeMoonTimer;
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;
public UpdateNotesUi(ILogger<UpdateNotesUi> logger,
LightlessMediator mediator,
@@ -66,7 +62,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService)
{
_configService = configService;
_uiShared = uiShared;
AllowClickthrough = false;
@@ -106,20 +101,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var windowPos = ImGui.GetWindowPos();
var windowPadding = ImGui.GetStyle().WindowPadding;
var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2);
var headerHeight = 140f * ImGuiHelpers.GlobalScale;
var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
var headerEnd = headerStart + new Vector2(headerWidth, headerHeight);
var headerEnd = headerStart + new Vector2(headerWidth, HeaderHeight);
var headerSize = new Vector2(headerWidth, HeaderHeight);
var extendedParticleSize = new Vector2(headerWidth, HeaderHeight + ExtendedParticleHeight);
DrawGradientBackground(headerStart, headerEnd);
DrawParticleEffects(headerStart, new Vector2(headerWidth, headerHeight));
DrawHeaderText(headerStart);
DrawHeaderButtons(headerStart, headerWidth);
DrawBottomGradient(headerStart, headerEnd, headerWidth);
ImGui.SetCursorPosY(windowPadding.Y + headerHeight + 5);
// Version badge with icon
ImGui.SetCursorPosX(12);
ImGui.SetCursorPosY(windowPadding.Y + HeaderHeight + 5);
ImGui.SetCursorPosX(20);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString());
@@ -140,16 +135,15 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" {_changelog.Subline}");
}
}
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
DrawParticleEffects(headerStart, extendedParticleSize);
}
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{
var drawList = ImGui.GetWindowDrawList();
// Dark night sky background with stars pattern
var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f);
@@ -162,8 +156,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
ImGui.GetColorU32(deepPurple)
);
// Add some static "distant stars" for depth
var random = new Random(42); // Fixed seed for consistent pattern
var random = new Random(42);
for (int i = 0; i < 50; i++)
{
var starPos = headerStart + new Vector2(
@@ -173,27 +166,44 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
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;
// Accent border at bottom with glow
drawList.AddLine(
new Vector2(headerStart.X, headerEnd.Y),
headerEnd,
ImGui.GetColorU32(UIColors.Get("LightlessPurple")),
2f
);
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)
{
// Title text overlay - drawn after particles so it's on top
ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 30));
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(20, 75));
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes");
}
@@ -203,8 +213,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var spacing = 8f * ImGuiHelpers.GlobalScale;
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
var topPadding = 15f * ImGuiHelpers.GlobalScale;
// Position for buttons in top right
var buttonY = headerStart.Y + topPadding;
var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
var discordButtonX = gitButtonX - buttonSize.X - spacing;
@@ -239,372 +247,202 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
{
var deltaTime = ImGui.GetIO().DeltaTime;
_particleTimer += deltaTime;
_largeMoonTimer += deltaTime;
_particleSpawnTimer += deltaTime;
// Spawn new particles
if (_particleTimer > 0.3f && _particles.Count < 30)
if (_particleSpawnTimer > ParticleSpawnInterval && _particles.Count < MaxParticles)
{
SpawnParticle(bannerStart, bannerSize);
_particleTimer = 0f;
SpawnParticle(bannerSize);
_particleSpawnTimer = 0f;
}
// Spawn or update large moon
if (_largeMoon == null || _largeMoonTimer > 45f)
if (_random.NextDouble() < 0.003)
{
SpawnLargeMoon(bannerStart, bannerSize);
_largeMoonTimer = 0f;
SpawnShootingStar(bannerSize);
}
var drawList = ImGui.GetWindowDrawList();
// Update and draw large moon first (background layer)
if (_largeMoon != null)
{
var moon = _largeMoon.Value;
moon.Position += moon.Velocity * deltaTime;
moon.Life -= deltaTime;
// Keep moon within banner bounds with padding
var padding = moon.Size + 10;
if (moon.Life <= 0 ||
moon.Position.X < bannerStart.X - padding ||
moon.Position.X > bannerStart.X + bannerSize.X + padding ||
moon.Position.Y < bannerStart.Y - padding ||
moon.Position.Y > bannerStart.Y + bannerSize.Y + padding)
{
_largeMoon = null;
}
else
{
float alpha = Math.Min(1f, moon.Life / moon.MaxLife);
var color = moon.Color with { W = moon.Color.W * alpha };
DrawMoon(drawList, moon.Position, moon.Size, color);
_largeMoon = moon;
}
}
// Update and draw regular particles
for (int i = _particles.Count - 1; i >= 0; i--)
{
var particle = _particles[i];
// Update trail for stars
if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null)
var screenPos = bannerStart + particle.Position;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
{
particle.Trail.Insert(0, particle.Position);
var maxTrailLength = particle.Type == ParticleType.FastFallingStar ? 15 : 8;
if (particle.Trail.Count > maxTrailLength)
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;
particle.Rotation += particle.RotationSpeed * deltaTime;
if (particle.Life <= 0 ||
particle.Position.X > bannerStart.X + bannerSize.X + 50 ||
particle.Position.X < bannerStart.X - 50 ||
particle.Position.Y < bannerStart.Y - 50 ||
particle.Position.Y > bannerStart.Y + bannerSize.Y + 50)
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;
}
float alpha = Math.Min(1f, particle.Life / particle.MaxLife);
var color = particle.Color with { W = particle.Color.W * alpha };
// Draw trail for stars
if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null && particle.Trail.Count > 1)
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++)
{
float trailAlpha = alpha * (1f - (t / (float)particle.Trail.Count)) * (particle.Type == ParticleType.FastFallingStar ? 0.7f : 0.5f);
var trailColor = color with { W = trailAlpha };
float thickness = particle.Type == ParticleType.FastFallingStar ? 2.5f : 1.5f;
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(
particle.Trail[t - 1],
particle.Trail[t],
ImGui.GetColorU32(trailColor),
thickness
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
);
}
}
// Draw based on particle type
switch (particle.Type)
else if (particle.Type == ParticleType.TwinklingStar)
{
case ParticleType.Star:
case ParticleType.FastFallingStar:
DrawStar(drawList, particle.Position, particle.Size, color, particle.Rotation);
break;
case ParticleType.Moon:
DrawMoon(drawList, particle.Position, particle.Size, color);
break;
case ParticleType.Sparkle:
DrawSparkle(drawList, particle.Position, particle.Size, color, particle.Rotation);
break;
DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
}
_particles[i] = particle;
}
}
private void DrawStar(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation)
private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth)
{
// Draw a 5-pointed star
var points = new Vector2[10];
for (int i = 0; i < 10; i++)
{
float angle = (i * MathF.PI / 5) + rotation;
float radius = (i % 2 == 0) ? size : size * 0.4f;
points[i] = position + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius);
}
var color = HslToRgb(hue, 1.0f, 0.85f);
color.W = alpha;
// Draw filled star
for (int i = 0; i < 5; i++)
{
drawList.AddTriangleFilled(
position,
points[i * 2],
points[(i * 2 + 2) % 10],
ImGui.GetColorU32(color)
);
}
// Glow effect
var glowColor = color with { W = color.W * 0.3f };
drawList.AddCircleFilled(position, size * 1.5f, ImGui.GetColorU32(glowColor));
}
private void DrawMoon(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color)
{
// Enhanced glow for larger moons
var glowRadius = size > 15f ? 2.5f : 1.8f;
var glowColor = color with { W = color.W * (size > 15f ? 0.15f : 0.25f) };
drawList.AddCircleFilled(position, size * glowRadius, ImGui.GetColorU32(glowColor));
// Draw crescent moon
drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
// Draw shadow circle to create crescent
var shadowColor = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
drawList.AddCircleFilled(position + new Vector2(size * 0.4f, 0), size * 0.8f, ImGui.GetColorU32(shadowColor));
// Additional glow layer for large moons
if (size > 15f)
{
var outerGlow = color with { W = color.W * 0.08f };
drawList.AddCircleFilled(position, size * 3.5f, ImGui.GetColorU32(outerGlow));
}
var glowColor = color with { W = alpha * 0.3f };
drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
}
private void DrawSparkle(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation)
private static Vector4 HslToRgb(float h, float s, float l)
{
// Draw a 4-pointed sparkle (plus shape)
var thickness = size * 0.3f;
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;
// Horizontal line
drawList.AddLine(
position + new Vector2(-size, 0),
position + new Vector2(size, 0),
ImGui.GetColorU32(color),
thickness
);
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; }
// Vertical line
drawList.AddLine(
position + new Vector2(0, -size),
position + new Vector2(0, size),
ImGui.GetColorU32(color),
thickness
);
// Center glow
drawList.AddCircleFilled(position, size * 0.4f, ImGui.GetColorU32(color));
var glowColor = color with { W = color.W * 0.4f };
drawList.AddCircleFilled(position, size * 1.2f, ImGui.GetColorU32(glowColor));
return new Vector4(r + m, g + m, b + m, 1.0f);
}
private void SpawnParticle(Vector2 bannerStart, Vector2 bannerSize)
private void SpawnParticle(Vector2 bannerSize)
{
var typeRoll = _particleRandom.Next(100);
var particleType = typeRoll switch
{
< 35 => ParticleType.Star,
< 50 => ParticleType.Moon,
< 65 => ParticleType.FastFallingStar,
_ => ParticleType.Sparkle
};
var position = new Vector2(
(float)_random.NextDouble() * bannerSize.X,
(float)_random.NextDouble() * bannerSize.Y
);
Vector2 position;
Vector2 velocity;
var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
var depth = depthLayers[_random.Next(depthLayers.Length)];
// Stars: spawn from top, move diagonally down
if (particleType == ParticleType.Star)
{
// Spawn from top edge
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 10
);
// Move diagonally down (shooting star effect)
var angle = MathF.PI * 0.25f + (float)(_particleRandom.NextDouble() - 0.5) * 0.5f; // 45° ± variation
var speed = 30f + (float)_particleRandom.NextDouble() * 40f;
velocity = new Vector2(MathF.Cos(angle) * speed, MathF.Sin(angle) * speed);
}
// Fast falling stars: spawn from top, fall straight down very fast
else if (particleType == ParticleType.FastFallingStar)
{
// Spawn from top edge, random X position
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 10
);
// Fall almost straight down with slight horizontal drift
var horizontalDrift = -10f + (float)_particleRandom.NextDouble() * 20f;
var speed = 120f + (float)_particleRandom.NextDouble() * 80f; // Much faster!
velocity = new Vector2(horizontalDrift, speed);
}
// Moons: drift slowly across
else if (particleType == ParticleType.Moon)
{
// Spawn from left side
position = new Vector2(
bannerStart.X - 10,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
// Drift slowly to the right with slight vertical movement
velocity = new Vector2(
15f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
}
// Sparkles: float gently
else
{
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
-5f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
}
var velocity = new Vector2(
((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
((float)_random.NextDouble() - 0.5f) * 0.05f * depth
);
var particle = new Particle
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,
MaxLife = particleType switch
{
ParticleType.Star => 3f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.Moon => 8f + (float)_particleRandom.NextDouble() * 4f,
ParticleType.FastFallingStar => 1.5f + (float)_particleRandom.NextDouble() * 1f,
_ => 6f + (float)_particleRandom.NextDouble() * 4f
},
Size = particleType switch
{
ParticleType.Star => 2.5f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.Moon => 3f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.FastFallingStar => 3f + (float)_particleRandom.NextDouble() * 2f,
_ => 2f + (float)_particleRandom.NextDouble() * 2f
},
Color = particleType switch
{
ParticleType.Star => new Vector4(1.0f, 1.0f, 0.9f, 0.9f),
ParticleType.Moon => UIColors.Get("LightlessBlue") with { W = 0.7f },
ParticleType.FastFallingStar => new Vector4(1.0f, 0.95f, 0.85f, 1.0f), // Bright white-yellow
_ => UIColors.Get("LightlessPurple") with { W = 0.8f }
},
Type = particleType,
Rotation = (float)_particleRandom.NextDouble() * MathF.PI * 2,
RotationSpeed = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? 2f : -0.5f + (float)_particleRandom.NextDouble() * 1f,
Trail = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? new List<Vector2>() : null,
IsLargeMoon = false
};
particle.Life = particle.MaxLife;
_particles.Add(particle);
}
private void SpawnLargeMoon(Vector2 bannerStart, Vector2 bannerSize)
{
// Large moon travels across the banner like a celestial body
var spawnSide = _particleRandom.Next(4);
Vector2 position;
Vector2 velocity;
switch (spawnSide)
{
case 0:
// Spawn from left, move to right
position = new Vector2(
bannerStart.X - 50,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
15f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
break;
case 1:
// Spawn from top, move down and across
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 50
);
velocity = new Vector2(
-5f + (float)_particleRandom.NextDouble() * 10f,
10f + (float)_particleRandom.NextDouble() * 8f
);
break;
case 2:
// Spawn from right, move to left
position = new Vector2(
bannerStart.X + bannerSize.X + 50,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
-(15f + (float)_particleRandom.NextDouble() * 10f),
-5f + (float)_particleRandom.NextDouble() * 10f
);
break;
default:
// Spawn from top-left corner, move diagonally
position = new Vector2(
bannerStart.X - 30,
bannerStart.Y - 30
);
velocity = new Vector2(
12f + (float)_particleRandom.NextDouble() * 8f,
12f + (float)_particleRandom.NextDouble() * 8f
);
break;
}
_largeMoon = new Particle
{
Position = position,
Velocity = velocity,
MaxLife = 40f,
Life = 40f,
Size = 25f + (float)_particleRandom.NextDouble() * 10f,
Color = UIColors.Get("LightlessBlue") with { W = 0.35f },
Type = ParticleType.Moon,
Rotation = 0,
RotationSpeed = 0,
Life = maxLife,
MaxLife = maxLife,
Size = size,
Type = ParticleType.TwinklingStar,
Trail = null,
IsLargeMoon = true
};
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 DrawCloseButton()
{
@@ -653,7 +491,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{
var currentColor = entry.IsCurrent == true
? UIColors.Get("LightlessGreen")
: new Vector4(0.75f, 0.75f, 0.85f, 1.0f);
: new Vector4(0.95f, 0.95f, 1.0f, 1.0f);
bool isOpen;
var flags = entry.IsCurrent == true
@@ -670,7 +508,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
ImGui.SameLine();
ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), $" — {entry.Tagline}");
ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 1.0f), $" — {entry.Tagline}");
if (!isOpen)
return;
@@ -711,11 +549,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var backgroundMin = startPos + new Vector2(-8, -4);
var backgroundMax = startPos + new Vector2(availableWidth + 8, 28);
// Background with subtle gradient
var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f);
drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f);
// Accent line on left
drawList.AddRectFilled(
backgroundMin,
backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y),
@@ -723,7 +559,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
3f
);
// Subtle glow effect
var glowColor = accentColor with { W = 0.15f };
drawList.AddRect(
backgroundMin,
@@ -745,11 +580,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
try
{
var assembly = Assembly.GetExecutingAssembly();
ReadLines(assembly, "LightlessSync.UI.Changelog.supporters.txt", _supporters);
ReadLines(assembly, "LightlessSync.UI.Changelog.contributors.txt", _contributors);
ReadLines(assembly, "LightlessSync.UI.Changelog.credits.txt", _credits);
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.UI.Changelog.changelog.yaml");
if (changelogStream != null)
{
@@ -767,22 +597,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
// Ignore - window will gracefully render with defaults
}
}
private static void ReadLines(Assembly assembly, string resourceName, List<string> target)
{
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
return;
using var reader = new StreamReader(stream, Encoding.UTF8, true, 128);
string? line;
while ((line = reader.ReadLine()) != null)
{
if (!string.IsNullOrWhiteSpace(line))
target.Add(line.Trim());
}
}
private sealed record ChangelogFile
{
public string Tagline { get; init; } = string.Empty;