Code/FractalSierpinskiGenerator.cs
namespace FractalGen;

[Icon( "change_history" )]
[Title( "Fractal - Sierpinski" )]
[ClassName( "sierpinskigenerator" )]
public class FractalSierpinskiGenerator : Sandbox.Resources.TextureGenerator
{
    public int MaxSize { get; set; } = 1024;

    [KeyProperty]
    public Color Color { get; set; } = Color.Green;

    public Color BackgroundColor { get; set; } = Color.Black;

    [KeyProperty]
    public int Iterations { get; set; } = 7;

    public float Zoom { get; set; } = 1;

    public Vector2 Offset { get; set; }

    [KeyProperty]
    [HideIf( nameof( GradientColors ), true )]
    public bool RandomColors { get; set; } = false;

    [HideIf( nameof( RandomColors ), true )]
    public bool GradientColors { get; set; } = false;

    [HideIf( nameof( GradientColors ), false )]
    public Gradient Gradient { get; set; }

    public bool ShowBorders { get; set; } = false;

    [HideIf( nameof( ShowBorders ), false )]
    public Color BorderColor { get; set; } = Color.White;

    [HideIf( nameof( ShowBorders ), false )]
    public float BorderThickness { get; set; } = 1;

    public bool InvertColor { get; set; }

    [Hide, JsonIgnore]
    public override bool CacheToDisk => true;

    protected override ValueTask<Texture> CreateTexture( Options options, CancellationToken ct )
    {
        var bitmap = new Bitmap( MaxSize, MaxSize );
        bitmap.Clear( BackgroundColor );

        float centerX = bitmap.Width / 2f + Offset.x * bitmap.Width / 4f;
        float centerY = bitmap.Height / 2f + Offset.y * bitmap.Height / 4f;
        float size = bitmap.Width * 0.8f * Zoom;

        Vector2 top = new( centerX, centerY - size / 2 );
        Vector2 bottomLeft = new( centerX - size / 2, centerY + size / 2 );
        Vector2 bottomRight = new( centerX + size / 2, centerY + size / 2 );

        DrawSierpinskiTriangle( bitmap, top, bottomLeft, bottomRight, Iterations );

        if ( InvertColor )
            bitmap.InvertColor();

        return ValueTask.FromResult( bitmap.ToTexture() );
    }

    private void DrawSierpinskiTriangle( Bitmap bitmap, Vector2 a, Vector2 b, Vector2 c, int depth )
    {
        if ( depth <= 0 )
        {
            DrawTriangle( bitmap, a, b, c, GetColorForDepth( depth, Iterations ) );
            return;
        }

        Vector2 ab = new( (a.x + b.x) / 2, (a.y + b.y) / 2 );
        Vector2 bc = new( (b.x + c.x) / 2, (b.y + c.y) / 2 );
        Vector2 ca = new( (c.x + a.x) / 2, (c.y + a.y) / 2 );

        DrawSierpinskiTriangle( bitmap, a, ab, ca, depth - 1 );
        DrawSierpinskiTriangle( bitmap, ab, b, bc, depth - 1 );
        DrawSierpinskiTriangle( bitmap, ca, bc, c, depth - 1 );
    }

    private void DrawTriangle( Bitmap bitmap, Vector2 a, Vector2 b, Vector2 c, Color color )
    {
        DrawFilledTriangle( bitmap, a, b, c, color );

        if ( ShowBorders )
        {
            DrawLine( bitmap, a, b, BorderColor, BorderThickness );
            DrawLine( bitmap, b, c, BorderColor, BorderThickness );
            DrawLine( bitmap, c, a, BorderColor, BorderThickness );
        }
    }

    private void DrawFilledTriangle( Bitmap bitmap, Vector2 v1, Vector2 v2, Vector2 v3, Color color )
    {
        int minX = (int)Math.Max( 0, Math.Min( Math.Min( v1.x, v2.x ), v3.x ) );
        int maxX = (int)Math.Min( bitmap.Width - 1, Math.Max( Math.Max( v1.x, v2.x ), v3.x ) );
        int minY = (int)Math.Max( 0, Math.Min( Math.Min( v1.y, v2.y ), v3.y ) );
        int maxY = (int)Math.Min( bitmap.Height - 1, Math.Max( Math.Max( v1.y, v2.y ), v3.y ) );

        for ( int y = minY; y <= maxY; y++ )
        {
            for ( int x = minX; x <= maxX; x++ )
            {
                if ( PointInTriangle( new Vector2( x, y ), v1, v2, v3 ) )
                    bitmap.SetPixel( x, y, color );
            }
        }
    }

    private void DrawLine( Bitmap bitmap, Vector2 start, Vector2 end, Color color, float thickness )
    {
        // Bresenham algorithm for drawing a line
        int x0 = (int)start.x;
        int y0 = (int)start.y;
        int x1 = (int)end.x;
        int y1 = (int)end.y;

        int dx = Math.Abs( x1 - x0 );
        int dy = Math.Abs( y1 - y0 );
        int sx = (x0 < x1) ? 1 : -1;
        int sy = (y0 < y1) ? 1 : -1;
        int err = dx - dy;

        int halfThickness = (int)(thickness / 2);

        while ( true )
        {
            for ( int ty = -halfThickness; ty <= halfThickness; ty++ )
            {
                for ( int tx = -halfThickness; tx <= halfThickness; tx++ )
                {
                    int px = x0 + tx;
                    int py = y0 + ty;

                    if ( px >= 0 && px < bitmap.Width && py >= 0 && py < bitmap.Height )
                        bitmap.SetPixel( px, py, color );
                }
            }

            if ( x0 == x1 && y0 == y1 ) break;
            int e2 = 2 * err;

            if ( e2 > -dy )
            {
                err -= dy;
                x0 += sx;
            }
            if ( e2 < dx )
            {
                err += dx;
                y0 += sy;
            }
        }
    }

    private bool PointInTriangle( Vector2 pt, Vector2 v1, Vector2 v2, Vector2 v3 )
    {
        float d1 = Sign( pt, v1, v2 );
        float d2 = Sign( pt, v2, v3 );
        float d3 = Sign( pt, v3, v1 );

        bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
        bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);

        return !(hasNeg && hasPos);
    }

    private float Sign( Vector2 p1, Vector2 p2, Vector2 p3 )
    {
        return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
    }

    private Color GetColorForDepth( int depth, int maxDepth )
    {
        if ( GradientColors )
        {
            float normalizedDepth = (float)(maxDepth - depth) / maxDepth;
            return Gradient.Evaluate( normalizedDepth );
        }

        if ( RandomColors )
            return Random.Shared.Color();

        return Color;
    }
}