DiamondPanel.SparkAtlas.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	bool drawingReferenceSparks = false;
	static SparkGlowAtlas sharedSparkAtlas;
	SparkGlowAtlas sparkAtlas;

	sealed class SparkGlowAtlas : IDisposable
	{
		public const int Tile = 20, Columns = 10, Rows = 5, Density = 4;
		public Texture Texture;
		public Fill[] Tiles;
		public int Users, Builds;
		Color[] palette;

		public void Ensure()
		{
			if ( Texture?.IsValid == true && palette == Palette ) return;
			var texture = Sandbox.Texture.CreateRenderTarget().WithSize( Tile * Columns * Density, Tile * Rows * Density )
				.WithFormat( ImageFormat.RGBA8888 ).Create();
			try
			{
				using ( var p = Painter.Begin( texture ) )
				{
					p.Clear( Color.Transparent ); p.Scale( Density );
					for ( int index = 0; index < Columns * Rows; index++ )
					{
						using var scope = p.Scope();
						p.Translate( new Vector2( (index % Columns + 0.5f) * Tile, (index / Columns + 0.5f) * Tile ) );
						float h = 0.7f + index / 9 * 0.3f;
						float w = h * (0.65f + index / 3 % 3 * 0.175f);
						if ( index < 45 )
							DrawParticleGlow( p, [new( -w, h * 0.65f ), new( w, h * 0.65f ),
								new( (index % 3 - 1) * 0.35f * w, -h )], Color.White, 0.24f, 1 );
						else
						{
							w = 0.5f + (index - 45) * 0.35f;
							DrawParticleGlow( p, [new( -w, -w ), new( w, -w ), new( w, w ), new( -w, w )], Color.White, 0.24f, 1 );
						}
					}
				}
				// Interleaved vector cores use normal blending. A Painter render target
				// is premultiplied and would flush the batch twice per spark; convert once.
				using ( var bitmap = texture.GetBitmap() )
				{
					var straight = bitmap.ToTexture( false );
					texture.Dispose(); texture = straight;
				}
				var tiles = new Fill[Columns * Rows * Palette.Length];
				for ( int color = 0; color < Palette.Length; color++ )
				for ( int index = 0; index < Columns * Rows; index++ )
					tiles[color * Columns * Rows + index] = Fill.Image( texture, Palette[color],
						width: Tile * Columns, height: Tile * Rows,
						offsetX: -(index % Columns) * Tile, offsetY: -(index / Columns) * Tile );
				Texture?.Dispose(); Texture = texture; Tiles = tiles; palette = Palette; Builds++;
			}
			catch { texture.Dispose(); throw; }
		}

		public void Dispose() { Texture?.Dispose(); Texture = null; Tiles = null; palette = null; }
	}

	bool DrawSparkGlow( Painter p, DiamondDebris.Particle chip, float opacity )
	{
		if ( drawingReferenceSparks ) return false;
		var matrix = p.Transform;
		if ( MathF.Max( new Vector2( matrix.M11, matrix.M12 ).Length, new Vector2( matrix.M21, matrix.M22 ).Length ) > SparkGlowAtlas.Density ) return false;
		var vertices = chip.Vertices;
		int index;
		if ( vertices.Length == 3 )
		{
			float h = -vertices[2].y, w = vertices[1].x;
			if ( h < 0.65f || h > 1.95f || w <= 0 ) return false;
			index = Math.Clamp( (int)MathF.Round( (h - 0.7f) / 0.3f ), 0, 4 ) * 9
				+ Math.Clamp( (int)MathF.Round( (w / h - 0.65f) / 0.175f ), 0, 2 ) * 3
				+ Math.Clamp( (int)MathF.Round( vertices[2].x / w / 0.35f ) + 1, 0, 2 );
		}
		else if ( vertices.Length == 4 )
		{
			float w = vertices[1].x;
			if ( w < 0.4f || w > 2 ) return false;
			index = 45 + Math.Clamp( (int)MathF.Round( (w - 0.5f) / 0.35f ), 0, 4 );
		}
		else return false;
		if ( sparkAtlas is null ) { sparkAtlas = sharedSparkAtlas ??= new(); sparkAtlas.Users++; }
		sparkAtlas.Ensure();
		using var scope = p.Scope();
		p.Opacity *= opacity;
		p.Fill = sparkAtlas.Tiles[chip.ColorIndex * 50 + index]; p.Stroke = Stroke.None;
		p.Rect( new Rect( -10, -10, 20, 20 ) );
		return true;
	}

	void ReleaseSparkAtlas()
	{
		if ( sparkAtlas is null ) return;
		if ( --sparkAtlas.Users == 0 )
		{
			sparkAtlas.Dispose();
			if ( ReferenceEquals( sharedSparkAtlas, sparkAtlas ) ) sharedSparkAtlas = null;
		}
		sparkAtlas = null;
	}
}