Fundamentals of Computer Science I: Media Computing (CS151.01 2008S)

Drawings as Values


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.

Introduction

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.)

Basic Values and Basic Operations

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-circle and 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: (drawing->image drawing width height) 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.

Scaling and Shifting Images

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 (drawing-scale drawing 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-hscale and 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 (drawing-hshift drawing amt) to shift the drawing to the right (or to the left, if amt is negative) and (drawing-vshift drawing amt) to shift the drawing downward (or upward, if amt is negative). Once again, these don't really shift the original drawing. Rather, they make new copies of the drawing. For example, in the following, low-circle will be centered at (0,25) and right-circle will be centered at (60,0). That is, the call to drawing-vshift on big-circle 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.

Making Drawings More Lively: Adding Color and Outlines

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 (drawing-recolor drawing newcolor).

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. (drawing-outline drawing brush).

Combining Drawings

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 (drawing-group drawing1 drawing2) 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.

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-8 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.