Programming Project 2

By working through the steps in this handout, you will develop a Haskell program to draw a picture made up of many overlapping squares of different sizes and colors.

  1. The following data type will represent a single square:
    data Square = Square Point Int RGB
    
    This says that a Square will be specified by a Point (the center), an Int (the distance from the center to any of the sides, i.e., half the length of a side), and a color expressed as red, green, and blue components. The type Int is one of the built-in types (it is a variant of Integer that gives up the very large values in exchange for greater efficiency); each of the other types is already defined in the Graphics Library, so we just need to know how to use them.

    Create a Haskell script file named project2.hs in your directory on the I: drive. It should start with the following lines:

    -- CSC122, Project 2
    -- <your name>
    -- <the date>
    
    
    import SOEGraphics
    
    
    data Square = Square Point Int RGB
    
    Now :load your script into Hugs. For this item, all we will do is construct a Square object. To use the Square constructor function, we need a Point, an Int, and an RGB. A Point is simply a pair of integers, so we can use something like (200, 100). An Int is just a value like 50. An RGB needs three integers in the range 0 to 255; to get white, we can use the RGB constructor as follows: RGB 255 255 255. Putting all this together, we can form a white square, 100 units on each side, centered on the point (200, 100), by using the expression Square (200, 100) 50 (RGB 255 255 255). If you type this in, the system will complain that it cannot find an appropriate ``show'' function. This is OK; we can still use the Square type in a program, and we can check that we are using it correctly by only requesting the type of the above expression: :type Square (200, 100) 50 (RGB 255 255 255).
  2. We will need a function to draw one of our Square objects. The Haskell Graphics library provides a function polygon which takes a list of Points and produces a filled-in shape with the given points as vertices. We will write a convenience function fillSquare which takes a Square and makes the appropriate call to polygon:
    fillSquare (Square (x,y) d rgb) = withRGB rgb (polygon [(x-d,y-d),
                                                            (x-d,y+d),
                                                            (x+d,y+d),
                                                            (x+d,y-d)])
    
    As usual, we write the function to match its argument against a pattern so that the variables in the pattern will be bound to the corresponding values for the Square we wish to draw. The center point will be (x,y), the distance from the center to each side will be d, and the color will be rgb. We call the polygon function with an appropriate list of points for the four corners of the square, and pass the result to the withRGB function to apply a color to the shape. Add the above function definition to your script. If you try to apply fillSquare to the square mentioned previously, fillSquare (Square (200,100) 50 (RGB 255 255 255)), the system should again complain about the lack of an appropriate ``show'' function. The result is of the type Draw (), which the system might also call Graphic. The square has not been drawn yet, but this Graphic value packages up all the information that the Graphics Library will need to display it. The next step will do so.
  3. To display a square on the screen, we need to use the following function:
    showGraphic g = runGraphics $ do {
        w <- openWindow "Graphics Window" (400,300);
        drawInWindow w g;
        getKey w;
        closeWindow w
    }
    
    Add this to your script, load it, and try applying showGraphic to the result of various calls to fillSquare. Each time you do, it should create a graphics window, display a filled square, and wait for you to hit any key before closing the window. For testing, it will be convenient to define some sample squares. Add some lines like the following to your script and reload:
    redGiant = Square (150,150) 100 (RGB 255 0 0)
    whiteDwarf = Square (250,250) 25 (RGB 255 255 255)
    
    Now you can easily try drawing squares by entering showGraphic (fillSquare redGiant), etc., at the Main> prompt in Hugs. However, this only lets you draw one square at a time. To combine several squares in one picture, we need the overGraphic function. It will be easiest to use this as an operator, as follows: showGraphic ((fillSquare redGiant) `overGraphic` (fillSquare whiteDwarf)). Notice how the squares are combined, and see what happens if you reverse the order of the squares.
  4. Now let's do a list of Squares. We will need a function which takes a list of squares, fills each one, and combines them all into a single Graphic. As usual when working with a list of things, we will define the function by recursion. Here is the base case, which uses the special value emptyGraphic to produce a blank picture:
    fillSquareList [] = emptyGraphic
    
    The recursive case will have the following form:
    fillSquareList (s : ss) =
    
    When this pattern matches, s will be the first square on the list, and ss will be a list of the remaining squares. Write an appropriate right-hand side for this case of the function. You will need to use fillSquare to produce a Graphic from s, and fillSquareList to produce another Graphic from ss; then these two Graphics will need to be combined into one with overGraphic. When you have added the function to your script, try it out with showGraphic (fillSquareList [redGiant, whiteDwarf]).
  5. We will also want a function to generate a list of squares, so that we don't have to type them all in by hand. Here is an example:
    diagonalSquares (x,y) d rgb 0 = []
    diagonalSquares (x,y) d rgb n = Square (x,y) d rgb :
                                    diagonalSquares (x+d,y+d) d rgb (n-1)
    
    Try this: showGraphic (fillSquareList (diagonalSquares (50,50) 25 (RGB 255 255 255) 9)). The cases for this function mean that diagonalSquares (x,y) d rgb n will produce a list of n squares (because the list is empty when n is 0, and it gets one extra element for each recursive call as n counts down to 0). The first one will be centered at (x,y), with dimension d and color rgb. Succeeding squares will be offset by adding d to the x and y coordinates of the center; the effect will be that each square will be centered on the lower-right corner of the previous one. You should get a picture that looks like a white staircase.
  6. A simple modification to the previous code gives us a row of squares of different sizes. Create a new function named vanishingSquares which is similar to diagonalSquares except replace the argument d in the recursive call (to vanishingSquares, of course) with the expression (d `quot` 2). To try this out, you will probably want to start with a larger initial square; something like this should work: showGraphic (fillSquareList (vanishingSquares (200,100) 80 (RGB 255 255 255) 7))
  7. Now we can approach our original goal of creating an interesting picture by replacing the single recursive call in the previous item with four separate calls, one at each of the four corners of the central square. Each of these recursive calls will produce a list of squares, so we will need to append all of the lists together, using ++. Here is a skeleton of the code for you to fill in:
    squareDesign (x,y) d rgb 0 =
    squareDesign (x,y) d rgb n = Square (x,y) d rgb :
                                 (squareDesign (x-d,y-d) (d `quot` 2) rgb (n-1) ++
                                  squareDesign (x-d,y+d) (d `quot` 2) rgb (n-1) ++
                                  squareDesign                                  ++
                                  squareDesign                                 )
    
    When you have the finished function saved in your script file, load it into Hugs and try the following: showGraphic (fillSquareList (squareDesign (200,150) 70 (RGB 255 255 255) 5))
    You can change the 5 to 6 to draw one more level, but it will probably crash if you try to do 7 or more levels (each level has four times as many squares, so there are 4096 squares at level 7).
  8. The last step will be to change the color as well as the size. Modify the recursive calls so that the upper-left and lower-right corners have only half the red component of the current square, while the upper-right and lower-left corners have only half the green component; leave the blue component untouched. You will need to pattern-match on the color argument to extract the red, green, and blue components; that is, you will have something like (RGB r g b) instead of the simple variable rgb. Call the new function colorDesign.
  9. As a final problem, how could you change the code to draw the larger squares on the bottom, instead of on top? Whichever function needs to be modified, name the new version by appending a 2; for example, if you modify colorDesign, then your new function should be named colorDesign2.

When you are done, make sure your script file with all of the above code is saved in your folder on the I: drive.