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