読者です 読者をやめる 読者になる 読者になる

リアクティブプログラミングの技術を用いてマウスストーカーを実装する

古き良きインターネットアプリケーションであるマウスストーカー*1をリアクティブプログラミングの技術を活用して実装してみるという取り組みをしましたのでご紹介します。リアクティブプログラミングというと主語が大きめですが、ここではbacon.jsを使ってるくらいの意味です。

できたもの

まずは完成したマウスストーカーを紹介します。チェーンのように連なった星がマウスカーソルの軌跡を辿ってついてきます。工夫してうごかすとなかなか綺麗です。下のボタンを押すと実際にこの画面でマウスストーカーを有効にすることができます(requestAnimationFrameに対応したPCブラウザのみ)。いろいろ動かして遊んでみてください。

 f:id:hakobe932:20150308230637g:plain:w300





実装

このマウスストーカーがどのように実装されているか紹介します。ソースコードは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を作る

f:id:hakobe932:20150308235518g:plain:w300

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メソッドによって作られた新しいEventStreamStalkerオブジェクトの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));

f:id:hakobe932:20150309003632g:plain:w300

うまくいきました。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を使うことでskipDuplicatesdelayのような便利なメソッドも利用出来ました。

マウスストーカーがリアクティブプログラミングを学ぶ最適な課題というわけではないようには思いますが、今回は変化するイベントをオブジェクトとして扱い組み合わせられることの便利さを少しは感じられたと思います。

おまけ: http://mouse-stalkers.github.io/への寄稿を募集しています

マウスストーカーの実装を集める取り組みを始めました。みなさん何かしらのマウスストーカーを実装されたことがあるかと思います。おもしろいマウスストーカーが埋もれてしまうのは残念なので、ぜひmouse-stalkers.github.ioにPullRequestしてください。

*1:マウスカーソルの後ろに星がくっついてきたりするヤツ

*2:そしてアニメーションしているところのコードは雑です