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();
}
}
1 Answer 1
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);
}
}
-
\$\begingroup\$ Yep, having the color set as static probably saved a lot of computation time. \$\endgroup\$NeVada– NeVada2018年05月14日 00:44:16 +00:00Commented May 14, 2018 at 0:44