Solution for
Programming Exercise 7.7


THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.

Exercise 7.7: The applet at the end of Section 7.7 shows animations of moving symmetric patterns that look something like the image in a kaleidescope. Symmetric patterns are pretty. Make the SimplePaint3 applet do symmetric, kaleidoscopic patterns. As the user draws a figure, the applet should be able to draw reflected versions of that figure to make symmetric pictures.

The applet will have several options for the type of symmetry that is displayed. The user should be able to choose one of four options from a JComboBox menu. Using the "No symmetry" option, only the figure that the user draws is shown. Using "2-way symmetry", the user's figure and its horizontal reflection are shown. Using "4-way symmetry", the two vertical reflections are added. Finally, using "8-way symmetry", the four diagonal reflections are also added. Formulas for computing the reflections are given below.

The source code SimplePaint3.java already has a drawFigure() subroutine that draws all the figures. You can add a putMultiFigure() routine to draw a figure and some or all of its reflections. putMultiFigure should call the existing drawFigure to draw the figure and any necessary reflections. It decides which reflections to draw based on the setting of the symmetry menu. Where the mousePressed, mouseDragged, and mouseReleased methods call drawFigure, they should call putMultiFigure instead. The source code also has a repaintRect() method that calls repaint() on a rectangle that contains two given points. You can treat this in the same way as drawFigure(), adding a repaintMultiRect() that calls repaintRect() and replacing each call to repaintRect() with a call to repaintMultiRect(). Alternatively, if you are willing to let your applet be a little less efficient about repainting, you could simply replace each call to repaintRect() with a simple call to repaint(), without parameters. This just means that the applet will redraw a larger area than it really needs to.

If (x,y) is a point in a component that is width pixels wide and height pixels high, then the reflections of this point are obtained as follows:

The horizontal reflection is (width - x, y)

The two vertical reflections are (x, height - y) and (width - x, height - y)

To get the four diagonal reflections, first compute the diagonal reflection of (x,y) as

           a  =  (int)( ((double)y / height) * width );
           b  =  (int)( ((double)x / width) * height );

Then use the horizontal and vertical reflections of the point (a,b):

           (a, b)
           (width - a, b)
           (a, height - b)
           (width - a, height - b)

(The diagonal reflections are harder than they would be if the canvas were square. Then the height would equal the width, and the reflection of (x,y) would just be (y,x).)

To reflect a figure determined by two points, (x1,y1) and (x2,y2), compute the reflections of both points to get the reflected figure.

This is really not so hard. The changes you have to make to the source code are not as long as the explanation I have given here.

Here is my applet. Don't forget to try it with the symmetry menu set to "8-way Symmetry"!


Discussion

The original applet, SimplePaint3, has a method

       drawFigure(Graphics g, int shape, int x1, int y1, int x2, int y2)

that is used to draw all the lines and figures in the applet. As suggested in the exercise, the drawFigure method will still be used to draw individual lines and figures. However, it is called by another method, putMultiFigure, that takes care of drawing any necessary reflections. The putMultiFigure method checks the value of an instance variable, symmetry, to decide which reflections to draw. This variable will be set elsewhere in the applet to reflect the user's choice of symmetry type. The computations are exactly as indicated in the exercise. The points (x1,y1) and (x2,y2) are reflected using the formulas given there to get the points for the reflected figures.

      private void putMultiFigure(Graphics g, int shape, int x1, int y1, int x2, int y2) {
             // Draws the shape and possibly some of its reflections.
             // The reflections that are drawn depend on the selected
             // item in symmetryChoice.  The shapes are drawn by calling
             // the drawFigure method.

          int width = getWidth();
          int height = getHeight();

          drawFigure(g,shape,x1,y1,x2,y2);  // Draw the basic figure

          if (symmetry >= SYMMETRY_2) {  // Draw the horizontal reflection.
             drawFigure(g, shape, width - x1, y1, width - x2, y2);
          }

          if (symmetry >= SYMMETRY_4) {  // Draw the two vertical reflections.
             drawFigure(g, shape, x1, height - y1, x2, height - y2);
             drawFigure(g, shape, width - x1, height - y1, width - x2, height - y2);
          }

          if (symmetry == SYMMETRY_8) {  // Draw the four diagonal reflections.
             int a1 = (int)( ((double)y1 / height) * width );
             int b1 = (int)( ((double)x1 / width) * height );
             int a2 = (int)( ((double)y2 / height) * width );
             int b2 = (int)( ((double)x2 / width) * height );
             drawFigure(g, shape, a1, b1, a2, b2);
             drawFigure(g, shape, width - a1, b1, width - a2, b2);
             drawFigure(g, shape, a1, height - b1, a2, height - b2);
             drawFigure(g, shape, width - a1, height - b1, width - a2, height - b2);
          }

      }

In my applet, I followed a similar strategy for the repaintRect() method, as indicated in the exercise. You can check the details in my solution, below, if you are interested.

A third JComboBox menu is added to the applet to hold the four symmetry options. The option that is selected in this menu is checked in the mousePressed method when the user starts drawing. The value is stored in a new instance variable named symmetry. This variable is used in the putMultiFigure method as its sym parameter. The only other changes that are necessary is to change every call to putFigure in the original program into a call to putMultiFigure, and to change every call to repaintRect into a call to repaintMultiRect().


The Solution

Significant changes from SimplePaint3 are shown in red

     /*
        A simple program where the user can sketch curves and shapes in a 
        variety of colors on a variety of background colors.  The user can
        draw symmetric pictures in which shapes are reflected horizontally,
        vertically, and diagonally.  The user selects the type of symmetry
        from a pop-up menu at the top of the applet.  The user can
        select a color to be used for drawing from another pop-up menu.
        If the user clicks "Set Background", the background
        color is set to the current drawing color and the drawing
        area is filled with that color.  If the user clicks "Clear",
        the drawing area is just filled with the current background color.
        The user selects the shape to draw from another pop-up menu at the
        top of the applet.  The user can draw free-hand curves, straight
        lines, and one of six different types of shapes.

        The user's drawing is saved in an off-screen image, which is
        used to refresh the screen when repainting.  The picture is
        lost if the applet changes size, however.

        This file defines two classes, KaleidaPaint,class, and
        class, KaleidaPaint$Display.class.
     */


     import java.awt.*;
     import java.awt.event.*;
     import javax.swing.*;

     public class KaleidaPaint extends JApplet {

             // The main applet class simply sets up the applet.  Most of the
             // work is done in the Display class.

        JComboBox colorChoice, figureChoice;  // Pop-up menus, defined as instance
        JComboBox symmetryChoice;             // variables so that the Display
                                              // class can see them.


        public void init() {

           setBackground(Color.gray); 
           getContentPane().setBackground(Color.gray);

           Display canvas = new Display();  // The drawing area.
           getContentPane().add(canvas,BorderLayout.CENTER);

           JPanel buttonBar = new JPanel();       // A panel to hold the buttons.
           buttonBar.setBackground(Color.gray);
           getContentPane().add(buttonBar, BorderLayout.SOUTH);

           JPanel choiceBar = new JPanel();       // A panel to hole the pop-up menus
           choiceBar.setBackground(Color.gray);
           getContentPane().add(choiceBar, BorderLayout.NORTH);

           JButton fill = new JButton("Set Background");  // The first button.
           fill.addActionListener(canvas);
           buttonBar.add(fill);

           JButton clear = new JButton("Clear");   // The second button.
           clear.addActionListener(canvas);
           buttonBar.add(clear);

           colorChoice = new JComboBox();  // The pop-up menu of colors.
           colorChoice.addItem("Black");
           colorChoice.addItem("Red");
           colorChoice.addItem("Green");
           colorChoice.addItem("Blue");
           colorChoice.addItem("Cyan");
           colorChoice.addItem("Magenta");
           colorChoice.addItem("Yellow");
           colorChoice.addItem("White");
           colorChoice.setBackground(Color.white);
           choiceBar.add(colorChoice);

           figureChoice = new JComboBox();  // The pop-up menu of shapes.
           figureChoice.addItem("Curve");
           figureChoice.addItem("Straight Line");
           figureChoice.addItem("Rectangle");
           figureChoice.addItem("Oval");
           figureChoice.addItem("RoundRect");
           figureChoice.addItem("Filled Rectangle");
           figureChoice.addItem("Filled Oval");
           figureChoice.addItem("Filled RoundRect");
           figureChoice.setBackground(Color.white);
           choiceBar.add(figureChoice);

           symmetryChoice = new JComboBox();  // The menu of symmetry types.
           symmetryChoice.addItem("No Symmetry");
           symmetryChoice.addItem("2-way Symmetry");
           symmetryChoice.addItem("4-way Symmetry");
           symmetryChoice.addItem("8-way Symmetry");
           symmetryChoice.setBackground(Color.white);
           choiceBar.add(symmetryChoice);

        }  // end init()

        public Insets getInsets() {
               // Specify how wide a border to leave around the edges of the applet.
           return new Insets(3,3,3,3);
        }



        private class Display extends JPanel 
                   implements MouseListener, MouseMotionListener, ActionListener {

                // Nested class Display represents the drawing surface of the
                // applet.  It lets the user use the mouse to draw colored curves
                // and shapes.  The current color is specified by the pop-up menu
                // colorChoice.  The current shape is specified by another pop-up menu,
                // figureChoice.  (These are instance variables in the main class.)
                // The panel also listens for action events from buttons
                // named "Clear" and "Set Background".  The "Clear" button fills
                // the panel with the current background color.  The "Set Background"
                // button sets the background color to the current drawing color and
                // then clears.  These buttons are set up in the main class.


           private final static int 
                       BLACK = 0,
                       RED = 1,            // Some constants to make
                       GREEN = 2,          // the code more readable.
                       BLUE = 3,           // These numbers code for
                       CYAN = 4,           // the different drawing colors.
                       MAGENTA = 5,
                       YELLOW = 6,
                       WHITE = 7;

           private final static int
                      CURVE = 0,
                      LINE = 1,
                      RECT = 2,               // Some constants that code
                      OVAL = 3,               // for the different types of
                      ROUNDRECT = 4,          // figure the program can draw.
                      FILLED_RECT = 5,
                      FILLED_OVAL = 6,
                      FILLED_ROUNDRECT = 7;

           private final static int
                       NO_SYMMETRY = 0,       // Some constants that code for 
                       SYMMETRY_2 = 1,        // the different symmetry styles.
                       SYMMETRY_4 = 2,        
                       SYMMETRY_8 = 3;


           /* Some variables used for backing up the contents of the panel. */

           Image OSI;  // The off-screen image (created in checkOSI()).

           int widthOfOSI, heightOfOSI;  // Current width and height of OSI.  These
                                         // are checked against the size of the applet,
                                         // to detect any change in the panel's size.
                                         // If the size has changed, a new OSI is created.
                                         // The picture in the off-screen image is lost
                                         // when that happens.


           /* The following variables are used when the user is sketching a
              curve while dragging a mouse. */

           private int mouseX, mouseY;   // The location of the mouse.

           private int prevX, prevY;     // The previous location of the mouse.

           private int startX, startY;   // The starting position of the mouse.
                                         // (Not used for drawing curves.)

           private boolean dragging;     // This is set to true when the user is drawing.

           private int figure;    // What type of figure is being drawn.  This is
                                  //    specified by the figureChoice menu.

           private int symmetry;  // What type of symmetry style is being used.  This is
                                  //    specified by the symmetryChoice menu.

           private Graphics dragGraphics;  // A graphics context for the off-screen image,
                                           // to be used while a drag is in progress.

           private Color dragColor;  // The color that is used for the figure that is
                                     // being drawn.

           Display() {
                  // Constructor.  When this component is first created, it is set to
                  // listen for mouse events and mouse motion events from
                  // itself.  The initial background color is white.
              addMouseListener(this);
              addMouseMotionListener(this);
              setBackground(Color.white);
           }


           private void drawFigure(Graphics g, int shape, int x1, int y1, int x2, int y2) {
                 // This method is called to do ALL drawing in this applet!
                 // Draws a shape in the graphics context g.
                 // The shape parameter tells what kind of shape to draw.  This
                 // can be LINE, RECT, OVAL, ROUNTRECT, FILLED_RECT,
                 // FILLED_OVAL, or FILLED_ROUNDRECT.  (Note that a CURVE is
                 // drawn by drawing multiple LINES, so the shape parameter is
                 // never equal to CURVE.)  For a LINE, a line is drawn from
                 // the point (x1,y1) to (x2,y2).  For other shapes,  the
                 // points (x1,y1) and (x2,y2) give two corners of the shape
                 // (or of a rectangle that contains the shape).
              if (shape == LINE) {
                    // For a line, just draw the line between the two points.
                 g.drawLine(x1,y1,x2,y2);
                 return;
              }
              int x, y;  // Top left corner of rectangle that contains the figure.
              int w, h;  // Width and height of rectangle that contains the figure.
              if (x1 >= x2) {  // x2 is left edge
                 x = x2;
                 w = x1 - x2;
              }
              else {          // x1 is left edge
                 x = x1;
                 w = x2 - x1;
              }
              if (y1 >= y2) {  // y2 is top edge
                 y = y2;
                 h = y1 - y2;
              }
              else {          // y1 is top edge.
                 y = y1;
                 h = y2 - y1;
              }
              switch (shape) {   // Draw the appropriate figure.
                 case RECT:
                    g.drawRect(x, y, w, h);
                    break;
                 case OVAL:
                    g.drawOval(x, y, w, h);
                    break;
                 case ROUNDRECT:
                    g.drawRoundRect(x, y, w, h, 20, 20);
                    break;
                 case FILLED_RECT:
                    g.fillRect(x, y, w, h);
                    break;
                 case FILLED_OVAL:
                    g.fillOval(x, y, w, h);
                    break;
                 case FILLED_ROUNDRECT:
                    g.fillRoundRect(x, y, w, h, 20, 20);
                    break;
              }
           }


           private void putMultiFigure(Graphics g, int shape, int x1, int y1, int x2, int y2) {
                 // Draws the shape and possibly some of its reflections.
                 // The reflections that are drawn depend on the selected
                 // item in symmetryChoice.  The shapes are drawn by calling
                 // the drawFigure method.

              int width = getWidth();
              int height = getHeight();

              drawFigure(g,shape,x1,y1,x2,y2);  // Draw the basic figure

              if (symmetry >= SYMMETRY_2) {  // Draw the horizontal reflection.
                 drawFigure(g, shape, width - x1, y1, width - x2, y2);
              }

              if (symmetry >= SYMMETRY_4) {  // Draw the two vertical reflections.
                 drawFigure(g, shape, x1, height - y1, x2, height - y2);
                 drawFigure(g, shape, width - x1, height - y1, width - x2, height - y2);
              }

              if (symmetry == SYMMETRY_8) {  // Draw the four diagonal reflections.
                 int a1 = (int)( ((double)y1 / height) * width );
                 int b1 = (int)( ((double)x1 / width) * height );
                 int a2 = (int)( ((double)y2 / height) * width );
                 int b2 = (int)( ((double)x2 / width) * height );
                 drawFigure(g, shape, a1, b1, a2, b2);
                 drawFigure(g, shape, width - a1, b1, width - a2, b2);
                 drawFigure(g, shape, a1, height - b1, a2, height - b2);
                 drawFigure(g, shape, width - a1, height - b1, width - a2, height - b2);
              }

           }


           private void repaintRect(int x1, int y1, int x2, int y2) {
                 // Call repaint on a rectangle that contains the points (x1,y1)
                 // and (x2,y2).  (Add a 1-pixel border along right and bottom 
                 // edges to allow for the pen overhang when drawing a line.)
              int x, y;  // top left corner of rectangle that contains the figure
              int w, h;  // width and height of rectangle that contains the figure
              if (x2 >= x1) {  // x1 is left edge
                 x = x1;
                 w = x2 - x1;
              }
              else {          // x2 is left edge
                 x = x2;
                 w = x1 - x2;
              }
              if (y2 >= y1) {  // y1 is top edge
                 y = y1;
                 h = y2 - y1;
              }
              else {          // y2 is top edge.
                 y = y2;
                 h = y1 - y2;
              }
              repaint(x,y,w+1,h+1);
           }


           private void repaintMultiRect(int x1, int y1, int x2, int y2) {
                 // Call repaint on a rectangle that contains the points (x1,y1)
                 // and (x2,y2).  Also call repaint on reflections of this
                 // rectangle, depending on the type of symmetry.  The
                 // rects are repainted by calling repaintRect().
              int width = getWidth();
              int height = getHeight();
              repaintRect(x1,y1,x2,y2); // repaint the original rect
              if (symmetry >= SYMMETRY_2) {  // repaint the horizontal reflection.
                 repaintRect(width - x1, y1, width - x2, y2);
              }
              if (symmetry >= SYMMETRY_4) {  // repaint the two vertical reflections.
                 repaintRect(x1, height - y1, x2, height - y2);
                 repaintRect(width - x1, height - y1, width - x2, height - y2);
              }
              if (symmetry == SYMMETRY_8) {  // repaint the four diagonal reflections.
                 int a1 = (int)( ((double)y1 / height) * width );
                 int b1 = (int)( ((double)x1 / width) * height );
                 int a2 = (int)( ((double)y2 / height) * width );
                 int b2 = (int)( ((double)x2 / width) * height );
                 repaintRect(a1, b1, a2, b2);
                 repaintRect(width - a1, b1, width - a2, b2);
                 repaintRect(a1, height - b1, a2, height - b2);
                 repaintRect(width - a1, height - b1, width - a2, height - b2);
              }
           }


           private void checkOSI() {
                // This method is responsible for creating the off-screen image. 
                // It should be called before using the OSI.  It will make a new OSI if
                // the size of the panel changes.
              if (OSI == null || widthOfOSI != getSize().width || heightOfOSI != getSize().height) {
                     // Create the OSI, or make a new one if panel size has changed.
                 OSI = null;  // (If OSI already exists, this frees up the memory.)
                 OSI = createImage(getSize().width, getSize().height);
                 widthOfOSI = getSize().width;
                 heightOfOSI = getSize().height;
                 Graphics OSG = OSI.getGraphics();  // Graphics context for drawing to OSI.
                 OSG.setColor(getBackground());
                 OSG.fillRect(0, 0, widthOfOSI, heightOfOSI);
                 OSG.dispose();
              }
           }


           public void paintComponent(Graphics g) {
                // Copy the off-screen image to the screen,
                // after checking to make sure it exists.  Then,
                // if a shape other than CURVE is being drawn, 
                // draw it on top of the image from the OSI.
              checkOSI();
              g.drawImage(OSI, 0, 0, this);
              if (dragging && figure != CURVE) {
                 g.setColor(dragColor);
                 putMultiFigure(g,figure,startX,startY,mouseX,mouseY);
              }
           }


           public void actionPerformed(ActionEvent evt) {
                   // Respond when the user clicks on a button.  The
                   // command must be either "Clear" or "Set Background".
              String command = evt.getActionCommand();
              checkOSI();
              if (command.equals("Set Background")) {
                     // Set background color before clearing.
                     // Change the selected color so it is different
                     // from the background color.
                 setBackground(getCurrentColor());
                 if (colorChoice.getSelectedIndex() == BLACK)
                    colorChoice.setSelectedIndex(WHITE);
                 else
                    colorChoice.setSelectedIndex(BLACK);
              }
              Graphics g = OSI.getGraphics();
              g.setColor(getBackground());
              g.fillRect(0,0,getSize().width,getSize().height);
              g.dispose();
              repaint();
           }


           private Color getCurrentColor() {
                    // Check the colorChoice menu to find the currently
                    // selected color, and return the appropriate color
                    // object.
              int currentColor = colorChoice.getSelectedIndex();
              switch (currentColor) {
                 case BLACK:
                    return Color.black;
                 case RED:
                    return Color.red;
                 case GREEN:
                    return Color.green;
                 case BLUE:
                    return Color.blue;
                 case CYAN:
                    return Color.cyan;
                 case MAGENTA:
                    return Color.magenta;
                 case YELLOW:
                    return Color.yellow;
                 default:
                    return Color.white;
              }
           }


           public void mousePressed(MouseEvent evt) {
                   // This is called when the user presses the mouse on the
                   // panel.  This begins a draw operation in which the user
                   // sketches a curve or draws a shape.  (Note that curves
                   // are handled differently from other shapes.  For CURVE,
                   // a new segment of the curve is drawn each time the user
                   // moves the mouse.  For the other shapes, a "rubber band
                   // cursor" is used.  That is, the figure is drawn between
                   // the starting point and the current mouse location.)

              if (dragging == true)  // Ignore mouse presses that occur
                  return;            //    when user is already drawing a curve.
                                     //    (This can happen if the user presses
                                     //    two mouse buttons at the same time.)

              prevX = startX = evt.getX();  // Save mouse coordinates.
              prevY = startY = evt.getY();

              figure = figureChoice.getSelectedIndex();
              symmetry = symmetryChoice.getSelectedIndex();
              dragColor = getCurrentColor();         
              dragGraphics = OSI.getGraphics();
              dragGraphics.setColor(dragColor);

              dragging = true;  // Start drawing.

           } // end mousePressed()


           public void mouseReleased(MouseEvent evt) {
                   // Called whenever the user releases the mouse button.
                   // If the user was drawing a shape, we make the shape
                   // permanent by drawing it to the off-screen image.
               if (dragging == false)
                  return;  // Nothing to do because the user isn't drawing.
               dragging = false;
               mouseX = evt.getX();
               mouseY = evt.getY();
               if (figure == CURVE) {
                      // A CURVE is drawn as a series of LINEs
                   putMultiFigure(dragGraphics,LINE,prevX,prevY,mouseX,mouseY);
                   repaintMultiRect(prevX,prevY,mouseX,mouseY);
               }
               else if (figure == LINE) {
                  repaintMultiRect(startX,startY,prevX,prevY);
                  if (mouseX != startX || mouseY != startY) {
                        // Draw the line only if it has non-zero length.
                     putMultiFigure(dragGraphics,figure,startX,startY,mouseX,mouseY);
                     repaintMultiRect(startX,startY,mouseX,mouseY);
                  }
               }
               else {
                  repaintMultiRect(startX,startY,prevX,prevY);
                  if (mouseX != startX && mouseY != startY) {
                        // Draw the shape only if both its height
                        // and width are both non-zero.
                     putMultiFigure(dragGraphics,figure,startX,startY,mouseX,mouseY);
                     repaintMultiRect(startX,startY,mouseX,mouseY);
                  }
               }
               dragGraphics.dispose();
               dragGraphics = null;
           }


           public void mouseDragged(MouseEvent evt) {
                    // Called whenever the user moves the mouse while a mouse button
                    // is down.  If the user is drawing a curve, draw a segment of
                    // the curve on the off-screen image, and repaint the part
                    // of the panel that contains the new line segment.  Otherwise,
                    // just call repaint and let paintComponent() draw the shape on
                    // top of the picture in the off-screen image.

               if (dragging == false)
                  return;  // Nothing to do because the user isn't drawing.

               mouseX = evt.getX();   // x-coordinate of mouse.
               mouseY = evt.getY();   // y=coordinate of mouse.

               if (figure == CURVE) { 
                      // A CURVE is drawn as a series of LINEs.
                  putMultiFigure(dragGraphics,LINE,prevX,prevY,mouseX,mouseY);
                  repaintMultiRect(prevX,prevY,mouseX,mouseY);
               }
               else {
                     // Repaint two rectangles:  The one that contains the previous
                     // version of the figure, and the one that will contain the
                     // new version.  The first repaint is necessary to restore
                     // the picture from the off-screen image in that rectangle.
                  repaintMultiRect(startX,startY,prevX,prevY);
                  repaintMultiRect(startX,startY,mouseX,mouseY);
               }

               prevX = mouseX;  // Save coords for the next call to mouseDragged or mouseReleased.
               prevY = mouseY;

           } // end mouseDragged.


           public void mouseEntered(MouseEvent evt) { }   // Some empty routines.
           public void mouseExited(MouseEvent evt) { }    //    (Required by the MouseListener
           public void mouseClicked(MouseEvent evt) { }   //    and MouseMotionListener
           public void mouseMoved(MouseEvent evt) { }     //    interfaces).


        } // end nested class Display


     } // end class KaleidaPaint

[ Exercises | Chapter Index | Main Index ]