SICP: Fun with the Picture Language

So this week, I came across the picture language section in SICP and had some fun drawing with it.

In the book, the authors suppose the availability of the draw-line procedure and use it to implement segments->painter. Before we can draw anything, we need to find a graphics library which provides the required drawing capabilities (or write one of our own).

With some searching, I found two options, both available inside Racket:

  1. Use the SICP picture language library:

The picture language library does not expose any draw-line primitive but rather provides painters that are capable of drawing.

The drawback for me was that these painters cannot accept frames as arguments and instead must be called with a special paint procedure which can only draw rectangular frames.

  1. Use Racket’s legacy graphics library:

This old library provides the much needed draw-line procedure which draws within a viewport. With it, we can draw arbitrarily shaped frames, just like in the book.

I found it through Eric Scrivner’s blog post.

Here is the essential part (tailored a bit):

; Graphics (provides the drawing capabilities)
(require graphics/graphics)
(open-graphics)
(define viewport-width 500)
(define viewport-height 500)
(define vp (open-viewport "Picture Language Canvas" viewport-width viewport-height))

(define draw (draw-viewport vp))
(define (clear) ((clear-viewport vp)))
(define line (draw-line vp))

Scan through the original post to see the rest of the code. In it, you might notice some odd coordinates used to create frames:

(define unit-frame (make-frame (make-vect 0 500) (make-vect 500 0) (make-vect 0 -500)))

Notice the negative y-coordinate. It is there because in the graphics library, the viewport’s origin (0, 0) is at the upper-left corner, and positions increase to the right and down. We can avoid having to specify in terms of the graphic library’s semantics, by creating a function that converts regular y-coordinates to those that make sense for the library:

(define (normalize y-coord)
  (- viewport-height y-coord))

If we isolate our calls to the graphics library within the painters, we can use regular semantics everywhere else:

(define (segments->painter segment-list)
  (lambda (frame)
    (for-each
     (lambda (segment)
       (let ((start-coord ((frame-coord-map frame) (start-segment segment)))
             (end-coord ((frame-coord-map frame) (end-segment segment))))
         (line
          (make-posn (xcor-vect start-coord) (normalize (ycor-vect start-coord))) ; convert y-coords here
          (make-posn (xcor-vect end-coord) (normalize (ycor-vect end-coord))))))
     segment-list)))

(define unit-frame (make-frame (make-vect 0 0) (make-vect 500 0) (make-vect 0 500))) ; defined with normal semantics

With all this in place, you can draw by creating any painter from segments->painter and passing it a frame:

(define x-painter
  (segments->painter
   (list (make-segment (make-vect 0 0)
                       (make-vect 1 1))
         (make-segment (make-vect 0 1)
                       (make-vect 1 0)))))

(x-painter some-frame) ; draws to the viewport                  

So I went ahead and completed exercise 2.49 from the book, with a twist!

Here’s what I created:

Drawing the wave painter figure

The wave painter figure from the book (with a tattoo on it’s arm!)

I measured the points on the original image in macOS’ Preview app. After adjusting the location for the tattoo’s frame, I created a tattoo-painter to draw it:

(define tattoo-painter
  (lambda (frame)
    (x-painter frame)
    (diamond-painter frame)))

(tattoo-painter tattoo-frame)

You can see my full code here.

I had great fun doing this section. I hope you enjoy it too!


comments powered by Disqus