On this page:
<drawing>
<drawing-tests>
<constants>
<constants>2
<render-world>
<render-board>
<render-cell>
<crop-whiskers>
<world-width>
<world-height>
<cell-center-x-tests>
<cell-center-x-tests>2
<cell-center-x>
<cell-center-y-tests>
<cell-center-y>
1.18.7 Drawing the World🔗

There are a number of constants that are given names to make the code more readable.

These first two constants give the radius of the circles that are drawn on the board, plus the radius of an invisible circle that, if they were drawn on top of the circles, would touch each other. Accordingly, circle-spacing is used when computing the positions of the circles, but the circles are drawn using circle-radius.

(define circle-radius 20)
(define circle-spacing 22)

The other four constants specify the colors of the circles.

(define normal-color 'lightskyblue)
(define on-shortest-path-color 'white)
(define blocked-color 'black)
(define under-mouse-color 'black)

The main function for drawing a world is render-world. It is a fairly straightforward composition of helper functions. First, it builds the image of a board, and then puts the cat on it. Lastly, since the whiskers of the cat might now hang off of the edge of the board (if the cat is on a leftmost or rightmost cell), it trims them. This ensures that the image is always the same size and that the pinhole is always in the upper-left corner of the window.

(define/contract (render-world w)
  (-> world? image?)
  (define the-cat
    (cond
      [(equal? (world-state w) 'cat-won) happy-cat]
      [(equal? (world-state w) 'cat-lost) mad-cat]
      [else thinking-cat]))
  (crop-whiskers
   w
   (underlay/xy (render-board (world-board w)
                              (world-size w)
                              (on-cats-path? w)
                              (world-mouse-posn w))
                (- (cell-center-x (world-cat w))
                   (/ (image-width the-cat) 2))
                (- (cell-center-y (world-cat w))
                   (/ (image-height the-cat) 2))
                the-cat)))

The render-board function uses for/fold to iterate over all of the cells in cs. It starts with an empty rectangle and, one by one, puts the cells on image.

(define/contract (render-board cs world-size on-cat-path? mouse)
  (-> (listof cell?)
      natural-number/c
      (-> posn? boolean?)
      (or/c #f posn?)
      image?)
  (for/fold ([image (rectangle (world-width world-size)
                               (world-height world-size)
                               'solid
                               'white)])
            ([c (in-list cs)])
    (underlay/xy image
                 (- (cell-center-x (cell-p c)) circle-radius)
                 (- (cell-center-y (cell-p c)) circle-radius)
                 (render-cell c
                              (on-cat-path? (cell-p c))
                              (and (posn? mouse)
                                   (equal? mouse (cell-p c)))))))

The render-cell function accepts a cell, a boolean indicating if the cell is on the shortest path between the cat and the boundary, and a second boolean indicating if the cell is underneath the mouse. It returns an image of the cell, with the pinhole placed in such a way that overlaying the image on an empty image with pinhole in the upper-left corner results in the cell being placed in the right place.

(define/contract (render-cell c on-short-path? under-mouse?)
  (-> cell? boolean? boolean? image?)
  (define x (cell-center-x (cell-p c)))
  (define y (cell-center-y (cell-p c)))
  (define main-circle
    (cond
      [(cell-blocked? c)
       (circle circle-radius 'solid blocked-color)]
      [else
       (circle circle-radius 'solid normal-color)]))
  (cond
    [under-mouse?
     (underlay main-circle
               (circle (quotient circle-radius 2) 'solid under-mouse-color))]
    [on-short-path?
     (underlay main-circle
               (circle (quotient circle-radius 2) 'solid
                       on-shortest-path-color))]
    [else
     main-circle]))

The chop-whiskers function ensures that when the cat is near the edge of the board (and its whiskers would hang off) that the image that the board produces still has the same size (by clipping away the cat’s whiskers).

(define/contract (crop-whiskers w img)
  (-> world? image? image?)
  (define cat-posn (world-cat w))
  (define cat-x (posn-x cat-posn))
  (define cat-y (posn-y cat-posn))
  (define left-edge? (and (even? cat-y) (= cat-x 0)))
  (define right-edge? (and (odd? cat-y) (= cat-x (- board-size 1))))
  (define width (world-width (world-size w)))
  (define height (world-height (world-size w)))
  (cond
    [left-edge?
     (crop (- (/ (image-width happy-cat) 2) circle-radius)
           0
           width height img)]
    [right-edge? (crop 0 0 width height img)]
    [else img]))

The world-width function computes the width of the rendered world, given the world’s size by finding the center of the rightmost posn, and then adding an additional radius.

(define/contract (world-width board-size)
  (-> natural-number/c number?)
  (define rightmost-posn
    (make-posn (- board-size 1) (- board-size 2)))
  (+ (cell-center-x rightmost-posn) circle-radius))

Similarly, the world-height function computest the height of the rendered world, given the world’s size.

(define/contract (world-height board-size)
  (-> natural-number/c number?)
  (define bottommost-posn
    (make-posn (- board-size 1) (- board-size 1)))
  (ceiling (+ (cell-center-y bottommost-posn)
              circle-radius)))

The cell-center-x function returns the x coordinate of the center of the cell specified by p.

For example, the first cell in the third row (counting from 0) is flush with the edge of the screen, so its center is just the radius of the circle that is drawn.

(test (cell-center-x (make-posn 0 2))
      circle-radius)

The first cell in the second row, in contrast is offset from the third row by circle-spacing.

(test (cell-center-x (make-posn 0 1))
      (+ circle-spacing circle-radius))

The definition of cell-center-x multiplies the x coordinate of p by twice circle-spacing and then adds circle-radius to move over for the first circle. In addition if the y coordinate is odd, then it adds circle-spacing, shifting the entire line over.

(define/contract (cell-center-x p)
  (-> posn? number?)
  (define x (posn-x p))
  (define y (posn-y p))
  (+ circle-radius
     (* x circle-spacing 2)
     (if (odd? y)
         circle-spacing
         0)))

The cell-center-y function computes the y coordinate of a cell’s location on the screen. For example, the y coordinate of the first row is the radius of a circle, ensuring that the first row is flush against the top of the screen.

(test (cell-center-y (make-posn 1 0))
      circle-radius)

Because the grid is hexagonal, the y coordinates of the rows do not have the same spacing as the x coordinates. In particular, they are off by sin(pi/3). We approximate that by 433/500 in order to keep the computations and test cases simple and using exact numbers. A more precise approximation would be 0.8660254037844386, but it is not necessary at the screen resolution.

(define/contract (cell-center-y p)
  (-> posn? number?)
  (+ circle-radius
     (* (posn-y p)
        circle-spacing 2
        433/500)))