2012/05/16更新

[node] socket.IOとHTML5 Canvasを用いた手書きチャットアプリを作ってみた

このエントリーをはてなブックマークに追加      

node.jsとsocket.IOとHTML5のCanvasを用いて、 手書きチャットアプリを作ってみました。
socket.ioを用いるとリアルタイムWebが実現でき、Canvasのお絵描き機能と合わせることで、 Canvasでの描画を自分のブラウザと他人のブラウザとで、 同じ内容をリアルタイムで描画出来るサンプルアプリです。
今日は、その機能と実装内容を紹介したいと思います。

Socket.IO and Canvas Sample Apps




手書きチャットアプリの機能

手書きチャットアプリでは、自分のブラウザ上のCanvasに手書きで文字を描くと、 リアルタイムに接続している他のユーザーの画面にも描画されるという動きをします。
YouTubeに動画を載せてみました。動画上では、同一PC上での動きですが、それぞれのブラウザは 独立して動いている状態で、ローカルマシンにnode.jsのサーバーがある環境となります。

表示されない場合にはこちらから→http://youtu.be/aJam4zco3YQ

最初にnode.jsを起動して、色々なブラウザからnodeサーバーにつないで、お絵描きをします。
お絵描きをした内容は、接続中の他全てのブラウザに連携され、リアルタイムに描画されます。




実装内容の紹介

続いて、実装内容を紹介します。実装は、サーバー用のファイル1つと、 クライアント側のファイル1つの計2つです。
それぞれの中身もそれほどコード量は多くなく、実装は簡単な感じです。

サーバーサイドの実装

サーバーサイドでは、node.jsでのWebアプリケーションサーバー構築と、 socket.IOを用いたWebSocketプロトコルでの通信を実装しています。
// 必要なモジュールを読み込みます。
var http = require("http");
var socketIO = require("socket.io");
var fs = require("fs");

// node.jsでWebServerを作ります。
// アクセスされたら、クライアントに表示するsyncCanvas.htmlを返します。
var server = http.createServer(function (req, res) {
res.writeHead(200, {"Content-Type":"text/html"});
var output = fs.readFileSync("./syncCanvas.html", "utf-8");
res.end(output);
});
server.listen(8080);

// socket.IOを用いたリアルタイムWebを実装します。
var io = socketIO.listen(server);

// 接続されたら、connected!とコンソールにメッセージを表示します。
io.sockets.on("connection", function (socket) {
  console.log("connected");

  // 描画情報がクライアントから渡されたら、接続中の他ユーザーへ
  // broadcastで描画情報を送ります。
  // ちなみに、最近のsocket.IOでは、イベント名(以下だとdraw)は
  // 自由にネーミング出来るようになったようです。便利!!
  socket.on("draw", function (data) {
    console.log(data);
    socket.broadcast.emit("draw", data);
  });

  // 色変更情報がクライアントからきたら、
  // 他ユーザーへ変更後の色を通知します。
  socket.on("color", function (color) {
    console.log(color);
    socket.broadcast.emit("color", color);
  });

  // 線の太さの変更情報がクライアントからきたら、
  // 他ユーザーへ変更後の線の太さを通知します。
  socket.on("lineWidth", function (width) {
    console.log(width);
    socket.broadcast.emit("lineWidth", width);
  });
});



クライアント側の実装

クライアント側の実装のメインは、JavaScriptです。 Canvasへの描画処理と、socket.IOを用いる処理が記載されているので、 サーバーサイドよりもコード量は多くなっています。
<html>
<head>
  <meta charset="utf-8">
  <title>Sync Canvas</title>
  <style type="text/css">
    * {margin:0px; padding:0px;}
    body {width:500px;}
    .title {padding:10px;}
    .main {padding-left:10px; margin:auto;}
    .toolbar {}
    .toolbar li {width:44px; height:44px; margin:auto;list-style-type: none; border:1px solid #ccc; border-radius:6px; margin:10px; display:block; float:left;}
    #black {background-color:black;}
    #red {background-color:red;}
    #green {background-color:green;}
    #blue {background-color:blue;}
    #small {text-align:center; line-height:44px; font-size:100%;}
    #middle {text-align:center; line-height:44px; font-size:200%;}
    #large {text-align:center; line-height:44px; font-size:400%;}
    .canvas {width:450px; height:400px; border:1px solid #ccc;}
    .canvas canvas {width:450px; height:400px;}
  </style>
</head>
<body>
  <!-- Header -->
  <h1 class="title">Sync Canvas</h1>

  <!-- Main -->
  <div class="main">
    <div class="toolbar">
      <ul>
        <li id="black"> </li>
        <li id="blue"></li>
        <li id="red"></li>
        <li id="green"></li>
        <li id="small">S</li>
        <li id="middle">M</li>
        <li id="large">L</li>
      </ul>
    </div>
    <div style="clear:left;"></div>
    <div class="canvas"><canvas id="myCanvas"></canvas></div>
  </div>

  <!-- ここでsocket.IOのライブラリを読み込む --> 
  <script src="/socket.io/socket.io.js"></script>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
  <script type="text/javascript">
    window.addEventListener("load", function () {

      // サーバーサイドのsocket.IOに接続する
      // 接続出来たら、サーバー側のコンソールにconnected!と表示される
      var socket = io.connect("/");

      // Canvas描画に必要な変数を定義する
      var canvas = document.getElementById("myCanvas");
      var c = canvas.getContext("2d");
      var w = 450;
      var h = 400;
      var drawing = false;
      var oldPos;

      // Canvasを初期化する
      canvas.width = w;
      canvas.height = h;
      c.strokeStyle = "#000000";
      c.lineWidth = 5;
      c.lineJoin = "round";
      c.lineCap = "round";

      // Canvas上の座標を計算する為の関数たち
      function scrollX(){
        return document.documentElement.scrollLeft || document.body.scrollLeft;
      }
      function scrollY(){
        return document.documentElement.scrollTop || document.body.scrollTop;
      }
      function getPos (event) {
        var mouseX = event.clientX - $(canvas).position().left + scrollX();
        var mouseY = event.clientY - $(canvas).position().top + scrollY();
        return {x:mouseX, y:mouseY};
      }
      function getPosT (event) {
        var mouseX = event.touches[0].clientX - $(canvas).position().left + scrollX();
        var mouseY = event.touches[0].clientY - $(canvas).position().top + scrollY();
        return {x:mouseX, y:mouseY};
      }
                                   
      // ここからは、Canvasに描画する為の処理                             
      canvas.addEventListener("mousedown", function (event) {
        console.log("mousedown");
        drawing = true;
        oldPos = getPos(event);
      }, false);
      canvas.addEventListener("mouseup", function () {
        console.log("mouseup");
        drawing = false;
      }, false);
      canvas.addEventListener("mousemove", function (event) {
        var pos = getPos(event);
        console.log("mousemove : x=" + pos.x + ", y=" + pos.y + ", drawing=" + drawing);
        if (drawing) {
          c.beginPath();
          c.moveTo(oldPos.x, oldPos.y);
          c.lineTo(pos.x, pos.y);
          c.stroke();
          c.closePath();
         
          // socket.IOサーバーに、
          // どの点からどの点までを描画するかをの情報を送付する
          socket.emit("draw", {before:oldPos, after:pos});
          oldPos = pos;
        }
      }, false);
      canvas.addEventListener("mouseout", function () {
        console.log("mouseout");
        drawing = false;
      }, false);
     
      // 色や太さを選択した場合の処理
      // 選択した結果を、Canvasに設定して、
      // socket.IOサーバーにも送付している
      $("#black").click(function () {c.strokeStyle = "black";socket.emit("color", "black");});
      $("#blue").click(function () {c.strokeStyle = "blue";socket.emit("color", "blue");});
      $("#red").click(function () {c.strokeStyle = "red";socket.emit("color", "red");});
      $("#green").click(function () {c.strokeStyle = "green";socket.emit("color", "green");});
      $("#small").click(function () {c.lineWidth = 5;socket.emit("lineWidth", 5);});
      $("#middle").click(function () {c.lineWidth = 10;socket.emit("lineWidth", 10);});
      $("#large").click(function () {c.lineWidth = 20;socket.emit("lineWidth", 20);});
     
      // socket.IOサーバーから描画情報を受け取った場合の処理
      // 受け取った情報を元に、Canvasに描画を行う
      socket.on("draw", function (data) {
        console.log("on draw : " + data);
        c.beginPath();
        c.moveTo(data.before.x, data.before.y);
        c.lineTo(data.after.x, data.after.y);
        c.stroke();
        c.closePath();
      });
     
      // socket.IOサーバーから色情報を受け取った場合の処理
      // Canvasに色を設定している
      socket.on("color", function (data) {
        console.log("on color : " + data);
        c.strokeStyle = data;
      });

           
      // socket.IOサーバーから線の太さ情報を受け取った場合の処理
      // Canvasに線の太さを設定している
      socket.on("lineWidth", function (data) {
        console.log("on lineWidth : " + data);
        c.lineWidth = data;
      });

                               
    }, false);         
  </script>
</body>
</html>


サンプルアプリなので、色々なところでコンソールにデバッグ情報を出漁しています。
また、今回は描画情報を逐次送っていますが、パフォーマンスチューニングの観点から、ある程度纏めてから 送るのも良いかもしれません。 (というか、サーバーがローカル以外なら、通信時間が馬鹿にならないので、チューニングは必須となりそうです)




最後に

本当はこのデモアプリを触れる状態にしようと、Amazon EC2上に乗せようと思ったのですが、 起動しないというエラーが。。
socket.IOは面白い技術ですね。もっと色々と使ってみて、試してみたいと思います。
最後までお読み頂きまして、ありがとうございました。






こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。