Mandelbrot Set III

I wrote my first ever Mandelbrot Set renderer back in 2015 and used Python to slowly create fractal images. Over a year later, I revisited the project with a Java version which — due to its code being actually compiled — ran much faster, yet had the same clunky interface; a rectangle the user had to draw and a key they had to press to the view change to the selected region.
In this post, over half a year later, I present my newest Mandelbrot Set fractal renderer (download the .jar), written in Java, which both runs fast and allows a much more intuitive and immersive walk through the complex plane by utilizing mouse dragging and scrolling.
The still time demanding task of rendering fractals — even in compiled languages — is split up into a low quality preview rendering, a normal quality display rendering and a high quality 4K (UHD-1 at 3840×2160 pixels to keep a 16:9 image ratio) rendering, all running in seperate threads.

Rainbow spiral
Rainbow spiral

The color schemes where also updated, apart from the usual black-and-white look there are multiple rainbow color schemes which rely on the HSB color space, zebra color schemes which use the iterations taken modulo some constant to define the color and a prime color scheme which tests if the number of iterations taken is prime.

Zebra spiral
Zebra spiral

Apart from the mouse and keyboard control, there is also a menu bar (implemented using Java’s JMenuBar) which allows for more conventional user input through a proper GUI.

Controls

  • Left mouse dragging: pan view
  • Left mouse double click: set cursor’s complex number to image center
  • Mouse scrolling: zoom view
  • Mouse scrolling +CTLR: pan view
  • ‘p’: render high definition fractal
  • ‘r’: reset view to default
  • ‘w’, ‘s’: zoom frame
  • Arrow keys: pan view
  • Arrow keys +CTRL: zoom view
  • Menu bar
    • Fractal: extra info about current fractal rendering
    • Color Scheme: change color scheme and maximum iteration depth
    • HD: controls for high definition rendering
    • Extra: help and about
Blue spiral
Blue spiral

A bit more on how the three threads are implemented.
Whenever the user changes the current view, the main program thread renders a low quality preview and immediately draws it to the screen. In the background, the normal quality thread (its pixel dimensions match the frame’s pixel dimensions) is told to start working. Once this medium quality rendering is finished, it is preferred to the low quality rendering and gets drawn on the screen.
If the user likes a particular frame, they can initiate a high quality rendering (4K UHD-1, 3840×2160 pixels) either by pressing ‘q’ or selecting HD->Render current frame. This high quality rendering obviously takes some time and a lot of processing power, so this thread is throttled by default to allow the user to further explore the fractal. Throttling can be disabled through the menu option HD->Fast rendering. There is also the option to tell the program to exit upon having finished the last queued high definition rendering (HD->Quit when done).
The high definition renderings are saved as .png files and named with their four defining constants. Zim and Zre define the image’s complex center, Zom defines the complex length above the image’s center. Clr defines the number of maximum iterations.

Another blue spiral
Another blue spiral

Just to illustrate how resource intensive fractal rendering really is.
A 4K fractal at 3840×2160 pixels with a iteration depth of 256 would in the worst case scenario (no complex numbers actually escape) require 3840 \cdot 2160 \cdot 256 \cdot 4 = 8493465600 double multiplications. If you had a super-optimized CPU which could do one double multiplication a clock tick (which current CPUs definitely cannot) and ran at 4.00 GHz, it would still take that massively overpowered machine \frac{8493465600}{4 \cdot 10^9} = 2.123 seconds. Larger images and higher maximum iterations would only increase the generated overhead.
The program’s source code is listed below and can also be downloaded (.java), though the compiled .jar can also be downloaded.

Green self-similarity
Green self-similarity

Unrelated to algorithmically generating fractal renderings, I recently found a weed which seemed to be related to the Mandelbrot Set and makes nature’s intertwined relationship with fractals blatently obvious. I call it the Mandel Weed.

Mandel Weed
Mandel Weed
// Java Code; Jonathan Frech; 22nd, 23rd, 24th, 25th, 26th, 27th of July 2017

/* ========================== COMPILATION ========================== *
 * javac MandelbrotSetExplorer.jar;                                  *
 * jar -cvfe MandelbrotSetExplorer.jar MandelbrotSetExplorer *.class */

// import
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.Color;
import java.lang.Math.*;
import java.io.*;
import javax.imageio.ImageIO;
import java.util.ArrayList;

/* complex points of possible interest */
// Zre = -1.9855015245500716; Zim = 2.8144331545157963E-5; Zom = 4.982551584070099E-15
// Zre = -0.74890371160338; Zim = 0.056326520789766035; Zom = 1.0930616826167766E-4;
// Zre = -0.0021112446872002272; Zim = 0.833495773674259; Zom = 2.7784161392993696E-5;

// static info class
class Info {
	static final String VERSION = "Mandelbrot Set Fractal Viewer Version 1.1";
	static final String LAST_MODIFIED = "27th of July 2017, 18:55pm";
	
	static final int WIDTH_LARGE = 3840, HEIGHT_LARGE = 2160; // high definition rendering, 4K
	static final int WIDTH       =  640, HEIGHT       =  480; // normal rendering, windows size
	static final int WIDTH_SMALL =   64, HEIGHT_SMALL =   48; // low quality preview
}

// fractal function holder
class Fractal {
	// render mandelbrot set fractal
	static void MandelbrotSet(int W, int H, int[] ClrArr, double Zom, double Zre, double Zim, int[] pxl) {
		double h = Zom*2, w = h*W/H;                                // complex image size
		for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { // loop through pixels
			double c = Zre-w/2.+w*x/W, d = -Zim-h/2.+h*y/H;         // pixel's complex location
			double a = 0, b = 0, asqr = 0, bsqr = 0; int i = -1;    // initialize
			while (asqr+bsqr <= 4 && i  Z^2 + C
				b = 2*a*b+d; a = asqr-bsqr+c;                       // Z -> Z^2 + C
				asqr = a*a; bsqr = b*b;                             // save on floating-point multiplication
				i++;                                                // another iteration step was needed
			} pxl[x+y*W] = ClrArr[i];                               // set pixel
		}}
	}
	
	// slowly render mandelbrot set fractal (used in hd thread)
	static void SlowMandelbrotSet(int W, int H, int[] ClrArr, double Zom, double Zre, double Zim, int[] pxl, int rendersleep) {
		double h = Zom*2, w = h*W/H;                                // complex image size
		for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { // loop through pixels
			double c = Zre-w/2.+w*x/W, d = -Zim-h/2.+h*y/H;         // pixel's complex location
			double a = 0, b = 0, asqr = 0, bsqr = 0; int i = -1;    // initialize
			while (asqr+bsqr <= 4 && i  Z^2 + C
				b = 2*a*b+d; a = asqr-bsqr+c;                       // Z -> Z^2 + C
				asqr = a*a; bsqr = b*b; i++;                        // save on floating-point multiplication, iteration
			} pxl[x+y*W] = ClrArr[i];                               // set pixel
		} if (rendersleep > 0) { try { Thread.sleep(rendersleep); } // sleep for a bit
		catch (InterruptedException e) {} } }                       // catch possible sleep exception
	}
	
	// draw complex number's path whilst iterating
	static void MandelbrotSetPath(Graphics g, Color col, int width, int height, int Clr, int mx, int my, double Zre, double Zim, double Zom) {
		double im = Zom*2, re = im*width/height;           // complex image size
		g.setColor(col);                                   // set color
		double c = mx*re/width+Zre-re/2;                   // pixel to complex c
		double d = (height-my)*im/height+Zim-im/2;         // pixel to complex d
		int lx = 0, ly = 0;                                // initialize last coordinate
		double a = 0, b = 0, asqr = 0, bsqr = 0;           // initialize
		for (int i = -1; i  Z^2 + C
			asqr = a*a; bsqr = b*b; i++;                   // save on floating-point multiplication, iteration
			int x = (int) ((a-Zre+re/2)/re*width);         // complex to pixel x
			int y = (int) (height-(b-Zim+im/2)/im*height); // complex to pixel y
			if (i > 0) g.drawLine(lx, ly, x, y);           // draw line
			lx = x; ly = y;                                // update last coordinate
		}
	}
	
	// draw complex number's numerical value
	static void MandelbrotSetNumber(Graphics g, Color col, int width, int height, int mx, int my, double Zre, double Zim, double Zom) {
		double im = Zom*2, re = im*width/height;                 // complex image size
		g.setColor(col);                                         // set color
		double a = mx*re/width+Zre-re/2;                         // pixel to complex c
		double b = (height-my)*im/height+Zim-im/2;               // pixel to complex d
		g.drawString("z = "+a+(b>0?"+":"")+b+"i", mx+10, my-10); // draw complex number text
		g.drawLine(mx-10, my, mx+10, my);                        // draw one line of the cross
		g.drawLine(mx, my-10, mx, my+10);                        // draw another line of the cross
	}
	
	// draw complex circle with radius 2
	static void MandelbrotSetCircle(Graphics g, Color col, int width, int height, double Zre, double Zim, double Zom) {
		double im = Zom*2, re = im*width/height;     // complex image size
		g.setColor(col);                             // set color
		int x = (int) ((re/2-Zre)/re*width);         // complex 0 to pixel x
		int y = (int) (height-(im/2-Zim)/im*height); // complex 0 to pixel y
		g.drawLine(x-5, y-5, x+5, y+5);              // draw one line of the cross
		g.drawLine(x-5, y+5, x+5, y-5);              // draw another line of the cross
		int r = (int) ((2-Zre+re/2)/re*width)-x;     // complex to pixel x
		g.drawOval(x-r, y-r, r*2, r*2);              // draw circle
	}
	
	// jframe icon
	static BufferedImage MandelbrotGetIcon() {
		double Zre = -0.4897965122125676, Zim = 0.6241986777074198, Zom = 1.8634345573568202E-4;
		int Clr = 256, w = 512, h = 512;; int[] ClrArr = new int[Clr];
		for (int i = 0; i < Clr; i++) ClrArr[i] = Fractal.hsl(360./(Clr-1)*i, 1, 1); ClrArr[Clr-1] = 0;
		BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
		int[] pxl = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
		Fractal.MandelbrotSet(w, h, ClrArr, Zom, Zre, Zim, pxl);
		return img;
	}
	
	// more convenient hsl color funtion
	static int hsl(double h, double s, double l) { return Color.getHSBColor((float) (h/360), (float) s, (float) l).getRGB(); }
}

// drawer thread
class Drawer implements Runnable {
	// thread reference, parent panel, sleep time
	private Thread thread;
	private MainPanel parent;
	private final int delay = 100;

	// constructor
	Drawer(MainPanel p) {
		// get parent
		parent = p;

		// initialize and start thread
		thread = new Thread(this, "Drawer");
		thread.setPriority(Thread.NORM_PRIORITY);
		thread.start();
	}

	// thread run function
	public void run() {
		// loop indefinitely
		while (true) {
			// fractal needs to be rendered
			if (parent.dirty) {
				// save parameters
				int WIDTH = parent.WIDTH, HEIGHT = parent.HEIGHT;
				int[] ClrArr = parent.ClrArr;
				double Zom = parent.Zom, Zre = parent.Zre, Zim = parent.Zim;

				// render mandelbrot set
				Fractal.MandelbrotSet(WIDTH, HEIGHT, ClrArr, Zom, Zre, Zim, parent.pxl);

				// make sure that parameters have not changed while rendering
				if (WIDTH == parent.WIDTH && HEIGHT == parent.HEIGHT && ClrArr == parent.ClrArr
					&& Zom == parent.Zom && Zre == parent.Zre && Zim == parent.Zim)
					{ parent.dirty = false; parent.repaint(); }
			}

			// did not need to render, sleep
			else {
				try { Thread.sleep(delay); }
				catch (InterruptedException e) {}
			}
		}
	}
}

// queued job class, holding fractal paramters
class QueueJob {
	final int WIDTH, HEIGHT; final double Zom, Zre, Zim; final int[] ClrArr;                                                                                                // paramters
	QueueJob(int width, int height, double zom, double zre, double zim, int[] clrarr) { WIDTH = width; HEIGHT = height; Zom = zom; Zre = zre; Zim = zim; ClrArr = clrarr; } // constructor
	String getname() { return "MandelbrotSet("+WIDTH+"x"+HEIGHT+";Zre="+Zre+";Zim="+Zim+";Zom="+Zom+";Clr="+ClrArr.length+";).png"; }                                       // file name
	String getstatus() { return "Mandelbrot Set  (Zre="+Zre+" Zim="+Zim+" Zom="+Zom+" Clr="+ClrArr.length+")"; }                                      // menu status
}

// high definition drawer
class HDDrawer implements Runnable {
	// thread reference, parent panel, sleep time
	private Thread thread;
	private MainPanel parent;
	private final int delay = 100;
	
	private ArrayList Queue = new ArrayList(); // queued jobs
	void addjob(QueueJob j) { Queue.add(j); }              // add a job
	private static final int defaultrenderspeed = 1;                  // default render delay
	private int renderspeed = defaultrenderspeed;                     // render delay
	void setrenderspeed(      ) { renderspeed = defaultrenderspeed; } // set default render speed
	void setrenderspeed(int rs) { renderspeed = rs;                 } // set render speed
	
	// constructor
	HDDrawer(MainPanel p) {
		// get parent
		parent = p;
		
		// initialize and start thread
		thread = new Thread(this, "HDDrawer");
		thread.setPriority(Thread.MIN_PRIORITY);
		thread.start();
	}
	
	// finished rendering
	boolean finished() { return Queue.size()  0) {
				QueueJob j = Queue.get(0);
				BufferedImage img = new BufferedImage(j.WIDTH, j.HEIGHT, BufferedImage.TYPE_INT_RGB);
				int[] pxl = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
				Fractal.SlowMandelbrotSet(j.WIDTH, j.HEIGHT, j.ClrArr, j.Zom, j.Zre, j.Zim, pxl, renderspeed);
				try { ImageIO.write(img, "png", new File(j.getname())); } // write png
				catch (Exception e) { // catch exception whilst trying to write png
					StringWriter err = new StringWriter(); e.printStackTrace(new PrintWriter(err)); // convert exception to string to print it to the user
					JOptionPane.showOptionDialog(null, "Could not save high definiton fractal rendering.\n\n"+err.toString(), "Problem saving", JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, new String[]{"OK"}, "OK");
				} Queue.remove(0); // processed, remove from queue
			}

			// did not need to render, sleep
			else {
				parent.hdnojobs();
				try { Thread.sleep(delay); }
				catch (InterruptedException e) {}
			}
		}
	}
}

// main panel
class MainPanel extends JPanel {
	// copy dimensions from static info class
	static final int WIDTH_LARGE = Info.WIDTH_LARGE, HEIGHT_LARGE = Info.HEIGHT_LARGE, WIDTH = Info.WIDTH, HEIGHT = Info.HEIGHT, WIDTH_SMALL = Info.WIDTH_SMALL, HEIGHT_SMALL = Info.HEIGHT_SMALL;
	
	// high definition draw thread
	HDDrawer hdthread;
	JRadioButtonMenuItem fastrenderbutton;
	
	// small and large image
	BufferedImage img_small;
	BufferedImage img;
	int[] pxl_small;
	int[] pxl;

	// fractal parameters
	double Zom , Zre, Zim;
	int[] ClrArr;
	boolean dirty = false;
	
	private int mousex = 0, mousey = 0;
	boolean showpath = false;
	boolean showcomplex = false;
	boolean showcircle = false;
	Color pathcolor;

	// mouse paning
	private boolean paning = false;
	private int paningx, paningy;
	private double paningre, paningim, paningspeed = 50;
	
	// special key pressed, alters functionalities
	private boolean special = false;

	// zoom change, maximal zoom
	private double zoomspeed = .9, zoommax = 3;
	
	boolean quitwhendone = false;
	void hdnojobs() { if (quitwhendone) { System.exit(0); } }

	// constructor
	MainPanel() {
		// initialize images
		img       = new BufferedImage(WIDTH      , HEIGHT      , BufferedImage.TYPE_INT_RGB);
		img_small = new BufferedImage(WIDTH_SMALL, HEIGHT_SMALL, BufferedImage.TYPE_INT_RGB);
		pxl       = ((DataBufferInt) img      .getRaster().getDataBuffer()).getData();
		pxl_small = ((DataBufferInt) img_small.getRaster().getDataBuffer()).getData();
		
		// hd
		hdthread = new HDDrawer(this);
		
		// start thread, add listeners, set focus, reset and thereby start program
		new Drawer(this); addlisteners(); setFocusable(true); reset();
	}
	
	// panel paint function
	@Override public void paintComponent(Graphics g) {
		super.paintComponent(g);
		            g.drawImage(img_small, 0, 0, WIDTH, HEIGHT, null);
		if (!dirty) g.drawImage(img      , 0, 0, WIDTH, HEIGHT, null);
		
		if (showpath)    Fractal.MandelbrotSetPath  (g, pathcolor, WIDTH, HEIGHT, ClrArr.length, mousex, mousey, Zre, Zim, Zom);
		if (showcomplex) Fractal.MandelbrotSetNumber(g, pathcolor, WIDTH, HEIGHT,                mousex, mousey, Zre, Zim, Zom);
		if (showcircle)  Fractal.MandelbrotSetCircle(g, pathcolor, WIDTH, HEIGHT,                                Zre, Zim, Zom);
		
		g.dispose();
	}
	
	@Override public Dimension getPreferredSize() { return new Dimension(WIDTH, HEIGHT); }                      // panel size
	void saveimg() { hdthread.addjob(new QueueJob(WIDTH_LARGE, HEIGHT_LARGE, Zom, Zre, Zim, ClrArr)); } // save high definition image of current fractal
	
	// pan view based on mouse dragging
	private void pan(MouseEvent e) {
		int dx = paningx-e.getX(), dy = paningy-e.getY();   // pixel delta
		double im = Zom*2, re = im*WIDTH/HEIGHT;            // complex image size
		double dre = re*dx/WIDTH, dim = im*dy/HEIGHT;       // complex delta
		Zre = paningre + dre; Zim = paningim - dim; draw(); // update pan, redraw
	}
	
	// add listeners
	private void addlisteners() {
		// key listener
		addKeyListener(new KeyAdapter() {
			// key pressed
			public void keyPressed(KeyEvent e) {
				// complex frame size
				double im = Zom/2, re = im*WIDTH/HEIGHT;

				// zoom frame
					 if (e.getKeyCode() == KeyEvent.VK_W) { Zom *= zoomspeed; draw(); }
				else if (e.getKeyCode() == KeyEvent.VK_S) { Zom /= zoomspeed; draw(); }

				// move frame
				else if (e.getKeyCode() == KeyEvent.VK_UP   ) { if (special) Zom *= zoomspeed; else Zim += im; draw(); }
				else if (e.getKeyCode() == KeyEvent.VK_DOWN ) { if (special) Zom /= zoomspeed; else Zim -= im; draw(); }
				else if (e.getKeyCode() == KeyEvent.VK_LEFT ) { Zre -= re; draw(); }
				else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { Zre += re; draw(); }

				else if (e.getKeyCode() == KeyEvent.VK_CONTROL) special = true; // update special
				else if (e.getKeyCode() == KeyEvent.VK_R      ) reset();        // reset
				else if (e.getKeyCode() == KeyEvent.VK_P      ) saveimg();      // save
			}
			
			// key release
			public void keyReleased(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_CONTROL) special = false; // update special
			}
		});
		
		// mouse listener
		addMouseListener(new MouseAdapter() {
			// mouse pressed
			public void mousePressed(MouseEvent e) {
				// click count, number of successive clicks
				int c = e.getClickCount();

				// left mouse button was pressed
				if (SwingUtilities.isLeftMouseButton(e)) {
					// single click: initiate paning
					if (c == 1) {
						paning = true;
						paningx = e.getX(); paningy = e.getY();
						paningre = Zre; paningim = Zim;
					}

					// double click: center image part cursor is pointing at
					else if (c == 2) {
						paning = false;
						double im = Zom*2, re = im*WIDTH/HEIGHT;
						double r = Zre-re/2 + re*e.getX()/WIDTH;
						double i = Zim-im/2 + im*(HEIGHT-e.getY())/HEIGHT;
						Zre = r; Zim = i;
						draw();
					}
				}
			}
			
			// mouse released
			public void mouseReleased(MouseEvent e) {
				// left mouse button was pressed
				if (SwingUtilities.isLeftMouseButton(e)) {
					if (paning) {
						if (dirty) pan(e);
						paning = false;
					}
				}
			}
		});
		
		// mouse motion lister
		addMouseMotionListener(new MouseAdapter() {
			// mouse dragged
			public void mouseDragged(MouseEvent e) {
				// left mouse button was pressed
				if (SwingUtilities.isLeftMouseButton(e)) pan(e);
				
				// update mouse position
				mousex = e.getX(); mousey = e.getY(); if (showpath || showcomplex) repaint();
			}
			
			// mouse moved
			public void mouseMoved(MouseEvent e) {
				// update mouse position
				mousex = e.getX(); mousey = e.getY(); if (showpath || showcomplex) repaint();
			}
		});
		
		// mouse wheel listener
		addMouseWheelListener(new MouseAdapter() {
			public void mouseWheelMoved(MouseWheelEvent e) {
				int n = e.getWheelRotation();            // scroll intensity
				double im = Zom*2, re = im*WIDTH/HEIGHT; // complex frame size

				// pan view
				if (special) {
					if (e.isShiftDown()) Zre += re*n/paningspeed; // horizontal
					else                 Zim -= im*n/paningspeed; // vertical
				}

				// zooming frame
				else {
					if (n > 0 && Zom == zoommax) return; // do not scroll unnecessarily

					double r = Zre-re/2 + re*e.getX()/WIDTH;           // mouse cursor's real part
					double i = Zim-im/2 + im*(HEIGHT-e.getY())/HEIGHT; // mouse cursor's imaginary part

					Zom *= Math.pow(zoomspeed, -n);           					// zoom
					if (Zom > zoommax) Zom = zoommax;           // cap zoom
					im = Zom*2; re = im*WIDTH/HEIGHT;           // get new complex frame size
					Zre = re/2 - re*e.getX()/WIDTH+r;           // shift so that cursor is at its
					Zim = im/2 - im*(HEIGHT-e.getY())/HEIGHT+i; // ; previous relative position 
				}

				draw(); // redraw
			}
		});
	}

	// reset parameters
	private void reset() {
		Zom = 1.3; Zre = -.7; Zim = 0;
		setcolorscheme("Rainbow", 256);
	}

	// draw preview, tell thread to draw
	private void draw() {
		if (Zom > zoommax) Zom = zoommax;                                                   // cap zoom
		Fractal.MandelbrotSet(WIDTH_SMALL, HEIGHT_SMALL, ClrArr, Zom, Zre, Zim, pxl_small); // draw preview
		dirty = true; repaint();                                                            // tell thread to render fractal, repaint
	}
	
	// test if n is prime
	static boolean prime(int n) {
		for (int i = 2; i  1;
	}
	
	// avaiable color schemes, current color scheme
	final String[] COLORSCHEMES = {"Rainbow", "Rainbow II", "Rainbow III", "Rainbow IV", "Zebra", "Zebra (3)", "Zebra (4)", "B/W", "W/B", "Prime"}; String COLORSCHEME;

	// set color schemes using incomplete parameters
	void setcolorscheme(                      ) { setcolorscheme(             256          ); }
	void setcolorscheme(String scheme         ) { setcolorscheme(scheme     , ClrArr.length); }
	void setcolorscheme(               int Clr) { setcolorscheme(COLORSCHEME, Clr          ); }
	
	// set color scheme
	void setcolorscheme(String scheme, int Clr) {
		if (Clr < 2) Clr = 2; // cap color depth
		COLORSCHEME = scheme; // update current color scheme
		ClrArr = new int[Clr]; // color array
		
		// rainbow scheme
		if (scheme.equals("Rainbow")) {
			for (int i = 0; i < Clr; i++) ClrArr[i] = Fractal.hsl(360./(Clr-1)*i, 1, 1);
			ClrArr[Clr-1] = 0;
		pathcolor = Color.WHITE;
		}
		
		// rainbow II scheme
		else if (scheme.equals("Rainbow II")) {
			for (int i = 0; i < Clr; i++) ClrArr[i] = Fractal.hsl(210+40*Math.sin(i*10./(Clr-1)), 1, 1);
			ClrArr[Clr-1] = 0;
			pathcolor = Color.WHITE;
		}
		
		// rainbow III scheme
		else if (scheme.equals("Rainbow III")) {
			for (int i = 0; i < Clr; i++) {
				double c = (double) i/(Clr-1);
				ClrArr[i] = Fractal.hsl(Math.cos(c*4)*55+170, .5+.3*Math.cos(c*9), 1);
			} pathcolor = Color.WHITE;
		}
		
		// rainbow IV scheme
		else if (scheme.equals("Rainbow IV")) {
			for (int i = 0; i < Clr; i++) ClrArr[i] = Fractal.hsl(110+40*Math.sin(i*10./(Clr-1)), 1, 1);
			ClrArr[Clr-1] = 0;
			pathcolor = Color.WHITE;
		}
		
		// This scheme is not very interesting.
		/*else if (scheme.equals("Rainbow_")) {
			double h0 = 23, h1 = 50;
			for (int i = 0; i < Clr; i++) ClrArr[i] = hsl(h0+(h1-h0)/(Clr-1)*i, 1, 1);
			ClrArr[Clr-1] = 0;
		}*/
		
		// zebra scheme
		else if (scheme.equals("Zebra")) {
			int stripes = 2;
			for (int i = 0; i < Clr; i++) {
				int k = (i%stripes)*255/(stripes-1);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			ClrArr[Clr-1] = 0;
			pathcolor = Color.RED;
		}
		
		// zebra (3) scheme
		else if (scheme.equals("Zebra (3)")) {
			int stripes = 3;
			for (int i = 0; i < Clr; i++) {
				int k = (i%stripes)*255/(stripes-1);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			ClrArr[Clr-1] = 0;
			pathcolor = Color.RED;
		}
		
		// zebra (4) scheme
		else if (scheme.equals("Zebra (4)")) {
			int stripes = 4;
			for (int i = 0; i < Clr; i++) {
				int k = (i%stripes)*255/(stripes-1);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			ClrArr[Clr-1] = 0;
			pathcolor = Color.RED;
		}
		
		// B/W scheme
		else if (scheme.equals("B/W")) {
			for (int i = 0; i < Clr; i++) {
				int k = (int) (255./(Clr-1)*i);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			pathcolor = Color.RED;
		}
		
		// W/B scheme
		else if (scheme.equals("W/B")) {
			for (int i = 0; i < Clr; i++) {
				int k = 255-(int) (255./(Clr-1)*i);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			pathcolor = Color.RED;
		}
		
		// number of iterations needed is prime
		else if (scheme.equals("Prime")) {
			for (int i = 0; i < Clr; i++) {
				int k = (prime(i)?255:0);
				ClrArr[i] = new Color(k, k, k).getRGB();
			}
			pathcolor = Color.RED;
		}
		
		// fallback
		else setcolorscheme("Rainbow", 256);
		
		// redraw fractal
		draw();
	}
}

// menu bar
class MainMenuBar extends JMenuBar {
	// constructor, add individual submenus
	MainMenuBar(MainPanel panel) {
		add(Fractal(panel));
		add(ColorScheme(panel));
		add(HD(panel));
		add(Extra());
	}
	
	// fractal submenu
	JMenu Fractal(MainPanel panel) {
		JMenu menu = new JMenu("Fractal"); // menu label
		menu.setMnemonic(KeyEvent.VK_F);   // menu key
		
		// show complex number
		JRadioButtonMenuItem b0 = new JRadioButtonMenuItem("Show complex number");
		class cmp implements ActionListener {
			private JRadioButtonMenuItem B;
			private MainPanel panel;
			cmp(MainPanel p, JRadioButtonMenuItem b) { panel = p; B = b; }
			public void actionPerformed(ActionEvent e) { panel.showcomplex = B.isSelected(); panel.repaint(); }
		}
		b0.addActionListener(new cmp(panel, b0));
		menu.add(b0);
		
		// show path
		JRadioButtonMenuItem b1 = new JRadioButtonMenuItem("Show path");
		class pth implements ActionListener {
			private JRadioButtonMenuItem B;
			private MainPanel panel;
			pth(MainPanel p, JRadioButtonMenuItem b) { panel = p; B = b; }
			public void actionPerformed(ActionEvent e) { panel.showpath = B.isSelected(); panel.repaint(); }
		}
		b1.addActionListener(new pth(panel, b1));
		menu.add(b1);
		
		// show circle
		JRadioButtonMenuItem b2 = new JRadioButtonMenuItem("Show circle");
		class crc implements ActionListener {
			private JRadioButtonMenuItem B;
			private MainPanel panel;
			crc(MainPanel p, JRadioButtonMenuItem b) { panel = p; B = b; }
			public void actionPerformed(ActionEvent e) { panel.showcircle = B.isSelected(); panel.repaint(); }
		}
		b2.addActionListener(new crc(panel, b2));
		menu.add(b2);
		
		// current fractal frame info
		menu.add(new JMenuItem(new AbstractAction("Info") {
			public void actionPerformed(ActionEvent e) {
				double im = panel.Zom*2, re = im*panel.WIDTH/panel.HEIGHT; // complex image size
				JOptionPane.showOptionDialog(null, "MandelbrotSet(Zre = "+panel.Zre+", Zim = "+panel.Zim+"; Zom = "+panel.Zom+"; Clr = "+panel.ClrArr.length+")\n\n"+(panel.Zre-re/2)+" <= re <= "+(panel.Zre+re/2)+"; "+(panel.Zim-im/2)+" <= im <= "+(panel.Zim+im/2), "Fractal information", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK");
			}
		}));

		// return
		return menu;
	}
	
	// color scheme submenu
	JMenu ColorScheme(MainPanel panel) {
		JMenu menu = new JMenu("Color Scheme"); // menu label
		menu.setMnemonic(KeyEvent.VK_C);        // menu key
		
		// scheme button class
		class schemeAction extends AbstractAction {
			MainPanel panel; String id;
			schemeAction(MainPanel _panel, String _id) { super(_id); panel = _panel; id = _id; }
			public void actionPerformed(ActionEvent e) { panel.setcolorscheme(id); }
		}
		ButtonGroup group = new ButtonGroup(); // button group
		
		// parse color scheme string array and add menu items
		for(int i = 0; i < panel.COLORSCHEMES.length; i++) {
			JRadioButtonMenuItem item = new JRadioButtonMenuItem(new schemeAction(panel, panel.COLORSCHEMES[i])); // radio button
			item.setSelected(i == 0); group.add(item); menu.add(item);                                            // select first item add to group and menu
		}
		
		//menu.add(new JMenuItem()); // spacer
		
		// set custom color depth
		menu.add(new JMenuItem(new AbstractAction("Color depth...") {
			public void actionPerformed(ActionEvent e) {
				JSpinner spinner = new JSpinner(new SpinnerNumberModel(panel.ClrArr.length, 2, null, 1));
				int o = JOptionPane.showOptionDialog(null, spinner, "Set color depth", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK", "Cancel"}, "OK");
				if (o == 0) panel.setcolorscheme((int) spinner.getValue());
			}
		}));
		
		// reset color depth
		menu.add(new JMenuItem(new AbstractAction("Default color depth") { public void actionPerformed(ActionEvent e) { panel.setcolorscheme(); } }));
		
		// return
		return menu;
	}
	
	// hd submenu
	JMenu HD(MainPanel panel) {
		JMenu menu = new JMenu("HD");    // menu label
		menu.setMnemonic(KeyEvent.VK_H); // menu key
		
		// render current frame
		menu.add(new JMenuItem(new AbstractAction("Render current frame") {
			public void actionPerformed(ActionEvent e) {
				panel.saveimg();
			}
		}));
		
		// status on high definition render thread
		menu.add(new JMenuItem(new AbstractAction("Status...") { public void actionPerformed(ActionEvent e) { JOptionPane.showOptionDialog(null, panel.hdthread.getstatus(), "High definition rendering status", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK"); } }));
		
		// no render delay
		JRadioButtonMenuItem b0 = new JRadioButtonMenuItem("Fast rendering");
		class fst implements ActionListener {
			private JRadioButtonMenuItem B; private MainPanel panel;
			fst(MainPanel p, JRadioButtonMenuItem b) { panel = p; B = b; }
			public void actionPerformed(ActionEvent e) {
				if (B.isSelected()) panel.hdthread.setrenderspeed(0);
				else panel.hdthread.setrenderspeed();
			}
		}
		b0.addActionListener(new fst(panel, b0)); menu.add(b0);
		panel.fastrenderbutton = b0;
		
		// program will exit when hd thread is finished
		JRadioButtonMenuItem b1 = new JRadioButtonMenuItem("Quit when done");
		class qwd implements ActionListener {
			private JRadioButtonMenuItem B; private MainPanel panel;
			qwd(MainPanel p, JRadioButtonMenuItem b) { panel = p; B = b; }
			public void actionPerformed(ActionEvent e) {
				if (B.isSelected() && panel.hdthread.finished()) {
					JOptionPane.showOptionDialog(null, "There are currently no fractal rendering jobs queued.", "No jobs in process", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK");
					B.setSelected(false);
				}
				panel.quitwhendone = B.isSelected();
			}
		}
		b1.addActionListener(new qwd(panel, b1)); menu.add(b1);
		
		
		
		// return
		return menu;
	}
	
	// extra submenu
	JMenu Extra() {
		JMenu menu = new JMenu("Extra"); // menu label
		menu.setMnemonic(KeyEvent.VK_E); // menu key
		
		// help message
		String help = "Help\n\nMouse Control\n* Left mouse button dragging: pan view\n* Left mouse button double click: center cursor's complex number\n* Mouse scroll wheel: zoom view\n* Mouse scroll wheel +CTRL: pan view\n\nKey Control\n* 'p': save high definition rendering\n* 'r': reset view\n* 'w': zoom in\n* 's':zoom out\n* Arrow keys: pan view\n* Arrow keys +CTRL: zoom view";
		menu.add(new JMenuItem(new AbstractAction("Help...") {
			public void actionPerformed(ActionEvent e) {
				JOptionPane.showOptionDialog(null, help, "Help", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK");
			}
		}));
		
		// about message
		String about = Info.VERSION + "\n(Last modified " + Info.LAST_MODIFIED + ")\n\nThis Java Application lets the user explore one of the most famous fractals, the Mandelbrot Set.\nOne can change color schemes view the complex number's paths through their plane, change\nthe iteration depth used and export high definition renderings.\n\nWritten by Jonathan Frech\nBlog: https://jonathanfrech.wordpress.com/";
		menu.add(new JMenuItem(new AbstractAction("About...") {
			public void actionPerformed(ActionEvent e) {
				JOptionPane.showOptionDialog(null, about, "About", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, new String[]{"OK"}, "OK");
			}
		}));
		
		// return
		return menu;
	}
}

// main class
class MandelbrotSetExplorer extends JFrame {
	// constructor
	MandelbrotSetExplorer() {
		setTitle("Mandelbrot Set"); setLocationByPlatform(true); setResizable(false);       // setup
		MainPanel panel = new MainPanel(); add(panel); setJMenuBar(new MainMenuBar(panel)); // main panel, menu bar
		setIconImage((Image) Fractal.MandelbrotGetIcon());                                  // set custom icon
		
		// special closing behaviour (confirmation when hd thread still renders)
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				int o = 0; if (!panel.hdthread.finished()) o = JOptionPane.showOptionDialog(null, "Do you really want to quit?\nThere are fractal images being rendered in the background.", "Rendering in process", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[]{"Quit Anyways", "Activate fast rendering", "Cancel"}, "Cancel"); // hd thread not finished, ask user
				if (o == 0) { dispose(); System.exit(0); } // quit
				if (o == 1) { panel.fastrenderbutton.setSelected(true); panel.hdthread.setrenderspeed(0); }
			}
		});
		
		// pack frame, set visible and thereby start program
		pack(); setVisible(true);
	}
	
	// main function
	public static void main(String[] args) {
		Thread.currentThread().setPriority((int)(Thread.MAX_PRIORITY*0.9)); // set main thread's priority
		new MandelbrotSetExplorer();                                        // start program
	}
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s