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";
}
}
}