Code/FractalJuliaSetGenerator.cs
namespace FractalGen;

[Icon( "donut_small" )]
[Title( "Fractal - Julia Set" )]
[ClassName( "juliasetgenerator" )]
public class FractalJuliaSetGenerator : Sandbox.Resources.TextureGenerator
{
    public int MaxSize { get; set; } = 1024;

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

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

    [KeyProperty]
    public int MaxIterations { get; set; } = 10;

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

    public Vector2 Offset { get; set; }

    [Range( -2f, 2f )]
    public float ParameterA { get; set; } = -0.7f;

    [Range( -2f, 2f )]
    public float ParameterB { get; set; } = 0.27015f;

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

    [Range( 0f, 10f )]
    public float ColorScale { get; set; } = 1;

    [Range( 0f, 1f )]
    public float ColorShift { get; set; }

    [KeyProperty]
    public bool SmoothColoring { get; set; } = true;

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

        ComplexNumber c = new( ParameterA, ParameterB );

        Sandbox.Utility.Parallel.For( 0, bitmap.Height, y =>
        {
            for ( int x = 0; x < bitmap.Width; x++ )
            {
                if ( ct.IsCancellationRequested )
                    return;

                float realPart = (x - bitmap.Width / 2f) / (bitmap.Width / 4f * Zoom) + Offset.x;
                float imagPart = (y - bitmap.Height / 2f) / (bitmap.Height / 4f * Zoom) + Offset.y;

                ComplexNumber z = new( realPart, imagPart );
                Color pixelColor;

                (int iterations, float smoothValue) = CalculateJuliaValue( z, c, MaxIterations );

                if ( iterations < MaxIterations )
                {
                    float colorValue;

                    if ( SmoothColoring )
                    {
                        colorValue = iterations + 1 - MathF.Log( MathF.Log( smoothValue ) ) / MathF.Log( 2 );
                        colorValue = colorValue * ColorScale % MaxIterations;
                        colorValue = (colorValue / MaxIterations + ColorShift) % 1.0f;
                    }
                    else
                    {
                        colorValue = (float)iterations / MaxIterations;
                        colorValue = (colorValue * ColorScale + ColorShift) % 1.0f;
                    }

                    pixelColor = GetColorForValue( colorValue );
                }
                else
                {
                    pixelColor = BackgroundColor;
                }

                lock ( bitmap )
                {
                    bitmap.SetPixel( x, y, pixelColor );
                }
            }
        } );

        if ( InvertColor )
            bitmap.InvertColor();

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

    private (int iterations, float smoothValue) CalculateJuliaValue( ComplexNumber z, ComplexNumber c, int maxIter )
    {
        int iteration = 0;
        float zMagSquared = 0;

        while ( iteration < maxIter && zMagSquared <= 4.0f )
        {
            // z = z^2 + c
            z = ComplexNumber.Square( z ) + c;
            zMagSquared = z.MagnitudeSquared();
            iteration++;
        }

        return (iteration, zMagSquared);
    }

    private Color GetColorForValue( float value )
    {
        if ( RandomColors )
        {
            float hue = value * 359 % 360;
            return ColorFromHSV( hue, 0.8f, 0.95f );
        }

        if ( GradientColors )
            return Gradient.Evaluate( value );

        return Color.Lerp( BackgroundColor, Color, value );
    }

    private Color ColorFromHSV( float hue, float saturation, float value )
    {
        float c = value * saturation;
        float x = c * (1 - MathF.Abs( (hue / 60) % 2 - 1 ));
        float m = value - c;
        float r, g, b;

        if ( hue < 60 )
        {
            r = c; g = x; b = 0;
        }
        else if ( hue < 120 )
        {
            r = x; g = c; b = 0;
        }
        else if ( hue < 180 )
        {
            r = 0; g = c; b = x;
        }
        else if ( hue < 240 )
        {
            r = 0; g = x; b = c;
        }
        else if ( hue < 300 )
        {
            r = x; g = 0; b = c;
        }
        else
        {
            r = c; g = 0; b = x;
        }

        return new Color(
            r + m,
            g + m,
            b + m,
            1
        );
    }
}