import static java.awt.Color.HSBtoRGB; import static java.awt.Color.black; import static java.awt.event.KeyEvent.VK_BACK_SLASH; import static java.awt.event.KeyEvent.VK_ESCAPE; import static java.awt.image.BufferedImage.TYPE_INT_RGB; import static java.lang.Math.abs; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.signum; import static java.util.stream.Stream.of; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.function.Consumer; import java.util.function.Predicate; import javax.swing.JFrame; /* * click: point to center * ctrl-click: point to origin * drag: point to mouse release point * ctrl-drag: point to origin + zoom * back-slash: back to previous point * esc: back to previous zoom point - zoom */ public class Mandelbrot extends JFrame { private static final long serialVersionUID = 1L; private Insets insets; private int width, height; private double widthHeightRatio; private int minX, minY; private double Zoom; private int mpX, mpY, mdX, mdY; private boolean isCtrlDown, ctrl; private Stack stack = new Stack(); private BufferedImage image; private boolean paint = true; public static void main(String[] args) { new Mandelbrot(800, 600); // (800, 600), (1024, 768), (1600, 900) } public Mandelbrot(int width, int height) { super("Mandelbrot Set"); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); Dimension screen = getToolkit().getScreenSize(); setBounds( ((int) screen.getWidth() - width) / 2, ((int) screen.getHeight() - height) / 2, width, height ); addMouseListener(mouseAdapter); addMouseMotionListener(mouseAdapter); addKeyListener( new KeyAdapter() { public void keyPressed(KeyEvent e) { isCtrlDown = e.isControlDown(); } public void keyReleased(KeyEvent e) { isCtrlDown = e.isControlDown(); } public void keyTyped(KeyEvent e) { char c = e.getKeyChar(); boolean isEsc = c == VK_ESCAPE; if (!isEsc && c != VK_BACK_SLASH) return; if (stack.pop(isEsc)) { paint = true; printPoint(); } repaint(); } } ); setVisible(true); insets = getInsets(); this.width = width -= insets.left + insets.right; this.height = height -= insets.top + insets.bottom; widthHeightRatio = (double) width / height; minX = -width / 2 - 125; minY = -height / 2; Zoom = .005; paint = true; printPoint(); } private MouseAdapter mouseAdapter = new MouseAdapter() { public void mouseClicked(MouseEvent e) { stack.push(false); if (!ctrl) { minX -= width / 2 ; minY -= height / 2; } minX += e.getX() - insets.left; minY += e.getY() - insets.top; ctrl = false; paint = true; printPoint(); repaint(); } public void mousePressed(MouseEvent e) { mpX = e.getX(); mpY = e.getY(); ctrl = isCtrlDown; } public void mouseDragged(MouseEvent e) { if (!ctrl) return; setMdCoord(e); repaint(); } private void setMdCoord(MouseEvent e) { int dx = e.getX() - mpX; int dy = e.getY() - mpY; mdX = (int) (mpX + max(abs(dx), abs(dy)*widthHeightRatio) * signum(dx)); mdY = (int) (mpY + max(abs(dy), abs(dx)/widthHeightRatio) * signum(dy)); acceptIf(insets.left, ge(mdX), setMdXY); acceptIf(insets.top, ge(mdY), setMdYX); acceptIf(insets.left+width-1, le(mdX), setMdXY); acceptIf(insets.top+height-1, le(mdY), setMdYX); } private void acceptIf(int value, Predicate p, Consumer c) { if (p.test(value)) c.accept(value); } private Predicate ge(int md) { return v-> v >= md; } private Predicate le(int md) { return v-> v <= md; } private Consumer setMdXY = v-> mdY = (int) (mpY + abs((mdX=v)-mpX)/widthHeightRatio * signum(mdY-mpY)); private Consumer setMdYX = v-> mdX = (int) (mpX + abs((mdY=v)-mpY)*widthHeightRatio * signum(mdX-mpX)); public void mouseReleased(MouseEvent e) { if (e.getX() == mpX && e.getY() == mpY) return; stack.push(ctrl); if (!ctrl) { minX += mpX - (mdX = e.getX()); minY += mpY - (mdY = e.getY()); } else { setMdCoord(e); if (mdX < mpX) { int t=mpX; mpX=mdX; mdX=t; } if (mdY < mpY) { int t=mpY; mpY=mdY; mdY=t; } minX += mpX - insets.left; minY += mpY - insets.top; double rZoom = (double) width / abs(mdX - mpX); minX *= rZoom; minY *= rZoom; Zoom /= rZoom; } ctrl = false; paint = true; printPoint(); repaint(); } }; private void printPoint() { System.out.printf( "%.19e (%.19g%+.19gi) .. (%.19g%+.19gi) ", of(1, minX, minY, minX+width, minY+height).map(i->Zoom * i).toArray() ); } private class Stack extends java.util.Stack { private static final long serialVersionUID = 1L; public void push(boolean type) { push(new Object[] {type, minX, minY, Zoom}); } public boolean pop(boolean type) { for (;;) { if (empty()) return false; Object[] d = super.pop(); minX = (Integer) d[1]; minY = (Integer) d[2]; Zoom = (Double) d[3]; if (!type || (Boolean) d[0]) return true; } } } @Override public void paint(Graphics g) { if (paint) { image = newImage(); paint = false; System.out.println("done!"); } g.drawImage(image, insets.left, insets.top, this); //g.drawLine(insets.left+width/2, insets.top+0, insets.left+width/2, insets.top+height); //g.drawLine(insets.left+0, insets.top+height/2, insets.left+width, insets.top+height/2); if (!ctrl) return; g.drawRect(min(mpX, mdX), min(mpY, mdY), abs(mpX - mdX), abs(mpY - mdY)); } private BufferedImage newImage() { BufferedImage image = new BufferedImage(width, height, TYPE_INT_RGB); int maxX = minX + width; int maxY = minY + height; for (int x = minX; x < maxX; x+=1) { double r = Zoom * x; for (int y = minY; y < maxY; y+=1) { double i = Zoom * y; //System.out.printf("%+f%+fi\n", r, i); // 0f 1/6f 1/3f 1/2f 2/3f 5/6f //straight -> red yellow green cian blue magenta <- reverse image.setRGB(x-minX, y-minY, color(r, i, 360, false, 2/3f)); } } return image; } private int color(double r0, double i0, int max, boolean straight, float shift) { int n = -1; double r=0, i=0, r2=0, i2=0; do { i = r*(i+i) + i0; r = r2-i2 + r0; r2 = r*r; i2 = i*i; } while (++n < max && r2 + i2 < 4); return n == max ? black.getRGB() : HSBtoRGB(shift + (float) (straight ? n : max-n) / max /* * 11/12f + (straight ? 0f : 1/12f) */, 1, 1) ; } }