/** * Mandala.java * v. 0.4 * Home page: http://www.tiac.net/~sw/2005/03/Mandala * Steve Witham * derived from SpaceFillPanel by Frank Ruskey, Johannes Martin and Bette Bultena: * http://www.csc.uvic.ca/~csc160se/examples/SpaceFill/spacefillingcurves.html */ import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import java.lang.*; // for Math class AboutBox extends JDialog { public AboutBox( Frame parent ) { super(parent, false ); setTitle ( "About Mandala" ); JTextArea aboutText = new JTextArea(); aboutText.setText( "Mandala, v. 0.4 by Steve Witham \n" + "\n" + "Details, source code and updates:\n" + "http://www.tiac.net/~sw/2005/03/Mandala\n" + "\n" + "Hint:\tStretch the main window for more detail,\n" + "\tor shrink it for faster completion,\n" + "\tthen press \"Draw\"\n" + "\n" + "based on SpaceFillPanel\n" + "by Frank Ruskey, Johannes Martin and Bette Bultena:\n" + "http://www.csc.uvic.ca/~csc160se/examples/SpaceFill/spacefillingcurves.html" ); aboutText.setEditable(false); aboutText.setBackground( new Color( 0xF0F0F0 ) ); aboutText.setFont( new Font( "sansserif", Font.PLAIN, 12 ) ); aboutText.setBorder( new EmptyBorder( 20, 20, 20, 40 ) ); Container contents = getContentPane(); contents.setLayout(new BorderLayout()); contents.add( "Center", aboutText ); pack(); } // AboutBox code taken from http://java.sun.com/products/jlf/ed2/samcode/savecha.html // and stripped. } class MyCanvas extends JPanel { public Image image = null; // the "single buffer" actually drawn into and painted // The idea is that I might want to get a new Image and copy the old one // into it, so separate the part that gets the new Image. private Image newImageIfNeeded( ) { int width = this.getWidth(); int height = this.getHeight(); if( image == null || image.getWidth( this /* the observer */ ) != width || image.getHeight( this /* the observer */ ) != height ) { return( createImage( width, height ) ); } else { return( null ); } } /** * Clear the drawing canvas. */ public void clearCanvas() { Image newImage = newImageIfNeeded( ); if( newImage != null ) image = newImage; Graphics g = image.getGraphics(); g.setColor( this.getBackground() ); g.fillRect(0, 0, image.getWidth( this /* observer */ ), image.getHeight( this /* observer */ ) ); } /** * Redraw the canvas when necessary. * @param g The graphics object for this component */ // public void paintComponent( Graphics g ) { // System.out.println( "paintComponent" ); // super.paintComponent( g ); // System.out.println( "paintComponent2" ); // if( !drawn ) draw(); // } public void paint( Graphics g ) { // System.out.println( "canvas paint start" ); // super.paint( g ); // System.out.println( "canvas paint middle" ); if( image != null ) { g.drawImage( image, 0, 0, this /* observer */ ); } // System.out.println( "canvas paint end" ); } } /** * Class Mandala provides the main component for * stuff to be drawn in. * This particular version makes use of the Java * Swing capabilities as well as allowing flexibility * for displaying this in a browser or as a standalone * frame. */ public class Mandala extends JPanel implements ActionListener { private MyCanvas drawingCanvas; // the thingy (defined above) into which we draw private Choice numeratorChoice; private Choice denominatorChoice; private int denominatorValues[]; private Choice variationChoice; private Choice wobbleChoice; private Choice wrapChoice; private double wrapValues[]; private Choice xhskewChoice; private int xhskewValues[]; private AboutBox aboutBox; static private JFrame fr; private boolean drawn; protected static final Dimension canvasSize = new Dimension( 512, 512 ); /** * Create a frame with a "canvas" area where the drawing gets painted, list boxes to select options from, and buttons to start drawing and to show the "about" box. */ public Mandala () { super(); // create some list boxes and buttons numeratorChoice = new Choice(); for( int i = 1; i < 16; i++ ) { numeratorChoice.addItem( "" + i ); } denominatorValues = new int[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; denominatorChoice = new Choice(); for( int i = 0; i < denominatorValues.length; i++ ) { denominatorChoice.addItem( "" + denominatorValues[ i ] ); } variationChoice = new Choice(); variationChoice.addItem( "circular" ); variationChoice.addItem( "Minsky" ); wobbleChoice = new Choice(); wobbleChoice.addItem( "none" ); wobbleChoice.addItem( ".000003" ); wobbleChoice.addItem( ".00001" ); wobbleChoice.addItem( ".00003" ); wobbleChoice.addItem( ".0001" ); wobbleChoice.addItem( ".0003" ); wobbleChoice.addItem( ".001" ); wobbleChoice.addItem( ".003" ); wobbleChoice.addItem( ".01" ); wrapChoice = new Choice(); int WRAP_N_VALUES = 9; wrapValues = new double[ WRAP_N_VALUES ]; wrapValues[ 0 ] = 0.0; wrapChoice.addItem( "none" ); int j = ( WRAP_N_VALUES - 3 ) / 2; for( int i = 1; i < WRAP_N_VALUES; i += 2 ) { wrapValues[ i ] = 1.5 * ( 1 << j ); wrapChoice.addItem( "size x " + wrapValues[ i ] ); wrapValues[ i + 1 ] = 1.0 * ( 1 << j ); wrapChoice.addItem( "size x " + wrapValues[ i + 1 ] ); j--; } xhskewValues = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 15, 16, 23, 24, 25, 31, 32, 36, 47, 48, 63, 64 }; xhskewChoice = new Choice(); xhskewChoice.addItem( "none" ); for( int i = 1; i < xhskewValues.length; i++ ) { xhskewChoice.addItem( "" + xhskewValues[ i ] ); } // put the list boxes and the buttons into a panel JPanel commandPanel = new JPanel(); Box commandBox = Box.createVerticalBox(); // Box within JPanel by sw commandBox.add( new JLabel( "Numerator:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( numeratorChoice ); commandBox.add( Box.createVerticalStrut( 10 ) ); commandBox.add( new JLabel( "Denominator:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( denominatorChoice); commandBox.add( Box.createVerticalStrut( 10 ) ); commandBox.add( new JLabel( "Variation:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( variationChoice); commandBox.add( Box.createVerticalStrut( 10 ) ); commandBox.add( new JLabel( "Wobble:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( wobbleChoice); commandBox.add( Box.createVerticalStrut( 10 ) ); commandBox.add( new JLabel( "Wrap:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( wrapChoice); commandBox.add( Box.createVerticalStrut( 10 ) ); commandBox.add( new JLabel( "Skew:" ) ); commandBox.add( Box.createVerticalStrut( 5 ) ); commandBox.add( xhskewChoice); commandBox.add( Box.createVerticalStrut( 20 ) ); JButton goButton = new JButton( "Draw" ); goButton.setActionCommand( "Draw" ); goButton.addActionListener( this ); commandBox.add(goButton); // Don't know how to center. -sw JButton aboutButton = new JButton( "About" ); aboutButton.setActionCommand( "About" ); aboutButton.addActionListener( this ); commandBox.add( Box.createVerticalStrut( 20 ) ); commandBox.add(aboutButton); commandPanel.add( commandBox ); // create the canvas drawingCanvas = new MyCanvas(); drawingCanvas.setMinimumSize( canvasSize ); drawingCanvas.setPreferredSize( canvasSize ); // -sw drawingCanvas.setBackground( new Color( 0xF8F8F8 ) ); // put the canvas and the panel into the window this.setLayout(new BorderLayout()); add("Center", drawingCanvas); add("East", commandPanel); // enable window events (so we get notified when someone tries to // close the program) enableEvents(AWTEvent.WINDOW_EVENT_MASK); drawn = false; } void displayMessage( String msg ) { Graphics g = drawingCanvas.image.getGraphics(); g.setColor( Color.yellow ); g.fillRect( 90, 80, 300, 30 ); g.setColor( Color.black ); g.drawString( msg, 100, 100); drawingCanvas.paint( drawingCanvas.getGraphics() ); } /** * Redraw the screen when necessary. * @param g The graphics object for this component */ // public void paintComponent( Graphics g ) { // System.out.println( "paintComponent" ); // super.paintComponent( g ); // System.out.println( "paintComponent2" ); // if( !drawn ) draw(); // } public void paint( Graphics g ) { // System.out.println( "paint start" ); super.paint( g ); // System.out.println( "paint middle" ); if( !drawn ) { // This is not the right place, but I haven't found a better one. draw(); } // System.out.println( "paint end" ); } public void draw( ) { double wobble; drawingCanvas.clearCanvas(); drawn = true; RotatorMap map = new RotatorMap( drawingCanvas.getWidth(), drawingCanvas.getHeight() ); int numerator = numeratorChoice.getSelectedIndex() + 1; int denominator = denominatorValues[ denominatorChoice.getSelectedIndex() ]; boolean minsky = ( variationChoice.getSelectedIndex() == 1 ); int wobble_pick = wobbleChoice.getSelectedIndex(); if( wobble_pick == 0 ) wobble = 0.0; else wobble = Math.pow( 10.0, -6.0 + .5 * wobble_pick ); double wrap = wrapValues[ wrapChoice.getSelectedIndex() ]; int xhskew = xhskewValues[ xhskewChoice.getSelectedIndex() ]; if( ( numerator * 2 ) % denominator == 0 && wobble == 0.0 ) { displayMessage( "Sorry, can't do multiples of 1/2." ); } else { map.drawMap( drawingCanvas, numerator, denominator, minsky, wobble, wrap, xhskew ); } } /** * Deal with the input from the user. * @param event Clicks on the "Draw" and "About" buttons. */ public void actionPerformed( ActionEvent event ) { String command = event.getActionCommand(); double wobble; if( command == "Draw" ) { draw(); } else if( command == "About" ) { if( aboutBox == null ) { aboutBox = new AboutBox( fr ); aboutBox.pack(); } aboutBox.setVisible(true); } } /** * Creates a frame with the Mandala. * @param args Command line arguements which are not used. */ public static void main(String[] args) { try { UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); } catch (Exception e) { } fr = new JFrame("Mandala"); fr.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); Mandala m = new Mandala(); // m.setOpaque( false ); fr.setContentPane( m ); // fr.setSize( 700, 600 ); fr.pack(); // sw fr.show(); // m.draw(); } } class RotatorMap { int num, denom; double wobble; boolean minsky; double wrap_range; int extra_hskew; int width, height, skew_maps_size, skew_maps_base, skew_maps_min, skew_maps_max; int show_base = -32; // Lowest value for both x and y on screen--upper left corner. double angle, c, s, epsilon; int hskew1[], vskew[], hskew2[]; int step_counts [] []; int max_n_steps = 20000; Color colors []; void setColorFor_n_steps( Graphics g, int n_steps ) { float z, h, s, b, r, gr; int which_colormap; Color color = null; // Convince the compiler that it will be initialized. if( n_steps <= max_n_steps + 1 && colors[ n_steps ] != null ) { g.setColor( colors[ n_steps ] ); } else { if( n_steps == max_n_steps + 1) { color = Color.black; } else { if( wrap_range == 0.0 ) which_colormap = 1; else which_colormap = 1; if( which_colormap == 1 ) { z = (float) Math.log( n_steps ); h = z - (float) Math.floor( z ); b = (float) ( 1 - .5 * ( z / Math.log( max_n_steps ) ) ); if( b > .5 ) s = (float) ( ( 1.0 - b ) * 2 /* + .5 */ ); else s = 1; color = Color.getHSBColor( h, s, b ); } else if( which_colormap == 2 ) { // wrapping--assume all paths cycle z = (float) ( n_steps / 6.333333 ); h = z - (float) Math.floor( z ); b = (float) ( .25 + .75 / ( 1 + (float) n_steps / max_n_steps ) ); if( b > .5 ) s = (float) ( ( 1.0 - b ) + .5 ); else s = 1; color = Color.getHSBColor( h, s, b ); } else { b = (float) ( n_steps % 17 ) / 16; r = (float) ( n_steps % 91 ) / 90; gr = (float) ( n_steps % 123 ) / 122; color = new Color( r, gr, b ); } } if( n_steps <= max_n_steps + 1 ) colors[ n_steps ] = color; g.setColor( color ); } } RotatorMap( int width, int height ) { this.width = width; this.height = height; step_counts = new int [width] [height]; } // Here is the rotation transformation expressed as a procedure that changes // the x and y of a Point in-place. It's not actually used because calling // this procedure inside the inner loop was a lot slower than calculating // in-line. // Two wrongs don't make a right but three skews make a rotate. void rotate( Point p ) { p.x += (int) Math.round( c * p.y ); p.y += (int) Math.round( s * p.x ); p.x += (int) Math.round( c * p.y ); } // This procedure sets up hskew[] and vskew[] to have the same effect as // either rotate(), or Minsky's eliptical variation of it. // Plus sets up for the wobble and wrap effects. void set_up_skews( boolean minsky, double wobble, double wrap_range, int extra_hskew ) { int biggest_size; this.minsky = minsky; this.wobble = wobble; this.wrap_range = wrap_range; this.extra_hskew = extra_hskew; // The rotation, whether regular or Minsky, is done with skews, // adding an int function of (the int) y to x for horizontal skew, or // adding an int function of (the int) x to y for vertical skew. // Since the range of the functions is relatively small, and computing // them is a little complicated with "wobble," I make tables out of them. // There are two horizontal skew maps because in the Minsky variation the // second horizontal skew isn't done. // The range of the functions that is pre-calculated is generally larger than the range // of x and y values shown on the screen, so that the trajectories of the points can // take an off-screen trip around (0,0). But this depends on the "wrap_range" parameter. // If wrap_range zero, then the skew tables are expanded whenever a point goes out of range. // If wrap_range is nonzero, it's a multiplier for the largest of the screen width or // height. A fixed range is set, the same for both x and y, and x and y values are // wrapped to stay within this range. This whole operation is reversible because: // o the wrapping is done after each skew, // o horizontal wrap doesn't change the input to horizontal skew, // o vertical wrap doesn't change the input to vertical skew, & // o skew then wrap can be reversed by reverse skew then wrap. if( width > height ) biggest_size = width; else biggest_size = height; if( wrap_range == 0.0 ) skew_maps_size = biggest_size * 2; else skew_maps_size = (int) ( biggest_size * wrap_range ); // Make skew_maps_size odd so that it can have a center value: skew_maps_size |= 1; // Try to center the wrap range around zero, but if it's too tight for that, // adjust the center as little as possible to get the screen inside the range // --but the range is kept the same for x and y. skew_maps_base = skew_maps_size / 2; if( skew_maps_base + ( show_base + biggest_size ) > skew_maps_size ) { skew_maps_base = - ( show_base + biggest_size ) + skew_maps_size; // System.out.println( "skew_maps_base = " + skew_maps_base ); } hskew1 = null; // Having this null triggers recreating them. int x_or_y = adjust_skew_maps( 0 ); } int adjust_skew_maps( int x_or_y ) { if( wrap_range == 0.0 ) { // Can't count on skew_maps_min or skew_maps_max here. while( skew_maps_base + x_or_y < 0 || skew_maps_base + x_or_y >= skew_maps_size ) { skew_maps_size *= 2; skew_maps_base = skew_maps_size / 2; hskew1 = null; } } if( hskew1 == null ) { // System.out.println( "adjust_skew_maps( " + x_or_y + " ) skew_maps_size == " + skew_maps_size ); skew_maps_min = -skew_maps_base; skew_maps_max = skew_maps_min + skew_maps_size - 1; // System.out.println( "skew_maps_min = " + skew_maps_min + ", _max = " + skew_maps_max ); hskew1 = new int[ skew_maps_size ]; vskew = new int[ skew_maps_size ]; hskew2 = new int[ skew_maps_size ]; double lowangle = num / ( ( 1 + wobble ) * denom ); double hiangle = num / ( ( 1 - wobble ) * denom ); double wobble_frequency = 1 / 512.0; for( int i = -skew_maps_base; i + skew_maps_base < skew_maps_size; i++ ) { double x = i * 2 * Math.PI * wobble_frequency; angle = Math.PI * 2 * ( lowangle - (Math.cos(x)-1)/2 * (hiangle - lowangle) ); if( minsky ) { // eliptical HAKMEM items 149 (Minsky), 151 (Gosper) epsilon = Math.sqrt( 2 * ( 1 - Math.cos( angle ) ) ); // // See below about the .00000001 hskew1[ i + skew_maps_base ] = (int) -Math.round( epsilon * i + .00000001 ) + extra_hskew; vskew[ i + skew_maps_base ] = (int) Math.round( epsilon * i + .00000001 ); hskew2[ i + skew_maps_base ] = 0; } else { // Normal, circular, "Givens": s = Math.sin( angle ); c = ( Math.cos( angle ) - 1 ) / s; hskew1[ i + skew_maps_base ] = (int) Math.round( c * i ); // indexed by y // In Java the following things are true: // s = sin( Math.PI * 2 * ( 1 / 12 ) ) = .49999999999999994 // Math.round(s) = 1.0 (!?) // Math.round( s * 2 ) = 1.0 // Math.round( s * 3 ) = 1.0 // This causes a mess in the picture when denom == 12, // so this is the fix I came up with: vskew[ i + skew_maps_base ] = (int) Math.round( s * i + .00000001 ); // indexed by x hskew2[ i + skew_maps_base ] = hskew1[ i + skew_maps_base ] + extra_hskew; } } } return( x_or_y - skew_maps_size * (int) Math.floor( (double) ( skew_maps_base + x_or_y ) / skew_maps_size ) ); } void drawMap( MyCanvas canvas, int num, int denom, boolean minsky, double wobble, double wrap_range, int extra_hskew ) { Graphics g = canvas.image.getGraphics(); int n_steps; int min_show_x, max_show_x, min_show_y, max_show_y; this.num = num; this.denom = denom; // angle = Math.PI * 2 * num / denom ... on average, but see: set_up_skews( minsky, wobble, wrap_range, extra_hskew ); int offs_x = -show_base; int offs_y = -show_base; min_show_x = show_base; max_show_x = min_show_x + width - 1; min_show_y = show_base; max_show_y = min_show_y + height - 1; colors = new Color[ max_n_steps + 2 ]; int paintDelay = height; for( int x = min_show_x; x <= max_show_x; x++ ) { for( int y = min_show_y; y <= max_show_y; y++ ) { if( step_counts[ x + offs_x ][ y + offs_y ] == 0 ) { int px = x; int py = y; g.setColor( Color.black ); // show some kind of progress... for( n_steps = 1; n_steps <= max_n_steps /* || wrap_range > 0.0 */; n_steps++ ) { for( int i_denom = 0; i_denom < denom; i_denom++ ) { px += hskew1[ py + skew_maps_base ]; if( px < skew_maps_min || px > skew_maps_max ) px = adjust_skew_maps( px ); py += vskew[ px + skew_maps_base ]; if( py < skew_maps_min || py > skew_maps_max ) py = adjust_skew_maps( py ); px += hskew2[ py + skew_maps_base ]; if( px < skew_maps_min || px > skew_maps_max ) px = adjust_skew_maps( px ); } if( px == x && py == y ) { // Found a cycle... break; } if( n_steps > max_n_steps ) { g.fillRect( px + offs_x, py + offs_y, 1, 1 ); if( --paintDelay <= 0 ) { canvas.paint( canvas.getGraphics() ); paintDelay = height; } } } // Now retrace that path, knowing the (non-)cycle length: px = x; py = y; setColorFor_n_steps( g, n_steps ); for( int n = 1; n <= n_steps; n++ ) { if( px >= min_show_x && px <= max_show_x && py >= min_show_y && py <= max_show_y ) { if( step_counts[ px + offs_x ][ py + offs_y ] != 0 ) { break; // been there done...something... } g.fillRect( px + offs_x, py + offs_y, 1, 1 ); if( --paintDelay <= 0 ) { canvas.paint( canvas.getGraphics() ); paintDelay = height; } step_counts[ px + offs_x ][ py + offs_y ] = n_steps; } for( int i_denom = 0; i_denom < denom; i_denom++ ) { px += hskew1[ py + skew_maps_base ]; if( px < skew_maps_min || px > skew_maps_max ) px = adjust_skew_maps( px ); py += vskew[ px + skew_maps_base ]; if( py < skew_maps_min || py > skew_maps_max ) py = adjust_skew_maps( py ); px += hskew2[ py + skew_maps_base ]; if( px < skew_maps_min || px > skew_maps_max ) px = adjust_skew_maps( px ); } if( px == x && py == y ) { // End of the cycle... break; } } // for n_steps retracing path } // if step_count was zero } // for y } // for x canvas.paint( canvas.getGraphics() ); } // drawMap }