Topic: Graphics and Drawing Shapes Date: Jan. 12, 2009 Number: 5 Examples: SimpleGraphics.hs, SierpinskiAlt.hs, Draw.hs Reading: Chap. 4 Announce - SA 3 due Wed. PS 1 is posted, due a week from Wednesday. You might want to look at it before sections next Thursday. -- Graphics commands Graphics are not quite as easy to understand as I/O commands. There is a type Graphic that is imported with SOE, but trying to print a Graphic expression doesn't work. Look at book's example for main0: main0 = runGraphics ( do w <- openWindow "My First Graphics Program" (300,300) drawInWindow w (text (100,200) "Hello Graphics World") k <- getKey w closeWindow w ) NOTE to self: To demo all of these function types, use :type to avoid writing it out! runGraphics :: IO () -> IO () is the thing that actually "does" graphics. Any graphic command must be performed inside of a call to runGraphics. (Of course, the command can be created outside of the call in a function or saved in a list, but must be performed inside the call.) openWindow :: String -> (Int, Int) -> IO Window String is the title and (Int, Int) gives size in pixels (width, height). Pops up a window and the <- assigns it to w. drawInWindow :: Window -> Graphic -> IO () text :: Point -> String -> Graphic where type Point = (Int, Int) is exported from graphics package. (0,0) is upper left corner of window, x positive to right, y positive DOWN. (Same as Java graphics.) Converts string to a Graphic whose lower left-hand corner is at Point. getKey :: Window -> IO Char (Book has a typo here, claims IO ()). Reads a character when you release the key. Here not because we care about k, but because we don't want the window to immediately close. closeWindow :: Window -> IO () closes the window. Note the "do" to allow sequencing. Note: still works without the "k <-". Book defines spaceClose. spaceClose :: Window -> IO () spaceClose w = do k <- getKey w if k == ' ' || k == '\x0' then closeWindow w else spaceClose w Gets a key. If key is space (or some funny character not in book!) then close the window. Else call self recursively. The functional way to write a While loop! (Really an Until loop.) This is called tail recursion. Last thing done is a recursive call - no need to do anything on return from call except return yourself. Compilers can easily convert tail recursion to loops, so tail recursion not costly. -- Drawing stuff other than text For drawing pictures, the graphics library provides: ellipse :: Point -> Point -> Graphic Points are two corners of the bounding box. shearEllipse :: Point -> Point -> Point -> Graphic Points define a parallelogram containing graphic. line :: Point -> Point -> Graphic polyline :: [Point] -> Graphic Draws lines, no close, no fill polygon :: [Point] -> Graphic Closes, filled polygon polyBezier :: [Point] -> Graphic Uses points as Bezier control points, draws smooth curve. Otherwise like polyline. Also: withColor :: Color -> Graphic -> Graphic Makes a graphic of the given color. So can do: pic1 = withColor Red (ellipse (150,150) (300,200)) pic2 = withColor Blue (polyline [(100,50),(200,50), (200,250),(100,250),(100,50)]) main2 = runGraphics ( do w <- openWindow "Some Graphics Figures" (300,300) drawInWindow w pic1 drawInWindow w pic2 spaceClose w ) Note that the assignments for pic1, pic2 done outside the runGraphics. drawInWindow converts the Graphic to a IO (), and draws it when performed. -- Sierpinski's triangle You did this in CS 5. First, look at fillTri: fillTri :: Window -> Int -> Int -> Int -> IO () fillTri w x y size = drawInWindow w (withColor Blue (polygon [(x,y),(x+size,y),(x,y-size),(x,y)])) First comment: don't need the (x,y) at end! Closes automatically. But makes it easy to convert to a polyline (where IS needed). Demonstrate. Second comment - it returns an IO (), because of the drawInWindow call. When called from within runGraphics it will actually draw it. The recursive definition is what you expect. The (x,y) is the lower left hand corner of the fractal being drawn (a right triangle shape), and the size is how long the legs are: minSize :: Int minSize = 8 sierpinskiTri :: Window -> Int -> Int -> Int -> IO () sierpinskiTri w x y size = if size <= minSize then fillTri w x y size else let size2 = size `div` 2 in do sierpinskiTri w x y size2 sierpinskiTri w x (y-size2) size2 sierpinskiTri w (x+size2) y size2 As usual, the runGraphics is at the top level. Note: use div as an infix operator. Can do this with any binary function if enclose in back-quotes `div`. We already saw can turn any binary operator into a function by enclosing in (), e.g. (+). General rule - function names use alphanumeric characters (starting with a letter), operator names use special symbols. In this context ' is considered alphanumeric. main3 = runGraphics ( do w <- openWindow "Sirpinski's Triangle" (400,400) sierpinskiTri w 50 300 256 spaceClose w ) To see an alternate way of handling things, instead of using "do" we can build a list of commands: sierpinskiTri :: Window -> Int -> Int -> Int -> [IO ()] sierpinskiTri w x y size = if size <= minSize then [fillTri w x y size] else let size2 = size `div` 2 in (sierpinskiTri w x y size2) ++ (sierpinskiTri w x (y-size2) size2) ++ (sierpinskiTri w (x+size2) y size2) and sequence_ them in main: main = runGraphics ( do w <- openWindow "Sierpinski's Triangle" (400,400) sequence_ (sierpinskiTri w 50 300 256) spaceClose w ) [GOT TO HERE in class, but will leave the notes logically grouped] -- Drawing Shapes This module imports the Shape module, and then draws the various shapes. Problem - the Graphics coordinate system is not a good match for our usual idea of a graphics plane - would normally have the origin at the center, use real coordinates, and have more than a pixel's width between successive coordinates. We also are used to the y-axis going in the other direction. Common trick in CS - when what you have is not what you want, write a module to make it better. So what do we need to do? 1) Have a better coordinate system. Call it "user coordinates", as opposed to "graphics coordinates". In the best of all worlds, could specify the x and y ranges on the graphics window. We will look at a module that does this later. Book takes a different approach: 1 unit in user coord. = 100 pixels. To do this, writes two conversion functions: inchToPixel :: Float -> Int inchToPixel x = round (100*x) pixelToInch :: Int -> Float pixelToInch n = intToFloat n / 100 intToFloat :: Int -> Float intToFloat n = fromInteger (toInteger n) Book has 2 exercises here, asking: Why not "100 * round x" in inchToPixel? (Only get values at 100-pixel intevals.) Why not intToFloat(n/100) in pixelToInch? (Can't divide two integers.) Defines some constants: xWin, yWin :: Int xWin = 600 yWin = 500 xWin2, yWin2 :: Int xWin2 = xWin `div` 2 yWin2 = yWin `div` 2 and a transformation function: trans :: Vertex -> Point trans (x,y) = ( xWin2 + inchToPixel x, yWin2 - inchToPixel y ) Why? Because the center of the coordinate system is at (xWin2, yWin2). Thus (0,0) should convert to this point, which it does. For x-axis positive x moves to left, negative to right; for y-axis positive y moves DOWN and negative y moves UP (graphics coord. are backwards in y from user coord. so subtract.) So pick a point. Say lower right corner. What are its user coordinates? (pixelToInch xWin2, -(pixelToInch yWin2)) Try doing computation by calculation, using substitution: trans (pixelToInch xWin2, -(pixelToInch yWin2)) => (xWin2 + inchToPixel (pixelToInch xWin2), yWin2 - inchToPixel (-(pixelToInch yWin2))) => (xWin2 + round (100 * (intToFloat xWin2/100)), ywin2 - round (100 * (intToFloat (-yWin2)/100))) => (xWin2 + xWin2, yWin2 + yWin2) => (xWin, yWin) Correct. That is encouraging! Book supplies transList: transList :: [Vertex] -> [Point] transList [] = [] transList (p:ps) = trans p : transList ps How can we write this as a higher-order function? transList list = map trans list So how do we draw all of our shapes? Decide where they should go on screen, transform them. shapeToGraphic :: Shape -> Graphic What about a rectangle? No built-in rectangle, so use a polygon. Center at (0,0). Then corners are all offset by half the side length in each direction: shapeToGraphic (Rectangle s1 s2) = let s12 = s1/2 s22 = s2/2 in polygon (transList [(-s12,-s22),(-s12,s22), (s12,s22),(s12,-s22)]) Center ellipse at (0,0). then the upper left corner of the box is (-r1,-r2) and lower right is (r1,r2): shapeToGraphic (Ellipse r1 r2) = ellipse (trans (-r1,-r2)) (trans (r1,r2)) Decided to have right triangle in first quadrant with right angle at (0,0): shapeToGraphic (RtTriangle s1 s2) = polygon (transList [(0,0),(s1,0),(0,s2)]) Polygon is easy - translate all the vertices: shapeToGraphic (Polygon pts) = polygon (transList pts) So we can make a bunch of shapes: sh1,sh2,sh3,sh4 :: Shape sh1 = Rectangle 3 2 sh2 = Ellipse 1 1.5 sh3 = RtTriangle 3 2 sh4 = Polygon [(-2.5,2.5), (-1.5,2.0), (-1.1,0.2), (-1.7,-1.0), (-3.0,0)] and draw a couple, using "do" and drawInWindow: main0 = runGraphics ( do w <- openWindow "Drawing Shapes" (xWin,yWin) drawInWindow w (withColor Red (shapeToGraphic sh1)) drawInWindow w (withColor Blue (shapeToGraphic sh2)) spaceClose w ) Then decided to be more clever. Do something other than a separate drawInWindow for each shape. Need a way to pair a color with a shape: type ColoredShapes = [(Color,Shape)] shs :: ColoredShapes shs = [(Red,sh1),(Blue,sh2),(Yellow,sh3),(Magenta,sh4)] drawShapes :: Window -> ColoredShapes -> IO () drawShapes w [] = return () drawShapes w ((c,s):cs) = do drawInWindow w (withColor c (shapeToGraphic s)) drawShapes w cs NOTE - uses "return ()" to be able to do nothing on empty list. Alternate approach: How can we use map, sequence_ to achieve this? Will see more later. main1 = runGraphics ( do w <- openWindow "Drawing Shapes" (xWin,yWin) drawShapes w shs spaceClose w ) -- More higher-order functions Suppose I have a list of shapes: let shapeList = [sh1, sh2, sh3, sh4] and a list of colors: let colorList = [Red, Blue, Yellow, Magenta] Can I get shs from these easily? Yes: zipWith (,) colorList shapeList gives: [(Red,Rectangle 3.0 2.0),(Blue,Ellipse 1.0 1.5),(Yellow,RtTriangle 3.0 2.0), (Magenta,Polygon [(-2.5,2.5),(-1.5,2.0),(-1.1,0.2),(-1.7,-1.0),(-3.0,0.0)])] Comes up so often given the name zip.