Skip to main content

Assignment 4: Creating and testing drawings

Due: Tuesday, February 21 by 10:30pm

Summary: In this assignment, you will experiment with different kinds of transformations of both individual drawings and lists of drawings.

Purposes: To further practice with the drawings-as-values model. To consider concise algorithms for creating images with repetitive elements. To gain further experience writing procedures.

Collaboration: You must work with your assigned partners on this assignment. You must collaborate on every problem - do not break up the work so that one person works on problem 1, another on problem 2, and another on problem 3. (The “don’t break up the work” policy applies on every assignment. This note is just a reminder.) You may discuss this assignment with anyone, provided you credit such discussions when you submit the assignment.

Submitting: Email your answer to csc151-01-grader@grinnell.edu. The title of your email should have the form [CSC 151.01] Assignment 4 and should contain your answers to all parts of the assignment. Scheme code should be in the body of the message. You should not attach any images; we should be able to re-create them from your code.

Warning: So that this assignment is a learning experience for everyone, we may spend class time publicly critiquing your work.

Problem 1: Centering drawings

Topics: drawings

We’ve seen that we can shift drawings horizontally and vertically, so that we can move them to a position relative to their current position.
But what if we want to move them to an absolute position (that is, an exact position).

There are, of course, a variety of ways to think about the position of a drawing. Do we speak in terms of its edges, its “logical” center, its center of gravity, or ….?

We will choose a simple model, and place drawings according to their “center”. What’s the center? For circles and squares (and ellipses and rectangles), the answer is pretty easy, the center is horizontally midway between the leftmost point and the rightmost point, and vertically midway between the topmost point and the bottommost point. We’ll use the same rules for compound drawings: Even if it doesn’t look that way, we’ll call the center the point horizontally midway between the leftmost and rightmost point and vertically between the topmost point and bottomost point.

Here’s some code that allows you to find the center of a drawing using this approach.

;;; Procedure:
;;;   simple-center-x
;;; Parameters:
;;;   drawing, a drawing
;;; Purpose:
;;;   Find the x coordinate of the "center" of the drawing using a simple 
;;;   formula
;;; Produces:
;;;   center-x, a real number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   The distance from the center to the left and right edges is
;;;   identical (or as identical as Scheme will let it be).  That is
;;;     (abs (- (drawing-left drawing) center-x)) =~
;;;     (abs (- (drawing-right drawing) center-x))
(define drawing-center-x
  (lambda (drawing)
    (* 1/2 (+ (drawing-left drawing) (drawing-right drawing)) )))

;;; Procedure:
;;;   simple-center-y
;;; Parameters:
;;;   drawing, a drawing
;;; Purpose:
;;;   Find the y coordinate of the "center" of the drawing using a simple 
;;;   formula
;;; Produces:
;;;   center-y, a real number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   The distance from the center to the top and bottom edges is
;;;   identical (or as identical as Scheme will let it be).  That is
;;;     (abs (- (drawing-top drawing) center-y)) =~
;;;     (abs (- (drawing-bottom drawing) center-y))
(define drawing-center-y
  (lambda (drawing)
    (* 1/2 (+ (drawing-top drawing) (drawing-bottom drawing)) )))

Document and write a procedure, (drawing-center-at drawing x y) that creates a copy of drawing, centered at (x,y).

Problem 2: Testing your procedures

Topics: testing

Write a test suite for drawing-center-at.

You should make sure to conduct all of the following tests.

  • The original center is at the origin.
  • The original center is along the x axis.
  • The original center is along the y axis.
  • The original center is in quadrant I (and II and III and IV).
  • The target center is at the origin.
  • The target center is along the x axis.
  • The target center is along the y axis.
  • The target center is in quadrant I (and II and III and IV).
  • Circles, squares, ellipses, rectangles, and compound drawings.
  • Relatively small drawings.
  • Relatively large drawings.
  • Drawings whose center coordinates are integers.
  • Drawings whose center coordinates are not integers.
  • Anything else you think is applicable.

Problem 3: Smaller neighbors

Topics: drawings

We often ask students to write a procedure, add-smaller-neighbor, that adds a slightly smaller neighbor to an image.

In solving this problem, many students write something like the following code.

(define add-smaller-neighbor
(lambda (drawing)
  (drawing-group drawing
                 (hshift-drawing (drawing-width drawing)
                                 (scale-drawing .75 drawing)))))

This procedure works fine if the input drawing is on the left margin of the screen. Unfortunately, if the input drawing is not on the left margin of the screen, the smaller neighbor overlaps the input drawing, rather than being on the side.

(define thingy
 (hshift-drawing
  100
  (vshift-drawing
   60
   (scale-drawing
    80
    (drawing-group
     (recolor-drawing "black" drawing-unit-square)
     (recolor-drawing "red" drawing-unit-circle))))))

A red circle on a black square (image-show (drawing->image thingy 200 100))

A red circle on a black square with an overlaid smaller red circle
on a smaller black square (image-show (drawing->image (add-smaller-neighbor thingy) 200 100))

Why do we have this problem? Because drawing-scale scales everything: not just the shape (or shapes), but also the left and top offset.

How do we solve the problem? There are a variety of options. Here are two. You might choose to temporarily shift the drawing back to the left margin and then shift the pair back after making the neighbor. If that’s too much effort, you can come up with a better formula for how much to shift the right neighbor. (The math isn’t too hard.)

It will be easiest if we break the problem down into two parts: Making the right neighbor and adding it to the original drawing.

Once we know how to make a right neighbor, adding it is easy.

(define add-smaller-right-neighbor
  (lambda (drawing)
    (drawing-group drawing (smaller-right-neighbor drawing))))

Your goal is therefore to write smaller-right-neighbor. To help you in this endeavor, here’s some documentation.

;;; Procedure:
;;;   smaller-right-neighbor
;;; Parameters:
;;;   drawing, a drawing
;;; Purpose:
;;;   Create a smaller version of drawing, situated immediately to the
;;;   right of drawing.
;;; Produces:
;;;   neighbor, a drawing
;;; Preconditions:
;;;   (drawing-width drawing) > 0
;;;   (drawing-height drawing) > 0
;;; Postconditions:
;;;   (drawing-width neighbor) = (* 0.75 (drawing-width drawing))
;;;   (drawing-height neighbor) = (* 0.75 (drawing-height drawing))
;;;   (drawing-top neighbor) = (drawing-top drawing)
;;;   (drawing-left neighbor) = (drawing-right drawing)
;;;   The colors and shapes of neighbor are essentially the same as
;;;     those of drawing.

(a) Implement smaller-right-neighbor. That is, define the procedure.

Make sure that you try your procedure on a variety of drawings, including some drawings that have their left edge to the left of the image (that is, less than zero) and that have their top edge at various places.

(b) What if we want the right neighbor to be something other than 75% of the original drawing? We might add a second parameter to specify the scale factor. Implement, but do not document, a procedure, add-scaled-right-neighbor, that takes as parameters both a drawing and a scale factor. You can assume that the scale factor is positive.

Hint: You will probably want to write a scaled-right-neighbor procedure that also takes as parameters both a drawing and a scale factor. If you do choose to write that procedure, you should document it.

Once again, you should make sure to check a variety of cases, including scale factors both smaller and larger than 1.

(c) Document and implement a procedure, add-scaled-left-neighbor that behaves much like add-scaled-right-neighbor except that the neighbor is on the left, rather than the right. Make sure to specify the top, left, width, and height of the resulting drawing.

(d) Implement a procedure, add-scaled-bottom-neighbor that behaves much like add-scaled-right-neighbor, except that (i) the neighbor is below the original drawing, rather than to the side, and (ii) the center of the neighbor is directly below the center of the original drawing.

Problem 4: Drawing Circles

Topics: lists of drawings

In the lab on lists of drawings, you observed that we could create interesting patterns by making a list of a simple shape and two lists of horizontal and vertical offsets, and then combining it all with appropriate calls to map.

Let’s suppose we want to take some simple shape and make an ellipse from 36 copies of that shape. Here’s the start of a program to do just that.

;;; Procedure:
;;;   ellipse-of-drawings
;;; Parameters:
;;;   drawing, a drawing
;;;   horiz-radius, a positive real number
;;;   vert-radius, a positive real number
;;; Purpose:
;;;   Make an "ellipse" of 36 copies of drawing.  
;;; Produces:
;;;   ellipse, a compound drawing
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   The center of ellipse is the same as the center of drawing.
;;;   ellipse consists of 36 copies of drawing
;;;   The distance from the center of the leftmost copy to the center
;;;     of the rightmost copy is approximately 2*horiz-radius.
;;;   The distance from the center of the topmost copy to the center
;;;     of the bottommost copy is approximately 2*vert-radius.
(define ellipse-of-drawings
  (lambda (drawing horiz-radius vert-radius)
    (map vshift-drawing 
         (map (section ellipse-element-ycoord <> vert-radius) (iota 36))
         (map hshift-drawing
              (map (r-s ellipse-element-xcoord horiz-radius) (iota 36))
              (make-list 36 drawing)))))

Here’s another way to write that procedure.

(define ellipse-of-drawings
  (lambda (drawing horiz-radius vert-radius)
    (map (lambda (i)
           (vshift-drawing 
            (ellipse-element-ycoord i vert-radius)
            (hshift-drawing 
             (ellipse-element-xcoord i horiz-radius)
             drawing)))
          (iota 36))))

Here are a few sample uses of that procedure.

> (define circ5 (scale-drawing 5 drawing-unit-circle))
> (image-show 
   (drawing->image
    (hshift-drawing 100
                    (vshift-drawing 100
                                    (drawing-compose
                                     (ellipse-of-drawings circ5 100 75))))
    200 200))

An ellipse of circles

> (define ell (drawing-compose (ellipse-of-drawings circ5 20 40)))
> (image-show 
   (drawing->image
    (hshift-drawing 100
                    (vshift-drawing 100
                                    (drawing-compose
                                     (ellipse-of-drawings ell 100 75))))
    200 200))

An ellipse of ellipses of circles

As you may have noted, ellipse-of-drawings requires two procedures, ellipse-element-xcoord and ellipse-element-ycoord. Each take two parameters: an integer between 0 and 35, inclusive, and a radius. Each returns the corresponding coordinate of that copy of the drawing in the circle.

Document (4Ps) and write ellipse-element-xcoord and ellipse-element-ycoord.

You can compute the x coordinate by taking the cosine of the angle and multiplying by the horizontal radius. You can compute the y coordinate by taking the sine of the angle and then multiplying by the vertical radius.

Extra credit: Make the number of drawings a parameter to ellipse-of-drawings, ellipse-element-xcoord, and ellipse-element-ycoord.