blob: 4a4120e60375523098bcc2d004a0cbff598a960f [file] [log] [blame]
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Apache Tomcat WebSocket Examples: Drawboard</title>
<style type="text/css"><![CDATA[
body {
font-family: Arial, sans-serif;
font-size: 11pt;
background-color: #eeeeea;
padding: 10px;
}
#console-container {
float: left;
background-color: #fff;
width: 250px;
}
#console {
font-size: 10pt;
height: 600px;
overflow-y: scroll;
padding-left: 5px;
padding-right: 5px;
}
#console p {
padding: 0;
margin: 0;
}
#drawContainer {
float: left;
display: none;
margin-right: 25px;
}
#drawContainer canvas {
display: block;
-ms-touch-action: none;
touch-action: none; /* Disable touch behaviors, like pan and zoom */
cursor: crosshair;
}
#labelContainer {
margin-bottom: 15px;
}
#drawContainer, #console-container {
box-shadow: 0px 0px 8px 3px #bbb;
border: 1px solid #CCCCCC;
}
]]></style>
<script type="application/javascript"><![CDATA[
"use strict";
(function() {
document.addEventListener("DOMContentLoaded", function() {
// Remove elements with "noscript" class - <noscript> is not
// allowed in XHTML
var noscripts = document.getElementsByClassName("noscript");
for (var i = 0; i < noscripts.length; i++) {
noscripts[i].parentNode.removeChild(noscripts[i]);
}
// Add script for expand content.
var expandElements = document.getElementsByClassName("expand");
for (var ixx = 0; ixx < expandElements.length; ixx++) {
(function(el) {
var expandContent = document.getElementById(el.getAttribute("data-content-id"));
expandContent.style.display = "none";
var arrow = document.createTextNode("◢ ");
var arrowSpan = document.createElement("span");
arrowSpan.appendChild(arrow);
var link = document.createElement("a");
link.setAttribute("href", "#!");
while (el.firstChild != null) {
link.appendChild(el.removeChild(el.firstChild));
}
el.appendChild(arrowSpan);
el.appendChild(link);
var textSpan = document.createElement("span");
textSpan.setAttribute("style", "font-weight: normal;");
textSpan.appendChild(document.createTextNode(" (click to expand)"));
el.appendChild(textSpan);
var visible = true;
var switchExpand = function() {
visible = !visible;
expandContent.style.display = visible ? "block" : "none";
arrowSpan.style.color = visible ? "#000" : "#888";
return false;
};
link.onclick = switchExpand;
switchExpand();
})(expandElements[ixx]);
}
var Console = {};
Console.log = (function() {
var consoleContainer =
document.getElementById("console-container");
var console = document.createElement("div");
console.setAttribute("id", "console");
consoleContainer.appendChild(console);
return function(message) {
var p = document.createElement('p');
p.style.wordWrap = "break-word";
p.appendChild(document.createTextNode(message));
console.appendChild(p);
while (console.childNodes.length > 25) {
console.removeChild(console.firstChild);
}
console.scrollTop = console.scrollHeight;
}
})();
function Room(drawContainer) {
/* A pausable event forwarder that can be used to pause and
* resume handling of events (e.g. when we need to wait
* for a Image's load event before we can process further
* WebSocket messages).
* The object's callFunction(func) should be called from an
* event handler and give the function to handle the event as
* argument.
* Call pauseProcessing() to suspend event forwarding and
* resumeProcessing() to resume it.
*/
function PausableEventForwarder() {
var pauseProcessing = false;
// Queue for buffering functions to be called.
var functionQueue = [];
this.callFunction = function(func) {
// If message processing is paused, we push it
// into the queue - otherwise we process it directly.
if (pauseProcessing) {
functionQueue.push(func);
} else {
func();
}
};
this.pauseProcessing = function() {
pauseProcessing = true;
};
this.resumeProcessing = function() {
pauseProcessing = false;
// Process all queued functions until some handler calls
// pauseProcessing() again.
while (functionQueue.length > 0 && !pauseProcessing) {
var func = functionQueue.pop();
func();
}
};
}
// The WebSocket object.
var socket;
// ID of the timer which sends ping messages.
var pingTimerId;
var isStarted = false;
var playerCount = 0;
// An array of PathIdContainer objects that the server
// did not yet handle.
// They are ordered by id (ascending).
var pathsNotHandled = [];
var nextMsgId = 1;
var canvasDisplay = document.createElement("canvas");
var canvasBackground = document.createElement("canvas");
var canvasServerImage = document.createElement("canvas");
var canvasArray = [canvasDisplay, canvasBackground,
canvasServerImage];
canvasDisplay.addEventListener("mousedown", function(e) {
// Prevent default mouse event to prevent browsers from marking text
// (and Chrome from displaying the "text" cursor).
e.preventDefault();
}, false);
var labelPlayerCount = document.createTextNode("0");
var optionContainer = document.createElement("div");
var canvasDisplayCtx = canvasDisplay.getContext("2d");
var canvasBackgroundCtx = canvasBackground.getContext("2d");
var canvasServerImageCtx = canvasServerImage.getContext("2d");
var canvasMouseMoveHandler;
var canvasMouseDownHandler;
var isActive = false;
var mouseInWindow = false;
var mouseDown = false;
var currentMouseX = 0, currentMouseY = 0;
var currentPreviewPath = null;
var availableColors = [];
var currentColorIndex;
var colorContainers;
var previewTransparency = 0.65;
var availableThicknesses = [2, 3, 6, 10, 16, 28, 50];
var currentThicknessIndex;
var thicknessContainers;
var availableDrawTypes = [
{ name: "Brush", id: 1, continuous: true },
{ name: "Line", id: 2, continuous: false },
{ name: "Rectangle", id: 3, continuous: false },
{ name: "Ellipse", id: 4, continuous: false }
];
var currentDrawTypeIndex;
var drawTypeContainers;
var labelContainer = document.getElementById("labelContainer");
var placeholder = document.createElement("div");
placeholder.appendChild(document.createTextNode("Loading... "));
var progressElem = document.createElement("progress");
placeholder.appendChild(progressElem);
labelContainer.appendChild(placeholder);
function rgb(color) {
return "rgba(" + color[0] + "," + color[1] + ","
+ color[2] + "," + color[3] + ")";
}
function PathIdContainer(path, id) {
this.path = path;
this.id = id;
}
function Path(type, color, thickness, x1, y1, x2, y2, lastInChain) {
this.type = type;
this.color = color;
this.thickness = thickness;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.lastInChain = lastInChain;
function ellipse(ctx, x, y, w, h) {
/* Drawing a ellipse cannot be done directly in a
* CanvasRenderingContext2D - we need to use drawArc()
* in conjunction with scaling the context so that we
* get the needed proportion.
*/
ctx.save();
// Translate and scale the context so that we can draw
// an arc at (0, 0) with a radius of 1.
ctx.translate(x + w / 2, y + h / 2);
ctx.scale(w / 2, h / 2);
ctx.beginPath();
ctx.arc(0, 0, 1, 0, Math.PI * 2, false);
ctx.restore();
}
this.draw = function(ctx) {
ctx.beginPath();
ctx.lineCap = "round";
ctx.lineWidth = thickness;
var style = rgb(color);
ctx.strokeStyle = style;
if (x1 == x2 && y1 == y2) {
// Always draw as arc to meet the behavior
// in Java2D.
ctx.fillStyle = style;
ctx.arc(x1, y1, thickness / 2.0, 0,
Math.PI * 2.0, false);
ctx.fill();
} else {
if (type == 1 || type == 2) {
// Draw a line.
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
} else if (type == 3) {
// Draw a rectangle.
if (x1 == x2 || y1 == y2) {
// Draw as line
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
} else {
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
}
} else if (type == 4) {
// Draw a ellipse.
ellipse(ctx, x1, y1, x2 - x1, y2 - y1);
ctx.closePath();
ctx.stroke();
}
}
};
}
function connect() {
var host = (window.location.protocol == "https:"
? "wss://" : "ws://") + window.location.host
+ "/examples/websocket/drawboard";
socket = new WebSocket(host);
/* Use a pausable event forwarder.
* This is needed when we load an Image object with data
* from a previous message, because we must wait until the
* Image's load event it raised before we can use it (and
* in the meantime the socket.message event could be
* raised).
* Therefore we need this pausable event handler to handle
* e.g. socket.onmessage and socket.onclose.
*/
var eventForwarder = new PausableEventForwarder();
socket.onopen = function () {
// Socket has opened. Now wait for the server to
// send us the initial packet.
Console.log("WebSocket connection opened.");
// Set up a timer for pong messages.
pingTimerId = window.setInterval(function() {
socket.send("0");
}, 30000);
};
socket.onclose = function () {
eventForwarder.callFunction(function() {
Console.log("WebSocket connection closed.");
disableControls();
// Disable pong timer.
window.clearInterval(pingTimerId);
});
};
// Handles an incoming Websocket message.
var handleOnMessage = function(message) {
// Split joined message and process them
// invidividually.
var messages = message.data.split(";");
for (var msgArrIdx = 0; msgArrIdx < messages.length;
msgArrIdx++) {
var msg = messages[msgArrIdx];
var type = msg.substring(0, 1);
if (type == "0") {
// Error message.
var error = msg.substring(1);
// Log it to the console and show an alert.
Console.log("Error: " + error);
alert(error);
} else {
if (!isStarted) {
if (type == "2") {
// Initial message. It contains the
// number of players.
// After this message we will receive
// a binary message containing the current
// room image as PNG.
playerCount = parseInt(msg.substring(1));
refreshPlayerCount();
// The next message will be a binary
// message containing the room images
// as PNG. Therefore we temporarily swap
// the message handler.
var originalHandler = handleOnMessage;
handleOnMessage = function(message) {
// First, we restore the original handler.
handleOnMessage = originalHandler;
// Read the image.
var blob = message.data;
// Create new blob with correct MIME type.
blob = new Blob([blob], {type : "image/png"});
var url = URL.createObjectURL(blob);
var img = new Image();
// We must wait until the onload event is
// raised until we can draw the image onto
// the canvas.
// Therefore we need to pause the event
// forwarder until the image is loaded.
eventForwarder.pauseProcessing();
img.onload = function() {
// Release the object URL.
URL.revokeObjectURL(url);
// Set the canvases to the correct size.
for (var i = 0; i < canvasArray.length; i++) {
canvasArray[i].width = img.width;
canvasArray[i].height = img.height;
}
// Now draw the image on the last canvas.
canvasServerImageCtx.clearRect(0, 0,
canvasServerImage.width,
canvasServerImage.height);
canvasServerImageCtx.drawImage(img, 0, 0);
// Draw it on the background canvas.
canvasBackgroundCtx.drawImage(canvasServerImage,
0, 0);
isStarted = true;
startControls();
// Refresh the display canvas.
refreshDisplayCanvas();
// Finally, resume the event forwarder.
eventForwarder.resumeProcessing();
};
img.src = url;
};
}
} else {
if (type == "3") {
// The number of players in this room changed.
var playerAdded = msg.substring(1) == "+";
playerCount += playerAdded ? 1 : -1;
refreshPlayerCount();
Console.log("Player " + (playerAdded
? "joined." : "left."));
} else if (type == "1") {
// We received a new DrawMessage.
var maxLastHandledId = -1;
var drawMessages = msg.substring(1).split("|");
for (var i = 0; i < drawMessages.length; i++) {
var elements = drawMessages[i].split(",");
var lastHandledId = parseInt(elements[0]);
maxLastHandledId = Math.max(maxLastHandledId,
lastHandledId);
var path = new Path(
parseInt(elements[1]),
[parseInt(elements[2]),
parseInt(elements[3]),
parseInt(elements[4]),
parseInt(elements[5]) / 255.0],
parseFloat(elements[6]),
parseFloat(elements[7]),
parseFloat(elements[8]),
parseFloat(elements[9]),
parseFloat(elements[10]),
elements[11] != "0");
// Draw the path onto the last canvas.
path.draw(canvasServerImageCtx);
}
// Draw the last canvas onto the background one.
canvasBackgroundCtx.drawImage(canvasServerImage,
0, 0);
// Now go through the pathsNotHandled array and
// remove the paths that were already handled by
// the server.
while (pathsNotHandled.length > 0
&& pathsNotHandled[0].id <= maxLastHandledId)
pathsNotHandled.shift();
// Now me must draw the remaining paths onto
// the background canvas.
for (var i = 0; i < pathsNotHandled.length; i++) {
pathsNotHandled[i].path.draw(canvasBackgroundCtx);
}
refreshDisplayCanvas();
}
}
}
}
};
socket.onmessage = function(message) {
eventForwarder.callFunction(function() {
handleOnMessage(message);
});
};
}
function refreshPlayerCount() {
labelPlayerCount.nodeValue = String(playerCount);
}
function refreshDisplayCanvas() {
if (!isActive) { // Don't draw a curser when not active.
return;
}
canvasDisplayCtx.drawImage(canvasBackground, 0, 0);
if (currentPreviewPath != null) {
// Draw the preview path.
currentPreviewPath.draw(canvasDisplayCtx);
} else if (mouseInWindow && !mouseDown) {
canvasDisplayCtx.beginPath();
var color = availableColors[currentColorIndex].slice(0);
color[3] = previewTransparency;
canvasDisplayCtx.fillStyle = rgb(color);
canvasDisplayCtx.arc(currentMouseX, currentMouseY,
availableThicknesses[currentThicknessIndex] / 2,
0, Math.PI * 2.0, true);
canvasDisplayCtx.fill();
}
}
function startControls() {
isActive = true;
labelContainer.removeChild(placeholder);
placeholder = undefined;
labelContainer.appendChild(
document.createTextNode("Number of Players: "));
labelContainer.appendChild(labelPlayerCount);
drawContainer.style.display = "block";
drawContainer.appendChild(canvasDisplay);
drawContainer.appendChild(optionContainer);
canvasMouseDownHandler = function(e) {
if (e.button == 0) {
currentMouseX = e.pageX - canvasDisplay.offsetLeft;
currentMouseY = e.pageY - canvasDisplay.offsetTop;
mouseDown = true;
canvasMouseMoveHandler(e);
} else if (mouseDown) {
// Cancel drawing.
mouseDown = false;
currentPreviewPath = null;
currentMouseX = e.pageX - canvasDisplay.offsetLeft;
currentMouseY = e.pageY - canvasDisplay.offsetTop;
refreshDisplayCanvas();
}
};
canvasDisplay.addEventListener("mousedown", canvasMouseDownHandler, false);
canvasMouseMoveHandler = function(e) {
var mouseX = e.pageX - canvasDisplay.offsetLeft;
var mouseY = e.pageY - canvasDisplay.offsetTop;
if (mouseDown) {
var drawType = availableDrawTypes[currentDrawTypeIndex];
if (drawType.continuous) {
var path = new Path(drawType.id,
availableColors[currentColorIndex],
availableThicknesses[currentThicknessIndex],
currentMouseX, currentMouseY, mouseX,
mouseY, false);
// Draw it on the background canvas.
path.draw(canvasBackgroundCtx);
// Send it to the sever.
pushPath(path);
// Refresh old coordinates
currentMouseX = mouseX;
currentMouseY = mouseY;
} else {
// Create a new preview path.
var color = availableColors[currentColorIndex].slice(0);
color[3] = previewTransparency;
currentPreviewPath = new Path(drawType.id,
color,
availableThicknesses[currentThicknessIndex],
currentMouseX, currentMouseY, mouseX,
mouseY, false);
}
refreshDisplayCanvas();
} else {
currentMouseX = mouseX;
currentMouseY = mouseY;
if (mouseInWindow) {
refreshDisplayCanvas();
}
}
};
document.addEventListener("mousemove", canvasMouseMoveHandler, false);
document.addEventListener("mouseup", function(e) {
if (e.button == 0) {
if (mouseDown) {
mouseDown = false;
currentPreviewPath = null;
var mouseX = e.pageX - canvasDisplay.offsetLeft;
var mouseY = e.pageY - canvasDisplay.offsetTop;
var drawType = availableDrawTypes[currentDrawTypeIndex];
var path = new Path(drawType.id, availableColors[currentColorIndex],
availableThicknesses[currentThicknessIndex],
currentMouseX, currentMouseY, mouseX,
mouseY, true);
// Draw it on the background canvas.
path.draw(canvasBackgroundCtx);
// Send it to the sever.
pushPath(path);
// Refresh old coordinates
currentMouseX = mouseX;
currentMouseY = mouseY;
refreshDisplayCanvas();
}
}
}, false);
canvasDisplay.addEventListener("mouseout", function(e) {
mouseInWindow = false;
refreshDisplayCanvas();
}, false);
canvasDisplay.addEventListener("mousemove", function(e) {
if (!mouseInWindow) {
mouseInWindow = true;
refreshDisplayCanvas();
}
}, false);
// Create color and thickness controls.
var colorContainersBox = document.createElement("div");
colorContainersBox.setAttribute("style",
"margin: 4px; border: 1px solid #bbb; border-radius: 3px;");
optionContainer.appendChild(colorContainersBox);
colorContainers = new Array(3 * 3 * 3);
for (var i = 0; i < colorContainers.length; i++) {
var colorContainer = colorContainers[i] =
document.createElement("div");
var color = availableColors[i] =
[
Math.floor((i % 3) * 255 / 2),
Math.floor((Math.floor(i / 3) % 3) * 255 / 2),
Math.floor((Math.floor(i / (3 * 3)) % 3) * 255 / 2),
1.0
];
colorContainer.setAttribute("style",
"margin: 3px; width: 18px; height: 18px; "
+ "float: left; background-color: " + rgb(color));
colorContainer.style.border = '2px solid #000';
colorContainer.addEventListener("mousedown", (function(ix) {
return function() {
setColor(ix);
};
})(i), false);
colorContainersBox.appendChild(colorContainer);
}
var divClearLeft = document.createElement("div");
divClearLeft.setAttribute("style", "clear: left;");
colorContainersBox.appendChild(divClearLeft);
var drawTypeContainersBox = document.createElement("div");
drawTypeContainersBox.setAttribute("style",
"float: right; margin-right: 3px; margin-top: 1px;");
optionContainer.appendChild(drawTypeContainersBox);
drawTypeContainers = new Array(availableDrawTypes.length);
for (var i = 0; i < drawTypeContainers.length; i++) {
var drawTypeContainer = drawTypeContainers[i] =
document.createElement("div");
drawTypeContainer.setAttribute("style",
"text-align: center; margin: 3px; padding: 0 3px;"
+ "height: 18px; float: left;");
drawTypeContainer.style.border = "2px solid #000";
drawTypeContainer.appendChild(document.createTextNode(
String(availableDrawTypes[i].name)));
drawTypeContainer.addEventListener("mousedown", (function(ix) {
return function() {
setDrawType(ix);
};
})(i), false);
drawTypeContainersBox.appendChild(drawTypeContainer);
}
var thicknessContainersBox = document.createElement("div");
thicknessContainersBox.setAttribute("style",
"margin: 3px; border: 1px solid #bbb; border-radius: 3px;");
optionContainer.appendChild(thicknessContainersBox);
thicknessContainers = new Array(availableThicknesses.length);
for (var i = 0; i < thicknessContainers.length; i++) {
var thicknessContainer = thicknessContainers[i] =
document.createElement("div");
thicknessContainer.setAttribute("style",
"text-align: center; margin: 3px; width: 18px; "
+ "height: 18px; float: left;");
thicknessContainer.style.border = "2px solid #000";
thicknessContainer.appendChild(document.createTextNode(
String(availableThicknesses[i])));
thicknessContainer.addEventListener("mousedown", (function(ix) {
return function() {
setThickness(ix);
};
})(i), false);
thicknessContainersBox.appendChild(thicknessContainer);
}
divClearLeft = document.createElement("div");
divClearLeft.setAttribute("style", "clear: left;");
thicknessContainersBox.appendChild(divClearLeft);
setColor(0);
setThickness(0);
setDrawType(0);
}
function disableControls() {
document.removeEventListener("mousedown", canvasMouseDownHandler);
document.removeEventListener("mousemove", canvasMouseMoveHandler);
mouseInWindow = false;
refreshDisplayCanvas();
isActive = false;
}
function pushPath(path) {
// Push it into the pathsNotHandled array.
var container = new PathIdContainer(path, nextMsgId++);
pathsNotHandled.push(container);
// Send the path to the server.
var message = container.id + "|" + path.type + ","
+ path.color[0] + "," + path.color[1] + ","
+ path.color[2] + ","
+ Math.round(path.color[3] * 255.0) + ","
+ path.thickness + "," + path.x1 + ","
+ path.y1 + "," + path.x2 + "," + path.y2 + ","
+ (path.lastInChain ? "1" : "0");
socket.send("1" + message);
}
function setThickness(thicknessIndex) {
if (typeof currentThicknessIndex !== "undefined")
thicknessContainers[currentThicknessIndex]
.style.borderColor = "#000";
currentThicknessIndex = thicknessIndex;
thicknessContainers[currentThicknessIndex]
.style.borderColor = "#d08";
}
function setColor(colorIndex) {
if (typeof currentColorIndex !== "undefined")
colorContainers[currentColorIndex]
.style.borderColor = "#000";
currentColorIndex = colorIndex;
colorContainers[currentColorIndex]
.style.borderColor = "#d08";
}
function setDrawType(drawTypeIndex) {
if (typeof currentDrawTypeIndex !== "undefined")
drawTypeContainers[currentDrawTypeIndex]
.style.borderColor = "#000";
currentDrawTypeIndex = drawTypeIndex;
drawTypeContainers[currentDrawTypeIndex]
.style.borderColor = "#d08";
}
connect();
}
// Initialize the room
var room = new Room(document.getElementById("drawContainer"));
}, false);
})();
]]></script>
</head>
<body>
<div class="noscript"><div style="color: #ff0000; font-size: 16pt;">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
Javascript and reload this page!</div></div>
<div id="labelContainer"/>
<div id="drawContainer"/>
<div id="console-container"/>
<div style="clear: left;"/>
<h1 class="expand" data-content-id="expandContent" style="font-size: 1.3em;"
>About Drawbord WebSocket Example</h1>
<div id="expandContent">
<p>
This drawboard is a page where you can draw with your mouse or touch input
(using different colors) and everybody else which has the page open will
<em>immediately</em> see what you are drawing.<br/>
If someone opens the page later, they will get the current room image (so they
can see what was already drawn by other people).
</p>
<p>
It uses asynchronous sending of messages so that it doesn't need separate threads
for each client to send messages (this needs NIO or APR connector to be used).<br/>
Each "Room" (where the drawing happens) uses a ReentrantLock to synchronize access
(currently, only a single Room is implemented).
</p>
<p>
When you open the page, first you will receive a binary websocket message containing
the current room image as PNG image. After that, you will receive string messages
that contain the drawing actions (line from x1,y1 to x2,y2).<br/>
<small>Note that it currently only uses simple string messages instead of JSON because
I did not want to introduce a dependency on a JSON lib.</small>
</p>
<p>
It uses synchronization mechanisms to ensure that the final image will look the same
for every user, regardless of what their network latency/speed is – e.g. if two user
draw at the same time on the same place, the server will decide which line was the
first one, and that will be reflected on every client.
</p>
</div>
</body>
</html>