実例によるPureScript

ウェブのための関数型プログラミング

Phil Freeman, "PureScript by Example - Functional Programming for the Web"

目次に戻る

第9章 キャンバスグラフィックス

9.1 この章の目標

この章のコード例では、PureScriptでHTML5のCanvas APIを使用して2Dグラフィックスを生成する purescript-canvasパッケージに焦点をあててコードを拡張していきます。

9.2 プロジェクトの準備

このモジュールのプロジェクトでは、以下のBowerの依存関係が新しく追加されています。

この章のソースコードは、それぞれに mainメソッドが定義されている複数のモ​​ジュールへと分割されています。この章の節の内容はそれぞれ異なるファイルで実装されており、それぞれの節で対応するファイルの mainメソッドを実行できるように、Pulpビルドコマンドを変更することで Mainモジュールが変更できるようになっています。

HTMLファイル html/index.htmlには、各例で使用される単一の canvas要素、およびコンパイルされたPureScriptコードを読み込む script要素が含まれています。各節のコードをテストするには、ブラウザでこのHTMLファイルを開いてください。

9.3 単純な図形

Example/Rectangle.pursファイルにはキャンバスの中心に青い四角形をひとつ描画するという簡単な例が含まれています。このモジュールは、 Control.Monad.Effモジュールと、Canvas APIを扱うための Effモナドのアクションが定義されている Graphics.Canvasモジュールをインポートします。

他のモジュールでも同様ですが、 mainアクションは最初に getCanvasElementByIdアクションを使ってCanvasオブジェクトへの参照を取得しています。また、 getContext2Dアクションを使ってキャンバスの2Dレンダリングコンテキストを参照しています。

main = void $ unsafePartial do
  Just canvas <- getCanvasElementById "canvas"
  ctx <- getContext2D canvas

注意:このunsafePartialの呼び出しは必須です。これは getCanvasElementByIdの結果のパターンマッチングが部分的で、Just値構築子だけと照合するためです。ここではこれで問題ありませんが、実際の製品のコードではおそらくNothing値構築子と照合させ、適切なエラーメッセージを提供したほうがよいでしょう。

これらのアクションの型は、PSCiを使うかドキュメントを見ると確認できます。

getCanvasElementById :: forall eff. String ->
  Eff (canvas :: Canvas | eff) (Maybe CanvasElement)

getContext2D :: forall eff. CanvasElement ->
  Eff (canvas :: Canvas | eff) Context2D

CanvasElementContext2DGraphics.Canvasモジュールで定義されている型です。このモジュールでは、モジュール内のすべてのアクションで使用されている Canvas作用も定義されています。

グラフィックスコンテキスト ctxは、キャンバスの状態を管理し、プリミティブな図形を描画したり、スタイルや色を設定したり、座標変換を適用するためのメソッドを提供しています。

ctxの取得に続けて、 setFillStyleアクションを使って塗りのスタイルを青一色の塗りつぶしに設定しています。

  setFillStyle "#0000FF" ctx

setFillStyleアクションがグラフィックスコンテキストを引数として取っていることに注意してください。これは Graphics.Canvasで共通のパターンです。

最後に、 fillPathアクションを使用して矩形を塗りつぶしています。 fillPathは次のような型を持っています。

fillPath :: forall eff a. Context2D ->
                          Eff (canvas :: Canvas | eff) a ->
                          Eff (canvas :: Canvas | eff) a

fillPathはグラフィックスコンテキストとレンダリングするパスを構築する別のアクションを引数にとります。パスは rectアクションを使うと構築することができます。 rectはグラフィックスコンテキストと矩形の位置及びサイズを格納するレコードを引数にとります。

  fillPath ctx $ rect ctx
    { x: 250.0
    , y: 250.0
    , w: 100.0
    , h: 100.0
    }

mainモジュールの名前としてExample.Rectangleを指定して、この長方形のコード例をビルドしましょう。

$ mkdir dist/
$ pulp build -O --main Example.Rectangle --to dist/Main.js

それでは html/index.htmlファイルを開き、このコードによってキャンバスの中央に青い四角形が描画されていることを確認してみましょう。

9.4 行多相を利用する

パスを描画する方法は他にもあります。 arc関数は円弧を描画します。 moveTo関数、 lineTo関数、 closePath関数は細かい線分を組み合わせることでパスを描画します。

Shapes.pursファイルでは長方形と円弧セグメント、三角形の、3つの図形を描画しています。

rect関数は引数としてレコードをとることを見てきました。実際には、長方形のプロパティは型同義語で定義されています。

type Rectangle = { x :: Number
                 , y :: Number
                 , w :: Number
                 , h :: Number 
                 }

xyプロパティは左上隅の位置を表しており、 whのプロパティはそれぞれ幅と高さを表しています。

arc関数に以下のような型を持つレコードを渡して呼び出すと、円弧を描画することができます。

type Arc = { x     :: Number
           , y     :: Number
           , r     :: Number
           , start :: Number
           , end   :: Number
           }

ここで、 xyプロパティは弧の中心、 rは半径、 startendは弧の両端の角度を弧度法で表しています。

たとえば、次のコードは中心 (300、300)、半径 50の円弧を塗りつぶします。

  fillPath ctx $ arc ctx
    { x      : 300.0
    , y      : 300.0
    , r      : 50.0
    , start  : Math.pi * 5.0 / 8.0
    , end    : Math.pi * 2.0
    }

Number型の xyというプロパティが Rectangleレコード型と Arcレコード型の両方に含まれていることに注意してください。どちらの場合でもこの組は点を表しています。これは、いずれのレコード型にも適用できる、行多相な関数を書くことができることを意味します。

たとえば、 Shapesモジュールでは xyのプロパティを変更し図形を並行移動する translate関数を定義されています。

translate :: forall r. Number -> Number ->
              { x :: Number, y :: Number | r } ->
              { x :: Number, y :: Number | r }
translate dx dy shape = shape
  { x = shape.x + dx
  , y = shape.y + dy
  }

この行多相型に注目してください。これは trianglexyというプロパティと、それに加えて他の任意のプロパティを持ったどんなレコードでも受け入れるということを言っています。 xフィールドと yフィールドは更新されますが、残りのフィールドは変更されません。

これはレコード更新構文の例です。 shape { ... }という式は、 shapeを元にして、括弧の中で指定されたように値が更新されたフィールドを持つ新たなレコードを作ります。波括弧の中の式はレコードリテラルのようなコロンではなく、等号でラベルと式を区切って書くことに注意してください。

Shapesの例からわかるように、 translate関数は Rectangleレコードと Arcレコード双方に対して使うことができます。

Shapeの例で描画される3つめの型は線分ごとのパスです。対応するコードは次のようになります。

  setFillStyle "#FF0000" ctx

  fillPath ctx $ do
    moveTo ctx 300.0 260.0
    lineTo ctx 260.0 340.0
    lineTo ctx 340.0 340.0
    closePath ctx

ここでは3つの関数が使われています。

このコード片を実行すると、二等辺三角形を塗りつぶされます。

mainモジュールとしてExample.Shapesを指定して、この例をビルドしましょう。

$ pulp build -O --main Example.Shapes --to dist/Main.js

そしてもう一度 html/index.htmlを開き、結果を確認してください。キャンバスに3つの異なる図形が描画されるはずです。

演習

  1. (簡単) これまでの例のそれぞれについて、 strokePath関数や setStrokeStyle関数を使ってみましょう。

  2. (簡単) 関数の引数の内部でdo記法ブロックを使うと、 fillPath関数と strokePath関数で共通のスタイルを持つ複雑なパスを描画することができます。同じ fillPath呼び出しで隣り合った2つの矩形を描画するように、 Rectangleのコード例を変更してみてください。線分と円弧を組み合わせてを、円の扇形を描画してみてください。

  3. (やや難しい) 次のような2次元の点を表すレコードが与えられたとします。

    type Point = { x :: Number, y :: Number }
    

    多数の点からなる閉じたパスを描く関数 renderPath書いてください。

    renderPath :: forall eff. Context2D -> Array Point -> 
                                           Eff (canvas :: Canvas | eff) Unit
    

    次のような関数を考えます。

    f :: Number -> Point
    

    この関数は引数として 1から 0の間の Numberをとり、 Pointを返します。 renderPath関数を利用して関数 fのグラフを描くアクションを書いてください。そのアクションは有限個の点を fからサンプリングすることによって近似しなければなりません。

    関数 fを変更し、異なるパスが描画されることを確かめてください。

9.5 無作為に円を描く

Example/Random.pursファイルには2種類の異なる副作用が混在した Effモナドを使う例が含まれています。この例では無作為に生成された円をキャンバスに100個描画します。

mainアクションはこれまでのようにグラフィックスコンテキストへの参照を取得し、ストロークと塗りつぶしスタイルを設定します。

  setFillStyle "#FF0000" ctx
  setStrokeStyle "#000000" ctx

次のコードでは forEアクションを使って 0から 100までの整数について繰り返しをしています。

  for_ (1 .. 100) \_ -> do

これらの数は 0から 1の間に無作為に分布しており、それぞれ x座標、 y座標、半径 rを表しています。

    x <- random
    y <- random
    r <- random

次のコードでこれらの変数に基づいて Arcを作成し、最後に現在のスタイルに従って円弧の塗りつぶしと線描が行われます。

    let path = arc ctx
         { x     : x * 600.0
         , y     : y * 600.0
         , r     : r * 50.0
         , start : 0.0
         , end   : Math.pi * 2.0
         }
    fillPath ctx path
    strokePath ctx path

forEに渡された関数が正しい型を持つようにするため、最後の行は必要であることに注意してください。

mainモジュールとしてExample.Randomを指定して、この例をビルドしましょう。

$ pulp build -O --main Example.Random --to dist/Main.js

html/index.htmlを開いて、結果を確認してみましょう。

9.6 座標変換

キャンバスは簡単な図形を描画するだけのものではありません。キャンバスは変換行列を扱うことができ、図形は描画の前に形状を変形してから描画されます。図形は平行移動、回転、拡大縮小、および斜め変形することができます。

purescript-canvasライブラリではこれらの変換を以下の関数で提供しています。

translate :: forall eff. TranslateTransform -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
rotate    :: forall eff. Number             -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
scale     :: forall eff. ScaleTransform     -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
transform :: forall eff. Transform          -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D

translateアクションは TranslateTransformレコードのプロパティで指定した大きさだけ平行移動を行います。

rotateアクションは最初の引数で指定されたラジアンの値に応じて原点を中心とした回転を行います。

scaleアクションは原点を中心として拡大縮小します。 ScaleTransformレコードは X軸と y軸に沿った拡大率を指定するのに使います。

最後の transformはこの4つのうちで最も一般的なアクションです。このアクションは行列に従ってアフィン変換を行います。

これらのアクションが呼び出された後に描画される図形は、自動的に適切な座標変換が適用されます。

実際には、これらの関数のそれぞれの作用は、コンテキストの現在の変換行列に対して変換行列を右から乗算していきます。つまり、もしある作用の変換をしていくと、その作用は実際には逆順に適用されていきます。次のような座標変換のアクションを考えてみましょう。

transformations ctx = do
  translate { translateX: 10.0, translateY: 10.0 } ctx
  scale { scaleX: 2.0, scaleY: 2.0 } ctx
  rotate (Math.pi / 2.0) ctx

  renderScene

このアクションの作用では、まずシーンが回転され、それから拡大縮小され、最後に平行移動されます。

9.7 コンテキストの保存

一般的な使い方としては、変換を適用してシーンの一部をレンダリングし、それからその変換を元に戻します。

Canvas APIにはキャンバスの状態のスタックを操作する saverestoreメソッドが備わっています。 purescript-canvasではこの機能を次のような関数でラップしています。

save    :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D
restore :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D

saveアクションは現在のコンテキストの状態(現在の変換行列や描画スタイル)をスタックにプッシュし、 restoreアクションはスタックの一番上の状態をポップし、コンテキストの状態を復元します。

これらのアクションにより、現在の状態を保存し、いろいろなスタイルや変換を適用し、プリミティブを描画し、最後に元の変換と状態を復元することが可能になります。例えば、次の関数はいくつかのキャンバスアクションを実行しますが、その前に回転を適用し、そのあとに変換を復元します。

rotated ctx render = do
  save ctx
  rotate Math.pi ctx
  render
  restore ctx

こういったよくある使いかたの高階関数を利用した抽象化として、 purescript-canvasライブラリでは元のコンテキスト状態を維持しながらいくつかのキャンバスアクションを実行する withContext関数が提供されています。

withContext :: forall eff a. Context2D -> 
                             Eff (canvas :: Canvas | eff) a ->
                             Eff (canvas :: Canvas | eff) a          

withContextを使うと、先ほどの rotated関数を次のように書き換えることができます。

rotated ctx render = withContext ctx do
    rotate Math.pi ctx
    render

9.8 大域的な変更可能状態

この節では purescript-refsパッケージを使って Effモナドの別の作用について実演してみます。

Control.Monad.Eff.Refモジュールでは大域的に変更可能な参照のための型構築子、および関連する作用を提供します。

> import Control.Monad.Eff.Ref

> :kind Ref
Type -> Type

> :kind REF
Control.Monad.Eff.Effect

RefVal aの値は型 a値を保持する変更可能な領域への参照で、前の章で見た STRef h aによく似ています。その違いは、 ST作用は runSTを用いて除去することができますが、 Ref作用はハンドラを提供しないということです。 STは安全に局所的な状態変更を追跡するために使用されますが、 Refは大域的な状態変更を追跡するために使用されます。そのため、 Refは慎重に使用する必要があります。

Example/Refs.pursファイルには canvas要素上のマウスクリックを追跡するのに Ref作用を使用する例が含まれています。

このコー​​ドでは最初に newRefアクションを使って値 0で初期化された領域への新しい参照を作成しています。

  clickCount <- newRef 0

クリックイベントハンドラの内部では、 modifyRefアクションを使用してクリック数を更新しています。

    modifyRef clickCount (\count -> count + 1)

readRefアクションは新しいクリック数を読み取るために使われています。

    count <- readRef clickCount

render関数では、クリック数に応じて変換を矩形に適用しています。

    withContext ctx do
      let scaleX = Math.sin (toNumber count * Math.pi / 4.0) + 1.5
      let scaleY = Math.sin (toNumber count * Math.pi / 6.0) + 1.5

      translate { translateX: 300.0, translateY:  300.0 } ctx
      rotate (toNumber count * Math.pi / 18.0) ctx
      scale { scaleX: scaleX, scaleY: scaleY } ctx
      translate { translateX: -100.0, translateY: -100.0 } ctx

      fillPath ctx $ rect ctx
        { x: 0.0
        , y: 0.0
        , w: 200.0
        , h: 200.0
        }

このアクションでは元の変換を維持するために withContextを使用しており、それから続く変換を順に適用しています(変換が下から上に適用されることを思い出してください)。

このコード例をビルドしてみましょう。

$ pulp build -O --main Example.Refs --to dist/Main.js

html/index.htmlファイルを開いてみましょう。何度かキャンバスをクリックすると、キャンバスの中心の周りを回転する緑の四角形が表示されるはずです。

演習

  1. (簡単) パスの線描と塗りつぶしを同時に行う高階関数を書いてください。その関数を使用して Random.purs例を書きなおしてください。

  2. (やや難しい)Random作用と DOM作用を使用して、マウスがクリックされたときにキャンバスに無作為な位置、色、半径の円を描画するアプリケーションを作成してください。

  3. (やや難しい) シーンを指定された座標を中心に回転する関数を書いてください。ヒント:最初にシーンを原点まで平行移動しましょう。

9.9 L-Systems

この章の最後の例として、 purescript-canvasパッケージを使用してL-systems(Lindenmayer systems)を描画する関数を記述します。

L-Systemsはアルファベット、つまり初期状態となるアルファベットの文字列と、生成規則の集合で定義されています。各生成規則は、アルファベットの文字をとり、それを置き換える文字の配列を返します。この処理は文字の初期配列から始まり、複数回繰り返されます。

もしアルファベットの各文字がキャンバス上で実行される命令と対応付けられていれば、その指示に順番に従うことでL-Systemsを描画することができます。

たとえば、アルファベットが文字 L(左回転)、 R(右回転)、 F(前進)で構成されていたとします。また、次のような生成規則を定義します。

L -> L
R -> R
F -> FLFRRFLF

配列 "FRRFRRFRR" から始めて処理を繰り返すと、次のような経過を辿ります。

FRRFRRFRR
FLFRRFLFRRFLFRRFLFRRFLFRRFLFRR
FLFRRFLFLFLFRRFLFRRFLFRRFLFLFLFRRFLFRRFLFRRFLF...

この命令群に対応する線分パスをプロットすると、コッホ曲線と呼ばれる曲線に近似します。反復回数を増やすと、曲線の解像度が増加していきます。

それでは型と関数の言語へとこれを翻訳してみましょう。

アルファベットの選択肢は型の選択肢によって表すことができます。今回の例では、以下のような型で定義することができます。

data Alphabet = L | R | F

このデータ型では、アルファベットの文字ごとに1つづつデータ構築子が定義されています。

文字の初期配列はどのように表したらいいでしょうか。単なるアルファベットの配列でいいでしょう。これを Sentenceと呼ぶことにします。

type Sentence = Array Alphabet

initial :: Sentence
initial = [F, R, R, F, R, R, F, R, R]

生成規則は Alphabetから Sentenceへの関数として表すことができます。

productions :: Alphabet -> Sentence
productions L = [L]
productions R = [R]
productions F = [F, L, F, R, R, F, L, F]

これはまさに上記の仕様をそのまま書き写したものです。

これで、この形式の仕様を受け取りキャンバスに描画する関数 lsystemを実装することができます。 lsystemはどのような型を持っているべきでしょうか。この関数は初期状態 initialと生成規則 productionsのような値だけでなく、アルファベットの文字をキャンバスに描画する関数を引数に取る必要があります。

lsystemの型の最初の大まかな設計としては、次のようになるかもしれません。

forall eff. Sentence
         -> (Alphabet -> Sentence)
         -> (Alphabet -> Eff (canvas :: Canvas | eff) Unit)
         -> Int
         -> Eff (canvas :: Canvas | eff) Unit

最初の2つの引数の型は、値 initialproductionsに対応しています。

3番目の引数は、アルファベットの文字を取り、キャンバス上のいくつかのアクションを実行することによって翻訳する関数を表します。この例では、文字 Lは左回転、文字 Rで右回転、文字 Fは前進を意味します。

最後の引数は、実行したい生成規則の繰り返し回数を表す数です。

最初に気づくことは、現在の lsystem関数は Alphabet型だけで機能しますが、どんなアルファベットについても機能すべきですから、この型はもっと一般化されるべきです。それでは、量子化された型変数 aについて、 AlphabetSentenceaArray aで置き換えましょう。

forall a eff. Array a
           -> (a -> Array a)
           -> (a -> Eff (canvas :: Canvas | eff) Unit)
           -> Int
           -> Eff (canvas :: Canvas | eff) Unit

次に気付くこととしては、「左回転」と「右回転」のような命令を実装するためには、いくつかの状態を管理する必要があります。具体的に言えば、その時点でパスが向いている方向を状態として持たなければなりません。計算を通じて状態を関数に渡すように変更する必要があります。ここでも lsystem関数は状態がどんな型でも動作しなければなりませんから、型変数 sを使用してそれを表しています。

sを追加する必要があるのは3箇所で、次のようになります。

forall a s eff. Array a
             -> (a -> Array a)
             -> (s -> a -> Eff (canvas :: Canvas | eff) s)
             -> Int
             -> s
             -> Eff (canvas :: Canvas | eff) s

まず追加の引数の型として lsystemに型 sが追加されています。この引数はL-Systemの初期状態を表しています。

sは引数にも現れますが、翻訳関数(lsystemの第3引数)の返り値の型としても現れます。翻訳関数は今のところ、引数としてL-Systemの現在の状態を受け取り、返り値として更新された新しい状態を返します。

この例の場合では、次のような型を使って状態を表す型を定義することができます。

type State =
  { x :: Number
  , y :: Number
  , theta :: Number
  }

プロパティ xyはパスの現在の位置を表しており、プロパティ thetaは現在の向きを表しており、ラジアンで表された水平線に対するパスの角度です。

システムの初期状態としては次のようなものが考えられます。

initialState :: State
initialState = { x: 120.0, y: 200.0, theta: 0.0 }

それでは、 lsystem関数を実装してみます。定義はとても単純であることがわかるでしょう。

lsystemは第4引数の値(型 Number)に応じて再帰するのが良さそうです。再帰の各ステップでは、生成規則に従って状態が更新され、現在の文が変化していきます。このことを念頭に置きつつ、まずは関数の引数の名前を導入して、補助関数に処理を移譲することから始めましょう。

lsystem :: forall a s eff. Array a
        -> (a -> Array a)
        -> (s -> a -> Eff (canvas :: Canvas | eff) s)
        -> Int
        -> s
        -> Eff (canvas :: Canvas | eff) s
lsystem init prod interpret n state = go init n
  where

go関数は第2引数に応じて再帰することで動きます。 nがゼロであるときと nがゼロでないときの2つの場合で分岐します。

nがゼロの場合では再帰は完了し、解釈関数に応じて現在の文を解釈します。ここでは引数として与えられている、

を参照することができます。これらの引数の型を考えると、以前定義した foldMの呼び出しにちょうど対応していることがわかります。 foldMpurescript-controlパッケージでも定義されています。

  go s 0 = foldM interpret state s

ゼロでない場合ではどうでしょうか。その場合は、単に生成規則を現在の文のそれぞれの文字に適用して、その結果を連結し、そしてこの処理を再帰します。

  go s n = go (concatMap prod s) (n - 1)

これだけです!foldMconcatMapのような高階関数を使うと、このようにアイデアを簡潔に表現することができるのです。

しかし、まだ完全に終わったわけではありません。ここで与えた型は、実際はまだ特殊化されすぎています。この定義ではキャンバスの操作が実装のどこにも使われていないことに注目してください。それに、まったく Effモナドの構造を利用していません。実際には、この関数はどんなモナド mについても動作するのです!

この章に添付されたソースコードで定義されている、 lsystemのもっと一般的な型は次のようになっています。

lsystem :: forall a m s . Monad m =>
           Array a
        -> (a -> Array a)
        -> (s -> a -> m s)
        -> Int
        -> s
        -> m s

この型が言っているのは、この翻訳関数はモナド mで追跡される任意の副作用をまったく自由に持つことができる、ということだと理解することができます。キャンバスに描画したり、またはコンソールに情報を出力するかもしれませんし、失敗や複数の戻り値に対応しているかもしれません。こういった様々な型の副作用を使ったL-Systemを記述してみることを読者にお勧めします。

この関数は実装からデータを分離することの威力を示す良い例となっています。この手法の利点は、複数の異なる方法でデータを解釈する自由が得られることです。 lsystemは2つの小さな関数へと分解することができるかもしれません。ひとつめは concatMapの適用の繰り返しを使って文を構築するもので、ふたつめは foldMを使って文を翻訳するものです。これは読者の演習として残しておきます。

それでは翻訳関数を実装して、この章の例を完成させましょう​​。 lsystemの型は型シグネチャが言っているのは、翻訳関数の型は、何らかの型 as、型構築子 mについて、 s -> a -> m sでなければならないということです。今回は aAlphabetsState、モナド mEff (canvas :: Canvas)というように選びたいということがわかっています。これにより次のような型になります。

interpret :: State -> Alphabet -> Eff (canvas :: Canvas) State

この関数を実装するには、 Alphabet型の3つのデータ構築子それぞれについて処理する必要があります。文字 L(左回転)と R(右回転)の解釈では、 thetaを適切な角度へ変更するように状態を更新するだけです。

interpret state L = pure $ state { theta = state.theta - Math.pi / 3 }
interpret state R = pure $ state { theta = state.theta + Math.pi / 3 }

文字 F(前進)を解釈するには、パスの新しい位置を計算し、線分を描画し、状態を次のように更新します。

interpret state F = do
  let x = state.x + Math.cos state.theta * 1.5
      y = state.y + Math.sin state.theta * 1.5
  moveTo ctx state.x state.y
  lineTo ctx x y
  pure { x, y, theta: state.theta }

この章のソースコードでは、名前 ctxを参照できるようにするために、 interpret関数は main関数内で let束縛を使用して定義されていることに注意してください。 State型がコンテキストを持つように変更することは可能でしょうが、それはこのシステムの状態の変化部分ではないので不適切でしょう。

このL-Systemsを描画するには、次のような strokePathアクションを使用するだけです。

strokePath ctx $ lsystem initial productions interpret 5 initialState

L-Systemをコンパイルし、

$ pulp build -O --main Example.LSystem --to dist/Main.js

html/index.htmlを開いてみましょう。キャンバスにコッホ曲線が描画されるのがわかると思います。

演習

  1. (簡単)strokePathの代わりに fillPathを使用するように、上のL-Systemsの例を変更してください。ヒントclosePathの呼び出しを含め、 moveToの呼び出しを interpret関数の外側に移動する必要があります。

  2. (簡単) 描画システムへの影響を理解するために、コード中の様々な数値の定数を変更してみてください。

  3. (やや難しい)lsystem関数を2つの小さな関数に分割してください。ひとつめは concatMapの適用の繰り返しを使用して最終的な結果を構築するもので、ふたつめは foldMを使用して結果を解釈するものでなくてはなりません。

  4. (やや難しい)setShadowOffsetXアクション、 setShadowOffsetYアクション、 setShadowBlurアクション、 setShadowColorアクションを使い、塗りつぶされた図形にドロップシャドウを追加してください。ヒントPSCiを使って、これらの関数の型を調べてみましょう。

  5. (やや難しい) 向きを変えるときの角度の大きさは今のところ一定(pi/3)です。その代わりに、 Alphabetデータ型の中に角度の大きさを追加して、生成規則によって角度を変更できるようにしてください。

    type Angle = Number
    
    data Alphabet = L Angle | R Angle | F Angle
    

    生成規則でこの新しい情報を使うと、どんな面白い図形を作ることができるでしょうか。

  6. (難しい)L(60度左回転 )、 R(60度右回転)、 F(前進)、 M(これも前進)という4つの文字からなるアルファベットでL-Systemが与えられたとします。

    このシステムの文の初期状態は、単一の文字 Mです。

    このシステムの生成規則は次のように指定されています。

    L -> L
    R -> R
    F -> FLMLFRMRFRMRFLMLF
    M -> MRFRMLFLMLFLMRFRM
    

    このL-Systemを描画してください。注意:最後の文のサイズは反復回数に従って指数関数的に増大するので、生成規則の繰り返しの回数を削減することが必要になります。

    ここで、生成規則における LMの間の対称性に注目してください。ふたつの「前進」命令は、次のようなアルファベット型を使用すると、 Boolean値を使って区別することができます。

    data Alphabet = L | R | F Boolean
    

    このアルファベットの表現を使用して、もう一度このL-Systemを実装してください。

  7. (難しい) 翻訳関数で別のモナド mを使ってみましょう。 Trace作用を利用してコンソール上にL-Systemを出力したり、 Random作用を利用して状態の型に無作為の突然変異を適用したりしてみてください。

まとめ

この章では、 purescript-canvasライブラリを使用することにより、PureScriptからHTML5 Canvas APIを使う方法について学びました。マップや畳み込み、レコードと行多型、副作用を扱うための Effモナドなど、これまで学んできた手法を利用した実用的な例について多く見ました。

この章の例では、高階関数の威力を示すとともに、実装からのデータの分離も実演してみせました。これは例えば、代数データ型を使用してこれらの概念を次のように拡張し、描画関数からシーンの表現を完全に分離できるようになります。

data Scene = Rect Rectangle
           | Arc Arc
           | PiecewiseLinear (Array Point)
           | Transformed Transform Scene
           | Clipped Rectangle Scene
           | ...

この手法は purescript-drawingパッケージでも採用されており、描画前にさまざまな方法でデータとしてシーンを操作することができるという柔軟性をもたらしています。

次の章では、PureScriptの外部関数インタフェース(foreign function interface)を使って、既存のJavaScriptの関数をラップした purescript-canvasのようなライブラリを実装する方法について説明します。

目次に戻る