> ### 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.8 shows animations of moving symmetric patterns that look something like the image in a kaleidascope. Symmetric patterns are pretty. Make the `SimplePaint3` applet do symmetric, kaleidascopic 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 Choice 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 `putFigure()` subroutine that draws all the figures. You can add a `putMultiFigure()` routine to draw a figure and some or all of its reflections. `putMultiFigure` can call the existing `putFigure` to draw each figure. It decides which reflections to draw based on the setting of the symmetry Choice menu. Where the `mousePressed`, `mouseDragged`, and `mouseReleased` methods call `putFigure`, they should call `putMultiFigure` instead.

> 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 Choice menu set to "8-way Symmetry"!

> Discussion

> The original applet, SimplePaint3, has a method

>            private void putFigure(Graphics g, int kind,
>                              int x1, int y1, int x2, int y2, boolean outlineOnly)

> that is used to draw all the lines and figures in the applet. (The `outlineOnly` parameter is set to `true` when a figure is being drawn in XOR mode. This is a signal that only the outline of figures should be drawn, even if the selected figure style is filled. This is done because drawing filled figures in XOR mode looks bad.)

> As suggested in the exercise, the `putFigure` 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. It has an additional parameter, `sym`, to tell it which reflections to draw. 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 kind, int sym,
>                                 int x1, int y1, int x2, int y2, boolean outlineOnly) {
>                  // Draws a figure with corners at (x1,y1) and (x2,y2) and possibly
>                  // some of its reflections.  The reflections that are drawn depend
>                  // on the sym parameter, which has one of the values NO_SYMMETRY,
>                  // SYMMETRY_2, SYMMETRY_4, or SYMMETRY_8.  The
>                  // parameter "kind" codes for the type of figure to draw.

>               int width = getSize().width;
>               int height = getSize().height;

>               putFigure(g, kind, x1, y1, x2, y2, outlineOnly);

>               if (sym >= SYMMETRY_2) {  // Draw the horizontal reflection.
>                  putFigure(g, kind, width - x1, y1, width - x2, y2, outlineOnly);
>               }

>               if (sym >= SYMMETRY_4) {  // Draw the two vertical reflections.
>                  putFigure(g, kind, x1, height - y1, x2, height - y2, outlineOnly);
>                  putFigure(g, kind, width - x1, height - y1,
>                                                   width - x2, height - y2, outlineOnly);
>               }

>               if (sym == 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 );
>                  putFigure(g, kind, a1, b1, a2, b2, outlineOnly);
>                  putFigure(g, kind, width - a1, b1, width - a2, b2, outlineOnly);
>                  putFigure(g, kind, a1, height - b1, a2, height - b2, outlineOnly);
>                  putFigure(g, kind, width - a1, height - b1,
>                                                   width - a2, height - b2, outlineOnly);
>               }

>            } // end putMultiFigure

> A third `Choice` 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 passed to the `putMultiFigure` method as its `sym` parameter. The only other change that is necessary is to change every call to `putFigure` in the original program into a call to `putMultiFigure`.

> 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 selects
>            a drawing color form a pop-up menu at the top of the
>            applet.  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 can specify symmetric drawing of three types, by selecting
>            from a pop-up menu.  In 2-way Symmetry, the horizontal reflection of
>            each figure is drawn along with the figure.  4-way Symmetry adds the
>            vertical reflections.  8-way symmetry adds the diagonal reflections.
>            This gives pretty kaleidascope-like pictures.

>            The user's drawing is saved in an off-screen canvas, 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 itself, and a non-public
>            class, KaleidaCanvas, that is used by KaleidaPaint.
>         */

>         import java.awt.*;
>         import java.awt.event.*;
>         import java.applet.*;

>         public class KaleidaPaint extends Applet {

>                 // The main applet class simply sets up the applet.  Most of the
>                 // work is done in the canvas.  The canvas listens for events from
>                 // the buttons.

>            public void init() {

>               setBackground(Color.gray);         // Background color of applet, shows
>                                                  //   in borders and between components.
>               setLayout(new BorderLayout(3,3));  // Layout manager for applet.

>               KaleidaCanvas canvas = new KaleidaCanvas();  // The drawing area.
>               add(canvas,BorderLayout.CENTER);

>               Panel buttonBar = new Panel();       // A panel to hold the buttons.
>               buttonBar.setBackground(Color.lightGray);
>               add(buttonBar, BorderLayout.SOUTH);

>               Panel choiceBar = new Panel();       // A panel to hole the pop-up menus
>               choiceBar.setBackground(Color.lightGray);
>               add(choiceBar, BorderLayout.NORTH);

>               Button fill = new Button("Set Background");  // The first button.
>               fill.addActionListener(canvas);
>               fill.setBackground(Color.lightGray);
>               buttonBar.add(fill);

>               Button clear = new Button("Clear");   // The second button.
>               clear.addActionListener(canvas);
>               clear.setBackground(Color.lightGray);
>               buttonBar.add(clear);

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

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

>               Choice choice3 = new Choice();   // The pop-up menu of symmetry styles.
>               choice3.add("No symmetry");
>               choice3.add("2-way symmetry");
>               choice3.add("4-way symmetry");
>               choice3.add("8-way symmetry");
>               choice3.setBackground(Color.white);
>               choiceBar.add(choice3);

>               canvas.colorChoice = choice;      // Canvas needs access to the pop-up menus,
>               canvas.figureChoice = choice2;    //   so it can check it to find out what
>               canvas.symmetryChoice = choice3;  //   color, shape, and symmetry to use.

>            }  // 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);
>            }

>         } // end class KaleidaPaint

>         class KaleidaCanvas extends Canvas
>                          implements MouseListener, MouseMotionListener, ActionListener {

>                 // A KaleidaCanvas lets the user use the mouse to draw colored curves
>                 // and shapes.  The current color is specified by a pop-up menu which
>                 // must be created by the applet and assigned to the instance variable
>                 // colorChoice.   The current shape is specified by another pop-up menu,
>                 // figureChoice.  The applet listens for action events from buttons
>                 // named "Clear" and "Set Background".  The "Clear" button fills
>                 // the canvas with the current background color.  The "Set Background"
>                 // sets the background color to the current drawing color and
>                 // then clears.

>            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 differnet drawing colors.
>                        MAGENTA = 5,
>                        YELLOW = 6,
>                        WHITE = 7;

>            Choice colorChoice;  // A Choice object, containing the possible drawing
>                                 // colors, which must be created by the applet.

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

>            Choice figureChoice;  // A Choice object containing the possible figures.

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

>            Choice symmetryChoice;  // A Choice object containing the possible
>                                    //   symmetry styles.

>            /* Some variables used for double-buffering. */

>            Image OSC;  // The off-screen canvas (created in setupOSC()).

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

>            /* The following variables are used when the user is sketching a
>               curve while dragging a 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 graphicsForDrawing;  // A graphics context for the applet.

>            private Graphics offscreenGraphics;   // A graphics context for the off-screen canvas.

>            KaleidaCanvas() {
>                   // Constructor.  When the canvas 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 setupOSC() {
>                 // This method is responsible for creating the off-screen canvas.
>                 // It should be called before using the OSC.  It will make a new OSC if
>                 // the size of the applet changes.
>               if (OSC == null || widthOfOSC != getSize().width || heightOfOSC != getSize().height) {
>                      // Create the OSC, or make a new one if applet size has changed.
>                  OSC = null;  // (If OSC already exists, this frees up the memory.)
>                  OSC = createImage(getSize().width, getSize().height);
>                  widthOfOSC = getSize().width;
>                  heightOfOSC = getSize().height;
>                  Graphics OSG = OSC.getGraphics();  // Graphics context for drawing to OSC.
>                  OSG.setColor(getBackground());
>                  OSG.fillRect(0, 0, widthOfOSC, heightOfOSC);
>               }
>            }

>            private Graphics getOSG() {
>                  // Return a graphics context for drawing to the off-screen canvas.
>                  // A new canvas is created if necessary.  Note that the graphics
>                  // context should not be kept for any length of time, in case the
>                  // size of the canvas changes.
>                setupOSC();
>                return OSC.getGraphics();
>            }

>            private void clearOSC() {
>                  // Fill the off-screen canvas with the background color.
>                Graphics OSG = OSC.getGraphics();
>                OSG.setColor(getBackground());
>                OSG.fillRect(0, 0, widthOfOSC, heightOfOSC);
>                OSG.dispose();
>            }

>            public void update(Graphics g) {
>                  // Redefine update so it doesn't clear the canvas before calling paint().
>               paint(g);
>            }

>            public void paint(Graphics g) {
>                 // Just copy the off-screen canvas to the screen.
>               setupOSC();
>               g.drawImage(OSC, 0, 0, this);
>            }

>            public void actionPerformed(ActionEvent evt) {
>                    // Respond when the user clicks on a button.
>               String command = evt.getActionCommand();
>               if (command.equals("Clear")) {
>                      // Clear the off-screen canvas to current background color,
>                      // then call repaint() so the screen is also cleared.
>                  clearOSC();
>                  repaint();
>               }
>               else if (command.equals("Set Background")) {
>                      // Set background color, then clear.
>                  setBackground(getCurrentColor());
>                  clearOSC();
>                  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;
>               }
>            }

>            private void putFigure(Graphics g, int kind,
>                                       int x1, int y1, int x2, int y2, boolean outlineOnly) {
>                  // Draws a figure with corners at (x1,y1) and (x2,y2).  The
>                  // parameter "kind" codes for the type of figure to draw.  If the
>                  // figure is LINE, a line is drawn between the points.  For the
>                  // other shapes, we need the top-left corner, the width, and the
>                  // height of the shape.  We have to figure out whether x1 or x2
>                  // is the left edge of the shape and compute the width accordingly.
>                  // Similarly for y1 and y2.  If outlineOnly is true, then filled shapes
>                  // are drawn in outline only.
>               if (kind == LINE)
>                  g.drawLine(x1, y1, x2, y2);
>               else {
>                  int x, y, w, h;  // Top-left corner, width, and height.
>                  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;
>                  }
>                  switch (kind) {   // 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:
>                        if (outlineOnly)
>                           g.drawRect(x, y, w, h);
>                        else
>                           g.fillRect(x, y, w, h);
>                        break;
>                     case FILLED_OVAL:
>                        if (outlineOnly)
>                           g.drawOval(x, y, w, h);
>                        else
>                        g.fillOval(x, y, w, h);
>                        break;
>                     case FILLED_ROUNDRECT:
>                        if (outlineOnly)
>                           g.drawRoundRect(x, y, w, h, 20, 20);
>                        else
>                        g.fillRoundRect(x, y, w, h, 20, 20);
>                        break;
>                  }
>               }
>            }

>            private void putMultiFigure(Graphics g, int kind, int sym,
>                                       int x1, int y1, int x2, int y2, boolean outlineOnly) {
>                  // Draws a figure with corners at (x1,y1) and (x2,y2) and possibly
>                  // some of its reflections.  The reflections that are drawn depend
>                  // on the sym parameter, which has one of the values NO_SYMMETRY,
>                  // SYMMETRY_2, SYMMETRY_4, or SYMMETRY_8.  The
>                  // parameter "kind" codes for the type of figure to draw.

>               int width = getSize().width;
>               int height = getSize().height;

>               putFigure(g, kind, x1, y1, x2, y2, outlineOnly);

>               if (sym >= SYMMETRY_2) {  // Draw the horizontal reflection.
>                  putFigure(g, kind, width - x1, y1, width - x2, y2, outlineOnly);
>               }

>               if (sym >= SYMMETRY_4) {  // Draw the two vertical reflections.
>                  putFigure(g, kind, x1, height - y1, x2, height - y2, outlineOnly);
>                  putFigure(g, kind, width - x1, height - y1, width - x2, height - y2, outlineOnly);
>               }

>               if (sym == 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 );
>                  putFigure(g, kind, a1, b1, a2, b2, outlineOnly);
>                  putFigure(g, kind, width - a1, b1, width - a2, b2, outlineOnly);
>                  putFigure(g, kind, a1, height - b1, a2, height - b2, outlineOnly);
>                  putFigure(g, kind, width - a1, height - b1, width - a2, height - b2, outlineOnly);
>               }

>            } // end putMultiFigure

>            public void mousePressed(MouseEvent evt) {
>                    // This is called when the user presses the mouse on the
>                    // canvas.  This begins a draw operation in which the user
>                    // sketches a curve or draws a shape.

>               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(); // Type of figure being drawn.

>               symmetry = symmetryChoice.getSelectedIndex(); // Symmetry style for drawing.

>               graphicsForDrawing = getGraphics();  // For drawing on the screen.
>               graphicsForDrawing.setColor(getCurrentColor());

>               offscreenGraphics = getOSG();  // For drawing on the canvas.
>               offscreenGraphics.setColor(getCurrentColor());

>               if (figure != CURVE) {
>                      // Shapes are drawn in XOR mode so they can be erased by redrawing.
>                      // Curves are drawn directly to both the screen and to the OSC.
>                  graphicsForDrawing.setXORMode(getBackground());
>                  putMultiFigure(graphicsForDrawing, figure, symmetry,
>                                                   startX, startY, startX, startY, true);
>               }

>               dragging = true;  // Start drawing.

>            } // end mousePressed()

>            public void mouseReleased(MouseEvent evt) {
>                    // Called whenever the user releases the mouse button.
>                    // If the use was drawing a shape, we make the shape
>                    // permanent by drawing it in paintMode on both the screen
>                    // and the off-screen canvas (but only if the mouse is not
>                    // at its starting location.)  Then get rid of the graphics
>                    // contexts that we were using while dragging.
>                if (dragging == false)
>                   return;  // Nothing to do because the user isn't drawing.
>                dragging = false;
>                if (figure != CURVE) {
>                       // Erase the last XOR mode shape by redrawing it in XOR mode.
>                       // Then, if the mouse is not back where it started from,
>                       // Draw the final shape on both the screen and the off-screen
>                       // canvas in paintMode.
>                   putMultiFigure(graphicsForDrawing, figure, symmetry,
>                                                           startX, startY, prevX, prevY, true);
>                   if (startX != prevX || startY != prevY) {
>                      graphicsForDrawing.setPaintMode();
>                      putMultiFigure(graphicsForDrawing, figure, symmetry,
>                                                                startX, startY, prevX, prevY, false);
>                      putMultiFigure(offscreenGraphics, figure, symmetry,
>                                                                startX, startY, prevX, prevY, false);
>                   }
>                }
>                graphicsForDrawing.dispose();
>                offscreenGraphics.dispose();
>                graphicsForDrawing = null;
>                offscreenGraphics = 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 both the screen and the off-screen canvas.
>                     // If the user is drawing a shape, erase the previous shape
>                     // by redrawing it (in XOR mode), then draw the new shape.

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

>                int x = evt.getX();   // x-coordinate of mouse.
>                int y = evt.getY();   // y=coordinate of mouse.

>                if (figure == CURVE) {
>                       // Draw the line on the applet and on the off-screen canvas.
>                   putMultiFigure(graphicsForDrawing, LINE, symmetry, prevX, prevY, x, y, false);
>                   putMultiFigure(offscreenGraphics, LINE, symmetry, prevX, prevY, x, y, false);
>                }
>                else {
>                      // Erase previous figure and draw a new one using the new mouse position.
>                   putMultiFigure(graphicsForDrawing, figure, symmetry,
>                                                          startX, startY, prevX, prevY, true);
>                   putMultiFigure(graphicsForDrawing, figure, symmetry,
>                                                          startX, startY, x, y, true);
>                }

>                prevX = x;  // Save coords for the next call to mouseDragged or mouseReleased.
>                prevY = y;

>            } // 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 class KaleidaCanvas

> [ Exercises | Chapter Index | Main Index ]