@@ -6,7 +6,6 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services ;
using LightlessSync.Services ;
using LightlessSync.Services.Mediator ;
using LightlessSync.Services.Mediator ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging ;
using System.IO ;
using System.Numerics ;
using System.Numerics ;
using System.Reflection ;
using System.Reflection ;
using System.Text ;
using System.Text ;
@@ -16,20 +15,15 @@ using Dalamud.Interface;
namespace LightlessSync.UI ;
namespace LightlessSync.UI ;
// Inspiration taken from Brio and Character Select+ (goats)
public class UpdateNotesUi : WindowMediatorSubscriberBase
public class UpdateNotesUi : WindowMediatorSubscriberBase
{
{
private readonly LightlessConfigService _configService ;
private readonly UiSharedService _uiShared ;
private readonly UiSharedService _uiShared ;
private readonly List < string > _contributors = [ ] ;
private readonly List < string > _credits = [ ] ;
private readonly List < string > _supporters = [ ] ;
private ChangelogFile _changelog = new ( ) ;
private ChangelogFile _changelog = new ( ) ;
private bool _scrollToTop ;
private bool _scrollToTop ;
private int _selectedTab ;
private int _selectedTab ;
// Particle system for visual effects
private struct Particle
private struct Particle
{
{
public Vector2 Position ;
public Vector2 Position ;
@@ -37,27 +31,29 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
public float Life ;
public float Life ;
public float MaxLife ;
public float MaxLife ;
public float Size ;
public float Size ;
public Vector4 Color ;
public ParticleType Type ;
public ParticleType Type ;
public float Rotation ;
public float RotationSpeed ;
public List < Vector2 > ? Trail ;
public List < Vector2 > ? Trail ;
public bool IsLargeMoon ;
public float Twinkle ;
public float Depth ;
public float Hue ;
}
}
private enum ParticleType
private enum ParticleType
{
{
Star ,
Twinkling Star,
Moon ,
ShootingStar
Sparkle ,
FastFallingStar
}
}
private readonly List < Particle > _particles = [ ] ;
private readonly List < Particle > _particles = [ ] ;
private float _particleTimer ;
private float _particleSpawn Timer ;
private readonly Random _particleR andom = new ( ) ;
private readonly Random _r andom = new ( ) ;
private Particle ? _largeMoon ;
private float _largeMoonTimer ;
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 ,
public UpdateNotesUi ( ILogger < UpdateNotesUi > logger ,
LightlessMediator mediator ,
LightlessMediator mediator ,
@@ -66,7 +62,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
PerformanceCollectorService performanceCollectorService )
PerformanceCollectorService performanceCollectorService )
: base ( logger , mediator , "Lightless Sync — Update Notes" , performanceCollectorService )
: base ( logger , mediator , "Lightless Sync — Update Notes" , performanceCollectorService )
{
{
_configService = configService ;
_uiShared = uiShared ;
_uiShared = uiShared ;
AllowClickthrough = false ;
AllowClickthrough = false ;
@@ -106,20 +101,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var windowPos = ImGui . GetWindowPos ( ) ;
var windowPos = ImGui . GetWindowPos ( ) ;
var windowPadding = ImGui . GetStyle ( ) . WindowPadding ;
var windowPadding = ImGui . GetStyle ( ) . WindowPadding ;
var headerWidth = ( 800f * ImGuiHelpers . GlobalScale ) - ( windowPadding . X * 2 ) ;
var headerWidth = ( 800f * ImGuiHelpers . GlobalScale ) - ( windowPadding . X * 2 ) ;
var headerHeight = 140f * ImGuiHelpers . GlobalScale ;
var headerStart = windowPos + new Vector2 ( windowPadding . X , windowPadding . Y ) ;
var headerStart = windowPos + new Vector2 ( windowPadding . X , windowPadding . Y ) ;
var headerEnd = headerStart + new Vector2 ( headerWidth , h eaderHeight) ;
var headerEnd = headerStart + new Vector2 ( headerWidth , H eaderHeight) ;
var headerSize = new Vector2 ( headerWidth , HeaderHeight ) ;
var extendedParticleSize = new Vector2 ( headerWidth , HeaderHeight + ExtendedParticleHeight ) ;
DrawGradientBackground ( headerStart , headerEnd ) ;
DrawGradientBackground ( headerStart , headerEnd ) ;
DrawParticleEffects ( headerStart , new Vector2 ( headerWidth , headerHeight ) ) ;
DrawHeaderText ( headerStart ) ;
DrawHeaderText ( headerStart ) ;
DrawHeaderButtons ( headerStart , headerWidth ) ;
DrawHeaderButtons ( headerStart , headerWidth ) ;
DrawBottomGradient ( headerStart , headerEnd , headerWidth ) ;
ImGui . SetCursorPosY ( windowPadding . Y + h eaderHeight + 5 ) ;
ImGui . SetCursorPosY ( windowPadding . Y + H eaderHeight + 5 ) ;
ImGui . SetCursorPosX ( 20 ) ;
// Version badge with icon
ImGui . SetCursorPosX ( 12 ) ;
using ( ImRaii . PushFont ( UiBuilder . IconFont ) )
using ( ImRaii . PushFont ( UiBuilder . IconFont ) )
{
{
ImGui . TextColored ( UIColors . Get ( "LightlessGreen" ) , FontAwesomeIcon . Star . ToIconString ( ) ) ;
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 . TextColored ( new Vector4 ( 0.65f , 0.65f , 0.75f , 1.0f ) , $" – {_changelog.Subline}" ) ;
}
}
}
}
ImGui . Separator ( ) ;
ImGuiHelpers . ScaledDummy ( 3 ) ;
ImGuiHelpers . ScaledDummy ( 3 ) ;
DrawParticleEffects ( headerStart , extendedParticleSize ) ;
}
}
private void DrawGradientBackground ( Vector2 headerStart , Vector2 headerEnd )
private void DrawGradientBackground ( Vector2 headerStart , Vector2 headerEnd )
{
{
var drawList = ImGui . GetWindowDrawList ( ) ;
var drawList = ImGui . GetWindowDrawList ( ) ;
// Dark night sky background with stars pattern
var darkPurple = new Vector4 ( 0.08f , 0.05f , 0.15f , 1.0f ) ;
var darkPurple = new Vector4 ( 0.08f , 0.05f , 0.15f , 1.0f ) ;
var deepPurple = new Vector4 ( 0.12f , 0.08f , 0.20f , 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 )
ImGui . GetColorU32 ( deepPurple )
) ;
) ;
// Add some static "distant stars" for depth
var random = new Random ( 42 ) ;
var random = new Random ( 42 ) ; // Fixed seed for consistent pattern
for ( int i = 0 ; i < 50 ; i + + )
for ( int i = 0 ; i < 50 ; i + + )
{
{
var starPos = headerStart + new Vector2 (
var starPos = headerStart + new Vector2 (
@@ -173,27 +166,44 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var brightness = 0.3f + ( float ) random . NextDouble ( ) * 0.4f ;
var brightness = 0.3f + ( float ) random . NextDouble ( ) * 0.4f ;
drawList . AddCircleFilled ( starPos , 1f , ImGui . GetColorU32 ( new Vector4 ( 1f , 1f , 1f , brightness ) ) ) ;
drawList . AddCircleFilled ( starPos , 1f , ImGui . GetColorU32 ( new Vector4 ( 1f , 1f , 1f , brightness ) ) ) ;
}
}
}
// Accent border at bottom with glow
private void DrawBottomGradient ( Vector2 headerStart , Vector2 headerEnd , float width )
drawList . AddLine (
{
new Vector2 ( headerStart . X , headerEnd . Y ) ,
var drawList = ImGui . GetWindowDrawList ( ) ;
headerEnd ,
var gradientHeight = 60f ;
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 )
private void DrawHeaderText ( Vector2 headerStart )
{
{
// Title text overlay - drawn after particles so it's on top
var textX = 20f ;
ImGui . SetCursorScreenPos ( headerStart + new Vector2 ( 20 , 30 ) ) ;
var textY = 30f ;
ImGui . SetCursorScreenPos ( headerStart + new Vector2 ( textX , textY ) ) ;
using ( _uiShared . UidFont . Push ( ) )
using ( _uiShared . UidFont . Push ( ) )
{
{
ImGui . TextColored ( new Vector4 ( 0.95f , 0.95f , 0.95f , 1.0f ) , "Lightless Sync" ) ;
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" ) ;
ImGui . TextColored ( UIColors . Get ( "LightlessBlue" ) , "Update Notes" ) ;
}
}
@@ -203,8 +213,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var spacing = 8f * ImGuiHelpers . GlobalScale ;
var spacing = 8f * ImGuiHelpers . GlobalScale ;
var rightPadding = 15f * ImGuiHelpers . GlobalScale ;
var rightPadding = 15f * ImGuiHelpers . GlobalScale ;
var topPadding = 15f * ImGuiHelpers . GlobalScale ;
var topPadding = 15f * ImGuiHelpers . GlobalScale ;
// Position for buttons in top right
var buttonY = headerStart . Y + topPadding ;
var buttonY = headerStart . Y + topPadding ;
var gitButtonX = headerStart . X + headerWidth - rightPadding - buttonSize . X ;
var gitButtonX = headerStart . X + headerWidth - rightPadding - buttonSize . X ;
var discordButtonX = gitButtonX - buttonSize . X - spacing ;
var discordButtonX = gitButtonX - buttonSize . X - spacing ;
@@ -239,370 +247,200 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private void DrawParticleEffects ( Vector2 bannerStart , Vector2 bannerSize )
private void DrawParticleEffects ( Vector2 bannerStart , Vector2 bannerSize )
{
{
var deltaTime = ImGui . GetIO ( ) . DeltaTime ;
var deltaTime = ImGui . GetIO ( ) . DeltaTime ;
_particleTimer + = deltaTime ;
_particleSpawn Timer + = deltaTime ;
_largeMoonTimer + = deltaTime ;
// Spawn new particles
if ( _particleSpawnTimer > ParticleSpawnInterval & & _particles . Count < MaxParticles )
if ( _particleTimer > 0.3f & & _particles . Count < 30 )
{
{
SpawnParticle ( bannerStart , bannerSize) ;
SpawnParticle ( bannerSize ) ;
_particleTimer = 0f ;
_particleSpawn Timer = 0f ;
}
}
// Spawn or update large moon
if ( _random . NextDouble ( ) < 0.003 )
if ( _largeMoon = = null | | _largeMoonTimer > 45f )
{
{
SpawnLargeMoon ( banner Start , bannerSize ) ;
SpawnShooting Star ( bannerSize ) ;
_largeMoonTimer = 0f ;
}
}
var drawList = ImGui . GetWindowDrawList ( ) ;
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 - - )
for ( int i = _particles . Count - 1 ; i > = 0 ; i - - )
{
{
var particle = _particles [ i ] ;
var particle = _particles [ i ] ;
// Update trail for stars
var screenPos = bannerStart + particle . Position ;
if ( ( particle . Type = = ParticleType . Star | | particle . Type = = ParticleType . FastFallingStar ) & & particle . Trail ! = null )
if ( particle . Type = = ParticleType . ShootingStar & & particle . Trail ! = null )
{
{
particle . Trail . Insert ( 0 , particle . Position ) ;
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 ) ;
particle . Trail . RemoveAt ( particle . Trail . Count - 1 ) ;
}
}
if ( particle . Type = = ParticleType . TwinklingStar )
{
particle . Twinkle + = 0.005f * particle . Depth ;
}
particle . Position + = particle . Velocity * deltaTime ;
particle . Position + = particle . Velocity * deltaTime ;
particle . Life - = deltaTime ;
particle . Life - = deltaTime ;
particle . Rotation + = particle . RotationSpeed * deltaTime ;
if ( particle . Life < = 0 | |
var isOutOfBounds = particle . Position . X < - 50 | | particle . Position . X > bannerSize . X + 5 0 | |
particle . Position . X > bannerStart . X + bannerSize . X + 50 | |
particle . Position . Y < - 50 | | particle . Position . Y > bannerSize . Y + 50 ;
particle . Position . X < bannerStart . X - 50 | |
particle . Position . Y < bannerStart . Y - 50 | |
if ( particle . Life < = 0 | | ( particle . Type ! = ParticleType . TwinklingStar & & isOutOfBounds ) )
particle . Position . Y > bannerStart . Y + bannerSize . Y + 50 )
{
{
_particles . RemoveAt ( i ) ;
_particles . RemoveAt ( i ) ;
continue ;
continue ;
}
}
float alpha = Math . Min ( 1f , particle . Lif e / p article. MaxLife ) ;
if ( particle . Typ e = = P articleType . TwinklingStar )
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 . 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 + + )
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 trailProgress = ( float ) t / particle . Trail . Count ;
var trailColor = color with { W = trailAlpha } ;
var trailAlpha = Math . Min ( 1f , ( 1f - trailProgress ) * finalAlpha * 1.8f ) ;
float thickness = particle . Type = = ParticleType . FastFallingStar ? 2.5 f : 1.5 f ;
var trailWidth = ( 1f - trailProgress ) * 3 f + 1f ;
var glowAlpha = trailAlpha * 0.4f ;
drawList . AddLine (
drawList . AddLine (
particle . Trail [ t - 1 ] ,
bannerStart + particle. Trail [ t - 1 ] ,
particle . Trail [ t ] ,
bannerStart + particle. Trail [ t ] ,
ImGui . GetColorU32 ( trailColor ) ,
ImGui . GetColorU32 ( cyanColor with { W = glowAlpha } ) ,
thickness
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 )
// Draw based on particle type
switch ( particle . Type )
{
{
case ParticleType . Star :
DrawTwinklingStar ( drawList , screenPos , particle . Size , particle . Hue , finalAlpha , particle . Depth ) ;
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 ;
}
}
_particles [ i ] = particle ;
_particles [ i ] = particle ;
}
}
}
}
private void DrawStar ( ImDrawListPtr drawList , Vector2 position , float size , Vector4 color , float rotation )
private void DrawTwinkling Star ( ImDrawListPtr drawList , Vector2 position , float size , float hue , float alpha , float depth )
{
{
// Draw a 5-pointed star
var color = HslToRgb ( hue , 1.0f , 0.85f ) ;
var points = new Vector2 [ 10 ] ;
color . W = alpha ;
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 ) ;
}
// 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 ) ) ;
drawList . AddCircleFilled ( position , size , ImGui . GetColorU32 ( color ) ) ;
// Draw shadow circle to create crescent
var glowColor = color with { W = alpha * 0.3f } ;
var shadowColor = new Vector4 ( 0.08f , 0.05f , 0.15f , 1.0f ) ;
drawList . AddCircleFilled ( position , size * ( 1.2f + depth * 0.3f ) , ImGui . GetColorU32 ( glowColor ) ) ;
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 ) ) ;
}
}
}
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)
h = h / 360f ;
var thickness = size * 0.3f ;
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
float r , g , b ;
drawList . AddLine (
if ( h < 1f / 6f ) { r = c ; g = x ; b = 0 ; }
position + new Vector2 ( - size , 0 ) ,
else if ( h < 2f / 6f ) { r = x ; g = c ; b = 0 ; }
position + new Vector2 ( size , 0 ) ,
else if ( h < 3f / 6f ) { r = 0 ; g = c ; b = x ; }
ImGui . GetColorU32 ( color ) ,
else if ( h < 4f / 6f ) { r = 0 ; g = x ; b = c ; }
thickness
else if ( h < 5f / 6f ) { r = x ; g = 0 ; b = c ; }
) ;
else { r = c ; g = 0 ; b = x ; }
// Vertical line
return new Vector4 ( r + m , g + m , b + m , 1.0f ) ;
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 ) ) ;
}
}
private void SpawnParticle ( Vector2 bannerStart , Vector2 bannerSize )
private void SpawnParticle ( Vector2 bannerSize )
{
{
var typeRoll = _particleRandom . Next ( 100 ) ;
var position = new Vector2 (
var particleType = typeRoll switch
( float ) _random . NextDouble ( ) * bannerSize . X ,
{
( float ) _random . NextDouble ( ) * bannerSize . Y
< 35 = > ParticleType . Star ,
) ;
< 50 = > ParticleType . Moon ,
< 65 = > ParticleType . FastFallingStar ,
_ = > ParticleType . Sparkle
} ;
Vector2 position ;
var depthLayers = new [ ] { 0.5f , 1.0f , 1.5f } ;
Vector2 velocity ;
var depth = depthLayers [ _random . Next ( depthLayers . Length ) ] ;
// Stars: spawn from top, move diagonally down
var velocity = new Vector2 (
if ( particleType = = ParticleType . Star )
( ( float ) _random . NextDouble ( ) - 0.5f ) * 0.05f * depth ,
{
( ( float ) _random . NextDouble ( ) - 0.5f ) * 0.05f * depth
// Spawn from top edge
) ;
position = new Vector2 (
bannerStart . X + ( float ) _particleRandom . NextDouble ( ) * bannerSize . X ,
bannerStart . Y - 10
) ;
// Move diagonally down (shooting star effect)
var isBlue = _random . NextDouble ( ) < 0.5 ;
var angl e = MathF . PI * 0.25 f + ( float ) ( _particleR andom. NextDouble ( ) - 0.5 ) * 0.5f ; // 45° ± variation
var hu e = isBlue ? 220 f + ( float ) _r andom. NextDouble ( ) * 30f : 270f + ( float ) _random . NextDouble ( ) * 40f ;
var speed = 30 f + ( float ) _particleR andom . NextDouble ( ) * 40f ;
var size = ( 0.5 f + ( float ) _r andom . NextDouble ( ) * 2f ) * depth ;
velocity = new Vector2 ( MathF . Cos ( angle ) * speed , MathF . Sin ( angle ) * speed ) ;
var maxLife = 120f + ( float ) _random . NextDouble ( ) * 60f ;
}
// 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
_particles . Add ( new Particle
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 particle = new Particle
{
{
Position = position ,
Position = position ,
Velocity = velocity ,
Velocity = velocity ,
Max Life = particleType switch
Life = maxLife ,
{
MaxLife = maxLife ,
ParticleType . Star = > 3f + ( float ) _particleRandom . NextDouble ( ) * 2f ,
Size = size ,
ParticleType . Moon = > 8f + ( float ) _particleRandom . NextDouble ( ) * 4f ,
Type = ParticleType . TwinklingStar ,
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 ,
Trail = null ,
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
} ) ;
}
}
@@ -653,7 +491,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{
{
var currentColor = entry . IsCurrent = = true
var currentColor = entry . IsCurrent = = true
? UIColors . Get ( "LightlessGreen" )
? UIColors . Get ( "LightlessGreen" )
: new Vector4 ( 0.7 5f , 0.7 5f , 0.85 f, 1.0f ) ;
: new Vector4 ( 0.9 5f , 0.9 5f , 1.0 f, 1.0f ) ;
bool isOpen ;
bool isOpen ;
var flags = entry . IsCurrent = = true
var flags = entry . IsCurrent = = true
@@ -670,7 +508,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
}
ImGui . SameLine ( ) ;
ImGui . SameLine ( ) ;
ImGui . TextColored ( new Vector4 ( 0.7 5f , 0.7 5f , 0.8 5f , 1.0f ) , $" — {entry.Tagline}" ) ;
ImGui . TextColored ( new Vector4 ( 0.8 5f , 0.8 5f , 0.9 5f , 1.0f ) , $" — {entry.Tagline}" ) ;
if ( ! isOpen )
if ( ! isOpen )
return ;
return ;
@@ -711,11 +549,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var backgroundMin = startPos + new Vector2 ( - 8 , - 4 ) ;
var backgroundMin = startPos + new Vector2 ( - 8 , - 4 ) ;
var backgroundMax = startPos + new Vector2 ( availableWidth + 8 , 28 ) ;
var backgroundMax = startPos + new Vector2 ( availableWidth + 8 , 28 ) ;
// Background with subtle gradient
var bgColor = new Vector4 ( 0.12f , 0.12f , 0.15f , 0.7f ) ;
var bgColor = new Vector4 ( 0.12f , 0.12f , 0.15f , 0.7f ) ;
drawList . AddRectFilled ( backgroundMin , backgroundMax , ImGui . GetColorU32 ( bgColor ) , 6f ) ;
drawList . AddRectFilled ( backgroundMin , backgroundMax , ImGui . GetColorU32 ( bgColor ) , 6f ) ;
// Accent line on left
drawList . AddRectFilled (
drawList . AddRectFilled (
backgroundMin ,
backgroundMin ,
backgroundMin + new Vector2 ( 4 , backgroundMax . Y - backgroundMin . Y ) ,
backgroundMin + new Vector2 ( 4 , backgroundMax . Y - backgroundMin . Y ) ,
@@ -723,7 +559,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
3f
3f
) ;
) ;
// Subtle glow effect
var glowColor = accentColor with { W = 0.15f } ;
var glowColor = accentColor with { W = 0.15f } ;
drawList . AddRect (
drawList . AddRect (
backgroundMin ,
backgroundMin ,
@@ -745,11 +580,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
try
try
{
{
var assembly = Assembly . GetExecutingAssembly ( ) ;
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" ) ;
using var changelogStream = assembly . GetManifestResourceStream ( "LightlessSync.UI.Changelog.changelog.yaml" ) ;
if ( changelogStream ! = null )
if ( changelogStream ! = null )
{
{
@@ -767,22 +597,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
// Ignore - window will gracefully render with defaults
// 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
private sealed record ChangelogFile
{
{
public string Tagline { get ; init ; } = string . Empty ;
public string Tagline { get ; init ; } = string . Empty ;