uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Elmでやってみるシリーズ16: マウスストーカーを実装する

リアクティブプログラミングの技術を用いてマウスストーカーを実装する - はこべブログ ♨」という記事があり、興味深いのでElmのリアクティブプログラミングで似たようなことをやってみました。

全画面表示はこちらから。

コードは以下で、プロジェクト全体はこちらにあります。

import Text
import Window
import Time
import Mouse
import List
import Signal
import Graphics.Element(..)
import Graphics.Collage(..)
import Color(..)
import Signal(Signal,(<~),(~))
import AnimationFrame

-- マウスの座標をCollageの座標に変換するいつもの関数
mousePos : Int -> Int -> Int -> Int -> (Float, Float)
mousePos x y w h = (toFloat x-(toFloat w/2)
                   , -(toFloat y)+(toFloat h/2))

-- ★の表示
star : Int-> Int -> (Int, Int) -> Form
star w h (x, y) = Text.fromString "★"
                        |> Text.color orange
                        |> Text.centered
                        |> toForm
                        |> move (mousePos x y w h)

-- ビューの定義
view : List (Int, Int) -> (Int, Int) -> Element
view posList (w,h) = collage w h (List.map (star w h) posList)

-- ★の座標のリストのSignalを作る
stars : Signal(List (Int, Int))
stars = let 
          trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成
          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = trace p1 -- 以降、一個前の座標を追うようにする
          p3 = trace p2
          p4 = trace p3
          p5 = trace p4
          p6 = trace p5
          p7 = trace p6
          p8 = trace p7
          p9 = trace p8
          p10 = trace p9
          p11 = trace p10
          p12 = trace p11
          p13 = trace p12
          p14 = trace p13
        in (\a b c d e f g h i j k l m n -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n])
         <~ p1 ~ p2 ~ p3 ~ p4 ~ p5 ~ p6 ~ p7 ~ p8 ~ p9 ~ p10 ~ p11 ~ p12 ~ p13 ~ p14

-- 表示関数viewをliftして★の座標をあてがう
main : Signal Element
main = view <~ stars ~ Window.dimensions

非常に簡潔です。

説明と注意点など

  • Mouse.positionおよびTime.delayが、シグナル(beacon.jsでいうEventStreamに相当)を生成しています。ちなみに次期Elm 0.15では、Signalは名称変更(Stream/Varyingに分割)が検討されているようです
  • 上記コードではマウス位置のサンプリング間隔を効率化するために AnimationFrameというコミュニティパッケージを使用しています。なので他のパッケージを使用する機能がないtry-elmshare-elmでは実行できません。sampleOnをfps 60とかですれば、もしくはsampleOnを使用せず直接Mouse.positionを使用すれば、AnimationFrameへの依存は除去できます。
  • 制限としては、ElmのCanvasベースのAPIであるGraphics.Collageを用いているため、★がifameの枠外まで追随することはありません。Nativeやportを用いてJSと連携すれば、真のマウスストーカーができるかもしれませんが試してはおりません。ちなみに次期版0.15ではJSとの連携能力が大きく変わるようです。
  • 上記コードでp1〜p14を列挙していますが、例えばmapを使用してリストを生成していないのは、Signalは動的に生成できないからです。Elmでは、すべてのSignalの値の依存関係に対応する「Signalグラフ」というものがコンパイル時に静的に決定されます。そのこともあり、不定件数の「Signalのリスト(List Signal a)」というものは作成できたとしても、(Signal (List a))に変換できないのです。sequence :: [m a] -> m [a] が欲しいところですが、そういう関数はなく、定義することも型制約*1で意図的に禁止されています。

余談

もうすぐ出るらしいElm 0.15は、非常に楽しみです。

参考リンク

Elmシリーズの他のエントリはこちら

*1:Signalはアプリカティブでモナドではない。