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