Standard Scheme does not provide any facilities for graphics programming, but for particular implementations of Scheme it is often possible to obtain libraries that create windows, draw lines and curves onto them, add text in various fonts and colors, provide text-entry forms and labelled buttons and sliders and other ``widgets,'' and generally integrate Scheme programs with the multimedia resources of the machines on which they run.
As a small example of graphics programming in Scheme, we'll use Elk, an implementation of Scheme created by Oliver Laumann, to develop a kaleidoscope program. The program will open up a window on your workstation and create colorful designs of a random, symmetric nature in it.
The file kaleidoscope-project.scm contains a working, but primitive, version of the kaleidoscope program. Your project will be to refine it, adding symmetry and variety to its graphical displays.
To run the program, open an hpterm window and type the command
elk -i at the prompt to start Elk Scheme. At the Scheme prompt,
type (load "kaleidoscope-project.scm"). A square window will
appear, presenting a black background with one colored line drawn onto it.
Pressing any key on the keyboard will have some effect on the window: The
Q key will close the window and exit from the program, the
C key will erase the current pattern and start over with just one
colored line, and any other key will add a new colored line on top of the
existing pattern.
On MathLAN workstations, windows are normally created and displayed with the assistance of a ``window manager'' program, vuewm, by invoking procedures in a library known as ``X Windows.'' Although this library is intended primarily for programmers using the C and C++ languages, Laumann provided us with an interface that allows us to invoke X Windows procedures from inside Elk, using Scheme syntax, and thus to integrate X Windows programming with Scheme programming. A full list of the procedures that make up Elk's X Windows library can be found in the Elk/Xlib reference manual. Here, however, we shall cover only the twenty or so that are used in the kaleidoscope program.
One of X Windows data structures is the display, which contains
information about the output device on which any window we create will
appear. Open-display is a procedure of zero arguments that
allocates and returns an appropriately initialized display record
describing the workstation's monitor; it returns #f if the
monitor is not suitable for displaying windows (which might happen if, for
instance, one tried to run an X Windows program from a VAX terminal).
Display? is a type predicate that tests whether its argument
is a display record.
One of the fields of a display is its color map, a vector
containing all of the various colors that appear simultaneously in the
display. Our workstations are capable of displaying 16777216 different
colors, but no more than 256 of them can be on screen at the same time, and
the elements of the color map are the ones that have been selected by the
various programs that are running as ones that they wish to use. The
display-colormap procedure is a selector that extracts the
color map from a given display.
In order to add a new color to the color map, making it eligible for use on
screen, one invokes a procedure of two arguments named
alloc-color. The first argument to alloc-color
is the color map to which the new color is to be added; the second is a
record indicating the intensities of the red, green, and blue components of
the new color, as real numbers in the range from 0.0 to 1.0. The
make-color procedure is the constructor for color records; it
takes three arguments -- the red, green, and blue intensities -- and
returns the color characterized by those intensities. Here's a typical use
of these procedures:
(alloc-color current-color-map (make-color 1.0 0.0 0.0))
This expression builds a bright red color and adds it to the current color
map. Alloc-color returns the index of the position within the
color map at which the color was installed. Often this index is given a
mnemonic name that suggests the color:
(define sky-blue (alloc-color current-color-map (make-color 0.4 0.6 1.0)))
Another field of the display is its root window, which is the
window that typically occupies the entire display and serves as a backdrop
against which other windows are placed. The
display-root-window procedure is a selector for this field.
The internal representation of one specific window, such as the one in
which the kaleidoscope program will draw its graphics, is also a record.
The create-window procedure allocates and returns such a
record; it takes a variable number of parameters -- always, however, an
even number -- which are alternately symbols naming the fields that need to
be initialized and values to store in those fields. Some typical fields of
a window record are parent (another window within which the
new one is to be placed), width (the number of pixels from the
left edge of the window to the right edge), height (the number
of pixels from the top edge to the bottom), and
background-pixel (the color map's index for the color to be
used as the background within the new window).
In order to perform any drawing operations within a window, we'll also need
a record called a graphics context, which keeps track of various
quantities that control the drawing style -- the foreground color in which
new elements are drawn, the width of lines, the font in which text is to be
displayed, and so on. The root window of a display already possesses such
a graphics context, and one common way to get started in a new window is to
make a fresh copy of the root window's graphics context and associate it
with the new window. (This makes it possible to change some of the fields
of the graphics context without affecting the appearance of any other
window.) A procedure named copy-gcontext constructs and
returns a copy of a given graphics context record.
Elk provides mutator procedures for several fields of a graphics context, and the kaleidoscope program uses two of these:
The set-gcontext-line-width! procedure takes two
arguments, a graphics context and a positive integer n, and
sets the line-width field so that any lines subsequently drawn
will be n pixels wide. (In effect, this controls the width of
the pen or brush with which lines and arcs are drawn.)
The set-gcontext-foreground! procedure takes two
arguments, a graphics context and a color-map index representing a pixel
color, and sets the foreground field so that any lines or
shapes subsequently drawn will appear in the specified color.
The kaleidoscope program invokes both of these procedures before each drawing operation, to randomly change the line width and color.
The vuewm window manager keeps records containing information
about the windows it helps to put up, and there are procedures for mutating
various fields of those records, with such names as
set-wm-hints!, set-wm-normal-hints!,
set-wm-class!, set-window-event-mask!,
set-wm-name!, and set-wm-icon-name!. Perhaps the
most interesting of these is set-window-event-mask!, which
determines which of the various kinds of ``input events'' -- moving the
mouse, pressing and releasing mouse buttons, pressing and releasing
keyboard keys, and so on -- the window will pay attention to.
Once the record for a window has been created and the window manager has
been modified to register its existence, one can ask for the window to be
drawn onto the display by invoking the map-window procedure,
which takes the window record as its only argument.
X Windows includes several procedures for drawing shapes into a window. Here are some of the ones that you're most likely to use, with sample invocations:
The draw-line procedure takes six arguments: the window
within which the line is to be drawn, the graphics context that indicates
the style in which it is to be drawn, and the x- and y-coordinates of the
endpoints of the line segment. The coordinates are natural numbers
indicating the number of pixels from the left or top boundary of the window
to a designated point. Thus, for example, the procedure call
(draw-line my-window current-gcontext 0 64 128 192) causes a
line to be drawn in my-window, with the graphics context
current-gcontext, starting at the left edge of the window 64
pixels from the top, and proceeding downwards and to the right to a point
that is 128 pixels from the left edge and 192 pixels from the top. This is
the procedure that is used for the unsophisticated initial version of the
kaleidoscope program; every keypress causes one more invocation of
draw-line.
The draw-arc procedure takes eight arguments: the window,
the graphics context, the x- and y-coordinates of the upper left-hand
corner of the rectangle bounding the ellipse of which the arc is a part,
the width and height of that rectangle, the angle (relative to the
direction of the positive x-axis) at which the arc is to begin, and the
length of the arc (positive for a counterclockwise arc, negative for a
clockwise one). The x- and y-coordinates of the upper left-hand corner and
the width and height of the bounding rectangle are again measured in
pixels, and the angle and arc length in sixty-fourths of a degree. So, for
example, the procedure call (draw-arc my-window current-gcontext 200
150 30 40 (* 90 64) (* -270 64)) causes an arc to be drawn in
my-window, with the graphics context
current-gcontext. The arc will be part of an ellipse that is
bounded by a rectangle thirty pixels wide and 40 pixels high, with its
upper left-hand corner 200 pixels from the left edge of the window and 150
pixels from the top edge. The arc will start at the ``twelve-o'clock''
position -- 90 times 64 sixty-fourths of a degree counterclockwise from the
direction of the positive x-axis -- and sweep around clockwise for 270
degrees (-270 times 64 sixty-fourths of a degree) to the ``nine-o'clock''
position. By making the width and height arguments equal, you can get
circular arcs; by making the last argument (* 360 64), you can
get a complete ellipse instead of just an arc.
The fill-arc procedure takes the same arguments as
draw-arc, but colors the interior of the region enclosed by
the arc (as a pie-slice) rather than only the rim.
The fill-polygon procedure takes five arguments, of which
the first three are the window, the graphics context, and a vector
containing the vertices of a polygon (each vertex being a pair in which the
car is the x-coordinate and the cdr is the y-coordinate). In this
application, the fourth argument will always be #f and the
fifth will always be the symbol convex. The procedure draws
the outline of the polygon and fills it in solidly. So, for example,
the call (fill-polygon my-window current-gcontext '#((100 . 100) (200
. 50) (200 . 150)) #f 'convex) draws a triangle in
my-window, using the current-gcontext graphics
context, with vertices at the indicated points.
The clear-area procedure, which is used to erase part or
all of a window (restoring the background color) takes six arguments: the
window, the x- and y-coordinates of the upper left-hand corner of the
region to be erased, the x- and y-coordinates of the lower right-hand
corner of that region, and a final Boolean argument which in this
application will always be #t. For example, the call
(clear-area my-window 0 0 100 200 #t) erases a
100-by-200-pixel rectangle in the upper left-hand corner of
my-window.
From time to time, the kaleidoscope program will pause to process and
respond to input from the user, in the form of keystrokes. The pause is
produced by a call to a procedure called handle-events, which
examines all the ``input events'' that have occurred since the last such
pause and invokes an appropriate ``event handler'' procedure for each one.
The event handler is expected to work out exactly what happened and where
(which key was pressed, for instance) and to act on it.
When the program has finished its work, an appropriate sequence of X
Windows procedures must be invoked to free the various resources: The
window allocated by create-window must be deallocated with a
call to the destroy-window procedure, the color map returned
by display-colormap must be released with a call to
free-colormap, the graphics context constructed by
copy-gcontext must be freed by calling
free-gcontext, and finally the display obtained from
open-display must be released with a call to
close-display.
With this introduction to the Elk graphics library, you should be able to
read the source code for the kaleidoscope program. I recommend paying
special attention to the procedure named kaleidoscope,
beginning on line 109 of the file, and to the draw procedure,
which begins on line 218. The three improvements that proposed below
affect mainly the draw procedure.
Improvement 1: Symmetry. You'll notice that draw
contains only one call to the draw-line procedure (which is
why the lines appear one by one in response to keystrokes). To appear
truly kaleidoscopic, each line should be drawn several times, as if
reflected in a succession of mirrors. A number of symmetries can be
achieved without too much calculation:
Swapping the x- and y-coordinates of each endpoint of the line segment L yields the endpoints of a line that is the reflection of L in a mirror placed diagonally from the upper left-hand corner of the window to the lower right-hand one.
Replacing the x-coordinate of each endpoint of L with its difference from 511, while keeping the y-coordinate unchanged, yields the reflection of L in a vertical mirror down the center of the window.
Performing the same operation on the y-coordinates yields the reflection of L in a horizontal mirror across the center.
Combining these operations in all possible ways, one arrives at a total of
eight lines (including the original L). If you issue eight calls to
draw-line instead of just one, and the graphical displays will
become highly symmetrical. Go ahead and make this change in the
kaleidoscope program, then re-run it.
Improvement 2: Variety. Another way to improve the kaleidoscope
program is to have it draw small ellipses and polygons of various sizes and
shapes. Add calls to draw-arc, fill-arc, and
fill-polygon to the draw procedure to create this
effect. (Can you write the calls so as to preserve the symmetry introduced
in the preceding step?)
Improvement 3: Expanding the palette. In the initial version of the
kaleidoscope program, black is used only for the background; all the line
drawing is in brighter colors. Add black to the palette from which the
drawing color is selected, so that occasionally the lines, arcs, and
polygons will be drawn in black. Also, it is arguable that the palette is
overstocked with blues and greens and lacking in warm colors: add a bright
orange (0.97 red, 0.48 green, 0.09 blue) and a hot pink (0.96 red, 0.38
green, 0.67 blue). A list of the appropriate mixes for various familiar
colors can be found in /usr/lib/X11/rgb.txt; divide the integers
shown by 255 to get intensities in the form required by
make-color.
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/spring-1997/kaleidoscope-project.html