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]);
}
}
}