What follows is the highlighted source code with comments. You can also download this file directly: FractalPathPanel.java.
/* * Copyright (C) 2012 W. Patrick Hooper <wphooper@gmail.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package graphics; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.*; import javax.swing.JLabel; import javax.swing.JPanel; import number.Complex; import path.*; /** * A FractalPathPanel displays a fractal curve and allows the user to manipulate * the curve and the display of this curve. * * @author W. Patrick Hooper <wphooper@gmail.com> */ public class FractalPathPanel extends JPanel implements MouseListener, MouseMotionListener { /** * Construct a FractalPathPanel which by default displays part of the Koch * Snowflake. */ public FractalPathPanel() { setGamma(new VertexPath( new Complex(0.0), new Complex(1.0 / 3.0), new Complex(0.5, Math.sqrt(3) / 6), new Complex(2.0 / 3.0), new Complex(1.0))); // Define the bounding box to fit the fractal path: fit(); setBackground(Color.WHITE); // Make the background color white // Necessary for mouse interaction: addMouseListener(this); addMouseMotionListener(this); } //// CURVE MANIPULATION FUNCTIONS AND VARIABLES // Curve used to "seed" the substitution fractal private VertexPath gamma; // The fractal curve we display private SubstitutionFractal fractal; // Depth to look at for the approximation private int depth = 2; // The approximation to the fractal curve private PolygonalPath approx_path; /** * Set a different seed curve for the fractal. */ public final void setGamma(PolygonalPath gamma) { this.gamma = new VertexPath(gamma); fractal = new SubstitutionFractal(gamma, gamma); approx_path = new PolygonalApproximation(fractal, depth); repaint(); } /** Change the quality of the polygonal approximation of the fractal curve. * * @param new_depth Depth to descend in the tree of edges defining the * fractal curve. * @see PolygonalApproximation */ public final void setDepth(int new_depth) { // Prevent depth from becoming negative, and ignore false changes. if ((new_depth >= 0) && (new_depth != depth)) { depth = new_depth; approx_path = new PolygonalApproximation(fractal, depth); postStatus("Fractal approximation depth set to " + depth + "."); repaint(); } } /** Decrease the quality of the polygonal approximation. */ public final void decreaseDepth() { setDepth(depth - 1); } /** Increase the quality of the polygonal approximation. */ public final void increaseDepth() { setDepth(depth + 1); } //// FUNCTIONS RELATED TO THE STATUSBAR private JLabel status_bar = null; /** * Set the label to print status updates to. */ public void setStatusBar(JLabel new_bar) { status_bar = new_bar; // This will update the status bar with the current mode of interaction: setInteractionMode(current_mode); } private void postStatus(String status) { if (status_bar != null) { status_bar.setText(status); } } //// FUNCTIONS RELATED TO THE BOUNDING BOX AND COORDINATE CHANGES // We guarantee that this box will be displayed. private Rectangle2D display_box; // This affine transformation converts from math coordinates to screen coordinates. private AffineTransform current_transform = new AffineTransform(); /** * This function returns an affine transform which sends display_box into * the rectangle representing screen coordinates for this panel. * * The affine transformation we return has the following properties. It * sends the bounding_box into the panel rectangle, so that the center of * the bounding box is sent the center of the panel. The scaling preserves * the aspect ratio. */ private AffineTransform getTransform() { /* * The component has pixels whose x-coordinates are numbered 0 to * getWidth()-1 as we move rightward. The y-coordinates of these pixels * increases from 0 to getHeight()-1 as we move downward. * * We will return the transformation which takes the bounding box for * our curve into the rectangle representing coordinates for our * component on the screen. To move this box into the screen we use the * following steps: 1. We translate our box, moving the center of our * box to the origin. 2. We scale the box by a constant so that the * image of our box has width and height less than or equal to the width * and height of this component. 3. We negate the y-coordinate, because * in mathematics the y-coordinate increases as we move upward. 4. We * translate the origin so that it is moved to the center of the * component. * * These steps are carried out below. */ // Construct a transformation which translates the plane moving the center of // our box to the origin. AffineTransform transform = AffineTransform.getTranslateInstance(-display_box.getCenterX(), -display_box.getCenterY()); // The number scale is the minimal ratio of screen dimensions to bounding box dimensions. double scale = Math.min(getWidth() / display_box.getWidth(), getHeight() / display_box.getHeight()); // The following line post-composes by scaling the plane by the number "scale". // Because we use the same constant in each coordinate, we preserve the aspect ratio. transform.preConcatenate(AffineTransform.getScaleInstance(scale, scale)); // This has the effect of negating the y-coordinate: transform.preConcatenate(AffineTransform.getScaleInstance(1, -1)); // Now translate the origin until it is centered in the component. transform.preConcatenate( AffineTransform.getTranslateInstance(getWidth() / 2, getHeight() / 2)); return transform; } /** * Convert the Complex number z into screen coordinates. */ private Point2D toScreenCoordinates(Complex z) { // Convert z to a Point2D, apply the affine transform and return the result. return current_transform.transform(new Point2D.Double(z.re(), z.im()), null); } /** * Convert the point from screen coordinates into math coordinates. */ private Complex toMathCoordinates(Point2D p) { try { Point2D math_point = current_transform.inverseTransform(p, null); return new Complex(math_point.getX(), math_point.getY()); } catch (NoninvertibleTransformException e) { // We would enter here if the current_transform represents a matrix // which is not invertible. This should never happen, because // we constructed our transformation as a composition of // invertible transformations. System.err.println("FractalPathPanel failed to invert the " + "current_transform in the toMathCoordinates() method."); return new Complex(0); } } /** * Guarantee the whole curve is displayed in the window. */ public final void fit() { display_box = PathUtil.boundingBox(approx_path); postStatus("Zooming to fit the fractal curve in the window."); repaint(); } /** * Rescale the box we are guaranteed to display by a multiplicative constant * via a dilation fixing the provided complex number. * * All calculations are done in math coordinates. * * @param scaling_constant A positive constant. * @param fixed_point Point fixed by the dilation. */ public void scale(double scaling_constant, Complex fixed_point) { if (scaling_constant > 0) { Complex min = new Complex(display_box.getMinX(), display_box.getMinY()), max = new Complex(display_box.getMaxX(), display_box.getMaxY()); min = min.minus(fixed_point).mult(scaling_constant).add(fixed_point); max = max.minus(fixed_point).mult(scaling_constant).add(fixed_point); Complex diff = max.minus(min); display_box = new Rectangle2D.Double(min.re(), min.im(), diff.re(), diff.im()); repaint(); } } //// INTERACTION MODES public enum InteractionMode { DO_NOTHING, ZOOM_IN, ZOOM_OUT, MOVE_POINT, DELETE_POINT, ADD_POINT; } private InteractionMode current_mode = InteractionMode.DO_NOTHING; public void setInteractionMode(InteractionMode new_mode) { current_mode = new_mode; switch (current_mode) { case DO_NOTHING: postStatus("Welcome to the Fractal Path Explorer."); break; case ZOOM_IN: postStatus("Click to zoom in to a point."); break; case ZOOM_OUT: postStatus("Click to zoom out from a point."); break; case MOVE_POINT: postStatus("Drag a point to change the path."); dragging_point = false; break; case DELETE_POINT: postStatus("Click a point to delete."); break; case ADD_POINT: postStatus("Click a point to add to the path."); break; } repaint(); } // MOUSE INTERACTION: int point_dragged; boolean dragging_point = false; @Override public void mouseClicked(MouseEvent me) { Complex z = this.toMathCoordinates(me.getPoint()); switch (current_mode) { case ZOOM_IN: scale(0.8, z); break; case ZOOM_OUT: scale(1.25, z); break; case DELETE_POINT: EdgeIterator it = gamma.iterator(); if (it.hasNext()) { it.next(); // skip first edge. } int vertex = 1; while (it.hasNext()) { // Get the starting point of the segment, and convert it to // screen coordinates: Point2D pt = this.toScreenCoordinates(it.next().startingPoint()); if (pt.distance(me.getPoint()) < 5) { setGamma(gamma.removePoint(vertex)); break; } vertex = vertex + 1; } break; case ADD_POINT: it = gamma.iterator(); vertex = 1; while (it.hasNext()) { // Get the midpoint of the segment, and convert it to screen coordinates: Complex midpoint=it.next().midpoint(); Point2D pt = this.toScreenCoordinates(midpoint); if (pt.distance(me.getPoint()) < 5) { setGamma(gamma.addPoint(vertex, midpoint)); break; } vertex = vertex + 1; } break; } } @Override public void mouseEntered(MouseEvent me) { } @Override public void mouseExited(MouseEvent me) { } @Override public void mousePressed(MouseEvent me) { Complex z = this.toMathCoordinates(me.getPoint()); switch (current_mode) { case MOVE_POINT: EdgeIterator it = gamma.iterator(); if (it.hasNext()) { it.next(); // skip first edge. } int vertex = 1; while (it.hasNext()) { // Get the starting point of the segment, and convert it to // screen coordinates: Point2D pt = this.toScreenCoordinates(it.next().startingPoint()); if (pt.distance(me.getPoint()) < 5) { point_dragged = vertex; dragging_point = true; break; // leave the while loop. } vertex = vertex + 1; } break; } } @Override public void mouseReleased(MouseEvent me) { Complex z = this.toMathCoordinates(me.getPoint()); switch (current_mode) { case MOVE_POINT: dragging_point = false; break; } } @Override public void mouseDragged(MouseEvent me) { Complex z = this.toMathCoordinates(me.getPoint()); switch (current_mode) { case MOVE_POINT: if (dragging_point) { setGamma(gamma.movePoint(point_dragged, z)); } break; } } @Override public void mouseMoved(MouseEvent me) { } // DRAWING ROUTINE: /** * This is called when we need to draw the polygonal path. */ @Override public void paintComponent(Graphics gfx) { // This stuff is standard, and should be in any paintComponent method. super.paintComponent(gfx); Graphics2D g = (Graphics2D) gfx; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Reset the transformation which converts from math coordinates to screen coordinates. current_transform = getTransform(); // Draw in black, with lines of width equal to one pixel. g.setColor(Color.BLACK); g.setStroke(new BasicStroke(1)); PathDrawing.drawPolygonalPath(g, current_transform, approx_path); switch (current_mode) { case MOVE_POINT: drawGamma(g); break; case DELETE_POINT: drawGamma(g); break; case ADD_POINT: g.setColor(Color.BLUE); PathDrawing.drawPolygonalPath(g, current_transform, gamma); g.setColor(Color.RED); EdgeIterator it = gamma.iterator(); while (it.hasNext()) { // Get the midpoint of the segment, and convert it to screen coordinates: Point2D pt = this.toScreenCoordinates(it.next().midpoint()); // Draw a circle with screen-radius 5 about the point. g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10)); } } } /** Draw the curve gamma and its vertices. */ private void drawGamma(Graphics2D g) { g.setColor(Color.BLUE); PathDrawing.drawPolygonalPath(g, current_transform, gamma); g.setColor(Color.RED); EdgeIterator it = gamma.iterator(); if (it.hasNext()) { it.next(); // skip first edge. } while (it.hasNext()) { // Get the starting point of the segment, and convert it to // screen coordinates: Point2D pt = this.toScreenCoordinates(it.next().startingPoint()); // Draw a circle with screen-radius 5 about the point. g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10)); } } }