Geometric Art
Summary:
As we have seen in our various explorations, scripting can help us make
interesting images in a number of ways. One particularly appropriate
use of scripting is making what we call geometric art
, images
which include regularly generated geometric figures. In this reading, we
consider some simple forms of geometric art.
Introduction
From antiquity to the present day, artists have experimented with ways
in which repetition of simple geometric forms, such as lines, squares,
and circles, can create interesting effects. Scripting provides an
excellent opportunity to explore such geometric images, since MediaScript
already provides techniques for drawing simple geometric forms and
we can easily write scripts that draw these forms in several places
(perhaps even modified in various ways).
For example, consider the problem of drawing three parallel lines, with
their starting coordinates spaced horizontally by twenty columns. We
might express that with a sequence of MediaScript commands as follows.
> (define canvas (image-new 200 200))
> (image-show canvas)
> (context-set-brush! "Circle Fuzzy (05)")
> (context-set-fgcolor! "red")
> (define start-col 10)
> (define start-row 10)
> (define end-col 20)
> (define end-row 100)
> (image-draw-line! canvas start-col start-row end-col end-row)
> (image-draw-line! canvas (+ 20 start-col) start-row (+ 20 end-col) end-row)
> (image-draw-line! canvas (+ 40 start-col) start-row (+ 40 end-col) end-row)
> (context-update-displays!)
Of course, if drawing three regularly-spaced parallel lines is a task we
expect to do a lot, we might write these instructions as a separate procedure.
We can then use this procedure to draw a variety of parallel lines,
either using the same brush and color (by default) or by changing the
brushes and colors.
> (context-set-fgcolor! "black")
> (context-set-brush! "Circle Fuzzy (11)")
> (draw-three-parallel-lines! canvas 10 10 30 200 10 50)
> (context-set-fgcolor! "red")
> (context-set-brush! "Circle Fuzzy (05)")
> (draw-three-parallel-lines! canvas 10 10 30 200 10 50)
> (context-set-fgcolor! "blue")
> (draw-three-parallel-lines! canvas 50 0 50 80 20 20)
> (context-set-fgcolor! "green")
> (draw-three-parallel-lines! canvas 60 90 200 90 0 30)
> (context-update-displays!)
We can use similar techniques to draw concentric circles. In this case,
it may help to first write a procedure that draws centered circles (in
effect, encapsulating the selection, computation of points, and stroking).
So, to draw three concentric circles on our canvas, centered at
(100,100) and with radii of 30, 50, and 70, we might write something
like
> (context-set-fgcolor! "black")
> (context-set-brush! "Circle (11)")
> (draw-circle! canvas 100 100 30)
> (draw-circle! canvas 100 100 50)
> (draw-circle! canvas 100 100 70)
> (context-update-displays!)
We might also offset the centers of the circles slightly, as in the following.
> (context-set-fgcolor! "grey")
> (context-set-brush! "Calligraphic Brush")
> (draw-circle! canvas 80 100 40)
> (draw-circle! canvas 90 100 60)
> (draw-circle! canvas 100 100 80)
> (context-update-displays!)
Again, we might encapsulate this technique in a procedure. The particular
details of that procedure are left as an exercise to the reader.
Of course, once we draw more than a few concentric circles or a few
parallel lines, it becomes useful to write more general procedures,
procedures in which we specify not just the change in location or radius,
but even the number of items to draw. In the sections that follow,
and in the corresponding lab, you will have the opportunity to explore
such variants.
Exploring Parallel Lines
Let's begin by considering some of the ways in which we might draw
parallel lines. We'll start by looking at the parameters of
the draw-three-parallel-lines! procedure.
That procedure took as parameters an image, a starting
position (represented by start-col and
start-row), an ending position (represented by
end-col and end-row),
and horizontal and vertical offsets. We want to add another parameter
that keeps track of the number of repetitions to do. We'll call
that parameter n, and put it early in the
parameter list. So, the procedure header will look something like
(define draw-parallel-lines!
(lambda (image n start-col start-row end-col end-row hoffset voffset)
...))
Now, if we're going to have this draw an arbitrary number of lines,
we will probably need to use recursion to repeat the actions.
(It may be possible to do this using map and
iota, but it's useful for you to see some more
examples of recursion.) What is the base case of this recursion?
Presumably, when we have no lines left to draw. What should we do in
the recursive case? Draw one line, and then draw the remaining lines.
How many lines do we have left to draw after the first line? One fewer.
Putting all of this together, we get the following.
Note that we're using cond rather than
if because cond explicitly
allows multiple actions in the consequent, while if
does not.
What can we do once we've written a procedure like this? As you might
expect, in addition to drawing various collections of parallel lines,
we might consider interesting variants. We'll consider four: Varying
the color, varying the brush, varying the length of individual lines,
and varying the spacing between lines. Each may suggest some useful
design and programming techniques.
Varying the Color
Suppose we want the color of the lines to vary, so that each line is
somewhat different from its neighbor. How can we make such variation?
We can certainly build a color that depends on the row and column.
You've already experimented with a variety of techniques for choosing
such colors. Here's a new one.
What's going on here? Well, we know that sin and
cos give results between -1 and 1. By multiplying
that value by 128, we get numbers between -128 and 128. By adding 128,
we get numbers between 0 and 256, where are essentially the range of
valid component values. This certainly isn't the only technique to use,
but it gives us some interesting results.
Now that we can choose colors, we need only add a line to
draw-parallel-lines to make
draw-colored-parallel-lines.
We can see the effects of the coloring by drawing thin lines close together.
> (context-set-brush! "Circle (01)")
> (draw-colored-parallel-lines! canvas 50 0 0 0 100 2 4)
> (context-update-displays! canvas)
Varying the Brush
Suppose that instead of varying the color, we want to vary the width of the
brush used to draw each line. We might make provide a list of possible
brushes as a parameter and use n to select a brush.
Since we have an integer that may be outside the range of valid brushes,
we can use modulo to restrict that number to the
number of valid indices.
(context-set-brush! (list-ref brushes (modulo n (length brushes))))
You will have the opportunity to explore the use of this technique in
the laboratory.
Varying the Length
Varying the color and brush provides us with the opportunity to create
some interesting images. However, we might want to use parallel lines
to explore other concepts, such as the values of a function at various
x values. In this case, we'll draw vertical lines, using the column
as the x value and the height of the line as the y value. (If the function
produces negative values,
it may be helpful to place the x axis in the middle of the image.)
For example, here's a simple procedure that draws n
parallel lines, spaced by offset, with the height of each line
computed via a variant of the sine function.
Varying the Spacing
Here's another interesting variant of drawing parallel vertical lines. Rather
than spacing them equally, let's vary the spacing by making the spacing
between a pair of lines half the spacing between the preceding pairs,
stopping when the spacing gets below some value, which we call
close-enough.
There are several things to note in this procedure. First, we always
draw at least one line. Next, because there's only one thing we
need to do if we continue, we use if rather
than cond. Most importantly, even though we've
previously used subtract one
and stop at 0
as our simplification and base-case in numeric recursion, here we use
divide by two
and stop when small enough
as our simplification and base case.