Collaborative Whiteboard Tutorial, Part Two - Sun, Feb 15, 2015
Part one of this tutorial focused on the description of the project and the server-side code. In this part, we write the client side code that is supposed to handle the drawing operations on an HTML canvas
element.
As previously discussed, our web service is supposed to handle multiple separate whiteboards (which we call rooms). Therefore the client needs to know which room it is working on. To specify this, we pass the room name via a query string parameter, room. This parameter can then be read using a simple function (taken from the web via a quick Google search) that extracts the value of a parameter in the query string:
function get_param(name) {
var url = location.href;
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(url);
return results == null ? null : results[1];
}
The client side needs to store variables such as the room name, brush color, brush radius, and other objects in the memory for later use:
var ROOM = "0";
var COLOR = "black";
var RADIUS = 5;
var socket, canvas, context;
The layout of the app is fairly basic. It is made out of a full screen canvas with virtual dimensions of 1000×1000, a button for sending a clear signal, and a few buttons to change the brush color:
<canvas id="canvas" width="1000" height="1000"></canvas>
<div id="panel_tools">
<center>
<button class="action_button" onclick="clear_room();">Wipe</button>
<button class="color_button" style="background-color:black" onclick="change_color('black');"> </button>
<button class="color_button" style="background-color:white" onclick="change_color('white');"> </button>
<button class="color_button" style="background-color:green" onclick="change_color('green');"> </button>
<button class="color_button" style="background-color:red" onclick="change_color('red');"> </button>
<button class="color_button" style="background-color:blue" onclick="change_color('blue');"> </button>
<button class="color_button" style="background-color:purple" onclick="change_color('purple');"> </button>
<button class="color_button" style="background-color:orange" onclick="change_color('orange');"> </button>
</center>
</div>
The color buttons simply call the change_color
function with the specified color, which sets the brush color to the chosen one:
function change_color(color)
{
COLOR = color;
}
The wipe
button calls the clear_room
function which sends a clear signal to the server:
function clear_room() {
socket.emit("message", {
"room": ROOM,
"command": "clear"
});
}
As a response to the clear
command, the server also sends its own clear
command to all clients, telling them to wipe their canvas if they belong to this room:
function get_clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
The first time the page is loaded, the client sends the server a join
command, resulting in the server sending the client a snapshot of the room. This message is handled by the get_room
function, which iterates through the room data and draws everything in the room:
function get_room(room_data)
{
room_data.forEach(function (e, i) {
get_draw(e);
});
}
Also whenever someone draws, the server sends the clients the information of that brush. So to summarize, here are the commands that are handled by the clients:
if (msg["command"] == "draw") get_draw(msg["data"]);
if (msg["command"] == "clear") get_clear();
if (msg["command"] == "send_room") get_room(msg["data"]);
The get_draw
function is responsible for drawing on the canvas. It assumes that the drawing data contains information such as the position of the object (in percentages, i.e. between 0.0 and 1.0), the radius of the brush, its color and the shape. Currently we only support circle for the shape:
function get_draw(draw_data)
{
if (draw_data["shape"] == "circle")
{
draw_data["x"] = (draw_data["x"] / 1000.0) * canvas.width;
draw_data["y"] = (draw_data["y"] / 1000.0) * canvas.height;
context.beginPath();
context.arc(draw_data["x"], draw_data["y"], draw_data["radius"], 0, 2 * Math.PI, false);
context.fillStyle = draw_data["color"];
context.fill();
}
}
Now we need to map the mouse event handlers to functions that will allow the client to send draw messages to the server. The user can enable draw mode by clicking the mouse button, and disable it by releasing the mouse button. Draw signals containing brush data and click position in percentages are sent to the server on mouse move:
var draw_enabled = false;
function mouse_paint(event)
{
if (!draw_enabled) return;
var rect = canvas.getBoundingClientRect();
var x = event.x - rect.left;
var y = event.y - rect.top;
socket.emit("message", {
"room": ROOM,
"command": "draw",
"data": {
"shape": "circle",
"x": 1000.0 * (x / rect.width),
"y": 1000.0 * (y / rect.height),
"radius": RADIUS,
"color": COLOR
},
});
}
function mouse_down(event)
{
draw_enabled = true;
mouse_paint(event);
}
function mouse_move(event)
{
mouse_paint(event);
}
function mouse_up(event)
{
draw_enabled = false;
}
The last step is to initialize everything and send a join
message to begin the session:
function initialize() {
ROOM = get_param("room");
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.addEventListener("mousedown", mouse_down, false);
canvas.addEventListener("mousemove", mouse_move, false);
canvas.addEventListener("mouseup", mouse_up, false);
socket = io();
socket.on("servermessage", handle_message);
socket.emit("message", {
"room": ROOM,
"command": "join"
});
}
window.addEventListener("load", initialize);