Attempting a multi-threaded Mandelbrot Set viewer in Java using Swing graphics. I've seen a few Mandelbrot Set programs and other multi threaded Swing apps that use BufferedImage and/or SwingWorker, but I'm wondering if this approach can work/be redeemed, and if it cannot, why not?
On click (zoom in) and on key press (zoom out) I'm opening threads to calculate color values in quadrants (upper left, upper right, lower left, lower right), and updating these values in private static Color[][] colors = new Color[n][n];
. Then the repaint method is called on the component to draw from the colors
array.
Are these threads executing concurrently with the Event Dispatch Thread, and if so, why aren't these threads occurring ahead of the Event Dispatch Thread Swing component drawing with SwingUtilities.invokeLater
in place?
No error messages, just unpredictable redrawing behavior on click and key press. Thank you.
Mandelbrot.java
import java.awt.Color;
import java.awt.BorderLayout;
import java.awt.Graphics;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Stack;
public class Mandelbrot extends JPanel {
private static double size = 4.0; // complex plane is size units in width and length
private static int n = 1000; // window is n x n in pixels
private static int max = 255; // iterate through hex color values
private static double x0 = -size/2; // set window origin
private static double y0 = size/2; // set window origin
private static double xC = 0; // set complex plane origin
private static double yC = 0; // set complex plane origin
private static double scaleFactor = size/n; // translate complex plane to pixels
private static Stack<double[]> origins = new Stack<double[]>(); // track origins for zooming out
private static double zoom = 2.0; // modify to alter brightness scheme on zoom
private static Color[][] colors = new Color[n][n];
public Mandlebrot() {
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
double[] center = new double[]{xC, yC}; // rescale
origins.push(center);
zoom = (zoom * 2) - (0.5 * zoom);
xC = x0 + (scaleFactor * e.getX());
yC = y0 - (scaleFactor * e.getY());
size = size/2.0;
scaleFactor = size/n;
x0 = xC - (size/2);
y0 = yC + (size/2);
ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors);
ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors);
ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors);
ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors);
Thread thread1 = new Thread(quadrant1);
Thread thread2 = new Thread(quadrant2);
Thread thread3 = new Thread(quadrant3);
Thread thread4 = new Thread(quadrant4);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
repaint(0, 0, getWidth(), getHeight());
}
});
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
double[] center = origins.pop(); // rescale
zoom = (zoom + (zoom / 3)) / 2;
xC = center[0];
yC = center[1];
size = size*2.0;
scaleFactor = size/n;
x0 = xC - (size/2);
y0 = yC + (size/2);
ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors);
ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors);
ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors);
ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors);
Thread thread1 = new Thread(quadrant1);
Thread thread2 = new Thread(quadrant2);
Thread thread3 = new Thread(quadrant3);
Thread thread4 = new Thread(quadrant4);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
repaint(0, 0, getWidth(), getHeight());
}
}
});
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g.setColor(colors[j][i]);
g.drawLine(i, j, i, j);
}
}
}
public static int mand(Complex z0, int max) {
Complex z = z0;
for (int t = 0; t < max; t++) {
if (z.abs() > zoom) {
return t;
}
z = Complex.ad(Complex.mult(z, z), z0);
}
return max;
}
public static void main(String[] args) {
for (int i = 0; i < n; i++) { // initialize array for initial drawing
for (int j = 0; j < n; j++) {
double a = x0 + (i * scaleFactor);
double b = y0 - (j * scaleFactor);
Complex z0 = new Complex(a, b);
int gray = max - mand(z0, max);
Color color = new Color(gray, gray, gray);
colors[j][i] = color;
}
}
SwingUtilities.invokeLater(() -> {
var panel = new Threading();
panel.setBackground(Color.WHITE);
panel.setFocusable(true);
var frame = new JFrame("Mandelbrot");
frame.setSize(n, n);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel, BorderLayout.CENTER);
frame.setVisible(true);
});
}
}
ComputeThread.java
import java.awt.Color;
public class ComputeThread implements Runnable {
private static int n;
private static double x0;
private static double y0;
private static int xLow;
private static int xHigh;
private static int yLow;
private static int yHigh;
private static int max;
private static double scaleFactor;
private static double zoom;
private static Color[][] colors = new Color[1000][1000];
public ComputeThread(int n, double zoom, double x0, double y0, int xLow, int xHigh, int yLow, int yHigh, int max, double scaleFactor, Color[][] colors) {
this.n = n;
this.zoom = zoom;
this.x0 = x0;
this.y0 = y0;
this.xLow = xLow;
this.xHigh = xHigh;
this.yLow = yLow;
this.yHigh = yHigh;
this.max = max;
this.scaleFactor = scaleFactor;
this.colors = colors;
}
@Override
public void run() {
for (int i = xLow; i < xHigh; i++) {
for (int j = yLow; j < yHigh; j++) {
double a = x0 + (i * scaleFactor);
double b = y0 - (j * scaleFactor);
Complex z0 = new Complex(a, b);
int gray = max - mand(z0, max);
Color color = new Color(gray, gray, gray);
colors[j][i] = color;
}
}
}
public static int mand(Complex z0, int max) {
Complex z = z0;
for (int t = 0; t < max; t++) {
if (z.abs() > zoom) {
return t;
}
z = Complex.ad(Complex.mult(z, z), z0);
}
return max;
}
}
Complex.java
import java.lang.Math;
public class Complex {
double real;
double imag;
public Complex(double real, double imag) {
this.real = real;
this.imag = imag;
}
public static Complex ad(Complex num1, Complex num2) {
Complex num = new Complex(0.0, 0.0);
num.real = num1.real + num2.real;
num.imag = num1.imag + num2.imag;
return(num);
}
public static Complex mult(Complex num1, Complex num2) {
Complex num = new Complex(0.0, 0.0);
num.real = num1.real * num2.real - num1.imag * num2.imag;
num.imag = num1.real * num2.imag + num1.imag * num2.real;
return(num);
}
public double abs() {
return Math.hypot(real, imag);
}
}
You made a mistake by using static fields in ComputeThread. That way threads use the same arrays and they overwrite each other. So you get 1/4 of your image.
Repaint runs concurrently with threads so if it finishes earlier, not the whole image is drawn. You have to inform JFrame about finished calculations so it can repaint when all is ready.
Below the code that works better:
Mandelbrot.java
package mb;
import java.awt.Color;
import java.awt.BorderLayout;
import java.awt.Graphics;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.EventListener;
import java.util.Stack;
public class Mandelbrot extends JPanel implements CalcListener {
private static double size = 4.0; // complex plane is size units in width and length
private static int n = 1000; // window is n x n in pixels
private static int max = 255; // iterate through hex color values
private static double x0 = -size/2; // set window origin
private static double y0 = size/2; // set window origin
private static double xC = 0; // set complex plane origin
private static double yC = 0; // set complex plane origin
private static double scaleFactor = size/n; // translate complex plane to pixels
private static Stack<double[]> origins = new Stack<double[]>(); // track origins for zooming out
private static double zoom = 2.0; // modify to alter brightness scheme on zoom
private static Color[][] colors = new Color[n][n];
static Mandelbrot me;
public Mandelbrot() {
this.me=this;
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
double[] center = new double[]{xC, yC}; // rescale
origins.push(center);
zoom = (zoom * 2) - (0.5 * zoom);
xC = x0 + (scaleFactor * e.getX());
yC = y0 - (scaleFactor * e.getY());
size = size/2.0;
scaleFactor = size/n;
x0 = xC - (size/2);
y0 = yC + (size/2);
ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors, me);
ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors, me);
ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors, me);
ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors, me);
Thread thread1 = new Thread(quadrant1);
Thread thread2 = new Thread(quadrant2);
Thread thread3 = new Thread(quadrant3);
Thread thread4 = new Thread(quadrant4);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
repaint(0, 0, getWidth(), getHeight());
}
});
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
double[] center = origins.pop(); // rescale
zoom = (zoom + (zoom / 3)) / 2;
xC = center[0];
yC = center[1];
size = size*2.0;
scaleFactor = size/n;
x0 = xC - (size/2);
y0 = yC + (size/2);
ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors,me);
ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors,me);
ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors,me);
ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors,me);
Thread thread1 = new Thread(quadrant1);
Thread thread2 = new Thread(quadrant2);
Thread thread3 = new Thread(quadrant3);
Thread thread4 = new Thread(quadrant4);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
repaint(0, 0, getWidth(), getHeight());
}
}
});
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g.setColor(colors[j][i]);
g.drawLine(i, j, i, j);
}
}
}
public static int mand(Complex z0, int max) {
Complex z = z0;
for (int t = 0; t < max; t++) {
if (z.abs() > zoom) {
return t;
}
z = Complex.ad(Complex.mult(z, z), z0);
}
return max;
}
public static void main(String[] args) {
for (int i = 0; i < n; i++) { // initialize array for initial drawing
for (int j = 0; j < n; j++) {
double a = x0 + (i * scaleFactor);
double b = y0 - (j * scaleFactor);
Complex z0 = new Complex(a, b);
int gray = max - mand(z0, max);
Color color = new Color(gray, gray, gray);
colors[j][i] = color;
}
}
SwingUtilities.invokeLater(() -> {
var panel = new Mandelbrot();
panel.setBackground(Color.WHITE);
panel.setFocusable(true);
var frame = new JFrame("Mandelbrot");
frame.setSize(n, n);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel, BorderLayout.CENTER);
frame.setVisible(true);
});
}
public void calcDoneEvent()
{
repaint();
}
}
ComputeThread.java
package mb;
import java.awt.Color;
public class ComputeThread implements Runnable {
private int n;
private double x0;
private double y0;
private int xLow;
private int xHigh;
private int yLow;
private int yHigh;
private int max;
private double scaleFactor;
private double zoom;
private Color[][] colors = new Color[1000][1000];
private CalcListener listener;
public ComputeThread(int n, double zoom, double x0, double y0, int xLow, int xHigh, int yLow, int yHigh, int max, double scaleFactor, Color[][] colors,CalcListener l) {
this.n = n;
this.zoom = zoom;
this.x0 = x0;
this.y0 = y0;
this.xLow = xLow;
this.xHigh = xHigh;
this.yLow = yLow;
this.yHigh = yHigh;
this.max = max;
this.scaleFactor = scaleFactor;
this.colors = colors;
this.listener = l;
}
@Override
public void run() {
for (int i = xLow; i < xHigh; i++) {
for (int j = yLow; j < yHigh; j++) {
double a = x0 + (i * scaleFactor);
double b = y0 - (j * scaleFactor);
Complex z0 = new Complex(a, b);
int gray = max - mand(z0, max);
Color color = new Color(gray, gray, gray);
colors[j][i] = color;
}
}
listener.calcDoneEvent();
}
public int mand(Complex z0, int max) {
Complex z = z0;
for (int t = 0; t < max; t++) {
if (z.abs() > zoom) {
return t;
}
z = Complex.ad(Complex.mult(z, z), z0);
}
return max;
}
}
CalcListener.java
package mb;
public interface CalcListener
{
public void calcDoneEvent();
}
And Complex.java is without modifications.
Another issue is coding style. I made a shortcut by putting static "me" variable in Mandelbrot which is bad practice. Correctly, you should rather inherit Mouse and Key Adapters to own classes that would be capable of passing listener to threads.