<?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 | |
// individually. | |
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 Drawboard 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> |