Java Fun and Games: Explore the geometry of nature

news
Jun 19, 200722 mins

Journey into the realm of fractals

Fractals entertain and are fun to explore, as evidenced by this article’s fractal applets. Learn how to use math-based fractals that imitate nature’s geometry to enhance your Java games.

Euclidean geometry emphasizes circles, cubes and other simple shapes. This geometry is seen in buildings and other man-made objects. In contrast, the geometry of nature is more complex, but it does exhibit the property of self-similarity: Natural objects (such as clouds and coastlines) look similar at different levels. This self-similarity is also evidenced in mathematics, especially when working with complex numbers.

In 1975, IBM researcher Benoît Mandelbrot invented the term fractal, “a rough or fragmented geometric shape that can be subdivided in parts, each of which is (at least approximately) a reduced-size copy of the whole,” to describe natural and math-based self-similarity. Mandelbrot also discovered what is regarded to be the most famous math-based fractal, the Mandelbrot set.

This article takes you on a tour of math-based fractals, including the Mandelbrot set. You will examine algorithms for recursively generating fractals and play with applets that implement the algorithms. One of the fractals demonstrated imitates nature’s geometry by generating a mountain ridgeline. Perhaps you will use this fractal to create background terrain for one of your Java games.

Fractal generator and animator

I have created an infrastructure for generating and animating most of the fractals presented in this article. Before touring the fractals and their applets, you should have a basic understanding of the infrastructure. Let’s begin with the fractal generator, which I’ve chosen to describe via the FractalGenerator interface in Listing 1.

Listing 1. The FractalGenerator interface

interface FractalGenerator
{
      public void generate (Graphics2D g, int depth, int width, int height);

      public int maxDepth ();
}

The methods of the FractalGenerator interface assume that a fractal is recursively generated: they are invoked by a fractal animator when it is time to generate the fractal at a new depth. The depth ranges from 0 to a maximum positive integer (0 ends the recursion). At this point, a java.awt.Graphics2D object’s methods render the fractal in a rectangular area bounded by the upper-left-corner (0, 0) and dimensions width and height.

After creating an object from a class that implements FractalGenerator, a program connects this object to the fractal animator, which generates the fractal to a specific depth for each frame of the fractal’s animation. As you see in Listing 2, I have chosen to describe the fractal animator via FractalAnimator, a subclass of the javax.swing.JPanel container class:

Listing 2. FractalAnimator, a subclass of javax.swing.JPanel

class FractalAnimator extends JPanel {
     final static int DEFAULT_DELAY = 500;

     final static int MAX_DELAY = 1000;

     final static int MIN_DELAY = 100;

     final static int STEP = 100;

     private final static Color PANEL_COLOR = new Color (255, 255, 225);

     private FractalGenerator fg;

     private volatile int depth, ms = DEFAULT_DELAY;

     private volatile Thread animThd;

     FractalAnimator (FractalGenerator fg)
     {
        this.fg = fg;
     }

     public void paintComponent (Graphics g)
     {
        Graphics2D g2d = (Graphics2D) g.create ();

        Insets insets = getInsets ();
        g2d.translate (insets.left, insets.top);

        int width = getWidth ()-insets.left-insets.right;
        int height = getHeight ()-insets.top-insets.bottom;

        g2d.setColor (PANEL_COLOR);
        g2d.fillRect (0, 0, width, height);

        g2d.setColor (Color.black);
        fg.generate (g2d, depth, width, height);

        g2d.dispose ();
     }

     void setDelay (int ms)
     {
        if (ms < MIN_DELAY)
            throw new IllegalArgumentException (ms+" < MIN_DELAY");

        if (ms > MAX_DELAY)
            throw new IllegalArgumentException (ms+" > MAX_DELAY");

        this.ms = ms;
     }

     void start ()
     {
        animThd = new Thread (new Runnable ()
                              {
                                  public void run ()
                                  {
                                     depth = -1;

                                     Thread currThd = Thread.currentThread ();
                                     while (currThd == animThd)
                                     {
                                        if (++depth > fg.maxDepth ())
                                            depth = 0;

                                        repaint ();

                                        try
                                        {
                                            Thread.sleep (ms);
                                        }
                                        catch (InterruptedException e)
                                        {
                                        }
                                     }
                                  }
                              });
        animThd.start ();
     }

     void stop ()
     {
        animThd = null;
     }
}

The FractalAnimator class presents a simple API that includes a constructor and three additional methods. First, the constructor saves its fractal-generator argument. The void setDelay(int ms) method then specifies a milliseconds delay that inversely determines the animation frame rate (higher delay, slower rate). Finally, the void start() and void stop() methods initiate and terminate the animation.

The API also includes constants DEFAULT_DELAY, MIN_DELAY, MAX_DELAY, and STEP, which can be conveniently used with a GUI component (such as a slider) to let the user determine animation speed. An IllegalArgumentException is thrown for any value passed to setDelay() that lies outside the MIN_DELAY/MAX_DELAY range.

Caution!
For convenience, I initialize depth to -1 at the beginning of animThd‘s run() method. Although depth will not normally contain -1 when the component is repainted, it could happen. You should therefore treat any depth value less than or equal to 0 as the fractal recursion’s stopping condition.

A standardized GUI for fractal applets

Although it is possible to place the FractalGenerator interface and FractalAnimator class into their own package (they would need to be marked public), I have not done so — consider this an exercise. Instead, I’ve embedded them into fractal applets. Each applet’s public void createGUI() method creates the fractal generator and fractal animator, and integrates the fractal animator into the applet’s GUI, as Listing 3 shows.

Listing 3. public void createGUI() creates the fractal infrastructure

private void createGUI ()
{
      getContentPane ().setLayout (new GridBagLayout ());

      final FractalAnimator fa;
      fa = new FractalAnimator (new FractalGenerator ());
      fa.setBorder (BorderFactory.createEtchedBorder ());
      int size = Math.min (getWidth ()/2, getHeight ()/2);
      fa.setPreferredSize (new Dimension ((int) (size*1.15), size));

      GridBagConstraints gbc = new GridBagConstraints ();
      gbc.gridx = 0;
      gbc.gridy = 0;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridheight = 1;
      getContentPane ().add (fa, gbc);

      JLabel lbl = new JLabel ("Delay (milliseconds)");

      gbc = new GridBagConstraints ();
      gbc.gridx = 0;
      gbc.gridy = 1;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridheight = 1;
      gbc.insets = new Insets (10, 10, 10, 10);
      getContentPane ().add (lbl, gbc);

      sliderMS = new JSlider (JSlider.HORIZONTAL,
                              FractalAnimator.MIN_DELAY,
                              FractalAnimator.MAX_DELAY,
                              FractalAnimator.DEFAULT_DELAY);
      sliderMS.setMinorTickSpacing (FractalAnimator.STEP/2);
      sliderMS.setMajorTickSpacing (FractalAnimator.STEP);
      sliderMS.setPaintTicks (true);
      sliderMS.setPaintLabels (true);
      sliderMS.setLabelTable (sliderMS.createStandardLabels (FractalAnimator.STEP));
      Dimension prefSize = sliderMS.getPreferredSize ();
      prefSize.width += prefSize.width/2;
      sliderMS.setPreferredSize (prefSize);
      ChangeListener cl;
      cl = new ChangeListener ()
           {
               public void stateChanged (ChangeEvent e)
               {
                  fa.setDelay (sliderMS.getValue ());
               }
           };
      sliderMS.addChangeListener (cl);

      gbc = new GridBagConstraints ();
      gbc.gridx = 0;
      gbc.gridy = 2;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridheight = 1;
      gbc.insets = new Insets (10, 10, 10, 10);
      getContentPane ().add (sliderMS, gbc);

      JPanel pnl = new JPanel ();
      btnAnimate = new JButton ("Animate");
      ActionListener al;
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent e)
               {
                  fa.start ();
                  btnAnimate.setEnabled (false);
                  btnStop.setEnabled (true);
                  sliderMS.setEnabled (false);
               }
           };
      btnAnimate.addActionListener (al);
      pnl.add (btnAnimate);
      btnStop = new JButton ("Stop");
      btnStop.setEnabled (false);
      al = new ActionListener ()
           {
               public void actionPerformed (ActionEvent e)
               {
                  fa.stop ();
                  btnAnimate.setEnabled (true);
                  btnStop.setEnabled (false);
                  sliderMS.setEnabled (true);
               }
           };
      btnStop.addActionListener (al);
      pnl.add (btnStop);

      gbc = new GridBagConstraints ();
      gbc.gridx = 0;
      gbc.gridy = 3;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridheight = 1;
      getContentPane ().add (pnl, gbc);
}

The createGUI() method uses the java.awt.GridBagLayout class and its java.awt.GridBagConstraints support class to lay out the GUI. After installing this layout manager, createGUI() creates a FractalAnimator, passing an object whose class implements FractalGenerator to the constructor. Figure 1 presents the GUI.

You might have noticed an oddity in the createGUI() source code. Specifically, I have included an (int) (size*1.15) expression to calculate the width of the fractal animator component. I did this to compensate for an equilateral triangle not looking equilateral on my LCD screen (at a resolution of 1024 by 768 pixels). You might need to adjust this expression for your display.

Koch snowflake

In 1904, Swedish mathematician Helge von Koch presented a paper that included a fractal curve known as the Koch snowflake (also known as the Koch star). The following algorithm recursively generates this fractal, which is based on an equilateral triangle:

  1. For each of the equilateral triangle’s line segments, divide the line segment into three equal-length line segments.
  2. For each middle line segment, generate a smaller equilateral triangle whose base line segment replaces the middle line segment.
  3. Remove the base line segment from the previous step’s equilateral triangle. Repeat steps 1 through 3 with this new equilateral triangle.

As you recursively repeat the above steps, the equilateral triangle morphs into a snowflake. Figure 2 reveals the original triangle on the left, the fractal after one iteration in the middle, and the fractal after two iterations on the right.

Figure 2. Each side of the equilateral triangle approaches a limit known as the Koch curve. Click the thumbnail to view a full-sized image.

The Koch snowflake generator algorithm is described by a KSFractalGenerator class. Listing 4 is an excerpt of the KS applet. (See the Resources section for the Koch Snowflake applet, KS.java and KS.html.)

Listing 4. An excerpt of the Koch Snowflake applet

class KSFractalGenerator implements FractalGenerator {
     private final static double SIN60 = Math.sin (Math.PI/3.0);

     private final static int MAX_DEPTH = 4;

     private final static int OFFSET = 18;

     private Line2D line = new Line2D.Double (0.0, 0.0, 0.0, 0.0);

     private Stroke stroke = new BasicStroke (2.0f, BasicStroke.CAP_ROUND,
                                              BasicStroke.JOIN_ROUND);

     public void generate (Graphics2D g, int depth, int width, int
height)
     {
        // Precalculate height/5, width/2, and width/5 for speed.

        double h5 = height/5.0;
        double w2 = width/2.0;
        double w5 = width/5.0;

        ks (w2, h5-OFFSET, w5, 4.0*h5-OFFSET, depth, g); // left side
        ks (w5, 4.0*h5-OFFSET, 4.0*w5, 4.0*h5-OFFSET, depth, g); // bottom side
        ks (4.0*w5, 4.0*h5-OFFSET, w2, h5-OFFSET, depth, g); // right side
     }

     public int maxDepth ()
     {
        return MAX_DEPTH;
     }

     private void ks (double x1, double y1, double x2, double y2, int depth,
                      Graphics2D g)
     {
        if (depth <= 0)
        {
            g.setStroke (stroke);
            line.setLine (x1, y1, x2, y2);
            g.draw (line);
        }
        else
        {
            double x4 = x1*2.0/3.0+x2/3.0;
            double y4 = y1*2.0/3.0+y2/3.0;
            double x5 = x1/3.0+x2*2.0/3.0;
            double y5 = y1/3.0+y2*2.0/3.0;
            double x6 = (x4+x5)/2.0+(y4-y5)*SIN60;
            double y6 = (y4+y5)/2.0+(x5-x4)*SIN60;

            ks (x1, y1, x4, y4, depth-1, g);
            ks (x4, y4, x6, y6, depth-1, g);
            ks (x6, y6, x5, y5, depth-1, g);
            ks (x5, y5, x2, y2, depth-1, g);
        }
     }
}

The generate() method chooses the original triangle’s endpoints so that they are relative to the drawing area’s boundaries. It makes three calls to the private ks() method, to calculate the middle line segment on each side of the triangle. These calculations are somewhat tricky to grasp without visualizing them, so take a look at Figure 3.

After studying Figure 3, you should be able to understand a.x = A.x-(A.x-B.x)/3 (equivalent to double x4 = x1*2/3+x2*1/3;), a.y = A.y-(A.y-B.y)/3 (equivalent to double y4 = y1*2/3+y2*1/3;), b.x = A.x-2*(A.x-B.x)/3 (equivalent to double x5 = x1*1/3+x2*2/3;), and b.y = A.y-2*(A.y-B.y)/3 (equivalent to double y5 = y1*1/3+y2*2/3;).

For double x6 = (x4+x5)/2+(y4-y5)*SIN60; and double y6 = (y4+y5)/2+(x5-x4)*SIN60;, (x4+x5)/2 and (y4+y5)/2 identify the removed middle line segment’s midpoint, and (y4-y5)*SIN60 and (x5-x4)*SIN60 move the midpoint along an imaginary line perpendicular to the midpoint. Note that the SIN60 multiplication scales the line to prevent an exaggerated size in the resulting equilateral triangle.

Sierpinski triangle

In 1915, Polish mathematician Waclaw Sierpinski described a fractal that is an example of a self-similar set. This fractal is known as the Sierpinski triangle, but is also referred to as the Sierpinski gasket. The algorithm below recursively generates the Sierpinski triangle:

  1. Start with an equilateral triangle whose base is parallel to the horizontal axis. (In practice, any triangle with any orientation will work.)
  2. Shrink the triangle by 50%, make three copies, and translate the copies so that each triangle touches the other two triangles at a corner.
  3. Assume that each shrunken copy is the original equilateral triangle and apply the previous steps to each copy.

As you recursively repeat the above steps, the equilateral triangle morphs into a set of nested triangles. Figure 4 reveals the original equilateral triangle on the left, the fractal after a single iteration in the middle, and the fractal after two iterations on the right.

Figure 4. The area of the Sierpinski triangle tends to zero as the iterations tend to infinity — the triangular area between the three triangles (see the algorithm’s second step) is not part of the Sierpinski triangle’s area. Click the thumbnail to view a full-sized image.

The Sierpinski triangle generator algorithm is described by an STFractalGenerator class. Listing 5 is an excerpt of the class. (See the Resources section for the Sierpinski Triangle applet, ST.java and ST.html.)

Listing 5. The STFractalGenerator class

 
class STFractalGenerator implements FractalGenerator {
     private final static int MAX_DEPTH = 5;

     private final static int OFFSET = 10;

     private Line2D line = new Line2D.Double (0.0, 0.0, 0.0, 0.0);

     private Stroke stroke = new BasicStroke (2.0f, BasicStroke.CAP_ROUND,
                                              BasicStroke.JOIN_ROUND);

     public void generate (Graphics2D g, int depth, int width, int
height)
     {
        st (width/2, OFFSET, OFFSET, height-1-OFFSET, width-1-OFFSET,
            height-1-OFFSET, depth, g);
     }

     public int maxDepth ()
     {
        return MAX_DEPTH;
     }

     private void st (int x1, int y1, int x2, int y2, int x3, int y3, int depth,
                      Graphics2D g)
     {
        if (depth <= 0)
        {
            g.setStroke (stroke);
            line.setLine (x1, y1, x2, y2);
            g.draw (line);
            line.setLine (x2, y2, x3, y3);
            g.draw (line);
            line.setLine (x3, y3, x1, y1);
            g.draw (line);
        }
        else
        {
            int x4 = (x1+x2)/2;
            int y4 = (y1+y2)/2;
            int x5 = (x2+x3)/2;
            int y5 = (y2+y3)/2;
            int x6 = (x1+x3)/2;
            int y6 = (y1+y3)/2;

            st (x1, y1, x4, y4, x6, y6, depth-1, g);
            st (x4, y4, x2, y2, x5, y5, depth-1, g);
            st (x6, y6, x5, y5, x3, y3, depth-1, g);
        }
     }
}

The generate() method chooses the original triangle’s endpoints so that the triangle occupies the entire display area except for a small border. It makes a single call to the private st() method, to calculate the midpoint of each triangle line segment. The coordinates of these midpoints are combined with st()‘s coordinate arguments to form three new triangles.

Mandelbrot set

In 1979, Polish mathematician Benoît Mandelbrot began to study a class of fractals known as Julia sets. During this study, he discovered a fractal that has since been named after him. The Mandelbrot set fractal is defined as the set of all complex numbers c such that iterating z = z2 + c (where z is also a complex number) does not escape to infinity.

Complex numbers
A complex number is a number derived from the complex plane. The complex plane is a two-dimensional area of points where the x coordinate consists of a real number and the y coordinate consists of an imaginary number — a number multiplied by the square root of -1, known as i. Examples: 2 + 3i and -3 – 6i. A real number such as 7 is a complex number whose imaginary component is 0i. Similarly, an imaginary number such as 5i is a complex number whose real component is 0.

Use the following algorithm to recursively generate the Mandelbrot set:

  1. For each point c on the complex plane, repeatedly evaluate z = z2 + c, where z is initially 0.
  2. Determine the distance between the new z and the set’s 0 + 0i origin by squaring z‘s real component, squaring z‘s imaginary component, adding both squares together, and taking the square root of the sum.
  3. If the distance exceeds 2, the point does not lie in the set and the loop can end (for this point). Otherwise, keep iterating for at least 100 times. When the last iteration completes for a given point, and the distance between the point and the set’s origin is still less than 2, display the point at the corresponding (x, y) location on the screen.

To see what the set looks like, check out Figure 5.

In Figure 5, all points within the set are colored black; all points not within the set are colored white. The x axis serves as both the real number axis in the complex plane (with a minimum of -2.5 and a maximum of 1.5) and the horizontal pixel axis on the screen. The y axis serves as both the imaginary axis in the complex plane (with a minimum of -1.5 and a maximum of 1.5) and the vertical pixel axis on the screen.

Figure 5 does not indicate how many iterations were required to determine that a point is not in the Mandelbrot set. An iteration-based picture is achieved by assigning a non-black color to each iteration, and then displaying a point in this color when the point is found to not be in the set. All points with the same color require the same number of iterations to determine that they are not in the set, as Figure 6 illustrates.

Colors indicate the number of iterations to discover that a point is not in the Mandelbrot set.
Figure 6. Colors indicate the number of iterations to discover that a point is not in the Mandelbrot set.

An applet for the Mandelbrot set

I have created an applet that lets you explore the Mandelbrot set. (See the Resources section for MS.java and MS.html.) Along with the main MS class, this Swing-based applet contains MSPane, a subclass of JPanel. MSPane describes a component that presents the Mandelbrot set via its public void paintComponent(Graphics g) method, as shown in Listing 6.

Listing 6. An excerpt of the Mandelbrot Set applet

public void paintComponent (Graphics g)
{
     // The following increments make it possible to accurately map
     // floating-point numbers in the complex plane to integers on this
     // component's display surface.

     double imInc = (imMax-imMin)/getHeight ();
     double reInc = (reMax-reMin)/getWidth ();

     double dist = 0.0, im, re, tim, tre, zim, zre;

     int iter, x, y;

     // Generate Mandelbrot set. Each (re, im) combination corresponds
     // to point c on the complex plane.

     for (im = imMin, y = 0; im <= imMax; im += imInc, y++)
          for (re = reMin, x = 0; re <= reMax; re += reInc, x++)
          {
               // The (zre, zim) combination corresponds to complex
               // number z.

               zim = 0.0;
               zre = 0.0;

               for (iter = 1; iter < palette.length; iter++)
               {
                    // Calculate z*z.

                    tim = (zre+zre)*zim;
                    tre = zre*zre-zim*zim;

                    // Add c to z*z.

                    zim = tim+im;
                    zre = tre+re;

                    // Calculate distance squared.

                    dist = zre*zre+zim*zim;

                    // If distance squared exceeds 4.0, point is outside
                    // Mandelbrot set, so color point to a non-black
color.

                    if (dist > 4.0)
                    {
                        g.setColor (palette [iter]);
                        g.drawLine (x, y, x, y);
                        break;
                    }
               }

               // If distance squared is less than or equal to 4.0, point
               // is inside Mandelbrot set, so color point black.

               if (dist <= 4.0)
               {
                   g.setColor (Color.black);
                   g.drawLine (x, y, x, y);
               }
          }
}

I’ve included extensive comments in the above method, so I won’t bother to further discuss the source code. One thing to note, however, is that I’ve dispensed with the time-consuming square-root operation, thus speeding up the generation of the Mandelbrot set. I could do this because comparing the square of the distance between a point and the set’s origin with 4 is equivalent to comparing the distance with 2.

Zooming in and out

The MS applet presents a GUI that consists of MSPane and label components. The former component presents the Mandelbrot set at a specific zoom level; the label component displays this zoom level (starting at 0, no zoom) and the mouse coordinates at the time you click the mouse to zoom into the fractal by 50%. These coordinates are handy for creating an animated tour, such as the one revealed in Figure 7.

The logic for zooming into and out of the Mandelbrot set is contained within a pair of anonymous classes that listen for mouse-click and mouse-move events. These listener classes are registered with the MSPane component from within its constructor, as shown in Listing 7.

Listing 7. Registering listener classes with MSPane

MSPane (final JLabel lblStatus)
{
     addMouseListener (new MouseAdapter ()
                       {
                           public void mouseClicked (MouseEvent e)
                           {
                              int x = e.getX ();
                              int y = e.getY ();

                              if (!e.isShiftDown () && level < 31)
                              {
                                  double re =
reMin+x*(reMax-reMin)/getWidth ();
                                  double im =
imMin+y*(imMax-imMin)/getHeight ();

                                  complexNum cn = new complexNum ();
                                  cn.re = re;
                                  cn.im = im;
                                  stack.push (cn);

                                  reMax += re;
                                  reMin += re;
                                  imMax += im;
                                  imMin += im;
                                  reMax /= 2.0;
                                  reMin /= 2.0;
                                  imMax /= 2.0;
                                  imMin /= 2.0;

                                  level++;
                                  lblStatus.setText ("Level: "+level+
                                                     ", x: "+x+", y:  
"+y);
                              }
                              else
                              if (e.isShiftDown () && level != 0)
                              {
                                  complexNum cn = (complexNum) stack.pop
();
                                  double re = cn.re;
                                  double im = cn.im;

                                  reMax *= 2.0;
                                  reMin *= 2.0;
                                  imMax *= 2.0;
                                  imMin *= 2.0;
                                  reMax -= re;
                                  reMin -= re;
                                  imMax -= im;
                                  imMin -= im;

                                  level--;
                                  lblStatus.setText ("Level: "+level+
                                                     ", x: "+x+", y:  
"+y);
                              }

                              repaint ();
                           }
                       });

     addMouseMotionListener (new MouseMotionAdapter ()
                             {
                                 public void mouseMoved (MouseEvent e)
                                 {
                                    int x = e.getX ();
                                    int y = e.getY ();
                                    lblStatus.setText ("Level: "+level+
                                                       ", x: "+x+", y:
"+y);
                                 }
                             });
}

The zoom-in effect is accomplished by dividing the values in the reMax, reMin, imMax and imMin variables by 2. In contrast, zoom out multiplies these values by 2. Prior to the zoom in, and after the zoom out, these values are translated by the complex equivalent of the component location that was clicked. Doing this keeps the clicked area visible at the next zoom level.

The zoom-in logic is guarded by an expression, and the same is true for the zoom-out logic. The level < 31 expression prevents a program crash that arises when the increments (calculated in paintComponent()) become too small. The level != 0 expression prevents zooming out of the starting level (level 0), which results in an exception arising from an empty stack (which I use to remember clicked locations for zoom out).

Fractal trivia
Interesting facts about the Mandelbrot set abound. Consider the following:
  • Its area is unknown (but is quite small)
  • Its border is an infinitely-long fractal.
  • The barnacle-covered pear shape occurs an infinite number of times.
  • All of the black areas are connected.
  • It can be used to (inefficiently) calculate Pi.
  • No color intersects another color.
  • It has infinite detail — you can zoom in forever.
Unfortunately, my applet (as currently written) does not let you continually zoom into the Mandelbrot set. How would you solve this problem?

Mountain ridgeline

Fractals play a significant role in today’s terrain-generation software. Whether you are looking at a mountain or a cloud, chances are that the object is fractal based. For example, a Mountain ridgeline can be generated by a fractal that is similar to the Koch snowflake. My Mountain ridgeline fractal relies on the midpoint displacement algorithm, which begins with a horizontal line segment:

  1. Calculate the line segment’s midpoint.
  2. Move the midpoint up or down by a randomly selected distance.
  3. At the midpoint, divide the line segment into two line segments. Repeat all three steps for each line segment.

Figure 8 reveals the midpoint displacement algorithm at work. The original line segment appears at the top of the figure and the middle part shows the algorithm after one iteration (depth 1 recursion). Note that the midpoint has been displaced upwards, and the bottom part (depth 2 recursion) displaces one midpoint up and displaces the other midpoint down.

As with the algorithms for generating the Koch snowflake and Sierpinski triangle, the generator algorithm for the Mountain ridgeline fractal is described by a FractalGenerator class — MRFractalGenerator. The MRFractalGenerator class in Listing 8 is excerpted from the Mountain Ridgeline applet. (See the Resources section for MR.java and MR.html.)

Listing 8. The MRFractalGenerator class

class MRFractalGenerator implements FractalGenerator
{
     private final static int MAX_DEPTH = 8;

     private Color violet = new Color (191, 64, 251);

     private int [] heights;

     public void generate (Graphics2D g, int depth, int width, int  
height)
     {
        g.setPaint (new GradientPaint (0, 0, violet, 0, height,
Color.yellow));
        g.fill (new Rectangle (0, 0, width, height));

        if (depth == MAX_DEPTH)
        {
            heights = new int [width];
            for (int i = 0; i <heights.length; i++)
                 heights [i] = height/2;
        }

        g.setColor (Color.black);
        mr (0, height/2, width-1, height/2, depth, g);

        if (depth == MAX_DEPTH)
        {
            for (int i = 0; i < width; i++)
                 g.drawLine (i, heights [i], i, height-1);
            heights = null;
        }
     }

     public int maxDepth ()
     {
        return MAX_DEPTH;
     }

     private void mr (int x1, int y1, int x2, int y2, int depth,
Graphics2D g)
     {
        if (depth <= 0 || x1 == x2)
        {
            g.drawLine (x1, y1, x2, y2);
            return;
        }

        int midx = (x1+x2)/2;
        int midy = (y1+y2)/2;

        int len = (int) Math.sqrt ((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));

        if (rnd (2) == 0)
            midy -= rnd (x2-x1)/3;
        else
            midy += rnd (x2-x1)/3;

        if (heights != null)
            heights [midx] = midy;

        mr (x1, y1, midx, midy, depth-1, g);
        mr (midx, midy, x2, y2, depth-1, g);
     }

     private int rnd (int limit)
     {
        return (int) (Math.random ()*limit);
     }
}

Let’s review this class. For starters, I predefine the Color object violet for use in a violet-to-yellow gradient that simulates a sunrise or sunset. I also predefine a heights array variable to store integers that help to present the mountain ridge in black silhouette (and in front of the sunrise/sunset), but only after the maximum depth has been reached. Figure 9 reveals the result.

The recursive mr() method generates the fractal at a specific depth. If the depth is 0 or the line segment’s horizontal coordinates are identical (either serves as the recursion-stopping condition), this method draws the line segment and begins to unwind the recursion. Otherwise, mr() calculates the line segment’s midpoint and displaces the midpoint by one third of the segment’s length — and so the recursion continues.

Calculating maximum depth
The MR.html file specifies 360 as the applet’s width. Because this results in the fractal animator component having a width of 202, 8 is the lowest possible value for the MAX_DEPTH constant. When the maximum depth is less than 8, vertical streaks appear in or above the black silhouette because not all of the entries in the heights array have been set to the actual heights of the silhouette — the heights entry at index 201 never changes from its height/2 default.

Conclusion

In addition to their entertainment value, fractals have practical uses. For example, this article showed how you might use fractal algorithms to develop the graphical interface for computer games. You can also use fractals for common application use cases like image compression and music generation. Researchers are also using fractals in the fields of seismology and human biology (where they aid in the understanding of biological systems that exhibit fractal properties — such as the lungs and bronchial system). See the Resources section to learn more about fractals and their uses.

Friesen is a freelance software developer and educator who specializes in Java technology. Check out his Website to discover all of his published Java articles and more.