kagamihogeの日記

kagamihogeの日記です。

Play FrameworkのWebSocket + html5 canvasで共有ホワイトボード


お絵かきチャット的なアレ。ブラウザでテキトーに何か描くと、別のブラウザにも何かが描画されるヤツ。

やったこと

conf/routesに新しいパス追加する。

GET     /sharedwhiteboardindex      controllers.Application.sharedwhiteboardindex()
GET     /sharedwhiteboardchannel    controllers.Application.sharedwhiteboardchannel()

views/sharedwhiteboard.scala.htmlを新規作成する。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>title</title>
<script src="@routes.Assets.at("javascripts/jquery-2.0.2.min.js")" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
    $(function() {
        var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket;
        var socket = new WS("@routes.Application.sharedwhiteboardchannel().webSocketURL(request)");
        socket.onmessage = function(event) {
            var data = JSON.parse(event.data);
            drawWhiteboard(data.x, data.y, "#CC0000");
        }

        var isMousedrag = false;
        $("#whiteboardcanvas").mousedown(function(event) {
            isMousedrag = true;
        });
        $("#whiteboardcanvas").mousemove(function(e) {
            if (!isMousedrag) {
                return;
            }
            var rect = e.target.getBoundingClientRect();
            mouseX = e.clientX - rect.left;
            mouseY = e.clientY - rect.top;

            drawWhiteboard(mouseX, mouseY, "#000000");

            socket.send(JSON.stringify({
                x : mouseX,
                y : mouseY
            }));
        });

        function drawWhiteboard(x, y, fillStyle) {
            var c = document.getElementById('whiteboardcanvas');
            var boardctx = c.getContext('2d');
            boardctx.beginPath();
            boardctx.fillStyle = fillStyle;
            boardctx.arc(x, y, 3, 0, Math.PI * 2, false);
            boardctx.fill();
        }

        $("#whiteboardcanvas").mouseup(function(event) {
            isMousedrag = false;
        });
        $("#whiteboardcanvas").mouseout(function(event) {
            isMousedrag = false;
        });
    })
</script>
</head>
<body>
    <canvas id="whiteboardcanvas" width="1400" height="140"></canvas>
</body>
</html>

正直html5もcanvaもさわるの初めての人間のためテキトーにやったんで、その辺はご容赦願いたく。

isMousedragて変数は、マウスがcanvas上をドラッグ中のイベントハンドリングのやり方がわからなかったんで、コレで逃げた。

で、mousemoveで描画したらその座標をソケットに送信する。onmessageでは、ソケットからやってきた座標を描画する。判別が付くように、自分で描いたものは黒、サーバからやってきたものは赤で塗る。

controllers/Application.java

public class Application extends Controller {
    public static Result sharedwhiteboardindex() {
        return ok(sharedwhiteboard.render());
    }
    
    public static List<WebSocket.Out<JsonNode>> channels = new ArrayList<>();
    public static WebSocket<JsonNode> sharedwhiteboardchannel() {
        return new WebSocket<JsonNode>() {
            @Override
            public void onReady(WebSocket.In<JsonNode> in, WebSocket.Out<JsonNode> out) {
                try {
                    channels.add(out);
                    in.onMessage(new SharedwhiteboardCallback(out));
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };
    }
    
    public static class SharedwhiteboardCallback implements Callback<JsonNode> {
        private WebSocket.Out<JsonNode> out;
        
        public SharedwhiteboardCallback(Out<JsonNode> out) {
            this.out = out;
        }

        @Override
        public void invoke(JsonNode event) throws Throwable {
            ObjectNode echo = Json.newObject();
            echo.put("x", event.get("x").asInt());
            echo.put("y", event.get("y").asInt());
            
            for (WebSocket.Out<JsonNode> channel : channels) {
                if (out == channel) {
                    continue;
                }
                channel.write(echo);
            }
        }
    }
}

コレで動いたけど、play frameworksのWebSocketはこうやって使うものかどーか確証が無い。

onReadyでWebSocketが確立するたびに、WebSocket.Outをコレクションに保持する。コレクションから削除してるコードは……知らんな。

んで、コールバックのinvokeでは描画対象の座標が送られてくるんで、すべてのWebSocket.Out(ただし送り元はのぞく)に対して座標をサーバーから送ってやる。

playのチャットサンプルはactor使ってるんで、何かしらそういう工夫しないとパフォーマンスはアレになるんだと思われる。