Circles in Space Code

This article will dissect the source code for the Circles in Space series. Because the code is short, I’ve placed it at the bottom of this post.

STYLE

At first glance, the most remarkable thing about this student’s code is the length. To generate the images, the image-series function relies solely on two helper functions. Thus it is an excellent example of concise code and illustrates the fact that students don’t need to invent overly complex algorithms to create 1000 different images.

TECHNIQUE

The code relies on two variables, scalex and scaley, that determine the width and height of each ellipse (respectively) and the distance between adjacent ellipses. Notice that as the circles get bigger (i.e. scalex and scaley increase), so too does the distance between them.

An explanation of the image-select-ellipse! procedure is necessary here. The function takes five parameters: the image where we want to select the ellipse; a tag that determines whether we want to ADD, SUBTRACT or REPLACE the current selection to the new one; left, an integer that refers to the left most point of the ellipse; top, an integer that identifies the topmost point of the ellipse; width, which gives the x-axis of the ellipse, and height, which refers to the y-axis.

To illustrate, the expression

(image-select-ellipse! image ADD 0 50 76 89)

Would produce an ellipse on image whose leftmost point is aligned with the left side of the canvas (since left=0) and whose topmost point is 50 pixels from the top of the canvas. The ellipse would be taller than it is wide, with the width = 76 and the height = 89. If the user wished to draw a circle, they would enter equal values for width and height.

To return to the series, the program starts at left corner of the canvas (left and top both equal 0) and draws an ellipse whose width is given by scalex/n and whose height is scaley/n. In the example from the post, n=150. Since this n is fairly large, scalex/n and scaley/n are fairly small and so our first circle is tiny.

All of this takes place in the function circle-selector, whose primary purpose is to select circles (obviously):

(define circle-selector
  (lambda (n width height left top scalex scaley image)
    (cond ((>= top height)
           (image-series-helper (- n 1) (- width scalex) height (+ left scalex) 0 image))
          (else
           (image-select-ellipse! image ADD left top scalex scaley)
           (circle-selector n width height left (+ top scaley) scalex scaley image))))) 

Once the circle-selector has executed the image-select-ellipse! line, it calls itself recursively but changes the ‘top’ parameter to the sum of top and scaley. When circle-selector begins executing a second time, it first checks to make sure that the new top (top = previous top + scaley) is not greater than the height of the canvas (and thus off the page). If we haven’t yet reached the bottom of the page, the program continues to select ellipses, each time increasing the current value of top by scaley.

By the time the program reaches the bottom of the canvas, it has selected a column of ellipses whose topmost points are just touching the bottommost point of the ellipse above. After the last ellipse is drawn and circle-selector is called with a value of top that is greater than height, we call the image-series-helper function.

(define image-series-helper
  (lambda (n width height left top image)
    ; when n = 0, the recursion is cut off, the selections are colored and de-selected, and the image is finally displayed
    (cond ((= n 0)
           ; rgb-comp-width allows for an even color gradation, no matter the size of the image
           (let ((rgb-comp-width (/ 255 (image-width image))))
             (selection-compute-pixels! image
                                        (lambda (col row)
                                          (rgb-new
                                           (- 255 (* col rgb-comp-width))
                                           (- 255 (* col rgb-comp-width))
                                           (- 255 (* col rgb-comp-width))))))
           (image-select-nothing! image)
           (image-show image))
          (else
           ; scalex and scaley are used to determine the spacing of the circles in the image, as well as their size
           (let ((scalex (/ width n))
                 (scaley (/ height n)))
             (circle-selector n width height left top scalex scaley image))))))

Note that the parameters for image-series-helper are the same as circle-selector without scalex and scaley. This is because image-series-helper generates new values of scalex and scaley for us. Since each column of ellipses gets progressively larger as we move from left to right, scalex and scaley must also increase. Thus when we called image-series-helper within the circle-selector code, we decremented n so that scalex = width/n and scaley = height/n would return greater values (i.e. as n approaches 0, scalex  and scaley get larger). We also added the width of our previous column of ellipses to the left parameter so that the next line of ellipses would be drawn next to—not on top of—the ellipses we just generated.

When you looked at the image generated by this code, you might have noticed that the the heights of the ellipses have a greater rate of increase between columns than their widths. This is because each time we call image-series-helper from within circle-selector, we also change the width parameter by giving image-series-helper the difference between the width and scalex. This means that our width is also decreasing every time we generate new values of scalex and scaley so that the circles never get too wide.

Once we have fed the new parameters for n, width and left into the image-series-helper function, the function checks whether n=0 to see if we’re done generating circles. If we haven’t reached the right side of the canvas yet, image-series-helper proceeds to lines 16-19 where it recalculates scalex and scaley for our new parameters and calls circle-selector with these new values.

Circle-selector then proceeds to execute as it did before: it draws an ellipse, checks whether it has reached the bottom of the canvas and draws an ellipse if there is still room. Once it’s done with this column, it calls image-series-helper again with new parameters. Image-series-helper takes these new parameters, generates new scalex and scaley values, then recalls circle-selector. The cycle continues until n=0, which indicates that we have generated n columns of ellipses. For example, in the image from the post, n=150 and there are 150 columns. With all the circles selected, image-series-helper now executes image-compute-pixels!, which fills the circles with a gradient (I’ll talk about image-compute in another post with a better example of its capabilities.)

Just as in the Radial Series example, the ability to code has saved the student from a tedious task. To create the same image by hand, the student would have had to draw each ellipse while keeping track of the dimensions of each circle and the scale factors for each column.

Here’s the entire code:

;;; Procedure:
;;;   image-series
;;; Parameters:
;;;   n, an integer
;;;   width, an integer
;;;   height, an integer
;;; Purpose:
;;;   Generates a width x height image that varies in content according to the value of n.  Can Produce at least 1000 unique images.
;;; Produces:
;;;   [Nothing; Called for the side effect.]
;;; Preconditions:
;;;   the value of n does not exceed the value of the height or width
;;; Postconditions:
;;;   A width x height image

(define image-series
  (lambda (n width height)
    (let ((canvas (image-new width height)))
      (context-set-fgcolor! "black")
      (image-select-all! canvas)
      (image-fill-selection! canvas)
      (image-select-nothing! canvas)
      (image-series-helper n width height 0 0 canvas))))

(define image-series-helper
  (lambda (n width height left top image)
    ; when n = 0, the recursion is cut off, the selections are colored and de-selected, and the image is finally displayed
    (cond ((= n 0)
           ; rgb-comp-width allows for an even color gradation, no matter the size of the image
           (let ((rgb-comp-width (/ 255 (image-width image))))
             (selection-compute-pixels! image
                                        (lambda (col row)
                                          (rgb-new
                                           (- 255 (* col rgb-comp-width))
                                           (- 255 (* col rgb-comp-width))
                                           (- 255 (* col rgb-comp-width))))))
           (image-select-nothing! image)
           (image-show image))
          (else
           ; scalex and scaley are used to determine the spacing of the circles in the image, as well as their size
           (let ((scalex (/ width n))
                 (scaley (/ height n)))
             (circle-selector n width height left top scalex scaley image))))))

(define circle-selector
  (lambda (n width height left top scalex scaley image)
    (cond ((>= top height)
           (image-series-helper (- n 1) (- width scalex) height (+ left scalex) 0 image))
          (else
           (image-select-ellipse! image ADD left top scalex scaley)
           (circle-selector n width height left (+ top scaley) scalex scaley image)))))

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s