PixelDoc.cs
namespace PixelPusher;
public enum PushTool
{
Push,
Flood,
Sip,
Block,
Line,
Oval
}
public enum StampRole
{
Decor,
Solid,
OneWay,
Hazard,
Spring
}
public sealed class PixelDoc
{
const int MaxUndo = 100;
public string Name { get; set; } = "untitled";
public int Width { get; private set; }
public int Height { get; private set; }
public string PaletteName { get; set; } = "CRT Night";
public Color32[] Palette { get; private set; }
public byte[] Indices { get; private set; }
public int Version { get; private set; }
public int Frame { get; private set; }
public int FrameCount => _frames.Count;
readonly List<byte[]> _frames = new();
readonly List<byte[]> _undo = new();
readonly List<byte[]> _redo = new();
public PixelDoc( int width, int height, PalettePack pack = null )
{
pack ??= Palettes.Default;
Width = Math.Clamp( width, 8, 256 );
Height = Math.Clamp( height, 8, 256 );
PaletteName = pack.Name;
Palette = Palettes.Copy( pack );
Indices = new byte[Width * Height];
_frames.Add( Indices );
}
public int IndexAt( int x, int y )
{
if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
return 0;
return Indices[y * Width + x];
}
public Color32 ColorAt( int x, int y )
{
var i = IndexAt( x, y );
if ( (uint)i >= (uint)Palette.Length )
return Color32.Transparent;
return Palette[i];
}
public void BeginStroke()
{
PushUndo();
_redo.Clear();
}
bool _mirroring;
public void SetIndex( int x, int y, byte index )
{
if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
return;
if ( (uint)index >= (uint)Palette.Length )
index = 0;
var i = y * Width + x;
if ( Indices[i] != index )
{
Indices[i] = index;
Version++;
}
if ( !_mirroring && PixelStudio.Mirror )
{
_mirroring = true;
SetIndex( Width - 1 - x, y, index );
_mirroring = false;
}
}
public void FlipH()
{
BeginStroke();
for ( var y = 0; y < Height; y++ )
for ( var x = 0; x < Width / 2; x++ )
{
var a = y * Width + x;
var b = y * Width + (Width - 1 - x);
(Indices[a], Indices[b]) = (Indices[b], Indices[a]);
}
Version++;
}
public void FlipV()
{
BeginStroke();
for ( var y = 0; y < Height / 2; y++ )
for ( var x = 0; x < Width; x++ )
{
var a = y * Width + x;
var b = (Height - 1 - y) * Width + x;
(Indices[a], Indices[b]) = (Indices[b], Indices[a]);
}
Version++;
}
public void Rotate90()
{
if ( Width != Height )
return;
BeginStroke();
var next = new byte[Indices.Length];
for ( var y = 0; y < Height; y++ )
for ( var x = 0; x < Width; x++ )
next[x * Width + (Height - 1 - y)] = Indices[y * Width + x];
Bind( next );
Version++;
}
public void Replace( byte from, byte to )
{
BeginStroke();
for ( var i = 0; i < Indices.Length; i++ )
{
if ( Indices[i] == from )
Indices[i] = to;
}
Version++;
}
public void StampBrush( int x, int y, byte index, int size, bool circle )
{
var r = Math.Max( 1, size );
var r2 = r * r;
for ( var dy = -r + 1; dy < r; dy++ )
for ( var dx = -r + 1; dx < r; dx++ )
{
if ( circle && dx * dx + dy * dy >= r2 )
continue;
SetIndex( x + dx, y + dy, index );
}
}
public void Line( int x0, int y0, int x1, int y1, byte index, int size, bool circle )
{
var dx = Math.Abs( x1 - x0 );
var dy = Math.Abs( y1 - y0 );
var sx = x0 < x1 ? 1 : -1;
var sy = y0 < y1 ? 1 : -1;
var err = dx - dy;
while ( true )
{
StampBrush( x0, y0, index, size, circle );
if ( x0 == x1 && y0 == y1 )
break;
var e2 = 2 * err;
if ( e2 > -dy )
{
err -= dy;
x0 += sx;
}
if ( e2 < dx )
{
err += dx;
y0 += sy;
}
}
}
public void Rect( int x0, int y0, int x1, int y1, byte index, bool filled, int size )
{
if ( x0 > x1 ) (x0, x1) = (x1, x0);
if ( y0 > y1 ) (y0, y1) = (y1, y0);
if ( filled )
{
for ( var y = y0; y <= y1; y++ )
for ( var x = x0; x <= x1; x++ )
SetIndex( x, y, index );
return;
}
Line( x0, y0, x1, y0, index, size, false );
Line( x1, y0, x1, y1, index, size, false );
Line( x1, y1, x0, y1, index, size, false );
Line( x0, y1, x0, y0, index, size, false );
}
public void Ellipse( int x0, int y0, int x1, int y1, byte index, bool filled, int size )
{
if ( x0 > x1 ) (x0, x1) = (x1, x0);
if ( y0 > y1 ) (y0, y1) = (y1, y0);
var rx = (x1 - x0) * 0.5f;
var ry = (y1 - y0) * 0.5f;
var cx = (x0 + x1) * 0.5f;
var cy = (y0 + y1) * 0.5f;
if ( rx < 0.5f ) rx = 0.5f;
if ( ry < 0.5f ) ry = 0.5f;
if ( filled )
{
for ( var y = y0; y <= y1; y++ )
for ( var x = x0; x <= x1; x++ )
{
var nx = (x - cx) / rx;
var ny = (y - cy) / ry;
if ( nx * nx + ny * ny <= 1.05f )
SetIndex( x, y, index );
}
return;
}
for ( var y = y0; y <= y1; y++ )
for ( var x = x0; x <= x1; x++ )
{
var nx = (x - cx) / rx;
var ny = (y - cy) / ry;
var d = nx * nx + ny * ny;
if ( d <= 1.08f && d >= 0.72f )
StampBrush( x, y, index, size, false );
}
}
public void Flood( int x, int y, byte index )
{
if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
return;
var target = Indices[y * Width + x];
if ( target == index )
return;
var stack = new Stack<(int x, int y)>();
stack.Push( (x, y) );
var seen = new bool[Width * Height];
while ( stack.Count > 0 )
{
var (cx, cy) = stack.Pop();
if ( (uint)cx >= (uint)Width || (uint)cy >= (uint)Height )
continue;
var i = cy * Width + cx;
if ( seen[i] || Indices[i] != target )
continue;
seen[i] = true;
Indices[i] = index;
stack.Push( (cx + 1, cy) );
stack.Push( (cx - 1, cy) );
stack.Push( (cx, cy + 1) );
stack.Push( (cx, cy - 1) );
}
Version++;
}
public void Clear()
{
BeginStroke();
Array.Clear( Indices );
Version++;
}
public void ApplyPalette( PalettePack pack )
{
if ( pack is null )
return;
PaletteName = pack.Name;
Palette = Palettes.Copy( pack );
Version++;
}
public bool CanUndo => _undo.Count > 0;
public bool CanRedo => _redo.Count > 0;
public void Undo()
{
if ( _undo.Count == 0 )
return;
_redo.Add( CopyBytes( Indices ) );
Bind( _undo[^1] );
_undo.RemoveAt( _undo.Count - 1 );
Version++;
}
public void Redo()
{
if ( _redo.Count == 0 )
return;
_undo.Add( CopyBytes( Indices ) );
Bind( _redo[^1] );
_redo.RemoveAt( _redo.Count - 1 );
Version++;
}
public Color32 OnionAt( int x, int y )
{
if ( FrameCount < 2 )
return Color32.Transparent;
var prev = Frame == 0 ? FrameCount - 1 : Frame - 1;
var buf = _frames[prev];
if ( (uint)x >= (uint)Width || (uint)y >= (uint)Height )
return Color32.Transparent;
var i = buf[y * Width + x];
if ( i == 0 || (uint)i >= (uint)Palette.Length )
return Color32.Transparent;
Color c = Palette[i];
return new Color32(
(byte)MathF.Round( c.r * 255f ),
(byte)MathF.Round( c.g * 255f ),
(byte)MathF.Round( c.b * 255f ),
90 );
}
public void SetFrame( int index )
{
if ( _frames.Count == 0 )
return;
Frame = Math.Clamp( index, 0, _frames.Count - 1 );
Indices = _frames[Frame];
Version++;
}
public void AddFrame()
{
if ( _frames.Count >= 16 )
return;
BeginStroke();
_frames.Insert( Frame + 1, CopyBytes( Indices ) );
SetFrame( Frame + 1 );
}
public void DeleteFrame()
{
if ( _frames.Count <= 1 )
{
Clear();
return;
}
BeginStroke();
_frames.RemoveAt( Frame );
SetFrame( Math.Min( Frame, _frames.Count - 1 ) );
}
void Bind( byte[] buf )
{
Indices = buf;
if ( Frame >= 0 && Frame < _frames.Count )
_frames[Frame] = buf;
}
public PixelSave ToSave()
{
var pal = new string[Palette.Length];
for ( var i = 0; i < Palette.Length; i++ )
pal[i] = Palettes.Hex( Palette[i] ) == "transparent" ? "#00000000" : Palettes.Hex( Palette[i] );
return new PixelSave
{
Name = Name,
Width = Width,
Height = Height,
PaletteName = PaletteName,
Palette = pal,
Indices = CopyBytes( Indices ),
Frames = _frames.Count > 1 ? _frames.Select( CopyBytes ).ToList() : null,
Frame = Frame
};
}
public static PixelDoc FromSave( PixelSave save )
{
if ( save is null )
return new PixelDoc( 16, 16 );
var pack = Palettes.Named( save.PaletteName );
var doc = new PixelDoc( save.Width, save.Height, pack )
{
Name = save.Name ?? "untitled",
PaletteName = save.PaletteName ?? pack.Name
};
if ( save.Palette is { Length: > 0 } )
{
doc.Palette = new Color32[save.Palette.Length];
for ( var i = 0; i < save.Palette.Length; i++ )
doc.Palette[i] = Palettes.Parse( save.Palette[i] );
}
if ( save.Frames is { Count: > 1 } )
{
doc._frames.Clear();
foreach ( var frame in save.Frames )
{
var buf = new byte[doc.Width * doc.Height];
if ( frame is { Length: > 0 } )
Array.Copy( frame, buf, Math.Min( buf.Length, frame.Length ) );
doc._frames.Add( buf );
}
doc.SetFrame( save.Frame );
}
else if ( save.Indices is { Length: > 0 } )
{
var n = Math.Min( doc.Indices.Length, save.Indices.Length );
Array.Copy( save.Indices, doc.Indices, n );
}
return doc;
}
void PushUndo()
{
_undo.Add( CopyBytes( Indices ) );
if ( _undo.Count > MaxUndo )
_undo.RemoveAt( 0 );
}
public static byte[] CopyBytes( byte[] src )
{
if ( src is null )
return new byte[0];
var dst = new byte[src.Length];
Array.Copy( src, dst, src.Length );
return dst;
}
}
public sealed class PixelSave
{
public string Name { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string PaletteName { get; set; }
public string[] Palette { get; set; }
public byte[] Indices { get; set; }
public List<byte[]> Frames { get; set; }
public int Frame { get; set; }
}