package org.trevorstone.chaosgame; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.GridBagLayout; import java.awt.Polygon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.geom.Point2D; import java.util.Random; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; // /** * Applet to demonstrate the Chaos Game. Draw a random point inside a regular * polygon with n sides. Select a random vertex. Draw a point a * fraction r of the way from the previous point to the selected * vertex. Iterated many times, this produces a fractal for many values of * n and r. * * @see Chaos Game at * MathWorld * @author Trevor Stone * @license CC-BY * @version 0.2 */ public class ChaosGameApplet extends JApplet implements ChangeListener, ItemListener { /** The name of this applet */ public static final String APPLET_NAME = "Chaos Game"; /** The version of this applet */ public static final String VERSION = "0.3"; /** License and author name */ public static final String LICENSE = "CC-BY Trevor Stone"; private static final int MAX_SIDES = 360; private static final int MAX_POINTS = 360000; private static final int DEFAULT_SIDES = 5; private static final int DEFAULT_POINTS = 36000; private static final double DEFAULT_DISTANCE_MULTIPLIER = 0.625; private static final ColorScheme[] COLOR_SCHEMES = { new PreviousVertexColorScheme(), new SelectedVertexColorScheme(), new NearestVertexColorScheme(), new MonochromeColorScheme(), new RandomColorScheme(), new LongCyclicColorScheme(), new ShortCyclicColorScheme() }; JSpinner sidesField = new JSpinner(); JSpinner distanceMultiplierField = new JSpinner(); JSpinner pointsField = new JSpinner(); JComboBox colorSchemeBox = new JComboBox(COLOR_SCHEMES); MyCanvas myCanvas = new MyCanvas(); private Random random = new Random(); public String getAppletInfo() { return APPLET_NAME + " version " + VERSION + " " + LICENSE; } public void init() { getContentPane().setLayout(new BorderLayout()); sidesField.setModel(new SpinnerNumberModel(DEFAULT_SIDES, 3, MAX_SIDES, 1)); sidesField.getModel().addChangeListener(this); pointsField.setModel(new SpinnerNumberModel(DEFAULT_POINTS, 1, MAX_POINTS, 100)); pointsField.getModel().addChangeListener(this); distanceMultiplierField.setModel(new SpinnerNumberModel(DEFAULT_DISTANCE_MULTIPLIER, 0.0, 1.0, 0.025)); distanceMultiplierField.getModel().addChangeListener(this); ((JSpinner.DefaultEditor) distanceMultiplierField.getEditor()).getTextField().setColumns(4); colorSchemeBox.addItemListener(this); JButton repaintButton = new JButton("Draw"); repaintButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { myCanvas.repaint(); } }); getRootPane().setDefaultButton(repaintButton); JPanel fieldPanel = new JPanel(); fieldPanel.setLayout(new GridBagLayout()); fieldPanel.add(new JLabel("Sides")); sidesField.setToolTipText("Number of polygon sides (3-" + MAX_SIDES + ")"); fieldPanel.add(sidesField); fieldPanel.add(Box.createHorizontalStrut(5)); fieldPanel.add(new JLabel("Distance")); distanceMultiplierField.setToolTipText("Fractional distance (0-1) between last point and a vertex"); fieldPanel.add(distanceMultiplierField); fieldPanel.add(Box.createHorizontalStrut(5)); fieldPanel.add(new JLabel("Points")); pointsField.setToolTipText("Number of fill points (1-" + MAX_POINTS + ")"); fieldPanel.add(pointsField); JPanel buttonPanel = new JPanel(); buttonPanel.add(new JLabel("Color Scheme")); buttonPanel.add(colorSchemeBox); buttonPanel.add(Box.createHorizontalStrut(5)); buttonPanel.add(repaintButton); JPanel northPanel = new JPanel(); northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.PAGE_AXIS)); northPanel.add(fieldPanel); northPanel.add(buttonPanel); getContentPane().add(myCanvas, BorderLayout.CENTER); getContentPane().add(northPanel, BorderLayout.NORTH); super.init(); } public void stateChanged(ChangeEvent evt) { myCanvas.repaint(); } public void itemStateChanged(ItemEvent evt) { if (evt.getStateChange() == ItemEvent.SELECTED) { myCanvas.repaint(); } } int getSideCount() { int sides = ((SpinnerNumberModel) sidesField.getModel()).getNumber().intValue(); if (sides < 3 || sides > MAX_SIDES) { // sanity check showStatus("Must have between 3 and " + MAX_SIDES + " sides"); return DEFAULT_SIDES; } return sides; } int getPointCount() { int points = ((SpinnerNumberModel) pointsField.getModel()).getNumber().intValue(); if (points < 1 || points > MAX_POINTS) { // sanity check showStatus("Must have between 1 and " + MAX_POINTS + " points"); return DEFAULT_POINTS; } return points; } double getDistanceMultiplier() { double multiplier = ((SpinnerNumberModel) distanceMultiplierField.getModel()).getNumber().doubleValue(); if (multiplier < 0.0 || multiplier > 1.0) { // sanity check showStatus("Multiplier must be between 0 and 1"); return DEFAULT_DISTANCE_MULTIPLIER; } return multiplier; } Polygon regularPolygon(int sides, int centerX, int centerY, int radius) { if (sides < 3 || sides > MAX_SIDES) { throw new IllegalArgumentException("Polygon requires 3 <= sides <= " + MAX_SIDES + ", not " + sides); } if (radius <= 0) { throw new IllegalArgumentException("Radius must be larger than 0, not " + radius); } double angleInc = 2.0 * Math.PI / sides; Polygon poly = new Polygon(); for (int i = 0; i < sides; ++i) { double angle = Math.PI / 2 + angleInc * i; int x = centerX - Math.round((float) (radius * Math.cos(angle))); int y = centerY - Math.round((float) (radius * Math.sin(angle))); poly.addPoint(x, y); } return poly; } private void drawChaosPolygon(Graphics g, int width, int height) { final int pointCount = getPointCount(); int radius = Math.min(width, height) / 2; Polygon polygon = regularPolygon(getSideCount(), radius, radius, radius); ColorScheme colorScheme = (ColorScheme) colorSchemeBox.getSelectedItem(); colorScheme = colorScheme.init(g, polygon, width, height, pointCount); double distanceMultiplier = getDistanceMultiplier(); Point2D[] vertecies = new Point2D[polygon.npoints]; for (int i = 0; i < vertecies.length; ++i) { vertecies[i] = new Point2D.Double(polygon.xpoints[i], polygon.ypoints[i]); } Point2D currentPoint = vertecies[random.nextInt(vertecies.length)]; for (int i = 0; i < pointCount; ++i) { int selectedVertex = random.nextInt(vertecies.length); Point2D chosen = vertecies[selectedVertex]; double xdist = currentPoint.getX() - chosen.getX(); double ydist = currentPoint.getY() - chosen.getY(); double newx = currentPoint.getX() - xdist * distanceMultiplier; double newy = currentPoint.getY() - ydist * distanceMultiplier; currentPoint = new Point2D.Double(newx, newy); colorScheme.drawPoint(g, polygon, currentPoint, selectedVertex); } } class MyCanvas extends JComponent { public void paint(Graphics g) { drawChaosPolygon(g, getWidth(), getHeight()); } } static interface ColorScheme { /** * Initialize for a new run. If this color scheme maintains state it may * reinitialize or return a new object of the same class. If it's * stateless, it may return itself. Many color shchemes will want to * draw the polygon and a background color. * * @param g Where to draw. * @param polygon The dimensions of the polygon for this run. * @param width The width of the polygon. * @param height The height of the polygon. * @param pointCount The number of points which will be drawn. * @return This color scheme or one like it. */ ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount); /** * Draw one point in the process. * * @param g Where to draw. * @param polygon The bounding polygon. * @param point The point to draw. * @param selectedVertex The vertex selected by the algorithm. */ void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex); } static class RandomColorScheme implements ColorScheme { private Random random = new Random(); public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { g.setColor(Color.WHITE); g.fillPolygon(polygon); g.setColor(Color.BLACK); g.drawPolygon(polygon); return this; } public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Random"; } } static class MonochromeColorScheme implements ColorScheme { public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { g.setColor(Color.WHITE); g.fillPolygon(polygon); g.setColor(Color.BLACK); g.drawPolygon(polygon); return this; } public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { g.setColor(Color.BLACK); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Monochrome"; } } static class ShortCyclicColorScheme implements ColorScheme { private int count = 0; public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { g.setColor(Color.WHITE); g.fillPolygon(polygon); g.setColor(Color.BLACK); g.drawPolygon(polygon); return new ShortCyclicColorScheme(); } public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { g.setColor(Color.getHSBColor(count++ / 360f, 1, 1)); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Short Cyclic"; } } static class LongCyclicColorScheme implements ColorScheme { private int totalPoints = 1; private int count = 0; public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { g.setColor(Color.WHITE); g.fillPolygon(polygon); g.setColor(Color.BLACK); g.drawPolygon(polygon); LongCyclicColorScheme result = new LongCyclicColorScheme(); result.totalPoints = pointCount; return result; } public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { float percentComplete = (float) count++ / totalPoints; g.setColor(Color.getHSBColor(360f * percentComplete, 1f, 1f)); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Long Cyclic"; } } static abstract class VertexColorScheme implements ColorScheme { public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { drawPolygon(g, polygon); return this; } /** * Draw the polygon outline. * * @param g * @param polygon */ protected void drawPolygon(Graphics g, Polygon polygon) { g.setColor(Color.WHITE); g.fillPolygon(polygon); for (int i = 0; i < polygon.npoints; ++i) { g.setColor(Color.getHSBColor(hue(polygon, i), 1f, 1f)); int x = polygon.xpoints[i]; int y = polygon.ypoints[i]; int prevX = polygon.xpoints[(i - 1 + polygon.npoints) % polygon.npoints]; int prevY = polygon.ypoints[(i - 1 + polygon.npoints) % polygon.npoints]; int nextX = polygon.xpoints[(i + 1) % polygon.npoints]; int nextY = polygon.ypoints[(i + 1) % polygon.npoints]; g.drawLine(x, y, x - (x - prevX) / 2, y - (y - prevY) / 2); g.drawLine(x, y, x - (x - nextX) / 2, y - (y - nextY) / 2); } } /** * @param polygon * @param vertex * @return The hue for the nth vertex of the polygon */ protected float hue(Polygon polygon, int vertex) { int increment = Math.max(1, 360 / polygon.npoints); return (increment * vertex) / 360f; } } static class NearestVertexColorScheme extends VertexColorScheme { public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { double closestDistance = Double.MAX_VALUE; double maxEdgeLength = Double.MIN_VALUE; int nearestVertex = 0; for (int i = 0; i < polygon.npoints; ++i) { if (point.distance(polygon.xpoints[i], polygon.ypoints[i]) < closestDistance) { nearestVertex = i; closestDistance = point.distance(polygon.xpoints[i], polygon.ypoints[i]); } int nextI = (i + polygon.npoints) % polygon.npoints; maxEdgeLength = Math.max(maxEdgeLength, Point2D .distance(polygon.xpoints[i], polygon.ypoints[i], polygon.xpoints[nextI], polygon.ypoints[nextI])); } g.setColor(Color.getHSBColor(hue(polygon, nearestVertex), 1f, 1f)); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Nearest Vertex"; } } static class SelectedVertexColorScheme extends VertexColorScheme { public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { g.setColor(Color.getHSBColor(hue(polygon, selectedVertex), 1f, 1f)); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); } public String toString() { return "Selected Vertex"; } } static class PreviousVertexColorScheme extends VertexColorScheme { private int previousVertex = -1; public ColorScheme init(Graphics g, Polygon polygon, int width, int height, int pointCount) { super.init(g, polygon, width, height, pointCount); return new PreviousVertexColorScheme(); // reset previousVertex } public void drawPoint(Graphics g, Polygon polygon, Point2D point, int selectedVertex) { if (previousVertex == -1) { previousVertex = selectedVertex; } g.setColor(Color.getHSBColor(hue(polygon, previousVertex), 1f, 1f)); g.drawOval((int) point.getX(), (int) point.getY(), 1, 1); previousVertex = selectedVertex; } public String toString() { return "Previous Vertex"; } } }