Skip to main content

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! "2. Hardness 075" 5)
> (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.

;;; Procedure:
;;;   draw-three-parallel-lines!
;;; Parameters:
;;;   image, an image
;;;   start-col, an integer
;;;   start-row, an integer
;;;   end-col, an integer
;;;   end-row, an integer
;;;   hoffset, an integer
;;;   voffset, an integer
;;; Purpose:
;;;   Draw three parallel lines, with the first from (start-col,start-row)
;;;     to (end-col,end-row) and the starting point of the next two offset
;;;     horizontally by hoffset (and 2*hoffset) and vertically by voffset
;;;     (and 2*voffset).
;;; Produces:
;;;   [Nothing; Called for the side effect.]
;;; Preconditions:
;;;   All three parallel lines can be drawn on the image.
;;; Postcondtions:
;;;   The image has been appropriately modified.
(define draw-three-parallel-lines!
  (lambda (image start-col start-row end-col end-row hoffset voffset)
    (image-draw-line! image 
                      start-col start-row 
                      end-col end-row)
    (image-draw-line! image 
                      (+ hoffset start-col) (+ voffset start-row)
                      (+ hoffset end-col) (+ voffset end-row))
    (image-draw-line! image 
                      (+ (* 2 hoffset) start-col) (+ (* 2 voffset) start-row)
                      (+ (* 2 hoffset) end-col) (+ (* 2 voffset) end-row))))

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! "2. Hardness 075" 11)
> (draw-three-parallel-lines! canvas 10 10 30 200 10 50)
> (context-set-fgcolor! "red")
> (context-set-brush! "2. Hardness 075" 11)
> (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).

;;; 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 REPLACE
                           (- col radius) (- row radius)
                           (+ radius radius) (+ radius radius))
    (image-stroke-selection! image)
    (image-select-nothing! image)))

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! "2. Hardness 075" 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! "2. Block 03" 10)
> (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 ... start-row), an ending position (represented by *end-col ... 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.

;;; Procedure:
;;;   draw-parallel-lines!
;;; Parameters:
;;;   image, an image
;;;   n, an integer
;;;   start-col, an integer
;;;   start-row, an integer
;;;   end-col, an integer
;;;   end-row, an integer
;;;   hoffset, an integer
;;;   voffset, an integer
;;; Purpose:
;;;   Draw n parallel lines, with the first from (start-col,start-row)
;;;     to (end-col,end-row) and each subsequent one offset by the 
;;;     appropriate multiple of hoffset and voffset.
;;; Produces:
;;;   [Nothing; Called for the side effect.]
;;; Preconditions:
;;;   All parallel lines can be drawn on the image.
;;; Postcondtions:
;;;   The image has been appropriately modified.
(define draw-parallel-lines!
  (lambda (image n start-col start-row end-col end-row hoffset voffset)
    (when (> n 0)
      (image-draw-line! image 
                        start-col start-row 
                        end-col end-row)
      (draw-parallel-lines! image
                            (- n 1)
                            (+ hoffset start-col) (+ voffset start-row)
                            (+ hoffset end-col) (+ voffset end-row)
                            hoffset voffset))))

Note that we’re using when rather than if for two reasons. First when explicitly allows multiple actions in the consequent, while if does not. Second, there is no alternative. (Do you see why? The self checks ask you.)

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.

;;; Procedure:
;;;   position->color
;;; Parameters:
;;;   col, an integer
;;;   row, an integer
;;; Purpose:
;;;   Compute a color based on col and row.
;;; Produces:
;;;   color, an integer-encoded RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   Different col/row combinations are likely to give different colors.
;;;   Nearby col/row combinations may give similar colors.
(define position->color
  (lambda (col row)
    (irgb (+ 128 (* 128 (sin (* pi row 0.0625))))
          (+ 128 (* 128 (cos (* pi col 0.0625))))
          (+ 128 (* 128 (sin (* pi (+ row col) 0.0625)))))))

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.

(define draw-colored-parallel-lines!
  (lambda (image n start-col start-row end-col end-row hoff voff)
    (when (> n 0)
      (context-set-fgcolor! (position->color start-col start-row))
      (image-draw-line! image 
                        start-col start-row 
                        end-col end-row)
      (draw-colored-parallel-lines! image
                                    (- n 1)
                                    (+ hoff start-col) (+ voff start-row)
                                    (+ hoff end-col) (+ voff end-row)
                                    hoff voff))))

We can see the effects of the coloring by drawing thin lines close together.

> (context-set-brush! "2. Hardness 075" 1)
> (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 brush sizes as a parameter and use n to select a size 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! "2. Hardness 075" (list-ref sizes (modulo n (length sizes))))

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.

;;; Procedure:
;;;   draw-sin-with-parallel-lines!
;;; Parameters:
;;;   image, an image
;;;   n, an integer
;;;   offset, an integer
;;;   start-col, an integer
;;;   mid-row, an integer
;;; Purpose:
;;;   Draw a sequence of parallel lines, with the height of the
;;;   parallel line dependent on the column.
;;; Produces:
;;;   [Nothing, called for the side effect.]
(define draw-sin-with-parallel-lines!
  (lambda (image n offset start-col mid-row)
    (let kernel! ([i 0]
                  [col start-col]
                  [row mid-row])
      (when (< i n)
        (image-draw-line! image 
                          col row
                          col (- row (* row (sin (* i pi 0.05)))))
        (kernel! (+ i 1) (+ col offset) row)))))

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.

;;; Procedure:
;;;   draw-parallel-lines-with-decreasing-spacing!
;;; Parameters:
;;;   image, an image
;;;   col, an integer
;;;   start-row, an integer
;;;   end-row, an integer
;;;   spacing
;;;   close-enough
;;; Purpose:
;;;   Draw a sequence of vertical lines (each running from start-row
;;;   to end-row) starting at col, then spaced by spacing from col,
;;;   then by spacing/2 from that column, then spacing/4 from that
;;;   column, and so on and so forth until the distance between columns 
;;;   is less than or equal to close-enough.
;;; Produces:
;;;   [Nothing. Called for the side effects.]
(define draw-parallel-lines-with-decreasing-spacing!
  (lambda (image col start-row end-row spacing close-enough)
    (image-draw-line! image col start-row col end-row)
    (when (> spacing close-enough)
      (draw-parallel-lines-with-decreasing-spacing!
       image 
       (+ col spacing) start-row end-row
       (* spacing 0.5)
       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.

Self Checks

Check 1: Recursion Pattern Redux

These questions ask about the function draw-parallel-lines!.

a. What is the base case test?

b. What happens in the recursive case? (Hint: Make sure you note there are two procedure calls in the recursive case.)

c. Where is the base case computation?

d. What is the simplification within the recursive call? How is that a simplification with respect to the base case test?

Check 2: Recursion Pattern Re-Redux

These questions ask about the similar, but subtly distinct function draw-parallel-lines-with-decreasing-spacing!.

a. What is the base case test?

b. What happens in the recursive case? (Hint: Make sure you note there are still two procedure calls in the recursive case.)

c. Where is the base case computation? (Hint: Your answer will differ slightly from Check 1.c.)

d. What is the simplification within the recursive call? How is that a simplification with respect to the base case test?

Check 3: Exploring Examples

a. Make a copy of geometric-art-lab.rkt.

b. Create a new 200x200 image and call it canvas.

b. Look at each of the examples using draw-three-parallel-lines! and draw-circle! in the introduction to the corresponding reading, predict what you think the example code will do, and then run the example in MediaScript. (Remember: You may need to call context-update-displays! after drawing.)

c. The reading provides four procedures that draw parallel lines (with some variation between them), draw-parallel-lines!, draw-colored-parallel-lines!, draw-sin-with-parallel-lines!, and draw-parallel-lines-with-decreasing-spacing!. Make sure you understand what each of the procedures is supposed to do, and then try using each with two different sets of parameters.

Note: Be very careful if you use the same values for hoffset ... voffset as you may have trouble making sense of the resulting image. (Can you figure out why?) Your best approach might be to avoid using the same values in your initial explorations.