Canvasグラフィックス

この章の目標

この章はcanvasパッケージに焦点を当てる発展的な例となります。 このパッケージはPureScriptでHTML5のCanvas APIを使用して2Dグラフィックスを生成する手段を提供します。

プロジェクトの準備

このモジュールのプロジェクトでは以下の新しい依存関係が導入されます。

  • canvasはHTML5のCanvas APIメソッドの型を与えます。
  • refs大域的な変更可能領域への参照 を使うための副作用を提供します。

この章の各ソースコードは、mainメソッドが定義されているモジュールの集合へと分割されています。 この章の各節の内容は個別のファイルで実装されており、各時点での適切なファイルのmainメソッドを実行できるように、Spagoビルドコマンドを変更することで、Mainモジュールを合わせられるようになっています。

HTMLファイルhtml/index.htmlには、各例で使用される単一のcanvas要素、及びコンパイルされたPureScriptコードを読み込むscript要素が含まれています。 各節のコードを試すにはブラウザでHTMLファイルを開きます。 ほとんどの演習はブラウザを対象にしているので、この章には単体試験はありません。

単純な図形

Example/Rectangle.pursファイルには簡単な導入例が含まれています。 この例ではキャンバスの中心に青い四角形を1つ描画します。 このモジュールへは、EffectモジュールからのEffect型と、Canvas APIを扱うためのEffectモナドの動作を含むGraphics.Canvasモジュールをインポートします。

他のモジュールでも同様ですが、main動作は最初にgetCanvasElementById動作を使ってキャンバスオブジェクトへの参照を取得し、getContext2D動作を使ってキャンバスの2D描画文脈にアクセスします。

void関数は関手を取り値をUnitで置き換えます。 例ではmainがシグネチャに沿うようにするために使われています。

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

補足:このunsafePartialの呼び出しは必須です。 これはgetCanvasElementByIdの結果のパターン照合部分で、Just値構築子のみと照合するためです。 ここではこれで問題ありませんが、恐らく実際の製品のコードではNothing値構築子と照合させ、適切なエラー文言を提供したほうがよいでしょう。

これらの動作の型はPSCiを使うかドキュメントを見ると確認できます。

getCanvasElementById :: String -> Effect (Maybe CanvasElement)

getContext2D :: CanvasElement -> Effect Context2D

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

グラフィックス文脈ctxはキャンバスの状態を管理し、原始的な図形を描画したり、スタイルや色を設定したり、座標変換を適用したりするための手段を提供します。

話を進めると、setFillStyle動作を使うことで塗り潰しスタイルを濃い青に設定できます。 より長い16進数記法の#0000FFも青には使えますが、単純な色については略記法がより簡単です。

  setFillStyle ctx "#00F"

setFillStyle動作がグラフィックス文脈を引数として取っていることに注意してください。 これはGraphics.Canvasではよくあるパターンです。

最後に、fillPath動作を使用して矩形を塗り潰しています。 fillPathは次のような型を持っています。

fillPath :: forall a. Context2D -> Effect a -> Effect a

fillPathはグラフィックスの文脈と描画するパスを構築する他の動作を引数に取ります。 rect動作を使うとパスを構築できます。 rectはグラフィックスの文脈と矩形の位置及びサイズを格納するレコードを取ります。

  fillPath ctx $ rect ctx
    { x: 250.0
    , y: 250.0
    , width: 100.0
    , height: 100.0
    }

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

$ spago bundle-app --main Example.Rectangle --to dist/Main.js

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

行多相を利用する

パスを描画する方法は他にもあります。 arc関数は円弧を描画します。 moveTo関数、lineTo関数、closePath関数は断片的な線分のパスを描画できます。

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

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

type Rectangle =
  { x :: Number
  , y :: Number
  , width :: Number
  , height :: Number
  }

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

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

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

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

例えばこのコードは中心が(300, 300)に中心があり半径50の円弧を塗り潰します。 弧は1回転のうち2/3ラジアン分あります。 単位円が上下逆様になっている点に注意してください。 これはy軸がキャンバスの下向きに伸びるためです。

  fillPath ctx $ arc ctx
    { x      : 300.0
    , y      : 300.0
    , radius : 50.0
    , start  : 0.0
    , end    : Math.tau * 2.0 / 3.0
    }

Rectangleレコード型とArcレコード型の両方共、Number型のxyというプロパティを含んでいますね。 どちらの場合でもこの組は点を表しています。 つまり、何れのレコード型にも作用する行多相な関数を書けます。

例えば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
  }

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

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

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

Shapeの例で描画される3つ目の型は線分の断片からなるパスです。 対応するコードは次のようになります。

  setFillStyle ctx "#F00"

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

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

  • moveToはパスの現在地を指定された座標に移動します。
  • lineToは現在地と指定された座標の間の線分を描画し、現在地を更新します。
  • closePathは現在地と開始地点とを結ぶ線分を描画してパスを完結します。

このコード片の結果は二等辺三角形の塗り潰しになります。

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

$ spago bundle-app --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 }
    

    これは2次元の点を表現しています。 多数の点からなる閉じたパスを線描きする関数 renderPathを書いてください。

    renderPath
      :: Context2D
      -> Array Point
      -> Effect Unit
    

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

    f :: Number -> Point
    

    この関数は引数として1から0の間のNumberを取り、Pointを返します。 renderPath関数を使い、関数fのグラフを描く動作を書いてください。 その動作では有限個の点でfを標本化することによって近似しなければなりません。

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

無作為に円を描く

Example/Random.pursファイルには、Effectモナドを使って2種類の副作用を綴じ合わせる例が含まれています。 1つの副作用は乱数生成で、もう1つはキャンバスの操作です。 この例では無作為に生成された円をキャンバスに100個描画します。

main動作ではこれまでのようにグラフィックス文脈への参照を取得し、線描きと塗り潰しのスタイルを設定します。

  setFillStyle ctx "#F00"
  setStrokeStyle ctx "#000"

次のコードではfor_動作を使って0から100までの整数について反復しています。

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

各繰り返しで、do記法ブロックは01の間に分布する3つの乱数を生成することから始まります。 これらの数はそれぞれx座標、y座標、半径rを表しています。

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

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

    let path = arc ctx
         { x                  : x * 600.0
         , y                  : y * 600.0
         , radius             : r * 50.0
         , start              : 0.0
         , end                : Number.tau
         , useCounterClockwise: false
         }

    fillPath ctx path
    strokePath ctx path

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

$ spago bundle-app --main Example.Random --to dist/Main.js

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

座標変換

キャンバスは簡単な図形を描画するだけのものではありません。 キャンバスは座標変換を管理しており、描画の前に図形を変形するのに使えます。 図形は平行移動、回転、拡大縮小、及び斜めに変形できます。

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

translate :: Context2D
          -> TranslateTransform
          -> Effect Context2D

rotate    :: Context2D
          -> Number
          -> Effect Context2D

scale     :: Context2D
          -> ScaleTransform
          -> Effect Context2D

transform :: Context2D
          -> Transform
          -> Effect Context2D

translate動作はTranslateTransformレコードのプロパティで指定した大きさだけ平行移動します。

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

scale動作は原点を中心として拡大縮小します。 ScaleTransformレコードはx軸とy軸に沿った拡大率を指定するのに使います。

最後の transformはこの4つのうちで最も一般化された動作です。 この動作では行列に従ってアフィン変換します。

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

実際には、これらの関数の各作用は、文脈の現在の変換行列に対して変換行列を右から乗算していきます。 つまり、もしある作用の変換をしていくと、その作用は実際には逆順に適用されていきます。

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

  renderScene

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

文脈の保存

座標変換を使ってシーンの一部を描画し、それからその変換を元に戻す、という使い方はよくあります。

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

save
  :: Context2D
  -> Effect Context2D

restore
  :: Context2D
  -> Effect Context2D

save動作は現在の文脈の状態(現在の変換行列や描画スタイル)をスタックにプッシュし、restore動作はスタックの一番上の状態をポップし、文脈の状態を復元します。

これらの動作により、現在の状態を保存し、いろいろなスタイルや変換を適用してから原始的な図形を描画し、最後に元の変換と状態を復元できます。 例えば次の関数は幾つかのキャンバス動作を実行しますが、その前に回転を適用し、その後に変換を復元します。

rotated ctx render = do
  save ctx
  rotate (Math.tau / 3.0) ctx
  render
  restore ctx

こういったよくある高階関数の使われ方の抽象化として、canvasライブラリでは元の文脈状態を保存しつつ幾つかのキャンバス動作を実行するwithContext関数が提供されています。

withContext
  :: Context2D
  -> Effect a
  -> Effect a

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

rotated ctx render =
  withContext ctx do
    rotate (Math.tau / 3.0) ctx
    render

大域的な変更可能状態

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

Effect.Refモジュールでは、大域的に変更可能な参照のための型構築子、及びそれに紐付く作用を提供します。

> import Effect.Ref

> :kind Ref
Type -> Type

Ref aの値は型aの値を含む可変参照セルであり、大域的な変更を追跡するのに使われます。 そういったわけでこれは少しだけ使う分に留めておくべきです。

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

このコードでは最初にnew動作を使って値0を含む新しい参照を作成しています。

  clickCount <- Ref.new 0

クリックイベント制御子の内部では、modify動作を使用してクリック数を更新し、更新された値が返されています。

    count <- Ref.modify (\count -> count + 1) clickCount

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

  withContext ctx do
    let scaleX = Number.sin (toNumber count * Number.tau / 8.0) + 1.5
    let scaleY = Number.sin (toNumber count * Number.tau / 12.0) + 1.5

    translate ctx { translateX: 300.0, translateY:  300.0 }
    rotate ctx (toNumber count * Number.tau / 36.0)
    scale ctx { scaleX: scaleX, scaleY: scaleY }
    translate ctx { translateX: -100.0, translateY: -100.0 }

    fillPath ctx $ rect ctx
      { x: 0.0
      , y: 0.0
      , width: 200.0
      , height: 200.0
      }

この動作では元の変換を保存するためにwithContextを使用しており、それから一連の変換を適用しています(変換が下から上に適用されることを思い出してください)。

  • 矩形が(-100, -100)だけ平行移動し、中心が原点に来ます。
  • 矩形が原点を中心に拡大されます。
  • 矩形が原点を中心に10の倍数分の角度で回転します。
  • 矩形が(300, 300)だけ平行移動し、中心がキャンバスの中心に来ます。

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

$ spago bundle-app --main Example.Refs --to dist/Main.js

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

演習

  1. (簡単)パスの線描と塗り潰しを同時に行う高階関数を書いてください。 その関数を使用してRandom.pursの例を書き直してください。
  2. (普通)Random作用とDom作用を使用して、マウスがクリックされたときに、キャンバスに無作為な位置、色、半径の円を描画するアプリケーションを作成してください。
  3. (普通)指定された座標の点を中心として回転させることでシーンを変換する関数を書いてください。 手掛かり:変換を使い、最初にシーンを原点まで平行移動しましょう。

L-System

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

1つのL-Systemはアルファベット、つまりアルファベット由来の文字の初期の並びと、生成規則の集合で定義されます。 各生成規則は、アルファベットの文字を取り、それを置き換える文字の並びを返します。 この処理は文字の初期の並びから始まり、複数回繰り返されます。

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

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

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

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

FRRFRRFRR
FLFRRFLFRRFLFRRFLFRRFLFRRFLFRR
FLFRRFLFLFLFRRFLFRRFLFRRFLFLFLFRRFLFRRFLFRRFLF...

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

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

アルファベットの文字は以下のADTで表現できます。

data Letter = L | R | F

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

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

type Sentence = Array Letter

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

生成規則は以下のようにLetterから Sentenceへの関数として表すことができます。

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

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

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

lsystemの型の最初の大まかな設計は以下です。

Sentence
-> (Letter -> Sentence)
-> (Letter -> Effect Unit)
-> Int
-> Effect Unit

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

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

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

最初に気付くことは、このlsystem関数は1つの型Letterに対してのみ動作するのですが、どんなアルファベットについても機能すべきですから、この型はもっと一般化されるべきです。 それでは、量子化された型変数 aについて、LetterSentenceaArray aで置き換えましょう。

forall a. Array a
          -> (a -> Array a)
          -> (a -> Effect Unit)
          -> Int
          -> Effect Unit

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

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

forall a s. Array a
            -> (a -> Array a)
            -> (s -> a -> Effect s)
            -> Int
            -> s
            -> Effect 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引数の値(型はInt)に応じて再帰するのが良さそうです。 再帰の各ステップでは、生成規則に従って状態が更新され、現在の文が変化していきます。 このことを念頭に置きつつ、まずは関数の引数の名前を導入して、補助関数に処理を移譲することから始めましょう。

lsystem :: forall a s
         . Array a
        -> (a -> Array a)
        -> (s -> a -> Effect s)
        -> Int
        -> s
        -> Effect s
lsystem init prod interpret n state = go init n
  where

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

1つ目の場合は再帰は完了し、解釈関数に応じて現在の文を解釈します。 型Array aの文、型sの状態、型s -> a -> Effect sの関数があります。 以前定義したfoldMの出番のようです。 この関数はcontrolパッケージで手に入ります。

  go s 0 = foldM interpret state s

ゼロでない場合ではどうでしょうか。 その場合は、単に生成規則を現在の文のそれぞれの文字に適用して、その結果を連結し、そして再帰的にgoを呼び出すことによって繰り返します。

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

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

しかし、話はこれで終わりではありません。 ここで与えた型は、実際はまだ特殊化されすぎています。 この定義ではキャンバスの操作が実装のどこにも使われていないことに注目してください。 それに、全くEffectaモナドの構造を利用していません。 実際には、この関数はどんなモナド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つの小さな関数へと分解できます。 1つ目はconcatMapの適用の繰り返しを使って文を構築するもの、2つ目はfoldMを使って文を解釈するものです。 これは読者の演習として残しておきます。

それでは解釈関数を実装して、この章の例を完成させましょう。 lsystemの型が教えてくれているのは、型シグネチャが、何らかの型 as、型構築子 mについて、 s -> a -> m sでなければならないということです。 aLettersState、モナド mEffectというように選びたいということがわかっています。 これにより次のような型になります。

    interpret :: State -> Letter -> Effect State

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

    interpret state L = pure $ state { theta = state.theta - Number.tau / 6.0 }
    interpret state R = pure $ state { theta = state.theta + Number.tau / 6.0 }

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

    interpret state F = do
      let x = state.x + Number.cos state.theta * 1.5
          y = state.y + Number.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-Systemを描画するには、次のようなstrokePath動作を使用するだけです。

  strokePath ctx $ lsystem initial productions interpret 5 initialState

次のコマンドを使ってL-Systemをコンパイルします。

$ spago bundle-app --main Example.LSystem --to dist/Main.js

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

演習

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

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

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

  4. (普通)setShadowOffsetXsetShadowOffsetYsetShadowBlursetShadowColor動作を使い、塗りつぶされた図形にドロップシャドウを追加してください。 手掛かり:PSCiを使って、これらの関数の型を調べてみましょう。

  5. (普通)向きを変えるときの角度の大きさは今のところ一定 \( tau / 6 \) です。 これに代えて、Letterデータ型の中に角度を移動させ、生成規則によって変更できるようにしてください。

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

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

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

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

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

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

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

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

    data Letter = L | R | F Boolean
    

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

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

まとめ

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

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

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

この手法はdrawingパッケージで採られており、描画前に様々な方法でシーンをデータとして操作できる柔軟性を齎しています。

キャンバスに描画されるゲームの例についてはcookbookの「Behavior」と「Signal」のレシピを見てください。