Summary: We consider a very different representation of images, in which we think of an image as a drawing that can be built from other drawings.
Instead of thinking about images in terms of how we make them, we might also think of images in terms of the kinds of things that we are able to draw. For example, we might agree that a unit circle (a circle with diameter 1) is something we can draw, as is a unit square (a square with edge length 1). Why start with those two things? We have to start somewhere. You can imagine a few other things that one might draw.
Now, suppose we have something that everyone agrees can be drawn. Could we draw the same thing in a different color? Certainly. Suppose we can draw something that is all solid shapes. Could we draw something similar, in which the shapes are outlined? Probably. Let's call something that can be drawn a “drawing”.
Each of those questions dealt with a single drawing. Suppose we had two (or more) drawings and superimposed them. Would the result be a drawing? Certainly.
You may be able to think of other ways we can decide whether we can draw something. For example, if we can draw something at one scale, we can probably draw it at another scale.
The previous description of drawings may seem like a bit odd. However, Computer Scientists (and some Mathematicians) often find it useful to describe things in this way. That is, they describe some basic values (such as squares and circles) and then ways they modify those basic values to create new values. My Mathematician friends often describe the natural numbers (the non-negative integers) in a similar way: “Zero is a natural number. If you add one to a natural number, you get a natural number.”
In the remainder of this reading, we'll consider how we might describe
drawings using a similar technique, formalized in Scheme. That is,
we'll start with some basic drawing values and provide operations
that build new drawings from old. In particular, we'll design a
drawing data type. (For now, the implementation of
the procedures on this data type will be concealed. As the semester
progresses, we'll think about ways to implement those procedures.)
For our initial exploration of drawings as values, we'll start with two simple drawing values: the unit circle and the unit square, both centered at (0,0), and drawn in black. Why start with these two values? They're easy to think about, easy to draw, and relatively straightforward to think about.
In Scheme, we'll represent these two values as
drawing-unit-square. Note, these are
values, not procedures. That is, you will
not place them immediately after an open paren. Rather, you will
use them in various procedures that take drawings as parameters.
What procedures will take drawings as parameters? In the
subsequent sections, we'll consider a number of procedures that
build new drawings from old. But there's one other very important one:
will convert a drawing to an image that we can display. For example,
we could show the simplest circle drawing on a 100x100 canvas with
(image-show (drawing->image drawing-unit-circle 100 100))
Could we have other basic values in addition to the unit circle and the unit square? Certainly. However, for now, our exercises in design will involve only variations of these two values.
If all that we can draw are the unit circle and unit square, and only
draw them in one position, we're in a bit of trouble. So let's think
of how we might vary them. Well ... both are fairly small, so we might
want to scale them. Hence, we should provide a
procedure that scales drawings. (It will scale the two basic drawings,
but as we come up with other drawings, it will scale those, too.)
That procedure will be called (
factor), and, given
a drawing, will return a scaled version of the drawing. Note that
this procedure does not change the underlying drawing. Rather, it
builds a new one, just like those machines they advertise on television.
For example, we can make a circle with radius 50 with
(define big-circle (drawing-scale drawing-unit-circle 100))
And we could build an image for that circle, too.
(image-show (drawing->image big-circle 100 100))
To mix things up a bit, let's say that in addition to scaling drawings
in both directions (horizontally and vertically),
we can also scale them in a single direction (horizontally
or vertically). We'll call the procedures
to do so
drawing-vscale. So, to make a rectangle
that is 50 wide and 25 high, we might write
(define rect (drawing-vscale (drawing-hscale drawing-unit-square 50) 25))
But we don't just want our images centered at (0,0). Hence,
we should also be able to shift them
around. We'll use
to shift the drawing to the right (or to the
amt is negative)
shift the drawing downward (or upward, if
negative). Once again, these don't really shift the original drawing.
Rather, they make new copies of the drawing. For example, in the
low-circle will be centered at (0,25) and
right-circle will be centered at (60,0). That is,
the call to
does not affect the position of
big-circle. It is
forever centered at (0,0)
(define low-circle (drawing-vshift big-circle 25))
(define right-circle (drawing-hshift big-circle 60))
We can now describe drawings that consists of a single black square, a single black rectangle, a single black circle, or a single black ellipse that falls anywhere on the canvas. For example, the black ellipse of width 50 and height 25, centered at (30,10) can be built from the unit circle as follows:
(define sample-ellipse (drawing-vshift (drawing-hshift (drawing-vscale (drawing-hscale drawing-unit-circle 50) 25) 30) 10))
Yes, that's a bit long. However, one of the intellectual challenges of algorithm design (and of programming) is finding creative ways to use the basic operations you've been given. A consequence is that you'll sometimes write long code. And, as you'll see in a few days, if you find you're regularly writing similar long code, there are elegant ways to combine a group of small operations into a single big operation.
But the drawings we can describe are still black.
Let's make them a bit more lively. In particular, let's add a
procedure that recolors drawings. (That is, makes a copy of the
drawing in another color.) We'll call that procedure
For a bit more fun, let's also add a procedure that outlines a
drawing, rather than filling it in. In this case, we might even
specify the brush we use for outlining.
Our repertoire for describing drawings has expanded significantly.
We've can now describe circles, squares, rectangles, and ellipses
in a variety of colors and at any size and any location. But a
single shape rarely makes a compelling drawing. Hence, we'll
want to combine drawings into new drawings. The procedure
( combines drawings for us. We might render the
two circles we created above with
(image-show (drawing->image (drawing-group low-circle right-circle) 100 100))
Now, here's the really fun part. If we agree that a group of superimposed drawings is also a drawing, then we can do anything to that group that we did to the simpler drawings: We can recolor it, outline it, scale it, shift it, and combine it with other groups. Together, those different features will allow us to create some fairly interesting drawings. For example, once we've described a single face, we might make new copies of the face in different colors, at different locations, or both. We'll explore some of these more complex drawings in the lab.
As you might guess, one of the reasons we've looked at drawings in terms of operations that build new drawings from old is to encourage you to think in this new way about data. (Just as our Mathematician friends think of integers in terms of a basic value and the operation that creates new integers, you can now think of drawings as basic values and operations that create new drawings.)
However, there's another reason that we like this way of describing drawings: All of the operations are pure: They do not modify the underlying value they are applied to. Rather they build new values. Contrast this to the various image and turtle operations you've learned. If you tell a turtle to switch its pen color, the turtle's “state” changes, and the old pen color is lost. In contrast, if you recolor a drawing, the original drawing is still in the old color. There are both advantages and disadvantages to working with pure operations, but, like working with a small set of operations, it's a useful intellectual challenge.
By the way, here's one great advantage of working with pure operations: Since the original values are still around somewhere, you never need to undo an action.
Janet Davis (email@example.com)