&spirograph-prefix;&spirograph;-Like Drawings
Due: &spirograph-due;
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 &grader-email;. The title of your
email should have the form &spirograph-subject;
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!,
should 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've written an
image-draw-circle! procedure in the past. In case you've
forgotten, here's what that procedure looks like.
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)
(image-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! procedure.
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)
> (context-set-fgcolor! "blue")
> (context-set-brush! "Circle (01)")
> (draw-stamp! canvas 20 20 20 0)
> (context-update-displays!)
> (draw-stamp! canvas 100 20 20 (* pi 0.25))
> (context-update-displays!)
> (draw-stamp! canvas 180 20 20 (* pi 0.50))
> (context-update-displays!)
> (draw-stamp! canvas 20 100 20 (* pi 0.625))
> (context-update-displays!)
> (draw-stamp! canvas 100 100 20 (* pi -0.5))
> (context-update-displays!)
> (draw-stamp! canvas 180 100 20 pi)
> (context-update-displays!)
> (draw-stamp! canvas 20 180 20 (* pi 1.5))
> (context-update-displays!)
> (draw-stamp! canvas 100 180 20 (* pi -.5))
> (context-update-displays!)
> (draw-stamp! canvas 180 180 20 (* pi 3.5))
> (context-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)))
Warning! Math ahead. You may need to read the
following a few times, and even draw some diagrams to help yourself
understand.
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 as in the preceding paragraph, 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.
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)
> (context-set-brush! "Circle (01)")
> (context-set-fgcolor! "green")
> (image-draw-circle! canvas 100 100 50)
> (context-update-displays!)
> (context-set-fgcolor! "blue")
> (draw-rolled! canvas 100 100 50 25 0)
> (context-update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.125))
> (context-update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.25))
> (context-update-displays!)
> (draw-rolled! canvas 100 100 50 25 (* pi 0.5))
> (context-update-displays!)
> (draw-rolled! canvas 100 100 50 25 pi)
> (context-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.
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)
> (context-set-brush! "Circle (01)")
> (context-set-fgcolor! "green")
> (image-draw-circle! canvas 100 100 50)
> (context-update-displays!)
> (context-set-fgcolor! "blue")
> (roll! canvas 100 100 50 25 (/ pi 8) 16)
> (context-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)
> (context-set-brush! "Circle (01)")
> (context-set-fgcolor! "green")
> (image-draw-circle! canvas 100 100 50)
> (context-update-displays!)
> (context-set-fgcolor! "blue")
> (roll! canvas 100 100 50 25 (/ pi 128) 256)
> (context-update-displays!)
You may note that in the result, the circles are starting to get in the way.
Try commenting out the call to image-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.
We strongly recommend that you get some simple variations working before
trying something more complicated. Think about how you can make
(and test!) your changes in small steps.
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!.)