Life is a Mandelbrot! in Flutter

Ankit Mahato
Flutter Community
Published in
7 min readFeb 25, 2020

--

A couple of months back I came across a Flutter Community article on Fractals. It demonstrated how you can create “never-ending patterns that repeat themselves at different scales” which are called fractals.

There is a whole discipline in mathematics (and physics) where researchers study Chaos and Fractals, and in this article I will introduce you to Mandelbrot set and we will witness how you can progressively generate it in Flutter.

Mandelbrot Set (Flutter)

Before going through the complex arithmetic (link) behind it, let’s go through a poem I penned down which will help you grasp the essence of Mandelbrot.

Life is a Mandelbrot

Life is not so simple and plain ,
So i’ll take help of the complex plane.
With the domain having its edges fixed,
Doesn’t our timeline also has a limit ?

Iterating each point on this very plane ,
Some points vanish and some escape ;
But there are few points stubborn enough ,
Who continue to circle around and say that the iteration is not enough.
They depict the problems which we face in our lives ,
Some vanish , some grow and some with which we compromise.

Pour colour in this fractal array and see,
The beauty comparable to daughters of Eve.
You will realise, life is not so pale, my dear,
The diversity and colour surely gives us a reason to cheer.

Still our Soul is just like a sailing boat,
Travelling endlessly on the never-ending borderline of Mandelbrot.

Source:

After reading the poem!

So now let’s come back to our real world of Flutter!

Key takeaways from this article:

  1. Handling complex number calculations in Dart/Flutter.
  2. Using ChangeNotifier, ChangeNotifierProvider and Consumer to update the progress of mandelbrot set computation.
  3. Utilizing .drawPoints() method to render pixel colours inside CustomPainter.

Step 1

Add complex package as a dependency in your pubspec.yaml file

complex: ^0.5.0

This package provides us a Complex class which is useful in representing complex numbers, i.e. numbers which have both a real and an imaginary part. This class also provides various methods and operators which help in complex number calculations.

Next, add the provider package as a dependency -

provider: ^4.0.4

This package provides us access to useful classes which will help us in managing and injecting the computational state of the Mandelbrot set.

Now, let us import the required libraries and define max iteration.

Step 2

Add the main function and create a MaterialApp.

Step 3

ChangeNotifier provides change notification to its listeners (Consumer). We can extend our model class(MandelbrotClass) with ChangeNotifier and call its method notifyListeners() whenever there is a change that has to be communicated to the widgets that are listening to this model.

We are interested in the values of _currentPixelIteration (2D list of height number of rows and width number of columns) and _currentIter. To better understand the significance of these variables let us go through some theory.

“The” Mandelbrot set is the set obtained from the quadratic recurrence equation

Source: Link

Here z is a complex number and z = 0+0i for all pixels (x, y) at n=0.

C is a complex number representing a pixel (x, y) on the screen where x-axis is real axis and y-axis is imaginary axis. The center of the screen has the value of C as 0+0i.

If the width of the screen is less than the height of the screen (portrait mode), the real value of z ranges [-2.25, 0.75] and the imaginary value of z varies accordingly.

If the width of the screen is mode than the height of the screen (landscape mode), the imaginary value of z ranges [-1.5i, 1.5i] and the real value of z varies accordingly.

To keep the aspect ratio of real and imaginary axis as 1:1, we compute hratio and wratio.

Now, we initialize:

  • 2D Complex lists z and C to store the z and C values at each pixel.
  • 2D boolean list continueIteration which stores the information(true/false) if a z at (x, y) has escaped its threshold value.
  • _currentPixelIteration — It is the 2D list containing the maximum value of n+1 for a pixel at (x, y) for which the absolute value of z < 2, i.e., the iteration when z escapes the threshold.
  • _currentIter — It is the value n+1 as per the equation mentioned above. We initialize _currentIter as 0 at the start of the computation when the value of z at n=0 is 0+0i for all the points.

notifyListeners() communicates the change in value of _currentPixelIteration and _currentIter before the iterative computation starts.

Since the mandelbrot set computation is intensive, we are using notifyListeners() to update the progress which is reflected in the UI of our app. Note that we are taking a stepwise approach to increment the maximum iteration (1, 2, 4, 8, 16, 32). This is done to achieve a progressive display having 6 stages. A small Future.delayed(..) call is added to smoothen the progress of mandelbrot display.

The next code segment will clearly show why I went ahead with the entire hassle of using async function, provider package and ChangeNotifier. Any basic demo of Mandelbrot set generation available online has a single step where the entire computation is completed for each pixel till max iteration is reached (32) and then the image is rendered. This could have been achieved in Flutter by simply using a FutureBuilder which updates the UI when the computation is complete. But, it is not a good design from UI/UX perspective as the user has to patiently wait and stare at a blank canvas for a long time before the computation ends and the image gets updated. Even if you go ahead and add a progressbar or circular progress indicator in such cases, it is not helpful as the user still has to stare at a blank canvas with a small progress indication, thereby missing and being unaware of the beauty of the progressive computation happening underneath.

How is the above stepwise computation progressive?

  1. The result at the end of each step is carried forward instead of recomputing it all over again. For example, at the end of step 3 (max iteration=4) the results of the computed z values are used for the next step (max iteration=8). So the progress of iterations are step 3 (n=3..4) to step 4 (n=5..8) instead of step 3 (n=1..4) to step 4 (n=1..8).
  2. Using continueIteration 2D list helps keeping track of the points z which need to be computed further in the next step, thereby eliminating unnecessary computation for the entire complex plane. For example, if a pixel has converged before reaching the max iteration value in step 3 it is marked as converged and no operations are performed [not even calculation of abs()] in any future steps 4,5 or 6.

Step 4

We calculate the width and height of the screen and pass the information while creating the ChangeNotifierProvider.

A simple Stack widget if used to display the generated mandelbrot along with the current iteration for which the mandelbrot is being displayed.

There are 2 consumers of the MandelbrotClass which get notified whenever notifyListeners() is called:

  • CustomPaint(..)
  • Text(..)

FractalPainter is an extended CustomPainter whose purpose is to paint the pixels on the canvas based on the max iterations for each z.

Step 5

Let’s discuss the final piece of our puzzle. How do we actually paint the Mandelbrot?

We create a FractalPainter by extending the CustomPainter. We override the paint(..) method to paint the entire screen (pixel by pixel) and set the shouldRepaint(..) as true (note!) to refresh the mandelbrot painting for each step.

Done!

Hope you enjoyed the article!

You can find the above code in the repository:

Do connect with me on LinkedIn, Twitter and Github:

Also, don’t forget to subscribe my youtube channel where I share videos on scientific computing:

--

--