Transposer/Entities/Background.cs
namespace Sandbox.Transposer;

/// <summary>
/// Procedural animated background. Fills the entire screen with 2×2 pixel blocks
/// whose colours are computed from sine waves, position, and elapsed time.
/// Draws a decorative border: outer edge is yellow-tinted noise, inner is black.
///
/// Three visual modes are randomly chosen per scene activation.
/// </summary>
public class Background : Entity
{
	private int _width;
	private int _height;
	private float _elapsedTime;
	private bool _normalDirectionX;
	private bool _normalDirectionY;
	private int _visualMode;

	public Background( int x, int y, TransposerScene scene )
	{
		PixelPosition = new PixelPoint( x, y );
		_scene = scene;
		_type = "tile";
		_layer = Globals.DEPTH_TILE;

		_width = Screen.PixelWidth;
		_height = Screen.PixelHeight;

		_normalDirectionX = Game.Random.Next( 0, 2 ) == 0;
		_normalDirectionY = Game.Random.Next( 0, 2 ) == 0;
		_visualMode = Game.Random.Next( 0, 3 );
	}

	public override void UpdateEntity( float deltaTime )
	{
		base.UpdateEntity( deltaTime );
		_elapsedTime += deltaTime;
	}

	public override void Draw()
	{
		int xOffset = (int)MathF.Round( _elapsedTime * 10f ) % _width;
		int yOffset = (int)MathF.Round( _elapsedTime * 10f ) % _height;

		if ( !_normalDirectionX ) xOffset *= -1;
		if ( !_normalDirectionY ) yOffset *= -1;

		int px = PixelX;
		int py = PixelY;

		for ( int x = 0; x < _width; x += 2 )
		{
			for ( int y = 0; y < _height; y += 2 )
			{
				// Wrap the scrolled coordinates so the gradient tiles seamlessly.
				int adjX = x + xOffset;
				while ( adjX >= _width ) adjX -= _width;
				while ( adjX < 0 ) adjX += _width;

				int adjY = y - yOffset;
				while ( adjY >= _height ) adjY -= _height;
				while ( adjY < 0 ) adjY += _height;

				const float MIN = 0.20f;
				const float MAX = 0.80f;

				// Each axis produces a sine-shaped brightness gradient that peaks at
				// the centre and dips at the edges — creating a soft vignette look.
				float shadeX = adjX <= _width / 2
					? PixelUtils.Map( adjX, 0, _width / 2f, MIN, MAX, EaseType.SineIn )
					: PixelUtils.Map( adjX, _width / 2f + 1, _width, MAX, MIN, EaseType.SineOut );

				float shadeY = adjY <= _height / 2
					? PixelUtils.Map( adjY, 0, _height / 2f, MIN, MAX, EaseType.SineOut )
					: PixelUtils.Map( adjY, _height / 2f + 1, _height, MAX, MIN, EaseType.SineIn );

				// Slow sine wave mixed in to animate the overall brightness over time.
				float offset = MathF.Sin( _elapsedTime * 0.25f ) * 0.20f;

				// Mode 0: pulsing blue channel — cool colour that breathes over time.
				// Mode 1: blue derived from avg/(x+epsilon) — varies by column position.
				// Mode 2: blue derived from avg/(y+epsilon) — varies by row position.
				Color color = _visualMode switch
				{
					0 => new Color( shadeX + offset, shadeY - offset, 0.70f + MathF.Abs( MathF.Sin( _elapsedTime * 0.5f ) ) * 0.30f ),
					1 => new Color( shadeX + offset, shadeY - offset, (shadeX + shadeY) * 0.5f / (shadeX + 0.1f) ),
					_ => new Color( shadeX + offset, shadeY - offset, (shadeX + shadeY) * 0.5f / (shadeY + 0.1f) )
				};

				Color32 c32 = (Color32)color;
				Screen.SetPixel( px + x, py + y, c32 );
				Screen.SetPixel( px + x + 1, py + y, c32 );
				Screen.SetPixel( px + x, py + y + 1, c32 );
				Screen.SetPixel( px + x + 1, py + y + 1, c32 );
			}
		}

		// ── Decorative border ────────────────────────────────────────────
		Color32 inner = new( 0, 0, 0, 255 );

		const float MIN_SHADE = 0.20f;
		const float MAX_SHADE = 0.30f;
		const float MIN_OPACITY = 0.25f;
		const float MAX_OPACITY = 0.50f;

		// Top and bottom edges.
		for ( int x = 0; x < _width; x++ )
		{
			Color32 outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( x, 0, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( x, 1, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( x, _height - 1, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( x, _height - 2, outer );

			if ( x >= 2 && x <= _width - 3 )
			{
				Screen.AddPixel( x, 2, inner );
				Screen.AddPixel( x, 3, inner );
				Screen.AddPixel( x, _height - 3, inner );
				Screen.AddPixel( x, _height - 4, inner );
			}
		}

		// Left and right edges.
		for ( int y = 2; y < _height - 2; y++ )
		{
			Color32 outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( 0, y, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( 1, y, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( _width - 1, y, outer );
			outer = RandomBorderColor( MIN_SHADE, MAX_SHADE, MIN_OPACITY, MAX_OPACITY );
			Screen.AddPixel( _width - 2, y, outer );

			Screen.AddPixel( 2, y, inner );
			Screen.AddPixel( 3, y, inner );
			Screen.AddPixel( _width - 3, y, inner );
			Screen.AddPixel( _width - 4, y, inner );
		}
	}

	private static Color32 RandomBorderColor( float minShade, float maxShade, float minOp, float maxOp )
	{
		float shade = Game.Random.Float( minShade, maxShade );
		float op = Game.Random.Float( minOp, maxOp );
		return new Color32( (byte)(shade * 255), (byte)(shade * 255), 0, (byte)(op * 255) );
	}
}