544 lines
20 KiB
Java
544 lines
20 KiB
Java
import javax.swing.*;
|
|
import javax.swing.event.ChangeEvent;
|
|
import java.awt.*;
|
|
import java.awt.event.*;
|
|
import java.awt.image.BufferedImage;
|
|
import java.lang.reflect.Field;
|
|
import java.text.DecimalFormat;
|
|
|
|
public class JuliaSet {
|
|
public JFrame frame;
|
|
public JPanel panel;
|
|
public JPanel controlPanel1;
|
|
public JPanel controlPanel2;
|
|
public JPanel imagePanel;
|
|
public JPanel borderPanel1;
|
|
public JPanel borderPanel2;
|
|
public JPanel borderPanel3;
|
|
|
|
JTextField textFieldCY;
|
|
JTextField textFieldIterations;
|
|
JLabel labelZoom;
|
|
JLabel labelColorMethod;
|
|
JLabel imageLabel;
|
|
JLabel hueSliderLabel;
|
|
JLabel brightnessSliderLabel;
|
|
JLabel blackThresholdLabel;
|
|
JSlider sliderZoom;
|
|
JSlider sliderColorHue;
|
|
JSlider sliderColorBrightness;
|
|
JSlider sliderBlackThreshold;
|
|
|
|
BufferedImage juliaSetImage;
|
|
|
|
int sliderFactor;
|
|
int imageRectX;
|
|
int imageRectY;
|
|
int colorHueSliderFactor;
|
|
double colorBrightnessSliderFactor;
|
|
double blackThresholdSliderFactor;
|
|
|
|
public int juliaIterations;
|
|
public double juliaCX;
|
|
public double juliaCY;
|
|
public double juliaXAxisMin;
|
|
public double juliaXAxisMax;
|
|
public double juliaYAxisMin;
|
|
public double juliaYAxisMax;
|
|
|
|
public double distanceMinMaxX;
|
|
public double distanceMinMaxY;
|
|
public double offsetX;
|
|
public double offsetY;
|
|
public double zoom;
|
|
public double stretchFactorX;
|
|
public double stretchFactorY;
|
|
public double mouseOffsetX;
|
|
public double mouseOffsetY;
|
|
|
|
// Rounding to two decimal points
|
|
// Used for the slider
|
|
static final DecimalFormat df = new DecimalFormat("0.00");
|
|
|
|
// For the color picker
|
|
public int selectedColorMethod;
|
|
|
|
static ButtonGroup colorMethods;
|
|
static JRadioButton colorMethod1;
|
|
static JRadioButton colorMethod2;
|
|
|
|
// for the resize event
|
|
private javax.swing.Timer waitingTimer;
|
|
|
|
JuliaSet() {
|
|
initWindow();
|
|
initGuiElements();
|
|
showGUI();
|
|
drawJuliaSet();
|
|
}
|
|
|
|
public void drawJuliaSet() {
|
|
// Start timer
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
// Because the JPanel with the image on it doesn't want to reveal its own size...
|
|
int trueHeight = this.frame.getContentPane().getComponent(0).getHeight() - (this.controlPanel1.getHeight() + this.controlPanel2.getHeight() + 3 * 10);
|
|
|
|
this.imageRectX = this.imagePanel.getBounds().width;
|
|
this.imageRectY = trueHeight;
|
|
|
|
// If the user switches the variables around
|
|
double temp;
|
|
if (juliaXAxisMin > juliaXAxisMax) {
|
|
temp = juliaXAxisMax;
|
|
juliaXAxisMax = juliaXAxisMin;
|
|
juliaXAxisMin = temp;
|
|
}
|
|
if (juliaYAxisMin > juliaYAxisMax) {
|
|
temp = juliaYAxisMax;
|
|
juliaYAxisMax = juliaYAxisMin;
|
|
juliaYAxisMin = temp;
|
|
}
|
|
|
|
// All the calculations are done beforehand
|
|
// to save time. No need to calculate them
|
|
// every single time...
|
|
|
|
// zoom = slider value ^ 3
|
|
zoom = Math.pow((double) sliderFactor / 10.0, 3);
|
|
|
|
distanceMinMaxX = Math.abs(juliaXAxisMax - juliaXAxisMin);
|
|
distanceMinMaxY = Math.abs(juliaYAxisMax - juliaYAxisMin);
|
|
|
|
offsetX = (juliaXAxisMin + (Math.abs(distanceMinMaxX) / 2));
|
|
offsetY = (juliaYAxisMin + (Math.abs(distanceMinMaxY) / 2));
|
|
|
|
stretchFactorX = distanceMinMaxX / 2;
|
|
stretchFactorY = distanceMinMaxY / 2;
|
|
|
|
// Create image to draw on
|
|
juliaSetImage = new BufferedImage(this.imageRectX, this.imageRectY, BufferedImage.TYPE_INT_RGB);
|
|
|
|
int systemThreads = Runtime.getRuntime().availableProcessors();
|
|
|
|
JuliaDrawThread[] jA = new JuliaDrawThread[systemThreads];
|
|
|
|
for (int t = 0; t < systemThreads; t++) {
|
|
// Make sure that the reference is empty
|
|
jA[t] = null;
|
|
jA[t] = new JuliaDrawThread(t, systemThreads, this);
|
|
}
|
|
|
|
// Create array of threads
|
|
Thread[] tA = new Thread[systemThreads];
|
|
|
|
// here's where we
|
|
for (int t = 0; t < systemThreads; t++) {
|
|
tA[t] = new Thread(jA[t]);
|
|
tA[t].start();
|
|
}
|
|
|
|
BufferedImage[] partialImages = new BufferedImage[systemThreads];
|
|
for (int t = 0; t < systemThreads; t++) {
|
|
try {
|
|
tA[t].join();
|
|
partialImages[t] = jA[t].getPartialImage();
|
|
} catch (InterruptedException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
// At this point we've collected all the image data
|
|
// Merge into one image
|
|
int currentHeight = 0;
|
|
Graphics2D g2d = juliaSetImage.createGraphics();
|
|
for (BufferedImage pImage: partialImages) {
|
|
g2d.drawImage(pImage, 0, currentHeight, null);
|
|
currentHeight += pImage.getHeight();
|
|
}
|
|
|
|
g2d.dispose();
|
|
|
|
// Draw the image here
|
|
imageLabel.setIcon(new ImageIcon(juliaSetImage));
|
|
|
|
// End timer
|
|
long endTime = System.currentTimeMillis();
|
|
System.out.printf("Time: %dms%n", endTime - startTime);
|
|
}
|
|
|
|
public void zoomSliderStateChanged(ChangeEvent e) {
|
|
JSlider source = (JSlider) e.getSource();
|
|
sliderFactor = source.getValue();
|
|
|
|
// Rounding the number before writing it to the label
|
|
labelZoom.setText(df.format((Math.pow((double) sliderFactor / 10, 3))));
|
|
|
|
drawJuliaSet();
|
|
}
|
|
|
|
public void colorHueSliderStateChanged(ChangeEvent e) {
|
|
JSlider source = (JSlider) e.getSource();
|
|
colorHueSliderFactor = source.getValue();
|
|
drawJuliaSet();
|
|
}
|
|
|
|
public void brightnessSliderStateChanged(ChangeEvent e) {
|
|
JSlider source = (JSlider) e.getSource();
|
|
colorBrightnessSliderFactor = (float) source.getValue();
|
|
drawJuliaSet();
|
|
}
|
|
|
|
public void blackThresholdSliderStateChanged(ChangeEvent e) {
|
|
JSlider source = (JSlider) e.getSource();
|
|
blackThresholdSliderFactor = (float) source.getValue() / 250;
|
|
drawJuliaSet();
|
|
}
|
|
|
|
// Universal text field changer
|
|
// That way I don't have to re-write the float and integer parsing for every single text field
|
|
// Using reflection we just look for a variable with a matching name
|
|
|
|
// Help for key listener: https://kodejava.org/how-do-i-add-key-listener-event-handler-to-jtextfield/
|
|
public void textFieldChanged(JTextField jt, String variableToBeChanged) {
|
|
try {
|
|
// Get variable of object by name
|
|
Field f = this.getClass().getDeclaredField(variableToBeChanged);
|
|
if (f.getType().toString().equals("double")) {
|
|
try {
|
|
f.setDouble(this, Double.parseDouble(jt.getText()));
|
|
// Value changed successfully, redraw Julia set
|
|
drawJuliaSet();
|
|
} catch (NumberFormatException ex) {
|
|
jt.setText(Double.toString(f.getDouble(this)));
|
|
JOptionPane.showConfirmDialog(null, "Please only enter floating point numbers", "Floats only", JOptionPane.DEFAULT_OPTION);
|
|
} catch (IllegalAccessException e) {
|
|
// Just like the other exceptions these should never occur
|
|
// because the user doesn't have control over them
|
|
// Prints stack trace to the terminal, but the user can't see that *taps head*
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
if (f.getType().toString().equals("int")) {
|
|
try {
|
|
f.setInt(this, Integer.parseInt(jt.getText()));
|
|
drawJuliaSet();
|
|
} catch (NumberFormatException ex) {
|
|
jt.setText(Integer.toString(f.getInt(this)));
|
|
JOptionPane.showConfirmDialog(null, "Please only enter integers", "Integers only", JOptionPane.DEFAULT_OPTION);
|
|
} catch (IllegalAccessException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
// These are never supposed to be hit
|
|
// The user doesn't have control over any of this
|
|
// They're just here so IntelliJ doesn't complain
|
|
catch (NoSuchFieldException | IllegalAccessException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
class ColorRadioButtonActionListener implements ActionListener {
|
|
@Override
|
|
public void actionPerformed(ActionEvent event) {
|
|
JRadioButton button = (JRadioButton) event.getSource();
|
|
|
|
if (button == colorMethod1) {
|
|
selectedColorMethod = 1;
|
|
hueSliderLabel.setVisible(false);
|
|
brightnessSliderLabel.setVisible(false);
|
|
sliderColorBrightness.setVisible(false);
|
|
sliderColorHue.setVisible(false);
|
|
blackThresholdLabel.setVisible(false);
|
|
sliderBlackThreshold.setVisible(false);
|
|
} else if (button == colorMethod2) {
|
|
selectedColorMethod = 2;
|
|
hueSliderLabel.setVisible(true);
|
|
brightnessSliderLabel.setVisible(true);
|
|
sliderColorBrightness.setVisible(true);
|
|
sliderColorHue.setVisible(true);
|
|
blackThresholdLabel.setVisible(true);
|
|
sliderBlackThreshold.setVisible(true);
|
|
}
|
|
|
|
drawJuliaSet();
|
|
}
|
|
}
|
|
|
|
public void initGuiElements() {
|
|
this.panel = new JPanel();
|
|
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
|
|
|
// Create panels to serve as borders
|
|
borderPanel1 = new JPanel();
|
|
borderPanel2 = new JPanel();
|
|
borderPanel3 = new JPanel();
|
|
|
|
borderPanel1.setMinimumSize(new Dimension(10, 10));
|
|
borderPanel1.setPreferredSize(new Dimension(10, 10));
|
|
borderPanel2.setMinimumSize(new Dimension(10, 10));
|
|
borderPanel2.setPreferredSize(new Dimension(10, 10));
|
|
borderPanel3.setMinimumSize(new Dimension(10, 10));
|
|
borderPanel3.setPreferredSize(new Dimension(10, 10));
|
|
|
|
borderPanel1.setLayout(new BoxLayout(borderPanel1, BoxLayout.X_AXIS));
|
|
borderPanel2.setLayout(new BoxLayout(borderPanel2, BoxLayout.X_AXIS));
|
|
borderPanel3.setLayout(new BoxLayout(borderPanel3, BoxLayout.X_AXIS));
|
|
|
|
// Create JPanels for the controls at the top
|
|
this.controlPanel1 = new JPanel();
|
|
this.controlPanel2 = new JPanel();
|
|
|
|
|
|
this.controlPanel1.setLayout(new BoxLayout(this.controlPanel1, BoxLayout.X_AXIS));
|
|
this.controlPanel2.setLayout(new BoxLayout(this.controlPanel2, BoxLayout.X_AXIS));
|
|
|
|
// Add space in-between control elements
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
JLabel labelParameterC = new JLabel("Parameter C:");
|
|
this.controlPanel1.add(labelParameterC);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
JLabel labelCX = new JLabel("X");
|
|
this.controlPanel1.add(labelCX);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
JTextField textFieldCX = new JTextField("0.0");
|
|
textFieldCX.setMaximumSize(new Dimension(50, 30));
|
|
juliaCX = 0.0 f;
|
|
textFieldCX.addKeyListener(new KeyAdapter() {
|
|
public void keyReleased(KeyEvent e) {
|
|
textFieldChanged(textFieldCX, "juliaCX");
|
|
}
|
|
});
|
|
textFieldCX.setPreferredSize(new Dimension(40, 30));
|
|
textFieldCX.setMaximumSize(new Dimension(90, 30));
|
|
this.controlPanel1.add(textFieldCX);
|
|
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
JLabel labelCY = new JLabel("Y");
|
|
this.controlPanel1.add(labelCY);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
|
|
textFieldCY = new JTextField("0.0");
|
|
textFieldCY.setPreferredSize(new Dimension(40, 30));
|
|
textFieldCY.setMaximumSize(new Dimension(90, 30));
|
|
juliaCY = 0.0 f;
|
|
textFieldCY.addKeyListener(new KeyAdapter() {
|
|
public void keyReleased(KeyEvent e) {
|
|
textFieldChanged(textFieldCY, "juliaCY");
|
|
}
|
|
});
|
|
|
|
this.controlPanel1.add(textFieldCY);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
JLabel labelIterations = new JLabel("Iterations");
|
|
this.controlPanel1.add(labelIterations);
|
|
|
|
textFieldIterations = new JTextField("300");
|
|
textFieldIterations.setPreferredSize(new Dimension(40, 30));
|
|
textFieldIterations.setMaximumSize(new Dimension(90, 30));
|
|
juliaIterations = 300;
|
|
textFieldIterations.addKeyListener(new KeyAdapter() {
|
|
public void keyReleased(KeyEvent e) {
|
|
textFieldChanged(textFieldIterations, "juliaIterations");
|
|
}
|
|
});
|
|
this.controlPanel1.add(textFieldIterations);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
// Initial values to constrain the first drawn julia set
|
|
juliaXAxisMin = -2.0 f;
|
|
juliaXAxisMax = 2.0 f;
|
|
juliaYAxisMax = 2.0 f;
|
|
juliaYAxisMin = -2.0 f;
|
|
|
|
// Zoom label
|
|
labelZoom = new JLabel("Zoom");
|
|
this.controlPanel1.add(labelZoom);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
// Zoom slider
|
|
// unfortunately it only takes integers, so we'll have to divide by ten
|
|
// to get the number we actually want
|
|
sliderZoom = new JSlider(1, 1000, 10);
|
|
|
|
// + 2 because the slider feels slightly off-center otherwise
|
|
sliderZoom.addChangeListener(this::zoomSliderStateChanged);
|
|
this.controlPanel1.add(sliderZoom);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
|
|
// Text field right next to the slider
|
|
// shows the zoom factor, is not writable
|
|
labelZoom = new JLabel("1,0");
|
|
sliderFactor = 10;
|
|
this.controlPanel1.add(labelZoom);
|
|
this.controlPanel1.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
labelZoom.setMinimumSize(new Dimension(50, 30));
|
|
labelZoom.setPreferredSize(new Dimension(50, 30));
|
|
|
|
labelColorMethod = new JLabel("View:");
|
|
this.controlPanel2.add(Box.createRigidArea(new Dimension(7, 0)));
|
|
this.controlPanel2.add(labelColorMethod);
|
|
|
|
// Create buttons
|
|
colorMethod1 = new JRadioButton();
|
|
this.controlPanel2.add(colorMethod1);
|
|
|
|
// Set this one to the default
|
|
// We do this before we add the item listener
|
|
// No need to draw the set twice just because
|
|
// we clicked a button
|
|
colorMethod1.doClick();
|
|
selectedColorMethod = 1;
|
|
|
|
colorMethod2 = new JRadioButton();
|
|
this.controlPanel2.add(colorMethod2);
|
|
|
|
// Create group for the radio buttons
|
|
colorMethods = new ButtonGroup();
|
|
colorMethods.add(colorMethod1);
|
|
colorMethods.add(colorMethod2);
|
|
|
|
// Add listener for the buttons
|
|
ColorRadioButtonActionListener colorActionListener = new ColorRadioButtonActionListener();
|
|
colorMethod1.addActionListener(colorActionListener);
|
|
colorMethod2.addActionListener(colorActionListener);
|
|
|
|
// "H" for the slider
|
|
hueSliderLabel = new JLabel("H ");
|
|
this.controlPanel2.add(hueSliderLabel);
|
|
hueSliderLabel.setVisible(false);
|
|
|
|
// Hue slider
|
|
sliderColorHue = new JSlider(0, 100, 0);
|
|
// + 2 because the slider feels slightly off-center otherwise
|
|
sliderColorHue.addChangeListener(this::colorHueSliderStateChanged);
|
|
sliderColorHue.setVisible(false);
|
|
this.controlPanel2.add(sliderColorHue);
|
|
|
|
brightnessSliderLabel = new JLabel("B ");
|
|
this.controlPanel2.add(brightnessSliderLabel);
|
|
brightnessSliderLabel.setVisible(false);
|
|
|
|
// Brightness slider
|
|
sliderColorBrightness = new JSlider(1, 100, 1);
|
|
// + 2 because the slider feels slightly off-center otherwise
|
|
sliderColorBrightness.addChangeListener(this::brightnessSliderStateChanged);
|
|
sliderColorBrightness.setVisible(false);
|
|
this.controlPanel2.add(sliderColorBrightness);
|
|
colorBrightnessSliderFactor = 1;
|
|
|
|
// Black threshold label
|
|
blackThresholdLabel = new JLabel("Threshold ");
|
|
blackThresholdLabel.setVisible(false);
|
|
this.controlPanel2.add(blackThresholdLabel);
|
|
|
|
// Black threshold label
|
|
sliderBlackThreshold = new JSlider(0, 100, 0);
|
|
sliderBlackThreshold.addChangeListener(this::blackThresholdSliderStateChanged);
|
|
sliderBlackThreshold.setVisible(false);
|
|
this.controlPanel2.add(sliderBlackThreshold);
|
|
|
|
// Draw image onto label
|
|
// Every time setIcon() is called the image updates
|
|
imagePanel = new JPanel();
|
|
imageLabel = new JLabel();
|
|
this.imagePanel.add(imageLabel);
|
|
|
|
System.gc();
|
|
}
|
|
|
|
public void initWindow() {
|
|
frame = new JFrame();
|
|
frame.setTitle("Julia Set");
|
|
|
|
// Windows UI
|
|
try {
|
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
|
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
|
|
ex.printStackTrace();
|
|
}
|
|
|
|
frame.setMinimumSize(new Dimension(800, 800));
|
|
|
|
frame.setLocationRelativeTo(null);
|
|
|
|
// Actually make sure that the window gets closed when we press the X button
|
|
frame.addWindowListener(new WindowAdapter() {
|
|
public void windowClosing(WindowEvent we) {
|
|
we.getWindow().dispose(); // Throw away the window
|
|
System.exit(0); // Exit JVM
|
|
}
|
|
});
|
|
}
|
|
|
|
public void actionPerformed(ActionEvent ae) {
|
|
if (ae.getSource() == this.waitingTimer) {
|
|
//Stop timer, event has ended
|
|
this.waitingTimer.stop();
|
|
this.waitingTimer = null;
|
|
|
|
this.imagePanel.setPreferredSize(new Dimension(this.imagePanel.getBounds().width, this.imagePanel.getBounds().height));
|
|
drawJuliaSet();
|
|
}
|
|
}
|
|
|
|
public void showGUI() {
|
|
this.panel.add(borderPanel1);
|
|
this.panel.add(controlPanel1);
|
|
this.panel.add(borderPanel2);
|
|
this.panel.add(controlPanel2);
|
|
this.panel.add(borderPanel3);
|
|
this.panel.add(imagePanel);
|
|
this.frame.add(this.panel);
|
|
this.frame.setVisible(true);
|
|
|
|
// Implement timer to wait until the resize event is over
|
|
this.frame.addComponentListener(new ComponentAdapter() {
|
|
public void componentResized(ComponentEvent componentEvent) {
|
|
if (waitingTimer == null) {
|
|
waitingTimer = new Timer(50, JuliaSet.this::actionPerformed);
|
|
waitingTimer.start();
|
|
} else {
|
|
waitingTimer.restart();
|
|
}
|
|
}
|
|
});
|
|
|
|
MouseAdapter ma = new MouseAdapter() {
|
|
private Point startPoint;
|
|
|
|
@Override
|
|
public void mousePressed(MouseEvent e) {
|
|
startPoint = e.getPoint();
|
|
}
|
|
|
|
@Override
|
|
public void mouseReleased(MouseEvent e) {
|
|
Point p = e.getPoint();
|
|
mouseOffsetX += (p.x - startPoint.x) / zoom;
|
|
mouseOffsetY += (p.y - startPoint.y) / zoom;
|
|
startPoint = null;
|
|
drawJuliaSet();
|
|
}
|
|
|
|
@Override
|
|
public void mouseWheelMoved(MouseWheelEvent e) {
|
|
sliderZoom.setValue(sliderZoom.getValue() - e.getWheelRotation());
|
|
}
|
|
};
|
|
|
|
frame.addMouseListener(ma);
|
|
frame.addMouseMotionListener(ma);
|
|
frame.addMouseWheelListener(ma);
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
new JuliaSet();
|
|
}
|
|
} |