@uents blog

Code wins arguments.

SICP 読書ノート#13 - 2.2.4 図形言語(3) (pp.73-83)

引き続き「§2.2.4 図形言語」。

ベクトル (問題 2.46)

ベクトルのコンストラクタ、アクセサ、演算手続きを実装する。

(define (make-vect x y) (cons x y))
(define xcor-vect car)
(define ycor-vect cdr)

(define (add-vect vec1 vec2)
  (make-vect (+ (xcor-vect vec1) (xcor-vect vec2))
             (+ (ycor-vect vec1) (ycor-vect vec2))))

(define (sub-vect vec1 vec2)
  (make-vect (- (xcor-vect vec1) (xcor-vect vec2))
             (- (ycor-vect vec1) (ycor-vect vec2))))

(define (scale-vect s vec)
  (make-vect (* s (xcor-vect vec))
             (* s (ycor-vect vec))))

ベクトルの動作確認のためにキャンバスにベクトルを描画する手続きdraw-lineを実装する。

(define (draw-line start-vec end-vec)
  (define (vect->posn vec)
    (gfx:make-posn (xcor-vect vec) (ycor-vect vec)))
  ((gfx:draw-line vp) (vect->posn start-vec) (vect->posn end-vec)))

テスト。

racket@> (draw-line (make-vect 100 100) (make-vect 400 400))

image

セグメント (問題 2.48)

始点・終点からなる線分のコンストラクタとアクセサを実装。

(define (make-segment start-vec end-vec) (cons start-vec end-vec))
(define start-segment car)
(define end-segment cdr)

テスト。

racket@> (define seg
           (make-segment (make-vect 100 100)
                         (make-vect 400 400)))

racket@> (draw-line (start-segment seg) (end-segment seg))

ベクトルの時と同じ結果になる。

フレーム (問題 2.47)

フレームとは

そもそもフレームとは何か。

フレームとは図形を展開するための空間である。

単位座標系であるため、原点側の頂点は[0.0、0.0]、 反対側の頂点は[1.0, 1.0]で表される。

OpenGLで言うと、フレームはテクスチャ座標、キャンバスはスクリーン座標みたいな感じ。

また、キャンバス空間上でのフレームをマッピングする領域は以下で定義される。

  • 原点 (origin)
  • 2方向のエッジ (edge1, edge2)

フレームをキャンバス左下を原点として、キャンバス全体に拡げるには、

(define f
    (make-frame
     ;; 原点は、キャンバスの左下
     (make-vect canvas-margin
                (+ canvas-margin canvas-height))

     ;; 片方のエッジは、キャンバスの水平方向全体
     (make-vect canvas-width 0)

     ;; もう片方のエッジは、キャンバスの垂直方向全体
     (make-vect 0 (* -1 canvas-height))))

のようにする。(後々の見やすさのために少しマージンをもうけてます)

コンストラクタとアクセサ

(define (make-frame origin edge1 edge2)
  (list origin edge1 edge2))

(define origin-frame car)
(define edge1-frame cadr)
(define edge2-frame caddr)

フレームをキャンバスへマッピング

フレーム空間の座標をキャンバス空間の座標へマッピングするための 手続きがframe-coord-mapである。フレームを引数にとる手続きを返す、高階手続きである。

(define (frame-coord-map frame)
  (lambda (v)
    (add-vect
     (origin-frame frame)
     (add-vect (scale-vect (xcor-vect v)
                           (edge1-frame frame))
               (scale-vect (ycor-vect v)
                           (edge2-frame frame))))))

動かしてみる。

;; フレーム内の原点から対角方向への線分を描画
racket@> (draw-line ((frame-coord-map f) (make-vect 0.0 0.0))
                    ((frame-coord-map f) (make-vect 1.0 1.0)))

image

できた。

ペインタ (問題 2.49)

ペインタとはフレームを引数にとり、そのフレームのなかに図形を描画する手続きである。

segment->painter複数の線分リストを引数にとり、 フレームにそれらの線分を描画するペインタを返す。これも高階手続き。

(define (segments->painter segment-list)
  (lambda (frame)
    (for-each
     (lambda (segment)
       (draw-line
        ((frame-coord-map frame) (start-segment segment))
        ((frame-coord-map frame) (end-segment segment))))
     segment-list)))

draw-lineは前述の手続きと同じ。ここまでくると色々作れる。

フレームの外形を描くペインタ

(define outline
  (let* ((v0 (make-vect 0.0 0.0))
         (v1 (make-vect 1.0 0.0))
         (v2 (make-vect 0.0 1.0))
         (v3 (make-vect 1.0 1.0)))
    (segments->painter (list (make-segment v0 v1)
                              (make-segment v1 v3)
                              (make-segment v3 v2)
                              (make-segment v2 v0)))))

動作結果。

racket@> (outline f)

image

フレームの向かい側の頂点を結んで "X" を描くペインタ

(define diagonal
  (let* ((v1 (make-vect 0.0 0.0))
         (v2 (make-vect 1.0 0.0))
         (v3 (make-vect 0.0 1.0))
         (v4 (make-vect 1.0 1.0)))
    (segments->painter (list (make-segment v1 v4)
                             (make-segment v2 v3)))))

動作結果。

racket@> (diagonal f)

image

フレームの辺の中点を結んで菱形を描くペインタ

(define diamond
  (let* ((m1 (make-vect 0.5 0.0))
         (m2 (make-vect 0.0 0.5))
         (m3 (make-vect 1.0 0.5))
         (m4 (make-vect 0.5 1.0)))
    (segments->painter (list (make-segment m1 m3)
                             (make-segment m3 m4)
                             (make-segment m4 m2)
                             (make-segment m2 m1)))))

動作結果。

racket@> (diamond f)

image

waveペインタ

(define wave
  (segments->painter
   (list (make-segment (make-vect 0.2 0.0) (make-vect 0.4 0.4))
         (make-segment (make-vect 0.4 0.4) (make-vect 0.3 0.5))
         (make-segment (make-vect 0.3 0.5) (make-vect 0.1 0.3))
         (make-segment (make-vect 0.1 0.3) (make-vect 0.0 0.6))
         (make-segment (make-vect 0.0 0.8) (make-vect 0.1 0.5))
         (make-segment (make-vect 0.1 0.5) (make-vect 0.3 0.6))
         (make-segment (make-vect 0.3 0.6) (make-vect 0.4 0.6))
         (make-segment (make-vect 0.4 0.6) (make-vect 0.3 0.8))
         (make-segment (make-vect 0.3 0.8) (make-vect 0.4 1.0))
         (make-segment (make-vect 0.6 1.0) (make-vect 0.7 0.8))
         (make-segment (make-vect 0.7 0.8) (make-vect 0.6 0.6))
         (make-segment (make-vect 0.6 0.6) (make-vect 0.8 0.6))
         (make-segment (make-vect 0.8 0.6) (make-vect 1.0 0.4))
         (make-segment (make-vect 1.0 0.2) (make-vect 0.6 0.4))
         (make-segment (make-vect 0.6 0.4) (make-vect 0.8 0.0))
         (make-segment (make-vect 0.7 0.0) (make-vect 0.5 0.3))
         (make-segment (make-vect 0.5 0.3) (make-vect 0.3 0.0)))))

動作結果。

racket@> (wave f)

image

毎回 (painter f) とタイプするのは面倒なので、 以降は以下のペインタを描画する手続きをつかう。

(define (draw painter)
  (let ((f (make-frame
            ;; 原点は、キャンバスの左下
            (make-vect canvas-margin
                       (+ canvas-margin canvas-height))
            ;; 片方のエッジは、キャンバスの水平方向全体
            (make-vect canvas-width 0)
            ;; もう片方のエッジは、キャンバスの垂直方向全体
            (make-vect 0 (* -1 canvas-height)))))
    (painter f)))

例えばwaveペインタの描画は以下の通り。

racket@> (draw wave)

次回も引き続き「§ 2.4 図形言語」。


※「SICP読書ノート」の目次はこちら