PixelUtils.cs
namespace Sandbox;

/// <summary>
/// Easing types used by <see cref="PixelUtils.Map"/>.
/// </summary>
public enum EaseType
{
	Linear,
	ExpoIn,
	ExpoOut,
	ExpoInOut,
	CubicIn,
	CubicOut,
	CubicInOut,
	SineIn,
	SineOut,
	SineInOut
}

/// <summary>
/// Utility methods ported from the Unity pixelshitter framework.
/// Contains alpha compositing, value remapping with easing, and coordinate helpers.
/// </summary>
public static class PixelUtils
{
	/// <summary>
	/// Porter-Duff "source over" alpha composite.
	/// Blends <paramref name="src"/> on top of <paramref name="dst"/>.
	/// </summary>
	public static Color32 AddColors( Color32 src, Color32 dst )
	{
		if ( src.a == 0 ) return dst;   // Fully transparent source — nothing to blend.
		if ( src.a == 255 ) return src; // Fully opaque source — covers destination entirely.

		float srcA = src.a / 255f;
		float dstA = dst.a / 255f;

		// Porter-Duff "source over" composite:
		//   outA  = srcA + dstA * (1 - srcA)
		//   outRGB = (src.rgb * srcA + dst.rgb * dstA * (1 - srcA)) / outA
		// The (1 - srcA) term is how much of the destination shows through.
		float outA = srcA + dstA * (1f - srcA);

		if ( outA <= 0f )
			return new Color32( 0, 0, 0, 0 );

		byte r = (byte)((src.r * srcA + dst.r * dstA * (1f - srcA)) / outA);
		byte g = (byte)((src.g * srcA + dst.g * dstA * (1f - srcA)) / outA);
		byte b = (byte)((src.b * srcA + dst.b * dstA * (1f - srcA)) / outA);
		byte a = (byte)(outA * 255f);

		return new Color32( r, g, b, a );
	}

	/// <summary>
	/// Subtractive blend: subtracts <paramref name="src"/> RGB from <paramref name="dst"/>.
	/// Alpha of <paramref name="src"/> controls blend strength.
	/// </summary>
	public static Color32 SubtractColors( Color32 src, Color32 dst )
	{
		float t = src.a / 255f;
		byte r = (byte)Math.Max( 0, dst.r - (int)(src.r * t) );
		byte g = (byte)Math.Max( 0, dst.g - (int)(src.g * t) );
		byte b = (byte)Math.Max( 0, dst.b - (int)(src.b * t) );
		return new Color32( r, g, b, dst.a );
	}

	/// <summary>
	/// Remap a value from one range to another with optional easing.
	/// </summary>
	public static float Map( float value, float fromMin, float fromMax, float toMin, float toMax, EaseType ease = EaseType.Linear )
	{
		// Normalize 0..1
		float t = (fromMax - fromMin) == 0f ? 0f : (value - fromMin) / (fromMax - fromMin);
		t = Math.Clamp( t, 0f, 1f );

		t = ease switch
		{
			EaseType.ExpoIn    => ExpoIn( t ),
			EaseType.ExpoOut   => ExpoOut( t ),
			EaseType.ExpoInOut => ExpoInOut( t ),
			EaseType.CubicIn   => t * t * t,                                           // t³ — slow start
			EaseType.CubicOut  => 1f - CubicOutHelper( t ),                            // 1-(1-t)³ — slow end
			EaseType.CubicInOut => t < 0.5f ? 4f * t * t * t : 1f - CubicOutHelper( t ) * 0.5f, // S-curve, joins at t=0.5
			EaseType.SineIn    => 1f - MathF.Cos( t * MathF.PI / 2f ),
			EaseType.SineOut   => MathF.Sin( t * MathF.PI / 2f ),
			EaseType.SineInOut => -(MathF.Cos( MathF.PI * t ) - 1f) / 2f,
			_ => t
		};

		return toMin + (toMax - toMin) * t;
	}

	private static float ExpoIn( float t ) => t == 0f ? 0f : MathF.Pow( 2f, 10f * t - 10f );
	private static float ExpoOut( float t ) => t >= 1f ? 1f : 1f - MathF.Pow( 2f, -10f * t );
	private static float ExpoInOut( float t )
	{
		if ( t == 0f ) return 0f;
		if ( t >= 1f ) return 1f;
		return t < 0.5f
			? MathF.Pow( 2f, 20f * t - 10f ) / 2f
			: (2f - MathF.Pow( 2f, -20f * t + 10f )) / 2f;
	}

	// Helper to avoid repeating the cubic-out pattern: (1 - t)^3
	private static float CubicOutHelper( float t )
	{
		float inv = 1f - t;
		return inv * inv * inv;
	}

	/// <summary>
	/// Swap two values in place.
	/// </summary>
	public static void Swap<T>( ref T a, ref T b )
	{
		(a, b) = (b, a);
	}

	/// <summary>
	/// Fisher-Yates shuffle on any list.
	/// </summary>
	public static void Shuffle<T>( IList<T> list )
	{
		for ( int i = list.Count - 1; i > 0; i-- )
		{
			int j = Game.Random.Next( i + 1 );
			(list[i], list[j]) = (list[j], list[i]);
		}
	}
}