古き良きインターネットアプリケーションであるマウスストーカー*1をリアクティブプログラミングの技術を活用して実装してみるという取り組みをしましたのでご紹介します。リアクティブプログラミングというと主語が大きめですが、ここではbacon.jsを使ってるくらいの意味です。
できたもの
まずは完成したマウスストーカーを紹介します。チェーンのように連なった星がマウスカーソルの軌跡を辿ってついてきます。工夫してうごかすとなかなか綺麗です。下のボタンを押すと実際にこの画面でマウスストーカーを有効にすることができます(requestAnimationFrame
に対応したPCブラウザのみ)。いろいろ動かして遊んでみてください。
実装
このマウスストーカーがどのように実装されているか紹介します。ソースコードはGitHubに公開していますので、適宜ご参照ください。手元でビルドして試す場合には、READMEの通りにビルドして、example.html
を表示してみてください。
では順番に見ていきます。
マウスカーソルの位置の変化をEventStreamに変換する
まずは刻々と変化するマウスカーソルの位置変化を、取り扱いやすいようにbacon.jsのEventStream
に変換しましょう。document
要素に対して発生したmousemove
イベントをEventStream
に変換するには以下のようにします。
var mouseCursorStream = Bacon.fromEventTarget(document, 'mousemove').map(function(me) { return { x: me.pageX, y: me.pageY }; });
これでマウスカーソルの変化を1つのオブジェクトとして関数に渡したりすることができるようになりました。(pageX/pageYは標準化されていないフィールドですが、polyfillのためのコードを書くことは本題ではないので、ここでは雑に使っています。)
刻々と変化する位置を目指して動くオブジェクトを作る
次はマウスカーソル追いかける星に相当するStalker
オブジェクトを作ります。
このオブジェクトは初期化時に与えられたEventStream
によって表される刻々と変化する位置に向かって、星の画像を移動させます。例えば次のようにStalker
オブジェクトを作ると、マウスカーソルを1つの星が追いかけます。
new Stalker(mouseCursorStream); // マウスカーソルを目指して動くStalkerを作る
Stalker
オブジェクトの中では目指す位置のEventStream
( = targetStream
)の変化があるごとに、自分の現在の目的地を更新します。
function Stalker(targetStream) { // ... var targetPosition = {x: 0, y:0}; targetStream.onValue(function(p) { targetPosition.x = p.x; targetPosition.y = p.y; }); // ... }
ここで出てきたtargetPosition
を目指して星を一定速度で移動させるアニメーションのコードは、以下のような雰囲気なりますが、詳細は省略します*2。
// Stalker のコンストラクタ内にて // animationFrameごとにStalkerのpositionを更新する animationFrame.onValue(function() { position = next(); elem.style.left = position.x + 'px'; elem.style.top = position.y + 'px'; } });
刻々と変化するStalkerの位置をEventStreamにする
ここまでで星を1つ追いかけさせることはできるようになりましたが、あまりおもしろくありませんし、まったくリアクティブ感がありません。そこで少し工夫してみます。
Stalker
オブジェクトはtargetPosition
を目指して一定速度で移動します。この移動中のStalker
オブジェクト自身の位置をEventStream
として取り扱えるようにしてみます。
bacon.jsのBus
というオブジェクトを使うと自分で値をpush
できるようなEventStream
を作る事ができます。Stalker
が星の位置を更新する箇所でBus
として作ったEventStream
に値をpushします。pushされた値が最後にpushされた値と同じ場合は無視したいので、skipDuplicates
メソッドによって作られた新しいEventStream
をStalker
オブジェクトのpositionStream
フィールドにセットしておきます。
// Stalker のコンストラクタ内にて // animationFrameごとにStalkerのpositionを更新する var ps = new Bacon.Bus(); this.positionStream = ps.skipDuplicates(); // Busに同じ値がpushされても無視する animationFrame.onValue(function() { position = next(); ps.push(position); // 位置の変化をStreamにpushする elem.style.left = position.x + 'px'; elem.style.top = position.y + 'px'; });
これにより、移動中のStalker
の位置もmouseCursorStream
と同じ形式のEventStream
として取り扱えるようになりました。つまり、Stalker
はマウスカーソルだけではなく別のStalker
も追いかけられるようになりました。
Stalker をつなげる
お膳立ては済んだのでStalker
オブジェクトをたくさん作ってもう少しおもしろいマウスストーカーを作れます。
例えば、1つ目のStalker
がマウスカーソルを追いかけるようにし、3つ目のStalker
が1つ目のStalker
を、3つ目のStalker
が2つ目のStalker
を追いかけるようにしてみましょう。
var one = new Stalker(mouseCursorStream); var two = new Stalker(one.positionStream.delay(100)); var three = new Stalker(two.positionStream.delay(100));
うまくいきました。positionStream
で発生したイベントを100msほどdelay
しているのがポイントです。positionStream
の更新は頻繁に行っているため、delayしないと3つの星がほぼ同じ位置に表示されて面白みがありません。
はじめに紹介したマウスストーカーでは以下のように30個の星を鎖のようにつなげて、自分より前にならんだ星を追いかけるように設定しています。
var cur = new Stalker(mouseCursorStream); for (var i = 0; i < 30; i++) { cur = new Stalker(cur.positionStream.delay(100)); }
感想
リアクティブプログラミングの技術を活用したマウスストーカーを実装してみました。刻々と変化する位置の変化をEventStream
のオブジェクトとみなすことで、Stalker
を刻々と変わる目的地( = targetStream
)を目指して動くオブジェクトである、というすなおでわかりやすいモデルにすることができました。
例えば、EventStream
に頼らずにmousemove
イベントのハンドラで目的の位置を更新するコードをStalker
の外に実装することもできるでしょう。
var s = new Stalker(mouseCursorStream); document.addEventListener('mousemove', function(e) { s.updateCurrentTargetPosition({x: e.pageX, y: e:pageY}); });
ですが、目的の位置が変化した時にどのような反応をするかはStalker
である限りはそれほど変わりがない(現在の目的位置を更新する)と考えられるので、コードの重複や情報隠蔽の観点からStalker
内に実装されているのが良さそうに思います。かといって、mousemove
のハンドラの登録を直接Stalker
内に実装すれば、マウスカーソルではない別のStalker
の位置からイベントを受け取って星を連ねる、といった柔軟性を実現できなかったでしょう。
EventStream
という抽象化は大変便利です。EventStream
を使うことでskipDuplicates
やdelay
のような便利なメソッドも利用出来ました。
マウスストーカーがリアクティブプログラミングを学ぶ最適な課題というわけではないようには思いますが、今回は変化するイベントをオブジェクトとして扱い組み合わせられることの便利さを少しは感じられたと思います。
おまけ: http://mouse-stalkers.github.io/への寄稿を募集しています
マウスストーカーの実装を集める取り組みを始めました。みなさん何かしらのマウスストーカーを実装されたことがあるかと思います。おもしろいマウスストーカーが埋もれてしまうのは残念なので、ぜひmouse-stalkers.github.ioにPullRequestしてください。