

/*
Mandelbrot by Bjorn Carlin
Applet released June 2001
Source released December 2001

This source code is in the public domain,
you may use it in your own projects. Please
give credit where credit is due.

Applet works best with (width,height) = (500,500)
*/


import java.awt.*;
import java.applet.Applet;
import java.awt.image.*;
import java.awt.event.*;


public class Mandelbrot extends Applet
		implements Runnable, MouseListener, MouseMotionListener, KeyListener {
	final int						kFrameRate = 4;
	
	// these values give nice fade patterns for width = 500:
	final int[]						kFadePattern = new int[]
										{201, 207, 213, 279, 447, 477};
	
	// one of kFadePattern[], governs in what order pixel colors are calculated
	int								semiRandom = 477;
	
	// until the pixel is set to black
	int								maxIterations;
	
	int								width;
	int								height;
	
	// mouse state
	int								pressedX;
	int								pressedY;
	int								draggedX;
	int								draggedY;
	boolean							drawSelection;
	
	// borders
	double							minIm;
	double							maxIm;
	double							minRe;
	double							maxRe;
	
	// status display
	boolean							showZoom;
	boolean							showIter;
	
	// which fractal to generate
	int								formulaIndex = 1;
	
	// additional julia data
	boolean							waitForJulia;
	boolean							displayJulia;
	
	double							juliaRe;
	double							juliaIm;
	
	// buffer
	int[]							pixels;
	MemoryImageSource				bufferSource;
	Image							buffer;
	
	int[]							palette;
	
	Thread							generateThread;
	int								generationProgress;
	
	// force square selection
	boolean							square;
	
	
	public void init() {
		width = getSize().width;
		height = getSize().height;
		
		// set up buffer
		pixels = new int[width * height];
		for (int index = 0; index < pixels.length; index++)
			pixels[index] = 0xFF000000;
		bufferSource = new MemoryImageSource(width, height, pixels, 0, width);
		bufferSource.setAnimated(true);
		buffer = createImage(bufferSource);
		
		maxIterations = 20;
		
		square = true;
		
		displayJulia = false;
		waitForJulia = false;
		
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
		
		generateThread = new Thread(this);
		generateThread.setPriority(Thread.MIN_PRIORITY);
		
		generateThread.start();
		
		newFractal(1);
	}
	
	/*
	Generates palette of colors, invoked when a once when a new
	fractal is started.
	*/
	public void createPalette() {
		int						maxIterations;
		int						steps;
		int						index;
		
		steps = (this.maxIterations + 4) / 5;
		maxIterations = steps * 5; // i.e. round up to even 5
		
		palette = new int[maxIterations];
		
		for (index = 0; index < steps; index++)
			palette[index] = (255 << 16) + ((index * 255 / steps) << 8);
		
		for (index = 0; index < steps; index++)
			palette[index + steps] = ((255 - index * 255 / steps) << 16) + (255 << 8);
		
		for (index = 0; index < steps; index++)
			palette[index + steps * 2] = (255 << 8) + (index * 255 / steps);
		
		for (index = 0; index < steps; index++)
			palette[index + steps * 3] = ((255 - index * 255 / steps) << 8) + 255;
		
		for (index = 0; index < steps; index++)
			palette[index + steps * 4] = (255) + ((index * 255 / steps) << 16);
	}
	
	/*
	Applies the selection coordinates to new screen coordinates, and
	ensures (min < max).
	*/
	public void setMinMax(double minIm, double maxIm, double minRe, double maxRe) {
		if (minIm < maxIm) {
			this.minIm = minIm;
			this.maxIm = maxIm;
		} else {
			this.minIm = maxIm;
			this.maxIm = minIm;
		}
		
		if (minRe < maxRe) {
			this.minRe = minRe;
			this.maxRe = maxRe;
		} else {
			this.minRe = maxRe;
			this.maxRe = minRe;
		}
	}
	
	public synchronized void resetGeneration() {
		// determine a "somewhat reasonable" number of iterations,
		// attempting a balanced color scheme
		
		if (formulaIndex == 1)
			maxIterations = (int) (40 / Math.pow(maxRe - minRe, .15) / Math.pow(maxIm - minIm, .15));
		else if (formulaIndex == 2)
			maxIterations = (int) (35 / Math.pow(maxRe - minRe, .12) / Math.pow(maxIm - minIm, .12));
		else if (formulaIndex == 3)
			maxIterations = (int) (30 / Math.pow(maxRe - minRe, .09) / Math.pow(maxIm - minIm, .09));
		else if (formulaIndex == 4)
			maxIterations = (int) (25 / Math.pow(maxRe - minRe, .08) / Math.pow(maxIm - minIm, .08));
		else if (formulaIndex == 5)
			maxIterations = (int) (20 / Math.pow(maxRe - minRe, .07) / Math.pow(maxIm - minIm, .07));
		else if (formulaIndex == 6)
			maxIterations = (int) (40 / Math.pow(maxRe - minRe, .13) / Math.pow(maxIm - minIm, .13));
		createPalette();
		
		semiRandom = kFadePattern[(int) (Math.random() * kFadePattern.length)];
		
		// clear
		for (int pixelIndex = 0; pixelIndex < pixels.length; pixelIndex++)
			pixels[pixelIndex] = 0xFF000000;
		
		generationProgress = 0;
		generateThread.interrupt();
	}
	
	// reset screen
	public synchronized void newFractal(int formula) {
		if (formula > 0)
			formulaIndex = formula;
		
		setMinMax(-2, 2, -2, 2);
		
		resetGeneration();
	}
	
	/*
	Calculates the color value for a single pixel, depending on if and how fast
	the formula diverges at the coordinate (inRe, inIm).
	*/
	public int pixelColor(double inRe, double inIm) {
		double						re;
		double						im;
		double						nextRe;
		double						nextIm;
		double						cRe;
		double						cIm;
		int							iteration;
		
		if (displayJulia) {
			re = inRe;
			im = inIm;
			cRe = juliaRe;
			cIm = juliaIm;
		} else {
			re = 0;
			im = 0;
			cRe = inRe;
			cIm = inIm;
		}
		
		nextRe = 0;
		nextIm = 0;
		
		for (iteration = 0; iteration < maxIterations; iteration++) {
			if (formulaIndex == 1) {
				// (a + bi)2 = a2 + 2abi -b2
				
				nextRe = re * re - im * im + cRe;
				nextIm = 2 * re * im + cIm;
			} else if (formulaIndex == 2) {
				// (a + bi)3 = (a + bi)(a2 + 2abi -b2) = a3 + 3a2bi -3ab2 - b3i
				
				nextRe = re * re * re - 3 * re * im * im + cRe;
				nextIm = 3 * re * re * im - im * im * im + cIm;
			} else if (formulaIndex == 3) {
				// (a + bi)4 = (a2 + 2abi -b2)(a2 + 2abi -b2) =
				// a4 - 6a2b2 + b4 + 4a3bi - 4ab3i
				
				nextRe = re * re * re * re - 6 * re * re * im * im + im * im * im * im + cRe;
				nextIm = 4 * re * re * re * im - 4 * re * im * im * im + cIm;
			} else if (formulaIndex == 4) {
				// (a + bi)5 = 
				// a5 + 5a4bi - 10a3b2 - 10a2b3i + 5ab4 + b5i
				
				nextRe = re * re * re * re * re - 10 * re * re * re * im * im + 5 * re * im * im * im * im + cRe;
				nextIm = 5 * re * re * re * re * im - 10 * re * re * im * im * im + im * im * im * im * im + cIm;
			} else if (formulaIndex == 5) {
				// (a + bi)6 =
				// a6 + 6a5bi - 15a4b2 - 20a3b3i + 15a2b4 + 6ab5i - b6
				// a6 - 15a4b2 + 15a2b4 - b6 + 6a5bi - 20a3b3i + 6ab5i
				
				nextRe = re * re * re * re * re * re
					- 15 * re * re * re * re * im * im
					+ 15 * re * re * im * im * im * im
					- im * im * im * im * im * im
					+ cRe;
				nextIm = 6 * re * re * re * re * re * im
					- 20 * re * re * re * im * im * im
					+ 6 * re * im * im * im * im * im
					+ cIm;
			} else if (formulaIndex == 6) {
				// (a + bi)3 - (a + bi)2
				
				nextRe = re * re * re - 3 * re * im * im - re * re + im * im + cRe;
				nextIm = 3 * re * re * im - im * im * im - 2 * re * im + cIm;
			}
			
			re = nextRe;
			im = nextIm;
			
			if (im * im + re * re > 4) {
				// diverges after (iteration) steps!
				return palette[iteration];
			}
		}
		
		// convergent, black
		return 0;
	}
	
	// generate a number of pixel color values
	public synchronized void generate() {
		int							iteration;
		int							pixelIndex;
		int							x;
		int							y;
		double						re;
		double						im;
		
		for (iteration = 0; iteration < 2000; iteration++) {
			pixelIndex = (generationProgress * semiRandom) % (width * height);
			
			x = pixelIndex % width;
			y = pixelIndex / width;
			
			re = minRe + (maxRe - minRe) * x / width;
			im = minIm + (maxIm - minIm) * y / height;
			
			pixels[pixelIndex] = 0xFF000000 | pixelColor(re, im);
			
			generationProgress++;
			if (generationProgress >= width * height)
				return;
		}
	}
	
	public void run() {
		long						lastFinish;
		
		lastFinish = System.currentTimeMillis();
		
		while (true) {
			if (generationProgress >= width * height) {
				try {
					Thread.sleep(200);
				} catch (Exception e) {
					// interrupted = start drawing right away
				}
			}
			
			if (generationProgress < width * height) {
				// generate pixels until it is time to update the screen
				while (System.currentTimeMillis() - lastFinish < 1000 / kFrameRate)
					generate();
				
				// update the screen
				bufferSource.newPixels();
				repaint();
				lastFinish = System.currentTimeMillis();
				
				// give AWT thread a chance to run
				Thread.yield();
			}
		}
	}
	
	public void paint(Graphics theGraphics) {
		int									row = 20;
		
		theGraphics.drawImage(buffer, 0, 0, null);
		
		// draw some info at the top left
		if (displayJulia) {
			theGraphics.setColor(Color.black);
			theGraphics.drawString("julia coordinate: " + (int) (juliaRe * 1000) / 1000.0 + " + i(" + (int) (juliaIm * 1000) / 1000.0 + ")", 3, row);
			theGraphics.setColor(Color.white);
			theGraphics.drawString("julia coordinate: " + (int) (juliaRe * 1000) / 1000.0 + " + i(" + (int) (juliaIm * 1000) / 1000.0 + ")", 2, row - 1);
			row += 16;
		} else if (waitForJulia) {
			theGraphics.setColor(Color.black);
			theGraphics.drawString("click to confirm: julia coordinate: " + (int) (juliaRe * 1000) / 1000.0 + " + i(" + (int) (juliaIm * 1000) / 1000.0 + ")", 3, row);
			theGraphics.setColor(Color.white);
			theGraphics.drawString("click to confirm: julia coordinate: " + (int) (juliaRe * 1000) / 1000.0 + " + i(" + (int) (juliaIm * 1000) / 1000.0 + ")", 2, row - 1);
			row += 16;
		}
		if (showZoom) {
			theGraphics.setColor(Color.black);
			theGraphics.drawString("zoom: 10^" + (int) ((0-Math.log((maxRe - minRe) / 4.0) / Math.log(10)) * 100) / 100.0, 3, row);
			theGraphics.setColor(Color.white);
			theGraphics.drawString("zoom: 10^" + (int) ((0-Math.log((maxRe - minRe) / 4.0) / Math.log(10)) * 100) / 100.0, 2, row - 1);
			row += 16;
		}
		if (showIter) {
			theGraphics.setColor(Color.black);
			theGraphics.drawString("iterations: " + maxIterations, 3, row);
			theGraphics.setColor(Color.white);
			theGraphics.drawString("iterations: " + maxIterations, 2, row - 1);
			row += 16;
		}
		if (generationProgress < width * height) {
			theGraphics.setColor(Color.black);
			theGraphics.drawString((int) (generationProgress * 100 / width / height) + "%", 3, row);
			theGraphics.setColor(Color.white);
			theGraphics.drawString((int) (generationProgress * 100 / width / height) + "%", 2, row - 1);
			row += 16;
		}
		
		if (drawSelection) {
			theGraphics.setColor(Color.white);
			theGraphics.drawRect(Math.min(pressedX, draggedX), Math.min(pressedY, draggedY),
				Math.abs(pressedX - draggedX), Math.abs(pressedY - draggedY));
		}
	}
	
	public void update(Graphics theGraphics) {
		paint(theGraphics);
	}
	
	public void keyTyped(KeyEvent theEvent) {
		if (theEvent.getKeyChar() == '1')
			newFractal(1);
		else if (theEvent.getKeyChar() == '2')
			newFractal(2);
		else if (theEvent.getKeyChar() == '3')
			newFractal(3);
		else if (theEvent.getKeyChar() == '4')
			newFractal(4);
		else if (theEvent.getKeyChar() == '5')
			newFractal(5);
		else if (theEvent.getKeyChar() == '6')
			newFractal(6);
		else if (theEvent.getKeyChar() == 'z') {
			// toggle show zoom
			showZoom = !showZoom;
			repaint();
		} else if (theEvent.getKeyChar() == 'i') {
			// toggle show iter
			showIter = !showIter;
			repaint();
		} else if (theEvent.getKeyChar() == 'j') {
			// toggle julia
			if (waitForJulia) {
				waitForJulia = false;
				repaint();
			} else if (displayJulia) {
				displayJulia = false;
				waitForJulia = false;
				resetGeneration();
			} else {
				waitForJulia = true;
				repaint();
			}
		} else if (theEvent.getKeyChar() == 't') {
			// toggle force square selection
			square = !square;
		} else if (theEvent.getKeyChar() == 'm') {
			double							centerRe;
			double							centerIm;
			
			centerRe = (minRe + maxRe) / 2;
			centerIm = (minIm + maxIm) / 2;
			
			minRe = centerRe - 2 * (centerRe - minRe);
			maxRe = centerRe - 2 * (centerRe - maxRe);
			minIm = centerIm - 2 * (centerIm - minIm);
			maxIm = centerIm - 2 * (centerIm - maxIm);
			
			resetGeneration();
		} else if (theEvent.getKeyChar() == 'n') {
			double							centerRe;
			double							centerIm;
			
			centerRe = (minRe + maxRe) / 2;
			centerIm = (minIm + maxIm) / 2;
			
			minRe = centerRe - 0.5 * (centerRe - minRe);
			maxRe = centerRe - 0.5 * (centerRe - maxRe);
			minIm = centerIm - 0.5 * (centerIm - minIm);
			maxIm = centerIm - 0.5 * (centerIm - maxIm);
			
			resetGeneration();
		} else if (theEvent.getKeyChar() == 'c') {
			// random julia coordinate
			if (displayJulia) {
				juliaRe = Math.random() * 3.0 - 1.5;
				juliaIm = Math.random() * 3.0 - 1.5;
				resetGeneration();
			}
		}
	}
	
	public void mousePressed(MouseEvent theEvent) {
		pressedX = theEvent.getX();
		pressedY = theEvent.getY();
		
		// keep within bounds
		if (pressedX < 1)
			pressedX = 1;
		if (pressedY < 1)
			pressedY = 1;
		if (pressedX > width - 2)
			pressedX = width - 2;
		if (pressedY > height - 2)
			pressedY = height - 2;
		
		if (waitForJulia) {
			juliaRe = minRe + (maxRe - minRe) * pressedX / width;
			juliaIm = minIm + (maxIm - minIm) * pressedY / height;
			displayJulia = true;
			resetGeneration();
		}
		
		draggedX = pressedX;
		draggedY = pressedY;
	}
	
	public void mouseReleased(MouseEvent theEvent) {
		if (waitForJulia) {
			waitForJulia = false;
			return;
		}
		
		drawSelection = false;
		
		if ((pressedX - draggedX) * (pressedX - draggedX) +
				(pressedY - draggedY) * (pressedY - draggedY) < 10) {
			// a click, basically
			
			newFractal(-1);
			
			return;
		}
		
		setMinMax(
			minIm + (maxIm - minIm) * pressedY / height,
			minIm + (maxIm - minIm) * draggedY / height,
			minRe + (maxRe - minRe) * pressedX / width,
			minRe + (maxRe - minRe) * draggedX / width);
		resetGeneration();
	}
	
	public void mouseDragged(MouseEvent theEvent) {
		if (waitForJulia)
			return;
		
		draggedX = theEvent.getX();
		draggedY = theEvent.getY();
		
		if (draggedX < 1)
			draggedX = 1;
		if (draggedY < 1)
			draggedY = 1;
		if (draggedX > width - 2)
			draggedX = width - 2;
		if (draggedY > height - 2)
			draggedY = height - 2;
		
		if (square) {
			// force square selection, reduce size
			if (Math.abs(draggedX - pressedX) > Math.abs(draggedY - pressedY)) {
				// Æy is smaller
				if (draggedX > pressedX)
					draggedX = pressedX + Math.abs(draggedY - pressedY);
				else
					draggedX = pressedX - Math.abs(draggedY - pressedY);
			} else {
				// Æx is smaller
				if (draggedY > pressedY)
					draggedY = pressedY + Math.abs(draggedX - pressedX);
				else
					draggedY = pressedY - Math.abs(draggedX - pressedX);
			}
		}
		
		drawSelection = true;
		repaint();
	}
	
	public void mouseMoved(MouseEvent theEvent) {
		if (waitForJulia) {
			// update coordinate display
			int pressedX = theEvent.getX();
			int pressedY = theEvent.getY();
			
			// keep within bounds
			if (pressedX < 1)
				pressedX = 1;
			if (pressedY < 1)
				pressedY = 1;
			if (pressedX > width - 2)
				pressedX = width - 2;
			if (pressedY > height - 2)
				pressedY = height - 2;
			
			juliaRe = minRe + (maxRe - minRe) * pressedX / width;
			juliaIm = minIm + (maxIm - minIm) * pressedY / height;
			repaint();
		}
	}
	
	public void keyPressed(KeyEvent theEvent) {
		// scroll with the arrow keys
		if (theEvent.getKeyCode() == KeyEvent.VK_LEFT) {
			double temp = 2 * minRe - maxRe;
			maxRe = minRe;
			minRe = temp;
			resetGeneration();
		} else if (theEvent.getKeyCode() == KeyEvent.VK_RIGHT) {
			double temp = 2 * maxRe - minRe;
			minRe = maxRe;
			maxRe = temp;
			resetGeneration();
		} else if (theEvent.getKeyCode() == KeyEvent.VK_UP) {
			double temp = 2 * minIm - maxIm;
			maxIm = minIm;
			minIm = temp;
			resetGeneration();
		} else if (theEvent.getKeyCode() == KeyEvent.VK_DOWN) {
			double temp = 2 * maxIm - minIm;
			minIm = maxIm;
			maxIm = temp;
			resetGeneration();
		}
	}
	
	public void mouseExited(MouseEvent theEvent) {}
	public void mouseEntered(MouseEvent theEvent) {}
	public void mouseClicked(MouseEvent theEvent) {}
	public void keyReleased(KeyEvent theEvent) {}
}

