5
\$\begingroup\$

The purpose of this project is to generate an interactive Mandelbrot Set with smooth coloring. The user can interact with the W, A, S, D keys to move around and with + and - to zoom in and out. It divides the height of the window into multiple sections and uses threads to paint each section. It also paints on a BufferedImage first, and then the 2D Graphics paint the JPanel.

I would like to know what improvements could be made to boost its performance and code readability. Respecting OOP principles is also something I'm interested in.

MandelbrotSet.java

package com.flak231;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;

public class MandelbrotSet extends JFrame implements KeyListener{
    protected static final int IMAGE_WIDTH = 1080;
    protected static final int IMAGE_HEIGHT = 900;
    private static final double ZOOM_FACTOR = 0.9;
    private static final int OFFSET_FACTOR = 40;
    private Image image;
    private double offsetX;
    private double offsetY;
    private double zoom;
    private boolean stateChanged = true;

    MandelbrotSet(String title){
        super(title);
        image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        offsetX = -0.7;
        offsetY = 0.0;
        zoom = 0.004;
        addKeyListener(this);
    }

    @Override
    public void keyTyped(KeyEvent e) {
        // Nothing here yet
    }

    @Override
    public void keyPressed(KeyEvent e) {
        stateChanged = true;
        if(e.getKeyCode() == KeyEvent.VK_W) {
            offsetY -= OFFSET_FACTOR * zoom;
            repaint();
        }
        if(e.getKeyCode() == KeyEvent.VK_A) {
            offsetX -= OFFSET_FACTOR * zoom;
            repaint();
        }
        if(e.getKeyCode() == KeyEvent.VK_S) {
            offsetY += OFFSET_FACTOR * zoom;
            repaint();
        }
        if(e.getKeyCode() == KeyEvent.VK_D) {
            offsetX += OFFSET_FACTOR * zoom;
            repaint();
        }
        if(e.getKeyCode() == KeyEvent.VK_EQUALS) {
            zoom *= ZOOM_FACTOR;
            repaint();
        }
        if(e.getKeyCode() == KeyEvent.VK_MINUS) {
            zoom /= ZOOM_FACTOR;
            repaint();
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        // Nothing here yet
    }

    private static void createAndShowGUI() {
        MandelbrotSet ms = new MandelbrotSet("Mandelbrot Set");
        ms.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        ms.getContentPane().add(ms.new MandelbrotPane());
        ms.setSize(IMAGE_WIDTH, IMAGE_HEIGHT);
        ms.setResizable(false);
        ms.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI(); 
            }
        });
    }

    private class MandelbrotPane extends JPanel{

        private void updateImage() {
            int numberOfThreads = 100;
            int step = IMAGE_HEIGHT / numberOfThreads;
            List<ImageSlicePainter> threads = new ArrayList<>();
            for(int i = 0; i < IMAGE_HEIGHT; i += step) {
                threads.add(new ImageSlicePainter(zoom, offsetX, offsetY, image, i, Math.min(i + step, IMAGE_HEIGHT)));
            }
            for (ImageSlicePainter imageSlicePainter : threads) {
                imageSlicePainter.start();
            }
            try {
                for (ImageSlicePainter imageSlicePainter : threads) {
                    imageSlicePainter.join();
                }
            }
            catch (InterruptedException e) {
                e.getStackTrace();
            }
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            if(stateChanged) {
                updateImage();
                stateChanged = false;
            }
            g2d.drawImage(image, 0, 0, null);
            g2d.dispose();
        }
    }
}

ImageSlicePainter.java

package com.flak231;

import java.awt.Color;
import java.awt.Image;
import java.awt.image.BufferedImage;

public class ImageSlicePainter extends Thread{
    protected static final int MAX_ITERATIONS = 255;
    private double zoom; 
    private double offsetX;
    private double offsetY;
    private Image image; 
    private int minY;
    private int maxY;
    private Color[] colorSet;

    public ImageSlicePainter(double zoom, double offsetX, double offsetY, Image image, int minY, int maxY){
        this.zoom = zoom;
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        this.image = image;
        this.minY = minY;
        this.maxY = maxY;
        colorSet = new Color[MAX_ITERATIONS + 1];
        //fill the color array with colors generated by taking i into consideration
        for (int i = 0; i <= MAX_ITERATIONS; i++) {
            colorSet[i] = computeColor(i);
        }
    }

    void updateImageSlice() {
        double real = (0 - MandelbrotSet.IMAGE_WIDTH / 2.0) * zoom + offsetX;
        double imagStart = (minY - MandelbrotSet.IMAGE_HEIGHT / 2.0) * zoom + offsetY;
        for (int x = 0; x < MandelbrotSet.IMAGE_WIDTH; x++, real += zoom) {
            double imag = imagStart;
            for (int y = minY; y < maxY; y++, imag += zoom) {
                int value = computeMandelbrot(real, imag);
                ((BufferedImage)image).setRGB(x, y, colorSet[value].getRGB());
            }
        }
    }

    private Color computeColor(int iterations) {
        /*
        To obtain a smooth transition from one color to another, we need to use
        three smooth, continuous functions that will map every number t.
        A slightly modified version of the Bernstein polynomials will do, as they are
        continuous, smooth and have values in the [0, 1) interval.
        Therefore, mapping the results to the range for r, g, b is as easy as multiplying
        each value by 255.
        */
        double t = (double)iterations / (double)MAX_ITERATIONS;
        int r = (int)(9 * (1 - t)*t*t*t * 255);
        int g = (int)(15 * (1 - t)*(1 - t)*t*t * 255);
        int b = (int)(8.5*(1 - t)*(1 - t)*(1 - t)*t * 255);
        return new Color(r, g, b);
    }

    private int computeMandelbrot(double startReal, double startImag) {
        double zReal = startReal;
        double zImag = startImag;

        for (int counter = 0; counter < MAX_ITERATIONS; counter++) {
            double r2 = zReal * zReal;
            double i2 = zImag * zImag;
            if (r2 + i2 > 4.0) {
                return counter;
            }
            zImag = 2.0 * zReal * zImag + startImag;
            zReal = r2 - i2 + startReal;
        }
        return MAX_ITERATIONS;
    }

    @Override
    public void run() {
        updateImageSlice();
    }
}

The beauty of fractals

\$\endgroup\$
4
\$\begingroup\$
private Color[] colorSet;

in public class ImageSlicePainter is an instance property, and the (256) colors are computed each time an instance of ImageSlicePainter is created, that is 100 times (the number of threads) for each image update.

With a static property the colors are computed only once in the runtime of the program:

public class ImageSlicePainter extends Thread {

    // ... 
    private static Color[] colorSet = computeColors();

    private static Color[] computeColors() {
        Color[] colorSet = new Color[MAX_ITERATIONS + 1];
        for (int i = 0; i <= MAX_ITERATIONS; i++) {
            colorSet[i] = computeColor(i);
        }
        return colorSet;
    }

    private static Color computeColor(int iterations) {
        // ...
    }

    // ...
}

An alternative approach to achieve a “smooth” transition between consecutive colors would be to use the “HSB color model” (also called “HSV“), where the “hue”component is used to describe the angular position of the color on a color wheel.

In your case the hue can be chosen as the ratio

 float t = (float)iterations / (float)MAX_ITERATIONS;

plus an optional offset, for example:

private static Color computeColor(int iterations) {
    if (iterations == MAX_ITERATIONS) {
        return Color.black;
    } else {
        float blueHue = 2.0f/3.0f;
        float t = (float)iterations / (float)MAX_ITERATIONS;
        return Color.getHSBColor(t + blueHue, 1.0f, 1.0f);
    }
}
\$\endgroup\$
  • \$\begingroup\$ Yep, having the color set as static probably saved a lot of computation time. \$\endgroup\$ – NeVada May 14 '18 at 0:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.