commit 4cba9cd8ba7972465b830259b5826f64964b873c Author: Wain Date: Tue Apr 5 14:26:09 2022 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21b4487 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Project exclude paths +/out/ \ No newline at end of file diff --git a/src/com/alterdekim/fractals/Canvas.java b/src/com/alterdekim/fractals/Canvas.java new file mode 100644 index 0000000..70f43ab --- /dev/null +++ b/src/com/alterdekim/fractals/Canvas.java @@ -0,0 +1,208 @@ +package com.alterdekim.fractals; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import javax.swing.JPanel; + +public class Canvas extends JPanel implements KeyListener { + + private static final long serialVersionUID = 1L; + + private static final int MAX_ITER = 200; + + private static double RE_START = -2; + private static double RE_END = 0.5; + private static double IM_START = -1; + private static double IM_END = 1; + + private double juliaRez = 0; + private double juliaImz = 0; + + private int mArg = 3; + + private double sizeX = 1; + private double sizeY = 1; + + private int type = 0; + + public Canvas() { + this.addKeyListener(this); + } + + public void setInterval( double r_s, double r_e, double i_s, double i_e, double sX, double sY ) { + Canvas.RE_START = r_s; + Canvas.RE_END = r_e; + Canvas.IM_START = i_s; + Canvas.IM_END = i_e; + this.sizeX = sX; + this.sizeY = sY; + } + + private int mandelbrot( Complex c ) { + Complex z = new Complex(0,0); + int n = 0; + while ( ComplexUtils.abs(z) <= 2 && n < MAX_ITER) { + z = ComplexUtils.pow(z); + z = ComplexUtils.sum(z, c); + n++; + } + return n; + } + + private int bship( Complex c ) { + Complex z = new Complex(0,0); + int n = 0; + while ( ComplexUtils.abs(z) <= 2 && n < MAX_ITER) { + z = new Complex( Math.abs(z.Rez), Math.abs(z.Imz) ); + z = ComplexUtils.pow(z); + z = ComplexUtils.sum(z, c); + n++; + } + return n; + } + + private int customMandelbrot( Complex c ) { + Complex z = new Complex(0,0); + int n = 0; + while( ComplexUtils.abs(z) <= 2 && n < MAX_ITER ) { + z = ComplexUtils.pow(z, mArg); + z = ComplexUtils.sum(z, c); + n++; + } + return n; + } + + private int julia( Complex c, Complex j ) { + Complex z = c; + c = j; + int n = 0; + while ( ComplexUtils.abs(z) <= 2 && n < MAX_ITER) { + z = ComplexUtils.pow(z); + z = ComplexUtils.sum(z, c); + n++; + } + return n; + } + + public void paintMandel() { + this.type = 0; + this.repaint(); + } + + public void paintJulia( String arg, String _arg ) { + this.type = 1; + this.juliaRez = Double.parseDouble(arg); + this.juliaImz = Double.parseDouble(_arg); + this.repaint(); + } + + public void paintBurningship() { + this.type = 2; + this.repaint(); + } + + public void paintCustom( String arg ) { + this.mArg = Integer.parseInt(arg); + this.type = 3; + this.repaint(); + } + + @Override + public void paintComponent( Graphics g ) { + int width = this.getWidth(); + int height = this.getHeight(); + for( int x = 0; x < width; x++ ) { + for( int y = 0; y < height; y++ ) { + Complex c = new Complex( ((((x) * (RE_END-RE_START)) / (width) ) + RE_START) / sizeX, ((((y) * (IM_END-IM_START)) / (height)) + IM_START) / sizeY ); + float color = 0.0f; + if( this.type == 1 ) { + color = ((float) julia(c, new Complex(juliaRez, juliaImz))) / ((float) MAX_ITER); + } else if( this.type == 0 ) { + color = ((float) mandelbrot(c)) / ((float) MAX_ITER); + } else if( this.type == 2 ) { + color = ((float) bship(c)) / ((float) MAX_ITER); + } else if( this.type == 3 ) { + color = ((float) customMandelbrot(c)) / ((float) MAX_ITER); + } + + float val = rangeConvert( color, 0f, 1f, Window.hueStart, Window.hueEnd ) * 0.01f; + float sat = rangeConvert( color, 0f, 1f, Window.satStart, Window.satEnd ) * 0.01f; + g.setColor(Color.getHSBColor(val, sat, 1 - val)); + g.fillRect(x, y, 1, 1); + } + } + } + + public static float rangeConvert( float value, float leftMin, float leftMax, float rightMin, float rightMax ) { + float leftSpan = leftMax - leftMin; + float rightSpan = rightMax - rightMin; + float valueScaled = (value - leftMin) / (leftSpan); + return rightMin + (valueScaled * rightSpan); + } + + private static class ComplexUtils { + public static double abs( Complex c ) { + return Math.sqrt( Math.pow(c.Rez, 2) + Math.pow(c.Imz, 2) ); + } + + public static Complex sum( Complex f, Complex s ) { + return new Complex( f.Rez + s.Rez, f.Imz + s.Imz ); + } + + public static Complex pow( Complex c ) { + return new Complex((c.Rez * c.Rez) + ((-1) * (c.Imz * c.Imz)), (2 * c.Rez * c.Imz)); + } + + public static Complex pow( Complex c, int num ) { + Complex n = c; + for( int i = 0; i < num; i++ ) { + n = ComplexUtils.multiply(n, c); + } + return n; + } + + public static Complex multiply( Complex f, Complex s ) { + return new Complex(((f.Rez * s.Rez)-(f.Imz*s.Imz)), (f.Rez * s.Imz) + (f.Imz * s.Rez)); + } + + } + + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) { + if( e.getKeyCode() == KeyEvent.VK_RIGHT ) { + RE_START += 0.01/sizeX; + RE_END += 0.01/sizeY; + this.repaint(); + } else if( e.getKeyCode() == KeyEvent.VK_LEFT ) { + RE_START -= 0.01/sizeX; + RE_END -= 0.01/sizeY; + this.repaint(); + } else if( e.getKeyCode() == KeyEvent.VK_UP ) { + IM_START -= 0.01/sizeX; + IM_END -= 0.01/sizeY; + this.repaint(); + } else if( e.getKeyCode() == KeyEvent.VK_DOWN ) { + IM_START += 0.01/sizeX; + IM_END += 0.01/sizeY; + this.repaint(); + } else if( e.getKeyCode() == KeyEvent.VK_P ) { + sizeX += 0.01; + sizeY += 0.01; + this.repaint(); + } else if( e.getKeyCode() == KeyEvent.VK_O ) { + sizeX -= 0.01; + sizeY -= 0.01; + this.repaint(); + } + } + + @Override + public void keyReleased(KeyEvent e) {} +} diff --git a/src/com/alterdekim/fractals/Complex.java b/src/com/alterdekim/fractals/Complex.java new file mode 100644 index 0000000..f2aff0b --- /dev/null +++ b/src/com/alterdekim/fractals/Complex.java @@ -0,0 +1,11 @@ +package com.alterdekim.fractals; + +public class Complex { + public double Rez; + public double Imz; + + public Complex( double Rez, double Imz ) { + this.Rez = Rez; + this.Imz = Imz; + } +} \ No newline at end of file diff --git a/src/com/alterdekim/fractals/GradientChoice.java b/src/com/alterdekim/fractals/GradientChoice.java new file mode 100644 index 0000000..87a8848 --- /dev/null +++ b/src/com/alterdekim/fractals/GradientChoice.java @@ -0,0 +1,133 @@ +package com.alterdekim.fractals; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.JLabel; +import javax.swing.JSlider; +import java.awt.Dialog.ModalityType; + +public class GradientChoice extends JDialog { + + private static final long serialVersionUID = 1L; + + private final JPanel contentPanel = new JPanel(); + + public boolean isOk = false; + + /** + * Launch the application. + */ + /*public static void main(String[] args) { + try { + GradientChoice dialog = new GradientChoice(); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + }*/ + + /** + * Create the dialog. + */ + public GradientChoice() { + setModalityType(ModalityType.APPLICATION_MODAL); + setBounds(100, 100, 350, 300); + setResizable(false); + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(null); + + JLabel lblNewLabel = new JLabel("Hue:"); + lblNewLabel.setBounds(6, 6, 338, 25); + contentPanel.add(lblNewLabel); + + HuePanel huePanel = new HuePanel(); + huePanel.setBounds(6, 36, 338, 40); + contentPanel.add(huePanel); + huePanel.endX = 338; + huePanel.repaint(); + + RangeSlider hueSlider = new RangeSlider(); + hueSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + huePanel.startX = (int) ((((double)hueSlider.getValue()) / ((double)100)) * ((double)huePanel.getWidth())); + huePanel.endX = (int) ((((double)hueSlider.getUpperValue()) / ((double)100)) * ((double)huePanel.getWidth())); + huePanel.repaint(); + } + }); + hueSlider.setValue((int)Window.hueStart); + hueSlider.setUpperValue((int)Window.hueEnd); + hueSlider.setBounds(6, 81, 338, 29); + contentPanel.add(hueSlider); + huePanel.repaint(); + + JLabel lblNewSat = new JLabel("Saturation:"); + lblNewSat.setBounds(6, 115, 338, 25); + contentPanel.add(lblNewSat); + + SatPanel satPanel = new SatPanel(); + satPanel.setBounds(6, 140, 338, 40); + contentPanel.add(satPanel); + satPanel.endX = 338; + satPanel.repaint(); + + RangeSlider satSlider = new RangeSlider(); + satSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + satPanel.startX = (int) ((((double)satSlider.getValue()) / ((double)100)) * ((double)satPanel.getWidth())); + satPanel.endX = (int) ((((double)satSlider.getUpperValue()) / ((double)100)) * ((double)satPanel.getWidth())); + satPanel.repaint(); + } + }); + satSlider.setValue((int)Window.satStart); + satSlider.setUpperValue((int)Window.satEnd); + satSlider.setBounds(6, 180, 338, 29); + contentPanel.add(satSlider); + satPanel.repaint(); + + { + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + { + JButton okButton = new JButton("OK"); + okButton.setActionCommand("OK"); + buttonPane.add(okButton); + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Window.satStart = Canvas.rangeConvert(satPanel.startX, 0, 338, 0, 100); + Window.satEnd = Canvas.rangeConvert(satPanel.endX, 0, 338, 0, 100); + Window.hueStart = Canvas.rangeConvert(huePanel.startX, 0, 338, 0, 100); + Window.hueEnd = Canvas.rangeConvert(huePanel.endX, 0, 338, 0, 100); + isOk = true; + dispose(); + } + }); + getRootPane().setDefaultButton(okButton); + } + { + JButton cancelButton = new JButton("Cancel"); + cancelButton.setActionCommand("Cancel"); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dispose(); + } + }); + buttonPane.add(cancelButton); + } + } + } +} diff --git a/src/com/alterdekim/fractals/HuePanel.java b/src/com/alterdekim/fractals/HuePanel.java new file mode 100644 index 0000000..9ee90ab --- /dev/null +++ b/src/com/alterdekim/fractals/HuePanel.java @@ -0,0 +1,24 @@ +package com.alterdekim.fractals; + +import java.awt.Color; +import java.awt.Graphics; + +import javax.swing.JPanel; + +public class HuePanel extends JPanel { + + private static final long serialVersionUID = 1L; + + public int startX = 0; + public int endX = getWidth(); + + @Override + public void paintComponent(Graphics g) { + float color = ((float) 1) / ((float) getWidth()) * ((float) startX); + for( int x = startX; x < endX; x++ ) { + g.setColor(Color.getHSBColor(color, 1f, 0.7f)); + g.fillRect(x, 0, 1, getHeight()); + color += ((float) 1) / ((float) getWidth()); + } + } +} diff --git a/src/com/alterdekim/fractals/RangeSlider.java b/src/com/alterdekim/fractals/RangeSlider.java new file mode 100644 index 0000000..bd93878 --- /dev/null +++ b/src/com/alterdekim/fractals/RangeSlider.java @@ -0,0 +1,103 @@ +package com.alterdekim.fractals; + +import javax.swing.JSlider; + +/** + * An extension of JSlider to select a range of values using two thumb controls. + * The thumb controls are used to select the lower and upper value of a range + * with predetermined minimum and maximum values. + * + *

Note that RangeSlider makes use of the default BoundedRangeModel, which + * supports an inner range defined by a value and an extent. The upper value + * returned by RangeSlider is simply the lower value plus the extent.

+ */ +public class RangeSlider extends JSlider { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a RangeSlider with default minimum and maximum values of 0 + * and 100. + */ + public RangeSlider() { + initSlider(); + } + + /** + * Constructs a RangeSlider with the specified default minimum and maximum + * values. + */ + public RangeSlider(int min, int max) { + super(min, max); + initSlider(); + } + + /** + * Initializes the slider by setting default properties. + */ + private void initSlider() { + setOrientation(HORIZONTAL); + } + + /** + * Overrides the superclass method to install the UI delegate to draw two + * thumbs. + */ + @Override + public void updateUI() { + setUI(new RangeSliderUI(this)); + // Update UI for slider labels. This must be called after updating the + // UI of the slider. Refer to JSlider.updateUI(). + updateLabelUIs(); + } + + /** + * Returns the lower value in the range. + */ + @Override + public int getValue() { + return super.getValue(); + } + + /** + * Sets the lower value in the range. + */ + @Override + public void setValue(int value) { + int oldValue = getValue(); + if (oldValue == value) { + return; + } + + // Compute new value and extent to maintain upper value. + int oldExtent = getExtent(); + int newValue = Math.min(Math.max(getMinimum(), value), oldValue + oldExtent); + int newExtent = oldExtent + oldValue - newValue; + + // Set new value and extent, and fire a single change event. + getModel().setRangeProperties(newValue, newExtent, getMinimum(), + getMaximum(), getValueIsAdjusting()); + } + + /** + * Returns the upper value in the range. + */ + public int getUpperValue() { + return getValue() + getExtent(); + } + + /** + * Sets the upper value in the range. + */ + public void setUpperValue(int value) { + // Compute new extent. + int lowerValue = getValue(); + int newExtent = Math.min(Math.max(0, value - lowerValue), getMaximum() - lowerValue); + + // Set extent to set upper value. + setExtent(newExtent); + } +} \ No newline at end of file diff --git a/src/com/alterdekim/fractals/RangeSliderUI.java b/src/com/alterdekim/fractals/RangeSliderUI.java new file mode 100644 index 0000000..bd60d79 --- /dev/null +++ b/src/com/alterdekim/fractals/RangeSliderUI.java @@ -0,0 +1,596 @@ +package com.alterdekim.fractals; + + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Ellipse2D; + +import javax.swing.JComponent; +import javax.swing.JSlider; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicSliderUI; + +/** + * UI delegate for the RangeSlider component. RangeSliderUI paints two thumbs, + * one for the lower value and one for the upper value. + */ +class RangeSliderUI extends BasicSliderUI { + + /** Color of selected range. */ + private Color rangeColor = Color.BLACK; + + /** Location and size of thumb for upper value. */ + private Rectangle upperThumbRect; + /** Indicator that determines whether upper thumb is selected. */ + private boolean upperThumbSelected; + + /** Indicator that determines whether lower thumb is being dragged. */ + private transient boolean lowerDragging; + /** Indicator that determines whether upper thumb is being dragged. */ + private transient boolean upperDragging; + + /** + * Constructs a RangeSliderUI for the specified slider component. + * @param b RangeSlider + */ + public RangeSliderUI(RangeSlider b) { + super(b); + } + + /** + * Installs this UI delegate on the specified component. + */ + @Override + public void installUI(JComponent c) { + upperThumbRect = new Rectangle(); + super.installUI(c); + } + + /** + * Creates a listener to handle track events in the specified slider. + */ + @Override + protected TrackListener createTrackListener(JSlider slider) { + return new RangeTrackListener(); + } + + /** + * Creates a listener to handle change events in the specified slider. + */ + @Override + protected ChangeListener createChangeListener(JSlider slider) { + return new ChangeHandler(); + } + + /** + * Updates the dimensions for both thumbs. + */ + @Override + protected void calculateThumbSize() { + // Call superclass method for lower thumb size. + super.calculateThumbSize(); + + // Set upper thumb size. + upperThumbRect.setSize(thumbRect.width, thumbRect.height); + } + + /** + * Updates the locations for both thumbs. + */ + @Override + protected void calculateThumbLocation() { + // Call superclass method for lower thumb location. + super.calculateThumbLocation(); + + // Adjust upper value to snap to ticks if necessary. + if (slider.getSnapToTicks()) { + int upperValue = slider.getValue() + slider.getExtent(); + int snappedValue = upperValue; + int majorTickSpacing = slider.getMajorTickSpacing(); + int minorTickSpacing = slider.getMinorTickSpacing(); + int tickSpacing = 0; + + if (minorTickSpacing > 0) { + tickSpacing = minorTickSpacing; + } else if (majorTickSpacing > 0) { + tickSpacing = majorTickSpacing; + } + + if (tickSpacing != 0) { + // If it's not on a tick, change the value + if ((upperValue - slider.getMinimum()) % tickSpacing != 0) { + float temp = (float)(upperValue - slider.getMinimum()) / (float)tickSpacing; + int whichTick = Math.round(temp); + snappedValue = slider.getMinimum() + (whichTick * tickSpacing); + } + + if (snappedValue != upperValue) { + slider.setExtent(snappedValue - slider.getValue()); + } + } + } + + // Calculate upper thumb location. The thumb is centered over its + // value on the track. + if (slider.getOrientation() == JSlider.HORIZONTAL) { + int upperPosition = xPositionForValue(slider.getValue() + slider.getExtent()); + upperThumbRect.x = upperPosition - (upperThumbRect.width / 2); + upperThumbRect.y = trackRect.y; + + } else { + int upperPosition = yPositionForValue(slider.getValue() + slider.getExtent()); + upperThumbRect.x = trackRect.x; + upperThumbRect.y = upperPosition - (upperThumbRect.height / 2); + } + } + + /** + * Returns the size of a thumb. + */ + @Override + protected Dimension getThumbSize() { + return new Dimension(12, 12); + } + + /** + * Paints the slider. The selected thumb is always painted on top of the + * other thumb. + */ + @Override + public void paint(Graphics g, JComponent c) { + super.paint(g, c); + + Rectangle clipRect = g.getClipBounds(); + if (upperThumbSelected) { + // Paint lower thumb first, then upper thumb. + if (clipRect.intersects(thumbRect)) { + paintLowerThumb(g); + } + if (clipRect.intersects(upperThumbRect)) { + paintUpperThumb(g); + } + + } else { + // Paint upper thumb first, then lower thumb. + if (clipRect.intersects(upperThumbRect)) { + paintUpperThumb(g); + } + if (clipRect.intersects(thumbRect)) { + paintLowerThumb(g); + } + } + } + + /** + * Paints the track. + */ + @Override + public void paintTrack(Graphics g) { + // Draw track. + super.paintTrack(g); + + Rectangle trackBounds = trackRect; + + if (slider.getOrientation() == JSlider.HORIZONTAL) { + // Determine position of selected range by moving from the middle + // of one thumb to the other. + int lowerX = thumbRect.x + (thumbRect.width / 2); + int upperX = upperThumbRect.x + (upperThumbRect.width / 2); + + // Determine track position. + int cy = (trackBounds.height / 2) - 2; + + // Save color and shift position. + Color oldColor = g.getColor(); + g.translate(trackBounds.x, trackBounds.y + cy); + + // Draw selected range. + g.setColor(rangeColor); + for (int y = 0; y <= 3; y++) { + g.drawLine(lowerX - trackBounds.x, y, upperX - trackBounds.x, y); + } + + // Restore position and color. + g.translate(-trackBounds.x, -(trackBounds.y + cy)); + g.setColor(oldColor); + + } else { + // Determine position of selected range by moving from the middle + // of one thumb to the other. + int lowerY = thumbRect.x + (thumbRect.width / 2); + int upperY = upperThumbRect.x + (upperThumbRect.width / 2); + + // Determine track position. + int cx = (trackBounds.width / 2) - 2; + + // Save color and shift position. + Color oldColor = g.getColor(); + g.translate(trackBounds.x + cx, trackBounds.y); + + // Draw selected range. + g.setColor(rangeColor); + for (int x = 0; x <= 3; x++) { + g.drawLine(x, lowerY - trackBounds.y, x, upperY - trackBounds.y); + } + + // Restore position and color. + g.translate(-(trackBounds.x + cx), -trackBounds.y); + g.setColor(oldColor); + } + } + + /** + * Overrides superclass method to do nothing. Thumb painting is handled + * within the paint() method. + */ + @Override + public void paintThumb(Graphics g) { + // Do nothing. + } + + /** + * Paints the thumb for the lower value using the specified graphics object. + */ + private void paintLowerThumb(Graphics g) { + Rectangle knobBounds = thumbRect; + int w = knobBounds.width; + int h = knobBounds.height; + + // Create graphics copy. + Graphics2D g2d = (Graphics2D) g.create(); + + // Create default thumb shape. + Shape thumbShape = createThumbShape(w - 1, h - 1); + + // Draw thumb. + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g2d.translate(knobBounds.x, knobBounds.y); + + g2d.setColor(Color.GRAY); + g2d.fill(thumbShape); + + g2d.setColor(Color.GRAY); + g2d.draw(thumbShape); + + // Dispose graphics. + g2d.dispose(); + } + + /** + * Paints the thumb for the upper value using the specified graphics object. + */ + private void paintUpperThumb(Graphics g) { + Rectangle knobBounds = upperThumbRect; + int w = knobBounds.width; + int h = knobBounds.height; + + // Create graphics copy. + Graphics2D g2d = (Graphics2D) g.create(); + + // Create default thumb shape. + Shape thumbShape = createThumbShape(w - 1, h - 1); + + // Draw thumb. + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g2d.translate(knobBounds.x, knobBounds.y); + + g2d.setColor(Color.GRAY); + g2d.fill(thumbShape); + + g2d.setColor(Color.GRAY); + g2d.draw(thumbShape); + + // Dispose graphics. + g2d.dispose(); + } + + /** + * Returns a Shape representing a thumb. + */ + private Shape createThumbShape(int width, int height) { + // Use circular shape. + Ellipse2D shape = new Ellipse2D.Double(0, 0, width, height); + return shape; + } + + /** + * Sets the location of the upper thumb, and repaints the slider. This is + * called when the upper thumb is dragged to repaint the slider. The + * setThumbLocation() method performs the same task for the + * lower thumb. + */ + private void setUpperThumbLocation(int x, int y) { + Rectangle upperUnionRect = new Rectangle(); + upperUnionRect.setBounds(upperThumbRect); + + upperThumbRect.setLocation(x, y); + + SwingUtilities.computeUnion(upperThumbRect.x, upperThumbRect.y, upperThumbRect.width, upperThumbRect.height, upperUnionRect); + slider.repaint(upperUnionRect.x, upperUnionRect.y, upperUnionRect.width, upperUnionRect.height); + } + + /** + * Moves the selected thumb in the specified direction by a block increment. + * This method is called when the user presses the Page Up or Down keys. + */ + public void scrollByBlock(int direction) { + synchronized (slider) { + int blockIncrement = (slider.getMaximum() - slider.getMinimum()) / 10; + if (blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum()) { + blockIncrement = 1; + } + int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL); + + if (upperThumbSelected) { + int oldValue = ((RangeSlider) slider).getUpperValue(); + ((RangeSlider) slider).setUpperValue(oldValue + delta); + } else { + int oldValue = slider.getValue(); + slider.setValue(oldValue + delta); + } + } + } + + /** + * Moves the selected thumb in the specified direction by a unit increment. + * This method is called when the user presses one of the arrow keys. + */ + public void scrollByUnit(int direction) { + synchronized (slider) { + int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL); + + if (upperThumbSelected) { + int oldValue = ((RangeSlider) slider).getUpperValue(); + ((RangeSlider) slider).setUpperValue(oldValue + delta); + } else { + int oldValue = slider.getValue(); + slider.setValue(oldValue + delta); + } + } + } + + /** + * Listener to handle model change events. This calculates the thumb + * locations and repaints the slider if the value change is not caused by + * dragging a thumb. + */ + public class ChangeHandler implements ChangeListener { + public void stateChanged(ChangeEvent arg0) { + if (!lowerDragging && !upperDragging) { + calculateThumbLocation(); + slider.repaint(); + } + } + } + + /** + * Listener to handle mouse movements in the slider track. + */ + public class RangeTrackListener extends TrackListener { + + @Override + public void mousePressed(MouseEvent e) { + if (!slider.isEnabled()) { + return; + } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + + if (slider.isRequestFocusEnabled()) { + slider.requestFocus(); + } + + // Determine which thumb is pressed. If the upper thumb is + // selected (last one dragged), then check its position first; + // otherwise check the position of the lower thumb first. + boolean lowerPressed = false; + boolean upperPressed = false; + if (upperThumbSelected || slider.getMinimum() == slider.getValue()) { + if (upperThumbRect.contains(currentMouseX, currentMouseY)) { + upperPressed = true; + } else if (thumbRect.contains(currentMouseX, currentMouseY)) { + lowerPressed = true; + } + } else { + if (thumbRect.contains(currentMouseX, currentMouseY)) { + lowerPressed = true; + } else if (upperThumbRect.contains(currentMouseX, currentMouseY)) { + upperPressed = true; + } + } + + // Handle lower thumb pressed. + if (lowerPressed) { + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + offset = currentMouseY - thumbRect.y; + break; + case JSlider.HORIZONTAL: + offset = currentMouseX - thumbRect.x; + break; + } + upperThumbSelected = false; + lowerDragging = true; + return; + } + lowerDragging = false; + + // Handle upper thumb pressed. + if (upperPressed) { + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + offset = currentMouseY - upperThumbRect.y; + break; + case JSlider.HORIZONTAL: + offset = currentMouseX - upperThumbRect.x; + break; + } + upperThumbSelected = true; + upperDragging = true; + return; + } + upperDragging = false; + } + + @Override + public void mouseReleased(MouseEvent e) { + lowerDragging = false; + upperDragging = false; + slider.setValueIsAdjusting(false); + super.mouseReleased(e); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (!slider.isEnabled()) { + return; + } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + + if (lowerDragging) { + slider.setValueIsAdjusting(true); + moveLowerThumb(); + + } else if (upperDragging) { + slider.setValueIsAdjusting(true); + moveUpperThumb(); + } + } + + @Override + public boolean shouldScroll(int direction) { + return false; + } + + /** + * Moves the location of the lower thumb, and sets its corresponding + * value in the slider. + */ + private void moveLowerThumb() { + int thumbMiddle = 0; + + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + int halfThumbHeight = thumbRect.height / 2; + int thumbTop = currentMouseY - offset; + int trackTop = trackRect.y; + int trackBottom = trackRect.y + (trackRect.height - 1); + int vMax = yPositionForValue(slider.getValue() + slider.getExtent()); + + // Apply bounds to thumb position. + if (drawInverted()) { + trackBottom = vMax; + } else { + trackTop = vMax; + } + thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight); + thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight); + + setThumbLocation(thumbRect.x, thumbTop); + + // Update slider value. + thumbMiddle = thumbTop + halfThumbHeight; + slider.setValue(valueForYPosition(thumbMiddle)); + break; + + case JSlider.HORIZONTAL: + int halfThumbWidth = thumbRect.width / 2; + int thumbLeft = currentMouseX - offset; + int trackLeft = trackRect.x; + int trackRight = trackRect.x + (trackRect.width - 1); + int hMax = xPositionForValue(slider.getValue() + slider.getExtent()); + + // Apply bounds to thumb position. + if (drawInverted()) { + trackLeft = hMax; + } else { + trackRight = hMax; + } + thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth); + thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth); + + setThumbLocation(thumbLeft, thumbRect.y); + + // Update slider value. + thumbMiddle = thumbLeft + halfThumbWidth; + slider.setValue(valueForXPosition(thumbMiddle)); + break; + + default: + return; + } + } + + /** + * Moves the location of the upper thumb, and sets its corresponding + * value in the slider. + */ + private void moveUpperThumb() { + int thumbMiddle = 0; + + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + int halfThumbHeight = thumbRect.height / 2; + int thumbTop = currentMouseY - offset; + int trackTop = trackRect.y; + int trackBottom = trackRect.y + (trackRect.height - 1); + int vMin = yPositionForValue(slider.getValue()); + + // Apply bounds to thumb position. + if (drawInverted()) { + trackTop = vMin; + } else { + trackBottom = vMin; + } + thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight); + thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight); + + setUpperThumbLocation(thumbRect.x, thumbTop); + + // Update slider extent. + thumbMiddle = thumbTop + halfThumbHeight; + slider.setExtent(valueForYPosition(thumbMiddle) - slider.getValue()); + break; + + case JSlider.HORIZONTAL: + int halfThumbWidth = thumbRect.width / 2; + int thumbLeft = currentMouseX - offset; + int trackLeft = trackRect.x; + int trackRight = trackRect.x + (trackRect.width - 1); + int hMin = xPositionForValue(slider.getValue()); + + // Apply bounds to thumb position. + if (drawInverted()) { + trackRight = hMin; + } else { + trackLeft = hMin; + } + thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth); + thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth); + + setUpperThumbLocation(thumbLeft, thumbRect.y); + + // Update slider extent. + thumbMiddle = thumbLeft + halfThumbWidth; + slider.setExtent(valueForXPosition(thumbMiddle) - slider.getValue()); + break; + + default: + return; + } + } + } +} \ No newline at end of file diff --git a/src/com/alterdekim/fractals/SatPanel.java b/src/com/alterdekim/fractals/SatPanel.java new file mode 100644 index 0000000..4b992f4 --- /dev/null +++ b/src/com/alterdekim/fractals/SatPanel.java @@ -0,0 +1,24 @@ +package com.alterdekim.fractals; + +import java.awt.Color; +import java.awt.Graphics; + +import javax.swing.JPanel; + +public class SatPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + public int startX = 0; + public int endX = getWidth(); + + @Override + public void paintComponent(Graphics g) { + float color = ((float) 1) / ((float) getWidth()) * ((float) startX); + for( int x = startX; x < endX; x++ ) { + g.setColor(Color.getHSBColor(0f, color, 0.7f)); + g.fillRect(x, 0, 1, getHeight()); + color += ((float) 1) / ((float) getWidth()); + } + } +} diff --git a/src/com/alterdekim/fractals/Window.java b/src/com/alterdekim/fractals/Window.java new file mode 100644 index 0000000..6422898 --- /dev/null +++ b/src/com/alterdekim/fractals/Window.java @@ -0,0 +1,201 @@ +package com.alterdekim.fractals; + +import java.awt.EventQueue; +import java.awt.Graphics2D; + +import javax.swing.JFrame; +import java.awt.BorderLayout; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.imageio.ImageIO; +import javax.swing.DefaultComboBoxModel; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.File; + +import javax.swing.JMenuBar; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; + +public class Window { + + private JFrame frame; + + public static float hueStart = 0f; + public static float hueEnd = 100f; + + public static float satStart = 80f; + public static float satEnd = 100f; + + /** + * Launch the application. + */ + public static void main(String[] args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + Window window = new Window(); + window.frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /** + * Create the application. + */ + public Window() { + initialize(); + } + + /** + * Initialize the contents of the frame. + */ + private void initialize() { + frame = new JFrame(); + frame.setBounds(100, 100, 800, 600); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setTitle("Fractal generator"); + Canvas panel = new Canvas(); + frame.addKeyListener(panel); + frame.getContentPane().add(panel, BorderLayout.CENTER); + + JMenuBar mainMenu = new JMenuBar(); + frame.getContentPane().add(mainMenu, BorderLayout.NORTH); + + JMenu formula_menu = new JMenu("Formula"); + mainMenu.add(formula_menu); + + JMenuItem mntmmandelbrot = new JMenuItem("Mandelbrot"); + formula_menu.add(mntmmandelbrot); + + mntmmandelbrot.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + panel.paintMandel(); + } + }); + + JMenuItem juliamntm = new JMenuItem("Julia"); + formula_menu.add(juliamntm); + + juliamntm.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String arg = JOptionPane.showInputDialog("Enter arg0: "); + String _arg = JOptionPane.showInputDialog("Enter arg1: "); + if( arg != null && _arg != null ) { + panel.paintJulia(arg, _arg); + } + } + }); + + JMenuItem bshipmntm = new JMenuItem("Burning ship"); + formula_menu.add(bshipmntm); + + bshipmntm.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + panel.paintBurningship(); + } + }); + + JMenuItem cmmntm = new JMenuItem("Custom mandelbrot"); + formula_menu.add(cmmntm); + + JMenu mnNewMenu = new JMenu("GUI"); + mainMenu.add(mnNewMenu); + + JMenuItem mntmNewMenuItem = new JMenuItem("Choose"); + mnNewMenu.add(mntmNewMenuItem); + + JMenu mnPos = new JMenu("Position"); + mainMenu.add(mnPos); + + JMenuItem mntmRes = new JMenuItem("Reset"); + mnPos.add(mntmRes); + + mntmRes.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + panel.setInterval(-2, 0.5, -1, 1, 1, 1); + panel.repaint(); + } + }); + + JMenuItem mntmPosJump = new JMenuItem("Jump into..."); + mnPos.add(mntmPosJump); + + JMenu mnExport = new JMenu("Export"); + mainMenu.add(mnExport); + + JMenuItem mntmExportPNG = new JMenuItem("Export to PNG"); + mnExport.add(mntmExportPNG); + mntmExportPNG.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + BufferedImage image = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + panel.printAll(g); + g.dispose(); + try { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Choose file"); + + int userSelection = fileChooser.showSaveDialog(frame); + + if (userSelection == JFileChooser.APPROVE_OPTION) { + ImageIO.write(image, "png", fileChooser.getSelectedFile()); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + }); + + mntmPosJump.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String arg = JOptionPane.showInputDialog("Enter Rez: "); + String _arg = JOptionPane.showInputDialog("Enter Imz: "); + String __arg = JOptionPane.showInputDialog("Enter zoom: "); + if( arg != null && _arg != null && __arg != null ) { + double centerX = Double.parseDouble(arg); + double centerY = Double.parseDouble(_arg); + double size = Double.parseDouble(__arg); + panel.setInterval(centerX-size, centerX+size, centerY-size, centerY+size, 1, 1); + panel.repaint(); + } + } + }); + + mntmNewMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + GradientChoice dialog = new GradientChoice(); + dialog.setVisible(true); + if( dialog.isOk ) { + panel.repaint(); + } + } + }); + + cmmntm.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String arg = JOptionPane.showInputDialog("Enter arg0: "); + if( arg != null ) { + panel.paintCustom(arg); + } + } + }); + } + +}