Prologue
I have been fascinated with fractals ever since I was little. My first experience with them was at the Renaissance Fair where a vendor was selling prints of his artist renditions of fractals.
I have been thinking that I kind of want one of those prints now, but since I am extra I wanted to do it myself. After all a little math isn’t that scary.
The Math
Iteration
The first thing to understand is iteration. Imagine you have a sequence of numbers, where each number depends on the number that came before it, that’s the idea.
Lets imagine our function takes two inputs, n
which describes the “nth” place and a constant c
. Our function would return it’s previous state squared plus the constant.
f(n+1) = n^2 + c
For a c
of 1 you would get: 0, 1, 2, 5, 26… a sequence that continues to infinity.
For a c of -1 you would get: 0, −1, 0, −1, 0… a sequence that does not continue to infinity.
Exit Conditions
I won’t pretend to know the math behind it, but we know that if a sequence ever reaches the absolute value of 2 then it will continue to infinity forever.
Lets use this criteria to define a new function. This new function will take a constant c
and return the length of the sequence of numbers that stays below 2. Since this could repeat forever, lets define a maximum number of iterations.
The Code
There are many iterations (heh) of the code. But lets start with what we have covered so far:
// Define a sequence given a starting constant
function sequence($constant)
{
$next = $constant;
while (true) {
yield $next;
$next = $next**2 + $constant;
}
}
// Count the iterations in a given sequence
function iterations()
{
foreach (sequence() as $nth => $value) {
if ($nth > MAX_ITERATIONS) {
break;
}
if ($value >= 2) {
return $nth;
}
}
return 0;
}
It’s Never That Easy
We are not working with your regular numbers here. We are working with complex numbers. A complex number is a number that has both a real and an imaginary component. For example, 1+2i
is a complex number.
So let’s rewrite our function to take complex numbers instead. To accomplish this I am using the markbaker/complex-functions library.
function sequenceWithComplexNumbers($real, $imaginary) {
$next = $test = new Complex\Complex($real, $imaginary);
while (true) {
yield $next;
$next = $next->pow(2)->add($test);
}
}
function countIterationsOfSequence($sequence) {
foreach ($sequence as $nth => $value) {
if ($nth >= MAX_ITERATIONS) {
break;
}
if ($value->getReal() >= 2 || $value->getImaginary() >= 2) {
return $nth;
}
}
return 0;
}
Let’s Use OOP
We are starting to work with enough code that encapsulating it into a class is starting to make sense. Let’s create a class that takes an input real and imaginary number, as well as a maximum number of iterations for the sequence. This class should represent both the sequence of numbers and the iteration count of the sequence. This isn’t the whole code but describes the interface:
interface Mandelbrot
{
public function __construct(
private float $real,
private float $imaginary,
private int $maxIterations,
) {}
public function iterations(): int;
public function sequence(): Generator;
}
Generating The Image
Since we are working with complex numbers we can fill in the complex number plane. We can say that the X axis is the real component, and the Y axis is the imaginary component. For each X and Y, we:
- Create a Mandelbrot class instance for the given coordinate.
- Generate the sequence given our positions complex
c
constant. - Count the iterations in the sequence.
- Color the pixel in depending on the number of iterations.
For point #4 there, that’s the only real challenging part. I am using 256 iterations, or counting up to 0xff
in hex. We can convert the number to grayscale by converting the iteration count from decimal to hexadecimal, then concatenating itself three times. This gives us 0x000000
to 0xffffff
to work with, which is the entire gray color spectrum.
There may be a follow-up blog post about adding color, but for now lets stick with gray. Oh, and I am using the intervention/image library for generating images.
$width = $input->getArgument('width');
$height = $input->getArgument('height');
$realStart = -2;
$realEnd = 1;
$imaginaryStart = -1;
$imaginaryEnd = 1;
$filename = $input->getArgument('filename');
$canvas = ImageManagerStatic::canvas($width, $height, '#ffffff');
foreach (range(0, $width) as $x) {
foreach (range(0, $height) as $y) {
$mandelbrot = new Mandelbrot(
$realStart + ($x / $width) * ($realEnd - $realStart),
$imaginaryStart + ($y / $height) * ($imaginaryEnd - $imaginaryStart),
MAX_ITERATIONS,
);
$iterations = $mandelbrot->iterations();
$color = $iterations > 0 ? dechex(MAX_ITERATIONS - $iterations) : "00";
$colorCode = "#$color$color$color";
$canvas->pixel($colorCode, $x, $y);
}
}
$canvas->save($filename, 100, 'png');
Wrapping Things Up
The code presented here is just one iteration that got me to the end result. The actual code uses the symfony/console component to handle inputs and outputs for generating a customizable image. This isn’t my finest work yet, I may publish the full code someday.
The featured image on this post is a 4k-resolution image of the set, generated by this code. It took an hour or two to generate. My real goal for the project was a print-quality poster version, which unfortunately is a little too large for distribution on a blog, but that took a few days to generate.