Sunday, May 09, 2010

Creating an Archimedean Spiral generator in Java using the NetBeans IDE

[The images below can be enlarged by clicking on them]

My wife, Liberty, is an amalgam of seamstress, hippy, punk, old-school goth (real goth, not depressed teen sparkly lip gloss emo nonsense), WWII and vintage clothing buff, musical and action movie lover. I am a computer programmer, recovering video game and comic book geek, soccer and puzzle buff, ex helicopter parent. Other than a love of things not mainstream, we sometimes struggle to find areas where our interests overlap.

A few months ago, Liberty thought of something I could do with one of my specialties to assist her with one of her hobbies: write a program to design her next tattoo. Specifically, I would write a program to create interlaced spirals. I told her it would be tough, as most of my coding over the years hasn't included graphics. I experimented with a Java painting applet close to 10 years ago, and, although fearing what I used back then would have been deprecated from the language, or that I would be too far behind the curve with modern IDEs, I set out to give it the old college try. You see, I love my wife, and I'm usually up for a new challenge or puzzle to solve.

I tinkered away in the evenings and early mornings, learning how to use NetBeans, relearning how to create image and graphics objects in Java, how to correctly override the paint() method, and the math of spirals.

I decided to use the Archimedean Spiral, sometimes called the Hypnosis Spiral, whose spines are always equidistant from each other. Wikipedia has an article on that here, which I'll summarize briefly: r = aθ, meaning the radius is some constant times the angle.



The equation assumes polar coordinates. Where the more familiar Cartesian coordinates measure distance horizontally and vertically from the origin point, polar coordinates measure absolute distance from the origin, and at what angle. Since Java uses Cartesian coordinates, with the origin being in the upper-left corner and not the center of the canvas, some conversion needs to be done.

How do you convert polar to Cartesian? x = cos θ * r, y = sin θ * r. The x value will be the cosine of the angle (in radians) times the radius. The y value is the same, using the sine function instead of cosine. (I'd be smug here and say "basic trig" or some such, but I confess that I had to go look it up.)

To draw the spiral in Java, I just needed to figure out how many degrees long the spiral will be, iterate through one degree at a time, use my cosine and sine functions to get the coordinates, add to them the center x and y of the form I'll display them on, and use some mechanism to connect the dots and make an image object of the finished product.

And so I did. I'd like to share the complete build process of this, replacing my heavy solution with a simpler spiral generator. If you want to follow along, download and install NetBeans. I'm using version 6.8, which I believe is current at this writing. If you're using a different version, some of the screens I'll show below will likely be different. Or the whole thing could blow up and not work. Who knows?

Once NetBeans is installed, open it up, and add a new project. Click File, then New Project



When prompted for a project type, choose the category "Java", and the project type "Java Application".



The screen that follows will ask what to name the project, and where to put it. I have chosen "ArchimedeanSpiral" as the project name, but fill this in to your liking. Uncheck the box "Create Main Class". We will be adding a form that will function as the main class.



After this, your project space will be created, with some bells and whistles NetBeans uses to track and compile the project. The left pane of the IDE contains your project files, with a coffee cup icon beside it. To add the main form, right-click this, choose New, then JFrame Form.



The JFrame form will compile as its own class. The dialog box that appears now will prompt you for the class and package names. I'm choosing my "cea.demos" package, and "SpiralForm" as the class name, but, again, name as you will.



With the new form created, we're ready to rock and roll. As a reminder, you can click on these images to expand them, which will come in handy for the next image.

The middle pane of the IDE is where you build your form, using controls from the "Palette" pane on the right. In this regard, NetBeans functions similarly to the Microsoft Visual IDEs: Click on the type of object you want, drag it over to the form and drop it in place. Guide lines appear and try to give you hints on how best to line things up.

Just above the form are selectors "Source" and "Design", which change the view you are in. The Design view shows the form, the Source view shows the underlying code. Above that is a toolbar that contains a big green arrow. Clicking that saves and compiles everything, and runs the program.

Here I've grabbed a Button from the Swing Controls palette. There is also a button and other similar controls in the "AWT" section further down, but those are "heavyweight" controls, rather than the spiffy "lightweight" controls in the Swing section. Swing has been around since 1996, and if you aren't familiar with it and why it's better than AWT, you can look it up on Wikipedia. They are different types of objects for arcane reasons that I don't really care about; I prefer Swing objects because they look nice, whereas AWT objects look ugly.

Anyway, grab a Swing Button object, and put it where I have it below, close to the upper-right corner of the form. You'll notice dotted guidelines along the top and side when the button gets to the correct location. This indicates that the control is going to be bound to those edges and follow them when the form is stretched or maximized.



The name of the object, jButton1, will be displayed on the button when the application runs, so the first thing you want to do is change that text to indicate what the function of the button will be. Right-click on the button, and choose Edit Text, and change the name to "Draw Spiral".



After the button has been positioned, grab a Panel object from the Swing Containers palette, and position it as shown in the image below. The panel is where you will draw to. It has a graphics object that we can grab and draw shapes, text, or images to.

Once you drop it in place, you can stretch the outline of the control until it gets close to the sides, where arrows will appear indicating the control is bound to that edge. We want the panel to be bound to the left, bottom, and right edges, and also to the button at the top.



If you click over to the Source view, you'll see the button and panel mentioned near the bottom in the variable declaration section. All of the code written so far was done so by the IDE, and is for managing window placement, events, and code to initialize the form, most of which resides in the collapsed "Generated Code" section on line 31.

Any code highlighted in grey is locked by the IDE to prevent you from stepping on it. Any place else is fair game to write your own code.



To test what you have so far, run the application using the arrow I referred to earlier. You'll first be prompted to set a main class, and at this point there is only one option, so just click OK.



At this point you have a functioning application shell. It has a button that does nothing, and an invisible panel that you don't draw anything on.



Close the app after you've examined it. Next, register an event listener to fire off when the "Draw Spiral" button is clicked. In the design view, right-click the button, choose Events, then Action, then actionPerformed.



The following code will be generated in your class:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
  // TODO add your handling code here:
}

Whatever is in this method will be called whenever the button is pressed, so from here you can put in some sample code to draw to the panel, to get a feel for Graphics objects. Delete the TODO comment and replace it with "Graphics demo = jPanel1." as shown in the image below. When you do so, the NetBeans IDE will go crazy trying to help you. Read the Javadoc explanation of what a Graphics object is.



As a young programmer, I found this type of thing painfully annoying, making me eager to run back to emacs or UltraEdit to do all my coding. I now find these prompts and tips fantastically helpful, both for finding compile-time errors before you compile, and linking to the Javadocs to show what variable types get passed to a method.

In this case, the IDE has moved the getGraphics() method to the top of the list of things you might want, by doing some quick semantics on the line and guessing what you're trying to do. Pressing Enter fills in the line with the highlighted item from the list. What I've found with NetBeans is that you need to stop at the period and wait for the list to generate, then you can type a couple letters of the method or value you want, which narrows the list. If you type after the period before the list generates, it doesn't come up.

Before ending the line with a semi-colon, you'll see that the whole command is underlined with a red squiggly line. Red lines are an indicator that there is a compile-time error. The whole line being underlined may just indicate that you haven't finished the command with a semi-colon yet. If the line is still there after finishing the command, you've got a bigger problem.

After ending the line with a semi-colon, you'll see that only "Graphics" is underlined in red. "Demo" should be underlined in grey, indicating only that you haven't used this variable yet, and are so far just wasting memory with it. Put your cursor over "Graphics", and a tool tip will appear explaining the problem.



In this case, class Graphics can't be found, as it hasn't been imported yet. To fix this, right-click on "Graphics", and choose "Fix Imports".



A new "import" line will appear just below the package declaration, containing the correct class, java.awt.Graphics.



Other classes will need to be imported as well during this demonstration, so edit the import section to contain all of these:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.GeneralPath;
Return to the jButton1ActionPerformed method, and you will see that the Graphics class is no longer underlined, giving you a working object, "demo", that you can do things to. Set it's color to red, and fill a 100 pixel square with it, as shown in the completed method below:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    Graphics demo = jPanel1.getGraphics();
    demo.setColor(Color.red);
    demo.fillRect(0, 0, 100, 100);
}
After editing the method to match the above, run the app again. Now when you click the button, a box should appear.



Unfortunately there is nothing protecting the box once it is drawn. Whenever Java feels it needs to redraw the form, it doesn't pay attention to the box. If you maximize the app, the box will disappear completely. If you drag another window on top of the box, it will become damaged, as shown below.



To prevent this, the paint() method needs to be overridden, and code to draw the box needs to be put in there. The redBox image is what the button paints to now. The paint() method is then overridden, allowing us to always draw whatever is in redBox to jPanel1 whenever Java redraws the form. Finally, the last step of the button action method is to tell Java explicitly to redraw the form, now that there is something new to add to it.

Update your code as follows:
Image redBox;

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    redBox = createImage(100,100);
    Graphics rb = redBox.getGraphics();
    rb.setColor(Color.red);
    rb.fillRect(0, 0, 100, 100);
    repaint();
}                                        

public void paint(Graphics g){
    super.paint(g);
    Graphics demo = jPanel1.getGraphics();
    demo.drawImage(redBox, 0, 0, rootPane);
}
Now you will be able to run the app, draw the box, and it will not be subject to disappearing or getting damaged.

A good explanation of overriding paint() methods is available here if you want more information about that.

Test the app and verify the box is both created and not subject to damage. When satisfied, the code to draw a box can be replaced with code to actually draw a spiral.

Rather than start over, the existing code can be refactored. First, rename redBox to spiralImage. To do this, click any instance of the redBox variable so that it highlights yellow. Right-click, then choose Refactor, then Rename.



When prompted, give the new variable name.



Then you will see that NetBeans has found all instances of the variable name and updated them.



Next, replace everything in jButton1ActionPerformed with a simple call to a drawSpiral() method. This way, the code to draw won't be linked explicitly to the button handler.
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    drawSpiral();
}
The drawSpiral() method will find jPanel1's size, and create a matching spiralImage object to fit. Instead of drawing directly to the image with fill commands, we will exploit the Graphics2D class to paint an arbitrary shape.
public void drawSpiral() {
    Dimension d = jPanel1.getSize();
    spiralImage = createImage(d.width,d.height);
    Graphics2D si = (Graphics2D)spiralImage.getGraphics();
    si.setColor(Color.red);
    si.draw(spiralShape(d));
    repaint();
}
The spiralShape() method is where the real math will happen. It will leverage the GeneralPath class to connect dots using the moveTo and LineTo methods. A quick example of using those to draw an X follows (and then we'll replace that with the spiral, I promise.)
public GeneralPath spiralShape(Dimension d) {
    GeneralPath p = new GeneralPath();
    p.moveTo(0, 0);
    p.lineTo(d.width, d.height);
    p.moveTo(d.width, 0);
    p.lineTo(0, d.height);
    return p;
}
Verify that there are no red squigglies anywhere, and you should be able to run the app again, and draw an X. Your final code should look like this:
Image spiralImage;

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    drawSpiral();
}                                        

public void drawSpiral() {
    Dimension d = jPanel1.getSize();
    spiralImage = createImage(d.width,d.height);
    Graphics2D si = (Graphics2D)spiralImage.getGraphics();
    si.setColor(Color.red);
    si.draw(spiralShape(d));
    repaint();
}

public GeneralPath spiralShape(Dimension d) {
    GeneralPath p = new GeneralPath();
    p.moveTo(0, 0);
    p.lineTo(d.width, d.height);
    p.moveTo(d.width, 0);
    p.lineTo(0, d.height);
    return p;
}

public void paint(Graphics g){
    super.paint(g);
    Graphics j1 = jPanel1.getGraphics();
    j1.drawImage(spiralImage, 0, 0, rootPane);
}
...and this:



That's basically everything on the application side to worry about. We now have a working graphics architecture and are ready to replace the code drawing the big X with math that will create a spiral shape. Later we can come back and add controls to the form to change aspects of the spiral, e.g., rotation, how many loops, and the distance between spines.

The new spiralShape code first finds the center point and moves there. It then defines how many degrees to turn through, and converts it to radians using the handy mp8 variable. Then it iterates through the Archimedean Spiral equation one degree at a time, converting to Cartesian Coordinates as it goes, adding lines between the points to the GeneralPath object.
final double mp8 = Math.PI/180; // 1 Radian

public GeneralPath spiralShape(Dimension d) {
    GeneralPath p = new GeneralPath();
    float centerX = d.width/2;
    float centerY = d.height/2;

    // Hardcode some variables for now, change to accepting input from form later
    double end = 720 * mp8; // End at 720 degrees, two complete loops
    float a = 10;           // "scale" of the spiral, distance between spines

    p.moveTo(centerX, centerY);
    for (double theta = 0; theta < end; theta += mp8) {
        double r = a * theta; // Radius of current point
        double x = Math.cos(theta) * r + centerX;
        double y = Math.sin(theta) * r + centerY;
        p.lineTo(x, y);
    }

    return p;
}
And Bob's your uncle! You should now be able to run the app, click the "Draw Spiral" button, and actually draw a spiral.



If something went wrong during the development, or my instructions were difficult to follow, my SpiralForm.java file is available here.

That's enough for now. Next I'll show some mods to this to allow user input to change the dynamics of the spiral, and introduce the second spiral interlaced with the first.

No comments:

Post a Comment