DRAWING CURVES WITH HIGHER-ORDER PROCEDURES In this little project, we will extend the Scheme language to allow us to manipulate and draw curves, and then we will use this extended language to draw fractal designs. Most of the procedures you will need are already defined in file funcurves.scm, which you should download. Your project assignment is to do the exercises described in this document and turn in the specified files and pictures. The due date is FRIDAY, March 28th. Curves as Procedures We're going to develop a language for defining and drawing planar curves. We'd like to plot points, construct graphs of functions, transform curves by scaling and rotating, and so on. A planar curve in "parametric form" can be described mathematically as a function from parameter values to points in the plane. For example, we can describe the unit-circle as the function taking t to the point (cos(2*pi*t), sin(2*pi*t)) where t ranges over the unit interval [0,1]. Instead of inventing our own representation for points, we'll use the representation already built into DrScheme. The constructor for a point is called make-posn and the selectors are posn-x and posn-y. With these procedures available, we can define the unit-circle and the unit-line: (define (unit-circle t) (make-posn (sin (* 2 pi t)) (cos (* 2 pi t)))) (define (unit-line t) (make-posn t 0)) We also want to operate on curves. For example, the mapping (x,y) -> (-y,x) rotates the plane by pi/2, so (define (rotate-pi/2 curve) (lambda (t) (let ((ct (curve t))) (make-posn (- (posn-y ct)) (posn-x ct))))) defines a procedure that takes a curve and transforms it into another, rotated curve. In file funcurves.scm you will fine more operations on curves: * translate returns a curve-transform which rigidly moves a curve given distances along the x and y axes, * scale-x-y returns a curve-transform that stretches a curve along the x and y coordinates by the given scale factors, and * rotate-around-origin returns a curve-transform that rotates a curve by a given number of radians. The curve-transform put-in-standard-position is a convenient way to standardize open curves. A curve is in standard position if its start point is the point (0,0) and its end point is the point (1,0). To put a curve in standard position, we rigidly translate its starting point to the origin, then rotate the curve until the end point is on the x axis, and then scale the curve so that the end point is (1,0): (define (put-in-standard-position curve) (let* ((start-point (curve 0)) (curve-started-at-origin ((translate (- (posn-x start-point)) (- (posn-y start-point))) curve)) (new-end-point (curve-started-at-origin 1)) (theta (atan (posn-y new-end-point) (posn-x new-end-point))) (curve-ended-at-x-axis ((rotate-around-origin (- theta)) curve-started-at-origin)) (end-point-on-x-axis (posn-x (curve-ended-at-x-axis 1)))) ((scale (/ 1 end-point-on-x-axis)) curve-ended-at-x-axis))) (Note: the special form let* is just like let except that local variables are assigned their values in sequence. This allows the value of a local variable early in the list to be used in the calculation of the value of a local variable that comes later in the list. This is something that we could not do with let, because all the local variables in a let statement get their values simultaneously.) It is useful to have operations that combine curves into new ones. The binary-transform connect-rigidly, for example, takes two curves and draws them one after another: (define (connect-rigidly curve1 curve2) (lambda (t) (if (< t (/ 1 2)) (curve1 (* 2 t)) (curve2 (- (* 2 t) 1))))) Note, however, that if the endpoint of curve1 is not the same as the start point of curve2, the two curves are not connected together. Your first exercise in this project is to define a binary transform that does connect the curves together. *********************************************************************** EXERCISE 1. Define a binary-transform called connect-ends so that (connect-ends curve1 curve2) consists of a copy of curve1 followed by a copy of curve2 after it has been rigidly translated so that its starting point coincides with the end point of curve1. After defining connect-ends, use it and the transforms provided to define a curve we'll call Koch. The Koch curve consists of the unit line followed by another unit line that has been rotated by pi/3 radians and connected to the first unit line. Another unit line is rotated by -pi/3 radians and connected to the end of the second unit line. Finally, another unit line is connected to the end of the third unit line. The result is a curve that looks something like this: /\ / \ / \ / \ / \ / \ / \ / \ __________________/ \__________________ You should also scale it by a factor of 1/3 so that it will be in standard position. Note: to rotate a curve, say curve1 by r radians, use the expression ((rotate-around-origin r) curve1) to produce the rotated curve. We'll assume that curve1 is already in standard position. For this exercise, turn in the definitions of both connect-ends and Koch. *************************************************************************** Drawing Curves Loading file funcurves.scm into DrScheme will open a window in which you can draw curves. The window coordinates go from 0 to 1 in both x and y with (0,0) at the lower left. The size of the window is determined by the value of the global variable size, and the window is assigned to the variable w so that it can be passed as an input to the drawing procedures. Two drawing procedures have been provided: * (draw-connected window number-of-points) is a higher-order procedure that draws the curve to which it is applied using number-of-points line segments. * (draw-points-on window number-of-points) is a higher-order procedure that draws the curve to which it is applied by drawing number-of-points + 1 points along the curve without connecting them. If you want to look at a curve in standard position to see what it looks like, you can use the command ((draw-connected w 200) ((translate .25 .5) ((scale .5) curve1))) where curve1 is the curve you want to look at. The curve will be drawn in the middle of the window at 1/2 scale. Note how curve1 has been transformed by three different higher-order procedures. You might want to look at your Koch curve to see if it is correct. Fractal Curves A fractal curve is a "curve" that, if you expand a small piece of it, you get something similar to the original. An example of a fractal curve is the Gosper C Curve. Bill Gosper discovered that the infinite repetition of a very simple process creates this curve. The Gosper curve can be approximated by repeating the simple process to only a certain number of levels. A level-0 curve is just a straight line. A level-1 curve consists of two level-0 curves. A level-2 curve consists of two level-1 curves, and so on. The simple process that is repeated recursively is this: a level-n curve is made from two level-(n-1) curves, each scaled to be sqrt(2)/2 times the length of the original curve. One of the component curves is rotated by pi/4 (45 degrees) and the other is rotated by -pi/4. After each curve is scaled and rotated, it is translated so that the ending point of the first piece is continuous with the starting point of the second piece. We assume that the approximation we are given to improve (named curve in the procedure) is in standard position. By doing some geometry, you can figure out that the second curve, after being scaled and rotated, must be translated right by .5 and up by .5, so its beginning coincides with the endpoint of the rotated, scaled first curve. This leads to the curve- transform gosperize: (define (gosperize curve) (let ((scaled-curve ((scale (/ (sqrt 2) 2)) curve))) (connect-rigidly ((rotate-around-origin (/ pi 4)) scaled-curve) ((translate .5 .5) ((rotate-around-origin (/ (- pi) 4)) scaled-curve))))) Now we can generate approximations at any level to the Gosper curve by repeatedly gosperizing the unit line: (define (gosper-curve level) ((repeated gosperize level) unit-line)) To look at the level n Gosper curve, evaluate (show-connected-gosper n): (define (show-connected-gosper level) ((draw-connected w 200) ((squeeze-rectangular-portion -.5 1.5 -.5 1.5) (gosper-curve level)))) **************************************************************************** EXERCISE 2. Define a procedure show-points-gosper such that the evaluation of (show-points-gosper window leveln number-of-points initial-curve) will plot number-of-points unconnected points of the level leveln gosper curve in the window, but starting the gosper-curve approximation with an arbitrary initial-curve rather than the unit line. For instance, (show-points-gosper w n 200 unit-line) will display the same points as (show-connected-gosper n), but without connecting them. Turn in your definition of show-points-gosper (and any auxiliary procedures if you need them). Also, run the command (show-points-gosper w 20 10000 unit-line) and print out the curve that is produced. (This will take a while.) *************************************************************************** The Gosper fractals we have been playing with have had the angle of rotation fixed at 45 degrees. This angle need not be fixed. It need not even be the same for every step of the process. Many interesting shapes can be created by changing the angle from step to step. We can define a procedure param-gosper that generates Gosper curves with changing angles. Param-gosper takes a level number (the number of levels to repeat the process) and a second argument called angle-at. The procedure angle-at should take one argument, the level number, and return an angle (measured in radians) as its answer. Procedure param-gosper can use this to calculate the angle to be used at each step of the recursion. (define (param-gosper level angle-at) (if (= level 0) unit-line ((param-gosperize (angle-at level)) (param-gosper (- level 1) angle-at)))) The procedure param-gosperize is almost like gosperize, except that it takes another argument, the angle of rotation, and implements the process of rotating the curve in one direction, then rotating it by the same amount in the other direction, and connecting the two versions of the curve together. (define (param-gosperize theta) (lambda (curve) (let ((scale-factor (/ (/ 1 (cos theta)) 2))) (let ((scaled-curve ((scale scale-factor) curve))) (connect-rigidly ((rotate-around-origin theta) scaled-curve) ((translate .5 (* (sin theta) scale-factor)) ((rotate-around-origin (- theta)) scaled-curve))))))) For example, the ordinary Gosper curve at level n is returned by (param-gosper n (lambda (level) (/ pi 4))) **************************************************************************** EXERCISE 3. Generate some parameterized Gosper curves where the angle changes with the level n. I suggest starting with pi/4 when n is even and -pi/4 when n is odd. Submit sample printouts with your problem solutions. Note: You need to use 10,000 points or more when drawing the curve to get a real look at it. If you just sample it at a few hundred points, you may get a pretty picture, but it may look very different from the actual curve. **************************************************************************** EXERCISE 4. If you search the Web for basic information about fractals or for information about Koch curves, you will quickly find pictures of the Koch curve, another famous fractal curve. Usually, three copies of the Koch curve are put together like the sides of an equalateral triangle, but the resulting figure looks more like a snowflake. We can generate the Koch curve using param-gosper by providing the right angle-at function. The secret is to figure out what the right angle is for each level. Define a suitable angle-at procedure and use it with param-gosper to generate the Koch curve to 10 levels. Draw the curve in the graphics window using 10000 points and print the resulting curve. Turn in your code and your printout of the curve. Hint: The magnitude of the angle stays constant, but the sign of the angle changes with each level. Another hint: the Koch curve you created for EXERCISE 1 above is actually a level 2 Koch curve. ***************************************************************************