Arantium Maestum

プログラミング、囲碁、読書の話題

ClojureとQuilで桜を吹雪かせてみた

こういう記事を読んだ:

blog.livedoor.jp

とても面白そうだったのでひさしぶりにQuilで再現して遊んでみよう、と思ったら意外と色々忘れていてえらく時間がかかった。

とりあえず結果:

Quil -L7dtRVx3FipAGAAMkGE

Runボタンクリックで桜吹雪が見えるはず。

コードは以下の通り。

まずは基本形である花びらの座標を定義:

(defn sakura-fn [t]
  (let [radt (q/radians t)
        A    (* (/ 4 q/PI) 
                radt)
        md   (mod (q/floor A) 2)
        r    (+ md
                (* (q/pow -1 md)
                   (- A (q/floor A))))
        R    (+ r (* 2 
                     (min 0 (- 0.8 r))))
        x    (* R (q/cos radt))
        y    (* R (q/sin radt))]
    [x y]))

(def sakura-shape 
  (map sakura-fn (range 90)))

この座標情報の詳細を知りたい方は元記事とそのさらに元であるこの記事を見てほしい:

sites.google.com

次にその座標も含めた様々な花びら情報をある程度ランダムに作成する関数:

(defn make-sakura []
  (let [xDef (q/random 500)
        xAmp (q/random 50 100)
        xTheta (q/random 360)
        size (q/random 40 100)        
         ]
    {:shape sakura-shape
     :color (nth 
              [[244 191 252 150]
               [255 219 248 150]
               [246 204 252 150]]
              (q/floor (q/random 3)))
     :xDef xDef
     :xAmp xAmp
     :xTheta xTheta
     :size size
     :ox (+ xDef (* xAmp (q/sin (q/radians xTheta))))
     :oy  (q/random 500)
     :rotateT (q/random 360)
     :xSpeed    (q/random 1 2)
     :ySpeed (/ size 20)
     :sizeYScale 1
     :sizeYT (q/random 360)
     :sizeYSpeed (/ size 30)}))

その個々の花びら情報を描画する関数。基本形の座標を個々の花びらの情報ですこし変形させてからq/vertexで描画している:

(defn draw-sakura [sakura]
  (q/push-matrix)

  (apply q/fill (:color sakura))
  (q/translate (:ox sakura) (:oy sakura))
  (q/rotate (:rotateT sakura))

  (q/begin-shape)
  (doseq [[x y] (:shape sakura)]
    (q/vertex (* x (:size sakura))
              (* y (:size sakura)  (:sizeYScale sakura))))
  (q/end-shape :close)

  (q/pop-matrix))

1フレームごとに個々の花びらの情報をアップデートする関数:

(defn move-sakura [sakura]
  (let [oy     (+ (:oy sakura) (:ySpeed sakura))
        sizeYT (+ (:sizeYT sakura) (:sizeYSpeed sakura))]
    (-> sakura
      (assoc :ox (+ (:xDef sakura)
                    (* (:xAmp sakura)
                       (q/sin (q/radians (:xTheta sakura))))))
      (update :xTheta #(+ % (:xSpeed sakura)))
      (assoc :sizeYT sizeYT)
      (assoc :sizeYScale (-> sizeYT q/radians q/sin q/abs))
      (assoc :oy (if (> oy (+ 500 (:size sakura)))
                   (- (:size sakura))
                   oy)))))

全体的に元記事のコードをClojureに書き写しただけ。ただ、元記事はy += x; z += y; if (z > ...)的なコードがあるので、そこはClojureのimmutable性とバッティングしないよう気をつけている。

あとはQuilの関数であるsetupで初期化、update-stateで1フレームごとに全体の情報をアップデート、draw-stateで全体を描画:

(defn setup []
  (q/frame-rate 30)
  (q/color-mode :hsb)
  {:color 0
   :angle 0
   :sakura (repeatedly 20 make-sakura)})

(defn update-state [state]
  (update state :sakura #(map move-sakura %)))

(defn draw-state [state]
  (q/background 240)
  (doall (map draw-sakura (:sakura state))))

このコード、多分draw-stateにすごく仕事させている気がする。というのはsetupupdate-stateだとrepeatedlymapといった遅延評価シーケンスを返す関数しか使っていないので、draw-stateになるまでは情報がアップデートされていない。どうせupdate-statedraw-stateも1フレームに一回呼ぶんだしいいかと思っているが、気をつけるなら:

(defn update-state [state]
  (update state :sakura #(vec (map move-sakura %))))

で無理やり評価してしまう手もある。