CSC151.01 2009F Functional Problem Solving : Readings
Primary: [Front Door] [Syllabus] - [Academic Honesty] [Instructions]
Current: [Outline] [EBoard] [Reading] [Lab] [Assignment]
Groupings: [Assignments] [EBoards] [Examples] [Exams] [Handouts] [Labs] [Outlines] [Projects] [Readings]
References: [Primary] [A-Z] - [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [CSC151.02 2009F (Weinman)] [CSC151.02 2009S (Davis)] [CSC151 2008S (Rebelsky)]
Misc: [SamR] [MediaScript] [GIMP]
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.
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.
;;; 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! "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).
;;; 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! "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.
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.
;;; 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) (cond ((> 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 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.
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 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) (rgb-new (+ 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) (cond ((> 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! "Circle (01)")
>
(draw-colored-parallel-lines! canvas 50 0 0 0 100 2 4)
>
(context-update-displays! canvas)
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 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) (cond ((> n 0) (image-draw-line! image start-col mid-row start-col (+ mid-row (* mid-row (sin (* n pi 0.01))))) (draw-sin-with-parallel-lines! image (- n 1) offset (+ start-col offset) mid-row)))))
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) (if (> spacing close-enough) (draw-parallel-lines-with-decreasing-spacing! image (+ col spacing) start-row end-row (/ spacing 2) 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.
Primary: [Front Door] [Syllabus] - [Academic Honesty] [Instructions]
Current: [Outline] [EBoard] [Reading] [Lab] [Assignment]
Groupings: [Assignments] [EBoards] [Examples] [Exams] [Handouts] [Labs] [Outlines] [Projects] [Readings]
References: [Primary] [A-Z] - [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [CSC151.02 2009F (Weinman)] [CSC151.02 2009S (Davis)] [CSC151 2008S (Rebelsky)]
Misc: [SamR] [MediaScript] [GIMP]
Copyright (c) 2007-9 Janet Davis, Matthew Kluber, Samuel A. Rebelsky, and Jerod Weinman. (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.