Fundamentals of Computer Science I: Media Computing (CS151.02 2007F)

Assignment: Spirograph®-Like Drawings


Due: 4 p.m., Friday, 2 November 2007

Summary: In this assignment, you will experiment with drawings that are formed by rolling an outer “stamp” around an inner “disc”. The technique for making these diagrams is based, in part, on the structure of the Spirograph® toy.

Purposes: To give you more experience with numeric recursion. To give you an opportunity to play with interesting images.

Expected Time: Three to four hours.

Collaboration: We encourage you to work in groups of size three. You may, however, work alone or work in a group of size two or size four. You may discuss this assignment with anyone, provided you credit such discussions when you submit the assignment.

Submitting: Email your answer to . The title of your email should have the form CSC151.02 2007F Assignment: Spirograph-Like Drawings and should contain your answers to all parts of the assignment. Scheme code should be in the body of the message. Do not attach any images. We should be able to regenerate any images you create just from the instructions you submit.

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

Preliminaries: From Spirograph® to Rolling Stamps

Many of you may have played with a Spirograph® when you were growing up. These toys provide an interesting way to draw complex shapes with simple tools. In a typical Spirograph®, you work with two circular discs. You place a pen through a hole in one disc and rotate that disc around another disc. Together, the two discs describe a complex path for the pen.

While the more mathematically inclined among you might be able to write an equation for spirographic curves, our goal here is to use them as inspiration for techniques for creating interesting drawings algorithmically. In particular, we're going to consider how to simulate rolling one circle around another and how that might provide a mechanism for making drawings.

Here's what we'll do: Rather than trying to trace a line, we'll simply rotate the outer disc (which we'll call the “stamp”) around an inner disc, pausing every so often to place the contents of the stamp on the image. To implement that idea, we'll write three procedures:

  • (draw-stamp! image col row radius rotation), which draws one stamp, centered at (col,row), rotated by rotation (an angle in radians) from its original orientation;

  • (draw-rolled! image col row inner-radius stamp-radius revolution), which draws one stamp (with radius of stamp-radius) that has been rolled by an angle of revolution around a disc with radius inner-radius centered at (col,row); and

  • (roll! image n col row inner-radius stamp-radius theta), which draws n copies of a stamp of radius stamp-radius, rolled around a disc with radius inner-radius centered at (col,row), rotating each subsequent stamp by theta from the previous stamp.

The first procedure, draw-stamp!, can be fairly simple. To write the second procedure, draw-rolled!, we will need to figure out the center and angle of rotation for the rolled stamp. To write the third procedure, roll!, we will simply need to use numeric recursion to call draw-rolled! an appropriate number of times.

Step One: Drawing Stamps

We'll start with a relatively straightforward version of draw-stamp!. In this version, we'll draw a circle (so that we can visualize where the stamp is) and a line pointing in the direction of the angle (with horizontally to the right representing an angle of 0).

Drawing the circle should be easy, since we recently wrote a draw-circle! procedure. In case you've forgotten, here's what that procedure looks like.

;;; Procedure:
;;;   draw-circle!
;;; Parameters:
;;;   image, an image
;;;   col, an integer
;;;   row, an integer
;;;   radius, an integer
;;; Purpose:
;;;   Draws a circle with the specified in the current brush and color, centered at (col,row).
;;; Produces:
;;;   [Nothing; Called for the side effect]
;;; Preconditions:
;;;   0 <= col < (image.width image)
;;;   0 <= row < (image.height image)
;;;   0 < radius
;;; Postconditions:
;;;   The image now contains the specified circle.  (The circle may not be visible.)
(define draw-circle!
  (lambda (image col row radius)
    (image.select-ellipse! image selection.replace
                           (- col radius) (- row radius)
                           (+ radius radius) (+ radius radius))
    (image.stroke! image)
    (image.select-nothing! image)))

Drawing the circular part of the stamp means we have to do little more than call that procedure.

(define draw-stamp!
  (lambda (image col row radius rotation)
    (draw-circle! image col row radius)
    ...))

Now, what do we do about the rotated radial line? Well, if we remember our trigonometry correctly, the cosine of an angle gives the x coordinate and the sine of the angle gives the y coordinate of a unit vector from the origin. Since we're not using a unit vector, we need to scale by the radius. Since we're not starting at the origin, we need to offset by col and row. Putting those instructions together with those above, we get our complete draw-stamp! proedure.

;;; Procedure:
;;;   draw-stamp
;;; Parameters:
;;;   image, an image
;;;   col, an integer
;;;   row, an integer
;;;   radius, an integer
;;;   rotation, a real number
;;; Purpose:
;;;   Draws a rotatable stamp, centered at (col,row) and rotated by rotation
;;;     (an angle expressed in radians).
;;;   The details of the stamp drawn are intentionally left unspecified so
;;;     that implementers can choose different stamps for different effects.
;;; Produces:
;;;   [Nothing; Called for the side effect]
;;; Preconditions:
;;;   0 <= col < (image.width image)
;;;   0 <= row < (image.height image)
;;; Postconditions:
;;;   image has been extended with the stamp.
(define draw-stamp!
  (lambda (image col row radius rotation)
    (draw-circle! image col row radius)
    (image.draw-line! image
                      col row
                      (+ col (* radius (cos rotation)))
                      (+ row (* radius (sin rotation))))))

You may find it useful to use the preceding to draw a few stamps to make sure that you understand what the procedure does.

> (define canvas (image.new 200 200))
> (image.show canvas)
> (envt.set-fgcolor! "blue")
> (envt.set-brush! "Circle (01)") 
> (draw-stamp! canvas  20  20 20 0)
> (envt.update-displays!)
> (draw-stamp! canvas 100  20 20 (* pi 0.25))
> (envt.update-displays!)
> (draw-stamp! canvas 180  20 20 (* pi 0.50))
> (envt.update-displays!)
> (draw-stamp! canvas  20 100 20 (* pi 0.625))
> (envt.update-displays!)
> (draw-stamp! canvas 100 100 20 (* pi -0.5))
> (envt.update-displays!)
> (draw-stamp! canvas 180 100 20 pi)
> (envt.update-displays!)
> (draw-stamp! canvas 20 180 20 (* pi 1.5))
> (envt.update-displays!)
> (draw-stamp! canvas 100 180 20 (* pi -.5))
> (envt.update-displays!)
> (draw-stamp! canvas 180 180 20 (* pi 3.5))
> (envt.update-displays!)

Step Two: Drawing Rolled Stamps

Now, how do we roll the stamp around a center disc? As we indicated above, we need to find out the center of the rolled stamp and the angle the stamp has rolled. (The angle of rotation is not the same as the angle of revolution, since a smaller disc (the stamp) rotates more than it revolves when it revolves around a larger disc.) Finding the center should be a simple variant of the line drawing above. The center of the rolled stamp is inner-radius plus stamp-radius away from the center of the inner disc. As before, we find the cosine and sine of the angle of rotation, scale by this new distance, and then offset by the column and row of the center. That is, the column and row at the center of the stamp are given by the following two lines.

(+ col (* (+ inner-radius stamp-radius) (cos revolution)))
(+ row (* (+ inner-radius stamp-radius) (sin revolution)))

What about the angle of rotation? We need a way to compute that from the values we have so far: The angle of revolution (which we'll call theta), the inner radius (which we'll call R), and the stamp radius (which we'll call r). A reasonable first step is to figure out how far along the inner disc the stamp has traveled. We know that when we've for an angle of theta, we traverse theta/2pi of the circumference. (That is, we compute the ratio of the angle traveled to the full angle from start back to start.) What's the circumference of the inner disc? That's 2*pi*R. So, the distance traveled is theta*R.

Now, we need to compute what angle or rotation, alpha, is made when the stamp rotates enough to roll a distance of d. Using the same analysis, we get that the distance is alpha*r, so alpha is d/r. Putting this conclusion together with that from the previous analysis, we now know that the angle of rotation is theta*R/r.

We are now ready to express all of the above in code.

;;; Procedure:
;;;   draw-rolled!
;;; Parameters:
;;;   image, an image
;;;   col, an integer
;;;   row, an integer
;;;   inner-radius, an integer
;;;   stamp-radius, an integer
;;;   revolution, a real number
;;; Purpose:
;;;   Draws one rotated stamp, after rolling it clockwise from the starting 
;;;   position by an angle of revolution (expressed in radians).  
;;; Produces:
;;;   [Nothing, called for its side effects]
;;; Preconditions:
;;;   0 <= col < (image.width image)
;;;   0 <= row < (image.height image)
;;;   0 < inner-radius
;;; Postconditions:
;;;   The image has been extended appropriately.
;;; Package:
;;;   Part of the "roller pipe" drawing utilities.
;;; Props:
;;;   Inspired by James Clayson's "Visual Modeling with LOGO".
(define draw-rolled!
  (lambda (image col row inner-radius stamp-radius revolution)
    (draw-stamp! image
                 (+ col (* (+ inner-radius stamp-radius) (cos revolution)))
                 (+ row (* (+ inner-radius stamp-radius) (sin revolution)))
                 stamp-radius
                 (/ (* revolution inner-radius) stamp-radius))))

Once again, it is useful to see what this does by drawing a few sample images.

> (define canvas (image.new 200 200))
> (image.show canvas)
> (envt.set-brush! "Circle (01)")
> (envt.set-fgcolor! "green")
> (draw-circle! canvas 100 100 50)
> (envt.update-displays!)
> (envt.set-fgcolor! "blue")
> (draw-rolled! canvas 100 100 50 25 0)
> (envt.update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.125))
> (envt.update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.25))
> (envt.update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.5))
> (envt.update-displays!)
> (draw-rolled! canvas 100 100 50 25 pi)
> (envt.update-displays!)

Step Three: Drawing Multiple Stamps

We are now ready to draw multiple stamps. As we draw each stamp, we'll need to keep track of how many stamps we've drawn (so that we know when we're done) and the current angle of revolution (which we need for computation). Hence, we write a helper procedure that includes two additional parameters: the step and the angle of revolution. We start the step at 0 and the angle at 0. We stop when the step equals the desired number of steps, n. At each recursive call, we increment the angle of revolution and the number of steps.

(define roll!
  (lambda (image col row inner-radius stamp-radius theta n)
    (roll-kernel! image col row inner-radius stamp-radius theta n
                  0 0)))

(define roll-kernel!
  (lambda (image col row inner-radius stamp-radius theta n step revolution)
    (cond ((< step n)
           (draw-rolled! image col row inner-radius stamp-radius revolution)
           (roll-kernel! image col row inner-radius stamp-radius theta n
                         (+ step 1) (+ revolution theta))))))

Okay, we're ready for a simple test. Let's draw about sixteen stamps around the center.

> (define canvas (image.new 200 200))
> (image.show canvas)
> (envt.set-brush! "Circle (01)")
> (envt.set-fgcolor! "green")
> (draw-circle! canvas 100 100 50)
> (envt.update-displays!)
> (envt.set-fgcolor! "blue")
> (roll! canvas 100 100 50 25 (/ pi 8) 16)
> (envt.update-displays!)

If all went well, you should see an extended version of our example from the previous section.

Of course, the images we've created previously are not nearly as interesting as those we get from a Spirograph®. How do we get more spirographic images? We draw many more stamps. For example, we might repeat the previous instructions using 256 stamps, rather than sixteen.

> (define canvas (image.new 200 200))
> (image.show canvas)
> (envt.set-brush! "Circle (01)")
> (envt.set-fgcolor! "green")
> (draw-circle! canvas 100 100 50)
> (envt.update-displays!)
> (envt.set-fgcolor! "blue")
> (roll! canvas 100 100 50 25 (/ pi 128) 256)
> (envt.update-displays!)

You may note that in the result, the circles are starting to get in the way. Try commenting out the call to draw-circle! in draw-stamp! and trying the previous instructions again.

Now, let's think about varying the parameters to the roll! procedure. For each of the following, first consider what you think will happen when we make the change and then check your answer experimentally. In each case, assume that the change is from the original call to roll!.

  • What will happen if we change the inner radius from 50 to 25?

  • What will happen if we change the inner radius from 50 to 75?

  • What will happen if we change the stamp radius from 25 to 12.5?

  • What will happen if we change the stamp radius from 25 to 50?

  • What will happen if we change the stamp radius from 25 to -25?

  • What will happen if we change the stamp radius from 25 to 20 and the number of steps from 256 to 512?

  • What will happen if we change the angle from (/ pi 8) to (/ pi 16) and leave the number of steps at 256?

  • What will happen if we change the angle from (/ pi 8) to (/ pi 16) and the number of steps from 256 to 512?

Boy, that was a lot of preliminary exploration, wasn't it? (The amount of exploration is one of the reasons we've suggested this assignment might take a bit more time.) Okay, you're now ready to have an assignment.

Assignment

Using your own versions of draw-stamp!, draw-rolled!, and roll!, generate three interesting 512x512 images. You may use a different variant for each image. Or, you may use the same variant for all three images, and just vary the parameters. You may also find that you want to use multiple calls to roll! for the same image.

How might you vary the procedures? You could have draw-stamp! draw more than a single line or a line of different lengths. You could have roll! set the foreground color or brush before each call to draw-rolled!. You could have draw-rolled! send different radii to draw-stamp!, making the radius depend upon the angle. You may wish to experiment with techniques similar to those you used in the lab on geometric art. The options are nearly endless.

For each image, write a short paragraph explaining the techniques you've used to construct the image.

Important Evaluation Criteria

In assessing your assignments, we will emphasize the modifications you have made to the code. We will consider whether these modifications were interesting and subtle, or relatively straightforward. We will also assess assignments based on our impressions of the images and corresponding paragraphs.

roll!, revisited

The roll! procedure given above is not written as elegantly as possible. A programmer who has learned named let might rewrite it using a local kernel as follows. (In fact, this is how we first wrote roll!.

(define roll!
  (lambda (image col row inner-radius stamp-radius theta n)
    (let kernel ((step 0)
                 (revolution 0))
      (cond ((< step n)
             (draw-rolled! image col row inner-radius stamp-radius revolution)
             (kernel (+ step 1) (+ revolution theta)))))))

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright © 2007 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.