*

Drawing the Mandelbrot Set

Apostolos Syropoulos

366, 28th October Str.

GR-671 00 Xanthi, GRRECE

apostolo@obelix.ee.duht.gr

This Java applet draws the Mandelbrot set--- the most complex object in mathematics. One the initial area is drawn you can select an area of interest by clicking the mouse and then dragging it. Once you release the mouse button you have specified a rectangle. The rectangle's coordinates are displayed in the four text field at the bottom of the screen. The drawing process is activated only if you press the draw button.
<*>=
<imports>
<class mandel>
<class DataField>
<class MCanvas>


This code is written to a file (or else not used).

We initially specify which packages we are going to use. Since this program is being developed with the JDK version 1.1.1, we must import the new event handling mechanism from the packages in the java.awt.event bundle.

<imports>=
import java.awt.*;
import java.applet.Applet;
import java.awt.event.*;

Used above.

The program is splited in two pieces: one that performs the actual calculations and consequently draws the Mandelbrot set on the screen, and one that is responsible for the handling of the user interface. Class mandel handles the user interface and class MCanvas performs the calculations. In the new event handling mechanism there are different event classes for each kind of event. The actual event handling is done in methods that certain interfaces provide. (For more information consult the Java documentation.) Class mandel must be able to handle button event (i.e., has the user pressed a button?) and mouse events. Moreover, since computing the Mandelbrot Set is an computationally intense operation, we must put the calculations in a thread of its own. So, we must implement the interface Runnable. (We do not extend class Thread, because we want our application to run as an Internet applet.) Class mandel is defined in a relatively standard way.

<class mandel>=
public class mandel extends Applet implements Runnable, 
                                              ActionListener,
                                              MouseListener,
                                              MouseMotionListener
{
   <variable declarations I>
   <method init>
   <method run>
   <method main>
   <method actionPerformed>
   <method mousePressed>
   <method mouseReleased>
   <method mouseDragged>
   <dummy method definitions>
}

Used above.

We want our applet to initially draw an image of the Mandelbrot Set with real-``coordinates'' from -2.0 to 0.5 and imaginary-``coordinates'' from -1.5 to 1.0. The user interface is composed of a Canvas (our drawing board), two buttons to activate the painting procedure, and four text-fields to display the current ``coordinates''. Each element of the user interface, but the buttons, is implemented as an extension of a language class.

<variable declarations I>=

    final double minR=-2.0, maxR=0.5, minI=-1.5, maxI=1.0;
    DataField xmin, xmax, ymin, ymax;
    Button drawButton, resetButton;
    MCanvas drawingBoard;
    Thread MThread;

Used above.

Method init is responsible for setting up the user interface. The first thing we have to do is to select a layout manager. Next, we put each element on the screen. Finally, we start the thread.

<method init>=
    public void init()
    {
       <select layout manager>
       <put the canvas on the screen>
       <put the text fields on the screen>       
       <put the buttons on the screen>
       <start the thread>      
    }

Used above.

We opt to use the GridBagLayout manager, since this manager gives us maximum flexibility. With each GridBagLayout manager it is always associated a GridBagConstraints object. This object is used to define the way things appear on the screen.

<select layout manager>=
       GridBagLayout gridbag = new GridBagLayout();
       GridBagConstraints c = new GridBagConstraints();
       setLayout(gridbag);

Used above.

We now start to put components on the screen. The first component is our drawing board. It is an instance of class MCanvas, i.e., a Canvas capable to draw the Mandelbrot set. This components is big one so it must be on a line of its own. Moreover, it must occupy all the available space.

<put the canvas on the screen>=

       drawingBoard = new MCanvas(minR, maxR, minI, maxI);
       c.insets = new Insets(4,4,5,5);
       c.gridheight = 500;
       c.gridwidth  = GridBagConstraints.REMAINDER;
       c.weightx = 1.0;
       c.weighty = 1.0;
       c.fill = GridBagConstraints.BOTH;
       c.anchor = GridBagConstraints.CENTER;
       gridbag.setConstraints(drawingBoard, c);
       drawingBoard.addMouseListener(this);
       drawingBoard.addMouseMotionListener(this);
       add(drawingBoard);

Used above.

Text fields are not really text fields: they are instances of class DataField, which provides a ``compound'' object: a text field and label above it. The text field is used to display the range within we draw the Mandelbrot set. We need four DataFields, one for each maximum and for each minimum. (Maximum and minimum real and maximum and minimum real.)

<put the text fields on the screen>=
       
       xmin = new DataField("xmin", minR);
       c.insets= new Insets(1,1,1,1);
       c.gridx= GridBagConstraints.RELATIVE;
       c.gridy= GridBagConstraints.RELATIVE;
       c.gridheight = 1;
       c.gridwidth  = GridBagConstraints.RELATIVE;
       c.weightx = 0.0;
       c.weighty = 0.0;
       gridbag.setConstraints(xmin, c);
       add(xmin);

       xmax = new DataField("xmax", maxR);
       c.gridwidth = GridBagConstraints.REMAINDER;
       gridbag.setConstraints(xmax, c);
       add(xmax);
       
       ymin = new DataField("ymin", minI);
       c.gridwidth=GridBagConstraints.RELATIVE;
       gridbag.setConstraints(ymin, c);
       add(ymin);

       ymax = new DataField("ymax", maxI);
       c.gridwidth = GridBagConstraints.REMAINDER;
       gridbag.setConstraints(ymax, c);
       add(ymax);

Used above.

Now we turn our attension to the two buttons. The first one is used by the user to ``say'' to the program that he/she wants it to draw the selected area. The second, resetButton, is used to draw the image that is being drawn when the applet starts. We use the Courier typeface at 16 pt(?), for the label titles.

<put the buttons on the screen>=
       setFont(new Font("Courier", Font.BOLD, 16));
       drawButton = new Button("Draw M-Set");
       c.gridheight = 2;
       c.fill = GridBagConstraints.NONE;
       c.gridwidth  = GridBagConstraints.RELATIVE;
       gridbag.setConstraints(drawButton, c);
       drawButton.addActionListener(this);
       add(drawButton);

       resetButton = new Button("Draw Original M-Set");
       c.gridwidth  = GridBagConstraints.REMAINDER;
       gridbag.setConstraints(resetButton, c);
       resetButton.addActionListener(this);
       add(resetButton);

Used above.

A thread is started by first creating an instance of class Thread, in our case we use the constructor that accepts only a class/object as an argument, and then by calling is start method. N.B.: This method has nothing to do with the start method of the java.applet.Applet.

<start the thread>=
       MThread = new Thread(this);
       MThread.start();

Used above.

As we said above, we opt to implement multithreading by means of implementing the interface Runnable. So we must implement the method run. This method just calls the repaint method of the drawingBoard, which in turn calls the paint method of this class.

<method run>=
    public void run()
    {
       drawingBoard.repaint();
    }

Used above.

By adding the method main it is possible to make our program to work as a stand alone application. The way the method main is structired is pretty standard: we create a Frame that will hold all GUI components.

<method main>=

    public static void main(String[] args)
    {
       Frame f = new Frame("Mandelbrot Set Drawing Program");
       mandel m = new mandel();
       
       m.init();
       f.add("Center", m);
       f.pack();
       f.setVisible(true);
    }

Used above.

When the applet has drawn the Mandelbrot set or some detail of it, we can specify which section of the image we want to further magnify. This is done very simple: we put the mouse (actualy the cursor with the help of the pointing device) on some point. Then we press the left button and keep it pressed while we drag the mouse to some other point. While the mouse is being dragged the applet records its movements and displays its position on the four text fields. When we reach a point of interest (remember this is the second one), we release the mouse and we have specified the area of interest. If for some reason we have done some mistake, we reinitiate the procedure. One we have finished with the ``specification'' procedure we can either draw the specified area by pressing the drawButton, or re-draw the ``original'' area by pressing the resetButton.

Depending on the button the user presses, certain actions must take place. If the user has pressed the resetButton, we must reset the value the text fields' display and we must reset the value of the variables that control the actual drawing. This variables, quite naturally, are instance variables of class MCanvas. In case the user has pressed the drawButton nothing further happens apart from initiating the drawing process, something that must happen for the previous case too. This process is controled by a thread, so we msut be sure that the current thread is ``dead''. Once this is verified, we create a new thread which is then started.

<method actionPerformed>=
    public void actionPerformed(ActionEvent e)
    {
       Object source = e.getSource();
       if (source == resetButton)
       { 
          drawingBoard.pmin = minR;
          drawingBoard.pmax = maxR;                  
          drawingBoard.qmin = minI;
          drawingBoard.qmax = maxI;
          xmax.setDataField(maxR);
          ymax.setDataField(maxI); 
          xmin.setDataField(minR);
          ymin.setDataField(minI);
          if (!MThread.isAlive())
          {
             MThread = new Thread(this);
             MThread.start();
          }
       }
       else if (source == drawButton && !MThread.isAlive())
       {
                MThread = new Thread(this);
                MThread.start();
       }
    }


Used above.

We turn now our attension to MouseEvent handling. When the user presses the left mouse button, then he wishes to set the ``upper-corner'' of the new drawing area. The real work is done by object drawingBoard, here we merely set the text fields. This applies to all MouseEvent handling methods.

<method mousePressed>=
    public void mousePressed(MouseEvent e)
    {
       xmin.setDataField(drawingBoard.pmin);
       ymin.setDataField(drawingBoard.qmin);
    }

Used above.

<method mouseReleased>=
    public void mouseReleased(MouseEvent e)
    {
       xmax.setDataField(drawingBoard.pmax);
       ymax.setDataField(drawingBoard.qmax); 
       xmin.setDataField(drawingBoard.pmin);
       ymin.setDataField(drawingBoard.qmin);
    }

Used above.

<method mouseDragged>=
    public void mouseDragged(MouseEvent e)
    {
       xmax.setDataField(drawingBoard.pmax);
       ymax.setDataField(drawingBoard.qmax);
       xmin.setDataField(drawingBoard.pmin);
       ymin.setDataField(drawingBoard.qmin);
    }

Used above.

The following methods are defined in the interfaces the class implements, but since we are not going to make any use of them we define then to do nothing.

<dummy method definitions>=
    public void mouseClicked(MouseEvent e)
    { }

    public void mouseEntered(MouseEvent e)
    { }
  
    public void mouseExited(MouseEvent e)
    { }

    public void mouseMoved(MouseEvent e)
    { }

Used above.

Class DataField is somehow an auxiliary class, in the sence that an instance of this class does not affect the real computation in any way. The class defines a TextField with an associated Label.

<class DataField>=
class DataField extends Panel
{
    <variable declarations II>
    <DataField constructor>
    <method setText>
}

Used above.

The class defines three variables: a double which holds the value of the number displayed on the TextField and a Label which says what the TextField displays.

<variable declarations II>=
    double DataFieldValue; 
    private Label title;   
    TextField datafield;

Used above.

The contructor has two parameters: a String which is the label that the Label will display, and double which will be the initial value of variable DataFieldValue and, consequently, it will be displayed by the TextField. In order to proprerly arrange the two Components we use the GridLayout manager with 2 rows and 1 column.

<DataField constructor>=
    DataField (String LabelTitle, double initValue)
    {
        setLayout(new GridLayout(2,1,2,2));
        title = new Label(LabelTitle, Label.CENTER);
        add(title);
        datafield = new TextField(Double.toString(initValue), 25);
        datafield.setEditable(false);
        add(datafield);
        DataFieldValue = initValue;
    }

Used above.

Method setText is used when we want to change the displayed on the TextField. As a ``side effect'' we change the value of variable DataFieldValue.

<method setText>=
    void setDataField (double d)
    {
        datafield.setText(Double.toString(d));
        DataFieldValue = d;
    }

Used above.

We turn now our attention to the definition of class MCanvas, which is, sort to say, the kernel of this applet. However, I will not make any attempt to describe the algorithm that draws the Mandelbrot set. However, it is important to say that the user screen is 500 pixels wide and 500 pixels high. Most variables are related to the algorithm that performs the calculations.

<class MCanvas>=
class MCanvas extends Canvas implements MouseListener, MouseMotionListener
{
    final int XScreen = 500;
    final int YScreen = 500;
    final double overflow = 1.0e100;
    private double DeltaP, DeltaQ;      
    private int iter;
    private double Pmin, Pmax, Qmax, Qmin;
    double pmin, pmax, qmin, qmax;
    Dimension minSize;
    
    <MCanvas constructor>
    <set height and width of canvas>
    <handle mouse events>
    <draw Mandelbrot set>
}

Used above.

When we create an instance of class MCanvas we pass as parameters the dimensions related to the "subset" of the M-set. The user can specify a subset of the M-set by drawing a rectangle on the screen. Currently the rectangle is invisible, but the as the user moves the mouse with the left button pressed, he can observe the current position of the cursor on the screen. The mouse events are handled in two levels: at the top level we just print the positions and the low level we calculate the positions. We have covered the way we print the cursor positions, here, among othewrs, we will cover the way we make these calculations.

<MCanvas constructor>=
    public MCanvas(double pmin, double pmax, double qmin, double qmax)
    {  
        super();
        this.pmin = Pmin = pmin;
        this.pmax = Pmax = pmax;
        this.qmin = Qmin = qmin;
        this.qmax = Qmax = qmax;
        minSize = new Dimension(XScreen, YScreen);
        addMouseListener(this);
        addMouseMotionListener(this); 
    }

Used above.

Setting the dimensions is standard Java.

<set height and width of canvas>=
    public Dimension getPreferredSize()
    {    
       return getMinimumSize();
    }

    public synchronized Dimension getMinimumSize()
    {
       return minSize;
    }

Used above.

In order to determine the position of the cursor while it is moving on the canvas we must use a trick: Initially we get the integer position by using methods getY() and getX() of the stantard java.awt.event.MouseEvent class. Then we must make sure that the position lies in the canvas. Next we determine the real position by using the relation Xr=Xmin + DeltaX*X, where Xmin is the minimum real value of X, and DeltaX is equal to Xmax minus Xmin devided by the number of pixels in the X-direction.

<handle mouse events>=
    public void mouseClicked(MouseEvent event)
    { }
    
    public void mousePressed(MouseEvent event)
    {  
       int x=event.getX(), y=event.getY(), X, Y;
  
       X = (x > (XScreen - 1)) ? (XScreen - 1) : ((x < 0) ? 0 : x);
       Y = (y > (YScreen - 1)) ? (YScreen - 1) : ((y < 0) ? 0 : y);
       pmin = Pmin + DeltaP*X;
       qmax = Qmax - DeltaQ*(YScreen-1-Y);
    }

    public void mouseEntered(MouseEvent event)
    { }

    public void mouseExited(MouseEvent event)
    { }

    public void mouseReleased(MouseEvent event)
    {
       int x=event.getX(), y=event.getY(), X, Y;
       double temp;
   
       X = (x > (XScreen - 1)) ? (XScreen - 1) : ((x < 0) ? 0 : x);
       Y = (y > (YScreen - 1)) ? (YScreen - 1) : ((y < 0) ? 0 : y);
       pmax = Pmin + DeltaP*X;
       qmin = Qmin + DeltaQ*(YScreen - 1 - Y);
       if (qmin > qmax )
       {
         temp = qmin;
         qmin = qmax;
         qmax = temp;
       }
       if (pmin > pmax )
       {
         temp = pmin;
         pmin = pmax;
         pmax = temp;
       }
    }
     
    public void mouseDragged(MouseEvent event)
    {
       int x=event.getX(), y=event.getY(), X, Y;
       double temp;

       X = (x > (XScreen - 1)) ? (XScreen - 1) : ((x < 0) ? 0 : x);
       Y = (y > (YScreen - 1)) ? (YScreen - 1) : ((y < 0) ? 0 : y);
       pmax = Pmin + DeltaP*X;
       qmin = Qmin + DeltaQ*(YScreen - 1 - Y);
       if (qmin > qmax )
       {
         temp = qmin;
         qmin = qmax;
         qmax = temp;
       }
       if (pmin > pmax )
       {
         temp = pmin;
         pmin = pmax;
         pmax = temp;
       }       
    }   

    public void mouseMoved(MouseEvent event)
    { }

Used above.

As we said above previously we will not explain at all the way we draw the M-Set.

<draw Mandelbrot set>=
    Color colour(int a)
    {
       Color paintingColor = Color.white;
       switch (a % 12)
         {
         case 0: paintingColor = Color.blue; 
                 break; 
         case 1: paintingColor = Color.cyan;
                 break; 
         case 2: paintingColor = Color.darkGray;
                 break; 
         case 3: paintingColor = Color.gray;
                 break; 
         case 4: paintingColor = Color.green;
                 break; 
         case 5: paintingColor = Color.lightGray;
                 break; 
         case 6: paintingColor = Color.magenta;
                 break; 
         case 7: paintingColor = Color.orange;
                 break;
         case 8: paintingColor = Color.pink;
                 break;
         case 9: paintingColor = Color.red;
                 break;
        case 10: paintingColor = Color.white;
                 break;
        case 11: paintingColor = Color.yellow;
         }
     return paintingColor;
    }

    double MSetDist(double cx, double cy)
    {
        final int MaxIterations = 100;
        final double huge = 1000.0;
        int i; 
        double[] xorbit = new double[MaxIterations];
        double[] yorbit = new double[MaxIterations];
        boolean flag;
        double dist, temp;
        double xder, yder, x2, y2;
        double x, y;
        /* initializations */

        x = y = 0.0;
        dist = 0.0;
        xorbit[0] = yorbit[0] = 0.0;
        x2 = y2 = 0.0;
        iter = 1;

        while ((iter < MaxIterations) && ((x2+y2) < huge))
        {
           temp=x2-y2+cx;
           y=2.0*x*y+cy;
           x=temp;
           x2=x*x;
           y2=y*y;
           xorbit[iter]=x;
           yorbit[iter]=y;
           iter++;
        }
        if ((x2+y2) > huge)
        {
           xder=0.0;
           yder=0.0;
           i=0;
           flag=false;
           while ((i < iter) && (! flag))
           {
              temp=2.0*(xorbit[i]*xder-yorbit[i]*yder+1.0);
              yder=2.0*(yorbit[i]*xder+xorbit[i]*yder);
              xder=temp;
              flag=Math.max(Math.abs(xder), Math.abs(yder)) > overflow;
              i++;
           }
           if (! flag)
           {
              dist= Math.log(x2+y2)*Math.sqrt(x2+y2)/
                    Math.sqrt(xder*xder+yder*yder);
           }
        }
        return dist;
    }    
         
    public void paint(Graphics g)
    {    
        double x, y;
        int iy, iynew;
        int irad;
        double D, Dnew;
        int np;

        Pmin = pmin;
        Pmax = pmax;
        Qmin = qmin;
        Qmax = qmax;
        DeltaP=(pmax-pmin)/(XScreen - 1);
        DeltaQ=(qmin-qmax)/(YScreen -1);
        for (np=0; np < (XScreen - 1); np++)
        { 
            iy=0;
            x=pmin+DeltaP*np;
            D=MSetDist(x,qmin);
            while (iy < (YScreen - 1))
            {  
               iynew=iy+(int)(Math.floor(Math.max(1.0, Math.min(20.0, D))));
               y=iynew*DeltaQ+qmax;
               Dnew=MSetDist(x,y);
               if (D <= 0.0)
                  g.setColor(Color.black);
               else
                  g.setColor(colour(iter));
               g.drawLine(np, iy, np, iynew);
               iy=iynew;
               D=Dnew;
            }
        }
    }
Used above.