DiamondTitleEffects.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Diamonds;
/// <summary>Menu-only sparks with swept button collisions, independent of gameplay physics.</summary>
public sealed class DiamondTitleEffects
{
public sealed class Spark
{
public Vector2 Position, Velocity;
public Color Color;
public float Radius, Age, Lifetime;
}
public sealed class Burst
{
public Vector2 Position;
public Vector2? FaceEnd;
public Color Color;
public float Age, Rotation;
}
readonly Random random = new();
readonly List<Spark> particles = new();
readonly List<Burst> bursts = new();
public IReadOnlyList<Spark> Particles => particles;
public IReadOnlyList<Burst> Bursts => bursts;
public float Range( float min, float max ) => min + (max - min) * (float)random.NextDouble();
public void Clear() { particles.Clear(); bursts.Clear(); }
public void EmitSpark( Vector2 position, Vector2 velocity, Color color, float radius = 1.5f )
{
if ( particles.Count >= 500 ) particles.RemoveAt( 0 );
particles.Add( new Spark { Position = position, Velocity = velocity, Color = color,
Radius = radius, Lifetime = Range( 1.7f, 2.7f ) } );
}
public void EmitSlide( Vector2 position, Color color, float speed )
{
// Mostly escape backward along the underside, with a smaller leading spray.
var tangent = new Vector2( 0.62932f, -0.777146f );
var normal = new Vector2( 0.777146f, 0.62932f );
float along = random.NextDouble() < 0.8 ? 1 : -1;
var velocity = (tangent * (along * Range( 140, 470 )) + normal * Range( 40, 170 )) * (0.15f + speed * 1.5f);
EmitSpark( position, velocity, color, Range( 1, 2.3f ) );
}
public void EmitImpact( Vector2 point, Color color )
=> EmitImpact( point, color, null );
public void EmitFaceImpact( Vector2 start, Vector2 end, Color color )
=> EmitImpact( start, color, end );
void EmitImpact( Vector2 point, Color color, Vector2? end )
{
bursts.Add( new Burst { Position = point, FaceEnd = end, Color = color, Rotation = Range( 0, MathF.PI * 2 ) } );
for ( int i = 0; i < 64; i++ )
{
float angle = Range( 0, MathF.PI * 2 );
var velocity = new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * Range( 180, 730 );
EmitSpark( end.HasValue ? point + (end.Value - point) * Range( 0, 1 ) : point, velocity, color, Range( 1.1f, 2.7f ) );
}
}
public void Update( float delta, Rect viewport, ReadOnlySpan<Rect> buttons )
{
foreach ( var burst in bursts ) burst.Age += delta;
bursts.RemoveAll( b => b.Age >= 0.25f );
for ( int i = particles.Count - 1; i >= 0; i-- )
{
var spark = particles[i];
spark.Age += delta;
if ( spark.Age >= spark.Lifetime || !viewport.IsInside( spark.Position ) )
{
particles.RemoveAt( i );
continue;
}
spark.Velocity.y += 920 * delta;
float remaining = delta;
for ( int bounce = 0; bounce < 4 && remaining > 0; bounce++ )
{
var motion = spark.Velocity * remaining;
float first = 1;
var normal = Vector2.Zero;
foreach ( var button in buttons ) Sweep( spark.Position, motion, button.Grow( spark.Radius ), ref first, ref normal );
spark.Position += motion * first;
if ( normal == Vector2.Zero ) break;
spark.Position += normal * 0.05f;
float incoming = Vector2.Dot( spark.Velocity, normal );
var tangent = spark.Velocity - normal * incoming;
spark.Velocity = tangent * 0.88f - normal * (incoming * 0.62f);
remaining *= 1 - first;
}
if ( !viewport.IsInside( spark.Position ) ) particles.RemoveAt( i );
}
}
// Segment/slab intersection finds the first entering face, even at high speed.
static void Sweep( Vector2 position, Vector2 motion, Rect rect, ref float first, ref Vector2 normal )
{
float enter = float.NegativeInfinity, leave = float.PositiveInfinity;
var face = Vector2.Zero;
for ( int axis = 0; axis < 2; axis++ )
{
float origin = axis == 0 ? position.x : position.y;
float travel = axis == 0 ? motion.x : motion.y;
float min = axis == 0 ? rect.Left : rect.Top, max = axis == 0 ? rect.Right : rect.Bottom;
if ( MathF.Abs( travel ) < 0.00001f )
{
if ( origin < min || origin > max ) return;
continue;
}
float near = (min - origin) / travel, far = (max - origin) / travel;
if ( near > far ) (near, far) = (far, near);
if ( near > enter )
{
enter = near;
face = axis == 0 ? new Vector2( -MathF.Sign( travel ), 0 ) : new Vector2( 0, -MathF.Sign( travel ) );
}
leave = MathF.Min( leave, far );
if ( enter > leave ) return;
}
if ( enter < 0 || enter > first || leave < 0 ) return;
first = enter;
normal = face;
}
}