/* * The MIT License (MIT) * * Copyright (c) 2015 Camil Staps * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.camilstaps.mandelbrot; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Observable; import java.util.Observer; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.SwingWorker; /** * * @author camilstaps */ public class Grid extends JPanel implements Observer, MouseListener, MouseMotionListener { private final FractalModel fractalModel; private final BufferedImage image; private final WritableRaster raster; private final int width = 400, height = 400; private int pixelCounter; private static final int REPETITIONS_MAX = 1000, STEPS = 50; private final int[] colors; private final List updaters = new ArrayList<>(); private int start_x, start_y, old_x, old_y; private boolean dragging = false; Graphics graphics; private ProgressView progressView; private boolean useMultipleSwingWorkers = false; public Grid(FractalModel fractalModel) { this.fractalModel = fractalModel; fractalModel.addObserver(this); image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); raster = image.getRaster(); setPreferredSize(new Dimension(width, height)); colors = new int[REPETITIONS_MAX + 1]; for (int i = 0; i <=REPETITIONS_MAX; i++) { colors[i] = Color.HSBtoRGB( 0.07f, 0.5f + 0.5f * (float) i / (float) REPETITIONS_MAX, 1); } addMouseListener(this); addMouseMotionListener(this); update(fractalModel, null); } public ProgressView getProgressView() { if (progressView == null) { progressView = new ProgressView(); } return progressView; } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } public void setUseMultipleSwingWorkers(boolean value) { useMultipleSwingWorkers = value; } @Override public final synchronized void update(Observable o, Object o1) { if (updaters != null) { synchronized (updaters) { for (Updater updater : updaters) { updater.stop(); updater.cancel(true); } } } synchronized (updaters) { updaters.clear(); if (useMultipleSwingWorkers) { Updater u1 = new Updater(0, width / 2, 0, height / 2); u1.execute(); updaters.add(u1); Updater u2 = new Updater(width / 2, width, 0, height / 2); u2.execute(); updaters.add(u2); Updater u3 = new Updater(0, width / 2, height / 2, height); u3.execute(); updaters.add(u3); Updater u4 = new Updater(width / 2, width, height / 2, height); u4.execute(); updaters.add(u4); } else { Updater u = new Updater(0, width, 0, height); u.execute(); updaters.add(u); } } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(image, 0, 0, null); } public synchronized void setPixel(int x, int y, int[] rgb) { raster.setPixel(x, y, rgb); pixelCounter++; if (pixelCounter == width * height) { pixelCounter = 0; repaint(); } } protected double getRealX(int pixel_x) { return ((double) pixel_x * (fractalModel.getEndX() - fractalModel.getStartX()) / (double) width) + fractalModel.getStartX(); } protected double getRealY(int pixel_y) { return ((double) pixel_y * (fractalModel.getEndY() - fractalModel.getStartY()) / (double) height) + fractalModel.getStartY(); } @Override public void mouseClicked(MouseEvent me) { if ((me.getModifiers() & InputEvent.SHIFT_MASK) != 0) { zoomOut(me); } else { zoomIn(me); } } @Override public void mousePressed(MouseEvent me) { old_x = start_x = me.getX(); old_y = start_y = me.getY(); } @Override public synchronized void mouseReleased(MouseEvent me) { if (me.getX() != start_x || me.getY() != start_y) { zoomBox(me); } dragging = false; notifyAll(); } /** * Zoom in with factor 2 * @param me */ private void zoomIn(MouseEvent me) { double offset_x = (fractalModel.getEndX() - fractalModel.getStartX()) / 4; double offset_y = (fractalModel.getEndY() - fractalModel.getStartY()) / 4; double newCenterX = getRealX(me.getX()); fractalModel.setStartX(newCenterX - offset_x); fractalModel.setEndX(newCenterX + offset_x); double newCenterY = getRealY(me.getY()); fractalModel.setStartY(newCenterY - offset_y); fractalModel.setEndY(newCenterY + offset_y); } /** * Zoom out with factor 2 * @param me */ private void zoomOut(MouseEvent me) { double offset_x = fractalModel.getEndX() - fractalModel.getStartX(); double offset_y = fractalModel.getEndY() - fractalModel.getStartY(); double newCenterX = getRealX(me.getX()); fractalModel.setStartX(newCenterX - offset_x); fractalModel.setEndX(newCenterX + offset_x); double newCenterY = getRealY(me.getY()); fractalModel.setStartY(newCenterY - offset_y); fractalModel.setEndY(newCenterY + offset_y); } /** * Zoom to the selected box. * Intentionally chose to let the user only select squares, otherwise it's too easy to mess up the scale * @param me */ private void zoomBox(MouseEvent me) { double newStartX = getRealX(start_x), newEndX = getRealX(me.getX()), newStartY = getRealY(start_y), newEndY = getRealY(start_y + me.getX() - start_x); fractalModel.setStartX(newStartX); fractalModel.setEndX(newEndX); fractalModel.setStartY(newStartY); fractalModel.setEndY(newEndY); } @Override public void mouseEntered(MouseEvent me) {} @Override public void mouseExited(MouseEvent me) {} @Override public void mouseDragged(MouseEvent me) { Graphics g = getSafeGraphics(); if (g != null) { if (dragging) { eraseZoombox(); } g.drawRect(start_x, start_y, me.getX() - start_x, me.getX() - start_x); old_y = old_x = me.getX(); dragging = true; } } /** * Semi-singleton construction for graphics * @return */ private Graphics getSafeGraphics() { if (graphics == null) { graphics = getGraphics(); graphics.setXORMode(Color.white); } return graphics; } /** * Erase the old zoombox if it exists */ private void eraseZoombox() { if (start_x < 0 || start_y < 0 || old_x < 0 || old_y < 0) return; getSafeGraphics().drawRect(start_x, start_y, old_x - start_x, old_y - start_x); } @Override public void mouseMoved(MouseEvent me) {} protected class Updater extends SwingWorker, Map> { private boolean doneProcessing = true, stop = false; private final int start_x, end_x, start_y, end_y; public Updater(int start_x, int end_x, int start_y, int end_y) { super(); this.start_x = start_x; this.end_x = end_x; this.start_y = start_y; this.end_y = end_y; } @Override protected Map doInBackground() throws Exception { Map results = new HashMap<>(); for (int repetitions = REPETITIONS_MAX / STEPS; repetitions <= REPETITIONS_MAX && !stop; repetitions += REPETITIONS_MAX / STEPS) { for (int x = start_x; x < end_x && !stop; x++) { for (int y = start_y; y < end_y && !stop; y++) { Point p = new Point(x, y); results.put(p, fractalModel.getMandelNumber(getRealX(x), getRealY(y), repetitions)); } } // Since we're always publishing the same object, there's no point in calling publish if process() wasn't called yet if (doneProcessing && !stop) { doneProcessing = false; publish(results); } setProgress(repetitions * 100 / REPETITIONS_MAX); } return results; } @Override protected void process(List> results) { if (progressView != null) { progressView.setValue(); } for (Map resultMap : results) { if (stop || dragging) break; for (Entry result : resultMap.entrySet()) { int rgbValue = colors[result.getValue()]; int[] rgb = { (rgbValue >> 16) & 0xff, (rgbValue >> 8) & 0xff, rgbValue & 0xff }; setPixel(result.getKey().x, result.getKey().y, rgb); } } doneProcessing = true; } @Override public void done() { //updaters.remove(this); } public void stop() { stop = true; } } protected class Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object another) { if (another == null || another.getClass() != Point.class) { return false; } Point that = (Point) another; return that.x == x && that.y == y; } @Override public int hashCode() { return (x << 8) | y; } } protected class ProgressView extends JProgressBar { public ProgressView() { setMaximum(100); setMinimum(0); setValue(0); } @Override public final void setValue(int n) { setVisible(n != 100); super.setValue(n); } public void setValue() { int sum = 0, count = 0; for (Updater updater : updaters) { sum += updater.getProgress(); count++; } setValue(sum / count); } } }