| <?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> |