ちょっと知り合いの方に、アニメーションや操作の同期とかできないかと問われた時、まあ、WebSocket使えばいいんじゃない、とか気軽に言ったのですが。。。
実際に実証しなければ、よろしくないなーと思いましたので、一人ハッカソンしてみました。
2h位あればできると思います。
アニメーションと言ってもつまるところ、操作の同期です。
結果
https://github.com/niisan-tokyo/scratches/tree/master/nodejs/synchronous
こいつをおとして、npm install
を行い、
node app.js
でサーバを起動させることで、結果の動作を確認できます。
概要
アニメーションを同期すると言っても、描画処理丸々を同期するのではなく、フロントもしくはアプリに描画に必要な情報がほとんど整っていて、少数のパラメータを使うだけで、表示状態が同期できる、というものを目指しています。
例えば、授業などで物理運動のシミュレーションを各学生のPC上で共有しながら説明する、ということができるかどうかを確かめたいのです。
では、サンプルアプリの概要を見てみます
- 正方形をcanvasに描く
- 正方形はカーソルキーを押すと、速度が変化する
- 正方形の運動状態を他のユーザーに共有する
こんな感じです
フロントの作成
canvas上での正方形作成
canvas上で正方形を作るのですが、今回は一つのオブジェクトに全部管理させちゃおうと思います。
<script>
'use strict'
const canvas = document.getElementById('recta_anime');
//アニメーション対象
const rect = {
x: 0,
y: 0,
maxx: canvas.width,
maxy: canvas.height,
...
// (以下略)
</script>
このオブジェクトrect
は、次のパラメータを持っています。
- 左上の頂点の座標 $\textbf{r} = (x, y)$
- 速度 $\textbf{v} = (v_x, v_y)$
- 縦・横の長さ
- アニメーション再描画時間( 再描画されるまでの時間 )
- 同期周期( リモートの状態を取り入れる周期 )
正方形の描画、再描画はrect.redraw
メソッドで行います
const rect = {
//...
ctx: canvas.getContext('2d'),
redraw: () => {
// 図形削除
rect.ctx.clearRect(0, 0, rect.maxx, rect.maxy);
//図形描画
rect.ctx.beginPath();
rect.ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
},
//...
}
一旦canvasのclearRect
でcanvas内全体を消去してから、描画を行います。
今回はfillRect
を使っているので、黒塗りの正方形が現れることになります。
正方形の動作
正方形の変量は、位置座標 $\textbf{r}$ と速度 $\textbf{v}$ だけです。
また、再描画の際の変位は、時間間隔$\Delta t$に対して、 $\Delta \textbf{r} = \textbf{v} \Delta t$となります。
これを体現しているメソッドが、rect.update
になります。
const rect = {
//...
update: () => {
let base = rect.frame / 1000.0;
rect.x = rect.x + rect.vx * base;
rect.y = rect.y + rect.vy * base;
if (rect.x > rect.maxx - rect.width) {
rect.x = rect.maxx - rect.width;
rect.vx = 0;
}
//...
}
壁に衝突すると、そこから先には進めないので、$\textbf{v}$の衝突した壁方向の成分は0になります(左右の壁に衝突したら、$v_x = 0$)。
正方形の加速はカーソルキーで行います。
例えば、単純に▶ボタンをおすと、右方向に1だけ加速します( $v_x$ += 1 )
keydown
イベントの発生に伴い、rect.accel
メソッドが呼ばれて、処理を行います。
これでとりあえず、カーソルキーで速度をいじれる正方形の描画アニメーションが完成しました。

こんな感じの黒塗りの正方形が現れると思います。
この時点で、カーソルキーを使って動かすことは可能です。
状態の同期
次に、状態を同期してみましょう。
状態の同期とは、ある一人の操作状況を他の全員に適用することを指します。
仮に、適用する側をmasterとよび、適用される側をslaveと呼ぶことにします。
状態の同期は以下のようにして行われます。
- masterからWebSocketを通じてサーバに状態を表すメッセージが送信される
- サーバは、slaveに向けてmasterからのメッセージを、WebSocketを通じて送信する
- slaveはWebSocketを通じて送信されたメッセージから状態を復元し、自身の状態に適用する
- masterとslaveの状態が同じになる
この同期処理を一定時間ごとに繰り返します。
さきほど、正方形の変量は、位置情報と速度情報だけだと述べました。
つまり、これらの情報さえ共有できれば、masterの状態とslaveの状態を同期させることができるということです。
フロントの処理
フロントはサイトに接続したら、WebSocketのコネクションを作ります。
//...
<script>
//...
const ws = new WebSocket('ws://192.168.33.10:8888/');
ws.onmessage = event => {
const str = event.data;
// 一番初めに接続した場合はmasterとなり、同期元となる
if (str === 'master') {
setInterval(() => {
ws.send(rect.packData());
}, rect.syncTime);
return true;
}
// masterでなければ、データを受け取って描画を更新する
const data = JSON.parse(str);
rect.x = data.x;
rect.y = data.y;
rect.vx = data.vx;
rect.vy = data.vy;
rect.update();
}
</script>
サーバからくるメッセージは2種類あり、一つは「あなたはmasterだ」というものです。
このメッセージが届いたら、自身がmasterであることを自覚するので、定期的に自分の状態をサーバに送るようになります。
送信するのは位置情報と速度情報をJSON文字列にしたもので、rect.packData
メソッドを用いて作成されます。
もう一つのメッセージはJSON化されたmasterの状態情報になります。
これが届いた場合は、自身は同期される側であるため、即座に届いた状態を使って自身を更新し、masterと同じ状態になります。
この時点でほぼ明らかですが、サーバはmasterの送信した情報を、そのままslaveに流すだけの中継点という形になります。
サーバの処理
WebSocketサーバの構築にはwebsocket.ioパッケージを使用しました
$ npm install websocket.io
サーバの役割は2つ有り、一つはだれがmasterなのか指定すること、もう一つがメッセージを中継することです。
今回はmasterがない状態で接続したユーザをmasterにするという単純なものにしました。
本来的には部屋の作成やログインとか権限で管理するのがいいのですが、事始めなので、これで十分でしょう
これらを踏まえたコードは、以下のとおりです。
const ws = require('websocket.io');
const wserver = ws.listen(8888);
wserver.on('connection', socket => {
// masterが存在しなければ、コネクション成立したものをmasterにする
if (master === null) {
master = socket;
socket.send('master');
}
socket.on('message', data => {
if (socket != master) return false;// master以外が情報を送信する権限はない
wserver.clients.forEach(client => {
if (client != master) client.send(data);// master以外の運動状態を送信
});
});
socket.on('close', () => {
if (socket === master) {
master = null;
}
});
});
ちなみに、masterのユーザとの接続が切れた場合、masterを空き状態にしています。
つまり、その後改めて接続したユーザーが新たなmasterとなります。
実際のapp.js
には、httpサーバも同居させています。
まとめ
WebSocketは少量のデータを断続的に流すときに、非常に便利です。
接続を作り直すコストも少なく、とても経済的です。
nodeのおかげで、この辺の実装がすぐできるようになっているのは、単純にスゴイなぁと思います。
参考資料
Node.jsによる必要最小限のhttpサーバとhttpsサーバとhttp proxyサーバ
Canvasアニメーションの要点
websocket.io
Canvasとは - Canvas - HTML5.JP