刘洪青 | 6266f99 | 2017-05-15 21:21:03 +0800 | [diff] [blame^] | 1 | <?xml version="1.0" encoding="UTF-8"?>
|
| 2 | <!--
|
| 3 | Licensed to the Apache Software Foundation (ASF) under one or more
|
| 4 | contributor license agreements. See the NOTICE file distributed with
|
| 5 | this work for additional information regarding copyright ownership.
|
| 6 | The ASF licenses this file to You under the Apache License, Version 2.0
|
| 7 | (the "License"); you may not use this file except in compliance with
|
| 8 | the License. You may obtain a copy of the License at
|
| 9 |
|
| 10 | http://www.apache.org/licenses/LICENSE-2.0
|
| 11 |
|
| 12 | Unless required by applicable law or agreed to in writing, software
|
| 13 | distributed under the License is distributed on an "AS IS" BASIS,
|
| 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 15 | See the License for the specific language governing permissions and
|
| 16 | limitations under the License.
|
| 17 | -->
|
| 18 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
| 19 | <head>
|
| 20 | <title>Apache Tomcat WebSocket Examples: Drawboard</title>
|
| 21 | <style type="text/css"><![CDATA[
|
| 22 |
|
| 23 | body {
|
| 24 | font-family: Arial, sans-serif;
|
| 25 | font-size: 11pt;
|
| 26 | background-color: #eeeeea;
|
| 27 | padding: 10px;
|
| 28 | }
|
| 29 |
|
| 30 | #console-container {
|
| 31 | float: left;
|
| 32 | background-color: #fff;
|
| 33 | width: 250px;
|
| 34 | }
|
| 35 |
|
| 36 | #console {
|
| 37 | font-size: 10pt;
|
| 38 | height: 600px;
|
| 39 | overflow-y: scroll;
|
| 40 | padding-left: 5px;
|
| 41 | padding-right: 5px;
|
| 42 | }
|
| 43 |
|
| 44 | #console p {
|
| 45 | padding: 0;
|
| 46 | margin: 0;
|
| 47 | }
|
| 48 |
|
| 49 | #drawContainer {
|
| 50 | float: left;
|
| 51 | display: none;
|
| 52 | margin-right: 25px;
|
| 53 | }
|
| 54 |
|
| 55 | #drawContainer canvas {
|
| 56 | display: block;
|
| 57 | -ms-touch-action: none;
|
| 58 | touch-action: none; /* Disable touch behaviors, like pan and zoom */
|
| 59 | cursor: crosshair;
|
| 60 | }
|
| 61 |
|
| 62 | #labelContainer {
|
| 63 | margin-bottom: 15px;
|
| 64 | }
|
| 65 |
|
| 66 | #drawContainer, #console-container {
|
| 67 | box-shadow: 0px 0px 8px 3px #bbb;
|
| 68 | border: 1px solid #CCCCCC;
|
| 69 | }
|
| 70 |
|
| 71 | ]]></style>
|
| 72 | <script type="application/javascript"><![CDATA[
|
| 73 | "use strict";
|
| 74 |
|
| 75 | (function() {
|
| 76 |
|
| 77 | document.addEventListener("DOMContentLoaded", function() {
|
| 78 | // Remove elements with "noscript" class - <noscript> is not
|
| 79 | // allowed in XHTML
|
| 80 | var noscripts = document.getElementsByClassName("noscript");
|
| 81 | for (var i = 0; i < noscripts.length; i++) {
|
| 82 | noscripts[i].parentNode.removeChild(noscripts[i]);
|
| 83 | }
|
| 84 |
|
| 85 | // Add script for expand content.
|
| 86 | var expandElements = document.getElementsByClassName("expand");
|
| 87 | for (var ixx = 0; ixx < expandElements.length; ixx++) {
|
| 88 | (function(el) {
|
| 89 | var expandContent = document.getElementById(el.getAttribute("data-content-id"));
|
| 90 | expandContent.style.display = "none";
|
| 91 | var arrow = document.createTextNode("◢ ");
|
| 92 | var arrowSpan = document.createElement("span");
|
| 93 | arrowSpan.appendChild(arrow);
|
| 94 |
|
| 95 | var link = document.createElement("a");
|
| 96 | link.setAttribute("href", "#!");
|
| 97 | while (el.firstChild != null) {
|
| 98 | link.appendChild(el.removeChild(el.firstChild));
|
| 99 | }
|
| 100 | el.appendChild(arrowSpan);
|
| 101 | el.appendChild(link);
|
| 102 |
|
| 103 | var textSpan = document.createElement("span");
|
| 104 | textSpan.setAttribute("style", "font-weight: normal;");
|
| 105 | textSpan.appendChild(document.createTextNode(" (click to expand)"));
|
| 106 | el.appendChild(textSpan);
|
| 107 |
|
| 108 |
|
| 109 | var visible = true;
|
| 110 |
|
| 111 | var switchExpand = function() {
|
| 112 | visible = !visible;
|
| 113 | expandContent.style.display = visible ? "block" : "none";
|
| 114 | arrowSpan.style.color = visible ? "#000" : "#888";
|
| 115 | return false;
|
| 116 | };
|
| 117 |
|
| 118 | link.onclick = switchExpand;
|
| 119 | switchExpand();
|
| 120 |
|
| 121 | })(expandElements[ixx]);
|
| 122 | }
|
| 123 |
|
| 124 |
|
| 125 | var Console = {};
|
| 126 |
|
| 127 | Console.log = (function() {
|
| 128 | var consoleContainer =
|
| 129 | document.getElementById("console-container");
|
| 130 | var console = document.createElement("div");
|
| 131 | console.setAttribute("id", "console");
|
| 132 | consoleContainer.appendChild(console);
|
| 133 |
|
| 134 | return function(message) {
|
| 135 | var p = document.createElement('p');
|
| 136 | p.style.wordWrap = "break-word";
|
| 137 | p.appendChild(document.createTextNode(message));
|
| 138 | console.appendChild(p);
|
| 139 | while (console.childNodes.length > 25) {
|
| 140 | console.removeChild(console.firstChild);
|
| 141 | }
|
| 142 | console.scrollTop = console.scrollHeight;
|
| 143 | }
|
| 144 | })();
|
| 145 |
|
| 146 |
|
| 147 | function Room(drawContainer) {
|
| 148 |
|
| 149 | /* A pausable event forwarder that can be used to pause and
|
| 150 | * resume handling of events (e.g. when we need to wait
|
| 151 | * for a Image's load event before we can process further
|
| 152 | * WebSocket messages).
|
| 153 | * The object's callFunction(func) should be called from an
|
| 154 | * event handler and give the function to handle the event as
|
| 155 | * argument.
|
| 156 | * Call pauseProcessing() to suspend event forwarding and
|
| 157 | * resumeProcessing() to resume it.
|
| 158 | */
|
| 159 | function PausableEventForwarder() {
|
| 160 |
|
| 161 | var pauseProcessing = false;
|
| 162 | // Queue for buffering functions to be called.
|
| 163 | var functionQueue = [];
|
| 164 |
|
| 165 | this.callFunction = function(func) {
|
| 166 | // If message processing is paused, we push it
|
| 167 | // into the queue - otherwise we process it directly.
|
| 168 | if (pauseProcessing) {
|
| 169 | functionQueue.push(func);
|
| 170 | } else {
|
| 171 | func();
|
| 172 | }
|
| 173 | };
|
| 174 |
|
| 175 | this.pauseProcessing = function() {
|
| 176 | pauseProcessing = true;
|
| 177 | };
|
| 178 |
|
| 179 | this.resumeProcessing = function() {
|
| 180 | pauseProcessing = false;
|
| 181 |
|
| 182 | // Process all queued functions until some handler calls
|
| 183 | // pauseProcessing() again.
|
| 184 | while (functionQueue.length > 0 && !pauseProcessing) {
|
| 185 | var func = functionQueue.pop();
|
| 186 | func();
|
| 187 | }
|
| 188 | };
|
| 189 | }
|
| 190 |
|
| 191 | // The WebSocket object.
|
| 192 | var socket;
|
| 193 | // ID of the timer which sends ping messages.
|
| 194 | var pingTimerId;
|
| 195 |
|
| 196 | var isStarted = false;
|
| 197 | var playerCount = 0;
|
| 198 |
|
| 199 | // An array of PathIdContainer objects that the server
|
| 200 | // did not yet handle.
|
| 201 | // They are ordered by id (ascending).
|
| 202 | var pathsNotHandled = [];
|
| 203 |
|
| 204 | var nextMsgId = 1;
|
| 205 |
|
| 206 | var canvasDisplay = document.createElement("canvas");
|
| 207 | var canvasBackground = document.createElement("canvas");
|
| 208 | var canvasServerImage = document.createElement("canvas");
|
| 209 | var canvasArray = [canvasDisplay, canvasBackground,
|
| 210 | canvasServerImage];
|
| 211 | canvasDisplay.addEventListener("mousedown", function(e) {
|
| 212 | // Prevent default mouse event to prevent browsers from marking text
|
| 213 | // (and Chrome from displaying the "text" cursor).
|
| 214 | e.preventDefault();
|
| 215 | }, false);
|
| 216 |
|
| 217 | var labelPlayerCount = document.createTextNode("0");
|
| 218 | var optionContainer = document.createElement("div");
|
| 219 |
|
| 220 |
|
| 221 | var canvasDisplayCtx = canvasDisplay.getContext("2d");
|
| 222 | var canvasBackgroundCtx = canvasBackground.getContext("2d");
|
| 223 | var canvasServerImageCtx = canvasServerImage.getContext("2d");
|
| 224 | var canvasMouseMoveHandler;
|
| 225 | var canvasMouseDownHandler;
|
| 226 |
|
| 227 | var isActive = false;
|
| 228 | var mouseInWindow = false;
|
| 229 | var mouseDown = false;
|
| 230 | var currentMouseX = 0, currentMouseY = 0;
|
| 231 | var currentPreviewPath = null;
|
| 232 |
|
| 233 | var availableColors = [];
|
| 234 | var currentColorIndex;
|
| 235 | var colorContainers;
|
| 236 | var previewTransparency = 0.65;
|
| 237 |
|
| 238 | var availableThicknesses = [2, 3, 6, 10, 16, 28, 50];
|
| 239 | var currentThicknessIndex;
|
| 240 | var thicknessContainers;
|
| 241 |
|
| 242 | var availableDrawTypes = [
|
| 243 | { name: "Brush", id: 1, continuous: true },
|
| 244 | { name: "Line", id: 2, continuous: false },
|
| 245 | { name: "Rectangle", id: 3, continuous: false },
|
| 246 | { name: "Ellipse", id: 4, continuous: false }
|
| 247 | ];
|
| 248 | var currentDrawTypeIndex;
|
| 249 | var drawTypeContainers;
|
| 250 |
|
| 251 |
|
| 252 | var labelContainer = document.getElementById("labelContainer");
|
| 253 | var placeholder = document.createElement("div");
|
| 254 | placeholder.appendChild(document.createTextNode("Loading... "));
|
| 255 | var progressElem = document.createElement("progress");
|
| 256 | placeholder.appendChild(progressElem);
|
| 257 |
|
| 258 | labelContainer.appendChild(placeholder);
|
| 259 |
|
| 260 | function rgb(color) {
|
| 261 | return "rgba(" + color[0] + "," + color[1] + ","
|
| 262 | + color[2] + "," + color[3] + ")";
|
| 263 | }
|
| 264 |
|
| 265 | function PathIdContainer(path, id) {
|
| 266 | this.path = path;
|
| 267 | this.id = id;
|
| 268 | }
|
| 269 |
|
| 270 | function Path(type, color, thickness, x1, y1, x2, y2, lastInChain) {
|
| 271 | this.type = type;
|
| 272 | this.color = color;
|
| 273 | this.thickness = thickness;
|
| 274 | this.x1 = x1;
|
| 275 | this.y1 = y1;
|
| 276 | this.x2 = x2;
|
| 277 | this.y2 = y2;
|
| 278 | this.lastInChain = lastInChain;
|
| 279 |
|
| 280 | function ellipse(ctx, x, y, w, h) {
|
| 281 | /* Drawing a ellipse cannot be done directly in a
|
| 282 | * CanvasRenderingContext2D - we need to use drawArc()
|
| 283 | * in conjunction with scaling the context so that we
|
| 284 | * get the needed proportion.
|
| 285 | */
|
| 286 | ctx.save();
|
| 287 |
|
| 288 | // Translate and scale the context so that we can draw
|
| 289 | // an arc at (0, 0) with a radius of 1.
|
| 290 | ctx.translate(x + w / 2, y + h / 2);
|
| 291 | ctx.scale(w / 2, h / 2);
|
| 292 |
|
| 293 | ctx.beginPath();
|
| 294 | ctx.arc(0, 0, 1, 0, Math.PI * 2, false);
|
| 295 |
|
| 296 | ctx.restore();
|
| 297 | }
|
| 298 |
|
| 299 | this.draw = function(ctx) {
|
| 300 | ctx.beginPath();
|
| 301 | ctx.lineCap = "round";
|
| 302 | ctx.lineWidth = thickness;
|
| 303 | var style = rgb(color);
|
| 304 | ctx.strokeStyle = style;
|
| 305 |
|
| 306 | if (x1 == x2 && y1 == y2) {
|
| 307 | // Always draw as arc to meet the behavior
|
| 308 | // in Java2D.
|
| 309 | ctx.fillStyle = style;
|
| 310 | ctx.arc(x1, y1, thickness / 2.0, 0,
|
| 311 | Math.PI * 2.0, false);
|
| 312 | ctx.fill();
|
| 313 | } else {
|
| 314 | if (type == 1 || type == 2) {
|
| 315 | // Draw a line.
|
| 316 | ctx.moveTo(x1, y1);
|
| 317 | ctx.lineTo(x2, y2);
|
| 318 | ctx.stroke();
|
| 319 | } else if (type == 3) {
|
| 320 | // Draw a rectangle.
|
| 321 | if (x1 == x2 || y1 == y2) {
|
| 322 | // Draw as line
|
| 323 | ctx.moveTo(x1, y1);
|
| 324 | ctx.lineTo(x2, y2);
|
| 325 | ctx.stroke();
|
| 326 | } else {
|
| 327 | ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
| 328 | }
|
| 329 | } else if (type == 4) {
|
| 330 | // Draw a ellipse.
|
| 331 | ellipse(ctx, x1, y1, x2 - x1, y2 - y1);
|
| 332 | ctx.closePath();
|
| 333 | ctx.stroke();
|
| 334 | }
|
| 335 | }
|
| 336 | };
|
| 337 | }
|
| 338 |
|
| 339 |
|
| 340 | function connect() {
|
| 341 | var host = (window.location.protocol == "https:"
|
| 342 | ? "wss://" : "ws://") + window.location.host
|
| 343 | + "/examples/websocket/drawboard";
|
| 344 | socket = new WebSocket(host);
|
| 345 |
|
| 346 | /* Use a pausable event forwarder.
|
| 347 | * This is needed when we load an Image object with data
|
| 348 | * from a previous message, because we must wait until the
|
| 349 | * Image's load event it raised before we can use it (and
|
| 350 | * in the meantime the socket.message event could be
|
| 351 | * raised).
|
| 352 | * Therefore we need this pausable event handler to handle
|
| 353 | * e.g. socket.onmessage and socket.onclose.
|
| 354 | */
|
| 355 | var eventForwarder = new PausableEventForwarder();
|
| 356 |
|
| 357 | socket.onopen = function () {
|
| 358 | // Socket has opened. Now wait for the server to
|
| 359 | // send us the initial packet.
|
| 360 | Console.log("WebSocket connection opened.");
|
| 361 |
|
| 362 | // Set up a timer for pong messages.
|
| 363 | pingTimerId = window.setInterval(function() {
|
| 364 | socket.send("0");
|
| 365 | }, 30000);
|
| 366 | };
|
| 367 |
|
| 368 | socket.onclose = function () {
|
| 369 | eventForwarder.callFunction(function() {
|
| 370 | Console.log("WebSocket connection closed.");
|
| 371 | disableControls();
|
| 372 |
|
| 373 | // Disable pong timer.
|
| 374 | window.clearInterval(pingTimerId);
|
| 375 | });
|
| 376 | };
|
| 377 |
|
| 378 | // Handles an incoming Websocket message.
|
| 379 | var handleOnMessage = function(message) {
|
| 380 |
|
| 381 | // Split joined message and process them
|
| 382 | // individually.
|
| 383 | var messages = message.data.split(";");
|
| 384 | for (var msgArrIdx = 0; msgArrIdx < messages.length;
|
| 385 | msgArrIdx++) {
|
| 386 | var msg = messages[msgArrIdx];
|
| 387 | var type = msg.substring(0, 1);
|
| 388 |
|
| 389 | if (type == "0") {
|
| 390 | // Error message.
|
| 391 | var error = msg.substring(1);
|
| 392 | // Log it to the console and show an alert.
|
| 393 | Console.log("Error: " + error);
|
| 394 | alert(error);
|
| 395 |
|
| 396 | } else {
|
| 397 | if (!isStarted) {
|
| 398 | if (type == "2") {
|
| 399 | // Initial message. It contains the
|
| 400 | // number of players.
|
| 401 | // After this message we will receive
|
| 402 | // a binary message containing the current
|
| 403 | // room image as PNG.
|
| 404 | playerCount = parseInt(msg.substring(1));
|
| 405 |
|
| 406 | refreshPlayerCount();
|
| 407 |
|
| 408 | // The next message will be a binary
|
| 409 | // message containing the room images
|
| 410 | // as PNG. Therefore we temporarily swap
|
| 411 | // the message handler.
|
| 412 | var originalHandler = handleOnMessage;
|
| 413 | handleOnMessage = function(message) {
|
| 414 | // First, we restore the original handler.
|
| 415 | handleOnMessage = originalHandler;
|
| 416 |
|
| 417 | // Read the image.
|
| 418 | var blob = message.data;
|
| 419 | // Create new blob with correct MIME type.
|
| 420 | blob = new Blob([blob], {type : "image/png"});
|
| 421 |
|
| 422 | var url = URL.createObjectURL(blob);
|
| 423 |
|
| 424 | var img = new Image();
|
| 425 |
|
| 426 | // We must wait until the onload event is
|
| 427 | // raised until we can draw the image onto
|
| 428 | // the canvas.
|
| 429 | // Therefore we need to pause the event
|
| 430 | // forwarder until the image is loaded.
|
| 431 | eventForwarder.pauseProcessing();
|
| 432 |
|
| 433 | img.onload = function() {
|
| 434 |
|
| 435 | // Release the object URL.
|
| 436 | URL.revokeObjectURL(url);
|
| 437 |
|
| 438 | // Set the canvases to the correct size.
|
| 439 | for (var i = 0; i < canvasArray.length; i++) {
|
| 440 | canvasArray[i].width = img.width;
|
| 441 | canvasArray[i].height = img.height;
|
| 442 | }
|
| 443 |
|
| 444 | // Now draw the image on the last canvas.
|
| 445 | canvasServerImageCtx.clearRect(0, 0,
|
| 446 | canvasServerImage.width,
|
| 447 | canvasServerImage.height);
|
| 448 | canvasServerImageCtx.drawImage(img, 0, 0);
|
| 449 |
|
| 450 | // Draw it on the background canvas.
|
| 451 | canvasBackgroundCtx.drawImage(canvasServerImage,
|
| 452 | 0, 0);
|
| 453 |
|
| 454 | isStarted = true;
|
| 455 | startControls();
|
| 456 |
|
| 457 | // Refresh the display canvas.
|
| 458 | refreshDisplayCanvas();
|
| 459 |
|
| 460 |
|
| 461 | // Finally, resume the event forwarder.
|
| 462 | eventForwarder.resumeProcessing();
|
| 463 | };
|
| 464 |
|
| 465 | img.src = url;
|
| 466 | };
|
| 467 | }
|
| 468 | } else {
|
| 469 | if (type == "3") {
|
| 470 | // The number of players in this room changed.
|
| 471 | var playerAdded = msg.substring(1) == "+";
|
| 472 | playerCount += playerAdded ? 1 : -1;
|
| 473 | refreshPlayerCount();
|
| 474 |
|
| 475 | Console.log("Player " + (playerAdded
|
| 476 | ? "joined." : "left."));
|
| 477 |
|
| 478 | } else if (type == "1") {
|
| 479 | // We received a new DrawMessage.
|
| 480 | var maxLastHandledId = -1;
|
| 481 | var drawMessages = msg.substring(1).split("|");
|
| 482 | for (var i = 0; i < drawMessages.length; i++) {
|
| 483 | var elements = drawMessages[i].split(",");
|
| 484 | var lastHandledId = parseInt(elements[0]);
|
| 485 | maxLastHandledId = Math.max(maxLastHandledId,
|
| 486 | lastHandledId);
|
| 487 |
|
| 488 | var path = new Path(
|
| 489 | parseInt(elements[1]),
|
| 490 | [parseInt(elements[2]),
|
| 491 | parseInt(elements[3]),
|
| 492 | parseInt(elements[4]),
|
| 493 | parseInt(elements[5]) / 255.0],
|
| 494 | parseFloat(elements[6]),
|
| 495 | parseFloat(elements[7]),
|
| 496 | parseFloat(elements[8]),
|
| 497 | parseFloat(elements[9]),
|
| 498 | parseFloat(elements[10]),
|
| 499 | elements[11] != "0");
|
| 500 |
|
| 501 | // Draw the path onto the last canvas.
|
| 502 | path.draw(canvasServerImageCtx);
|
| 503 | }
|
| 504 |
|
| 505 | // Draw the last canvas onto the background one.
|
| 506 | canvasBackgroundCtx.drawImage(canvasServerImage,
|
| 507 | 0, 0);
|
| 508 |
|
| 509 | // Now go through the pathsNotHandled array and
|
| 510 | // remove the paths that were already handled by
|
| 511 | // the server.
|
| 512 | while (pathsNotHandled.length > 0
|
| 513 | && pathsNotHandled[0].id <= maxLastHandledId)
|
| 514 | pathsNotHandled.shift();
|
| 515 |
|
| 516 | // Now me must draw the remaining paths onto
|
| 517 | // the background canvas.
|
| 518 | for (var i = 0; i < pathsNotHandled.length; i++) {
|
| 519 | pathsNotHandled[i].path.draw(canvasBackgroundCtx);
|
| 520 | }
|
| 521 |
|
| 522 | refreshDisplayCanvas();
|
| 523 | }
|
| 524 | }
|
| 525 | }
|
| 526 | }
|
| 527 | };
|
| 528 |
|
| 529 | socket.onmessage = function(message) {
|
| 530 | eventForwarder.callFunction(function() {
|
| 531 | handleOnMessage(message);
|
| 532 | });
|
| 533 | };
|
| 534 |
|
| 535 | }
|
| 536 |
|
| 537 |
|
| 538 | function refreshPlayerCount() {
|
| 539 | labelPlayerCount.nodeValue = String(playerCount);
|
| 540 | }
|
| 541 |
|
| 542 | function refreshDisplayCanvas() {
|
| 543 | if (!isActive) { // Don't draw a curser when not active.
|
| 544 | return;
|
| 545 | }
|
| 546 |
|
| 547 | canvasDisplayCtx.drawImage(canvasBackground, 0, 0);
|
| 548 | if (currentPreviewPath != null) {
|
| 549 | // Draw the preview path.
|
| 550 | currentPreviewPath.draw(canvasDisplayCtx);
|
| 551 |
|
| 552 | } else if (mouseInWindow && !mouseDown) {
|
| 553 | canvasDisplayCtx.beginPath();
|
| 554 | var color = availableColors[currentColorIndex].slice(0);
|
| 555 | color[3] = previewTransparency;
|
| 556 | canvasDisplayCtx.fillStyle = rgb(color);
|
| 557 |
|
| 558 | canvasDisplayCtx.arc(currentMouseX, currentMouseY,
|
| 559 | availableThicknesses[currentThicknessIndex] / 2,
|
| 560 | 0, Math.PI * 2.0, true);
|
| 561 | canvasDisplayCtx.fill();
|
| 562 | }
|
| 563 |
|
| 564 | }
|
| 565 |
|
| 566 | function startControls() {
|
| 567 | isActive = true;
|
| 568 |
|
| 569 | labelContainer.removeChild(placeholder);
|
| 570 | placeholder = undefined;
|
| 571 |
|
| 572 | labelContainer.appendChild(
|
| 573 | document.createTextNode("Number of Players: "));
|
| 574 | labelContainer.appendChild(labelPlayerCount);
|
| 575 |
|
| 576 |
|
| 577 | drawContainer.style.display = "block";
|
| 578 | drawContainer.appendChild(canvasDisplay);
|
| 579 |
|
| 580 | drawContainer.appendChild(optionContainer);
|
| 581 |
|
| 582 | canvasMouseDownHandler = function(e) {
|
| 583 | if (e.button == 0) {
|
| 584 | currentMouseX = e.pageX - canvasDisplay.offsetLeft;
|
| 585 | currentMouseY = e.pageY - canvasDisplay.offsetTop;
|
| 586 |
|
| 587 | mouseDown = true;
|
| 588 | canvasMouseMoveHandler(e);
|
| 589 |
|
| 590 | } else if (mouseDown) {
|
| 591 | // Cancel drawing.
|
| 592 | mouseDown = false;
|
| 593 | currentPreviewPath = null;
|
| 594 |
|
| 595 | currentMouseX = e.pageX - canvasDisplay.offsetLeft;
|
| 596 | currentMouseY = e.pageY - canvasDisplay.offsetTop;
|
| 597 |
|
| 598 | refreshDisplayCanvas();
|
| 599 | }
|
| 600 | };
|
| 601 | canvasDisplay.addEventListener("mousedown", canvasMouseDownHandler, false);
|
| 602 |
|
| 603 | canvasMouseMoveHandler = function(e) {
|
| 604 | var mouseX = e.pageX - canvasDisplay.offsetLeft;
|
| 605 | var mouseY = e.pageY - canvasDisplay.offsetTop;
|
| 606 |
|
| 607 | if (mouseDown) {
|
| 608 | var drawType = availableDrawTypes[currentDrawTypeIndex];
|
| 609 |
|
| 610 | if (drawType.continuous) {
|
| 611 |
|
| 612 | var path = new Path(drawType.id,
|
| 613 | availableColors[currentColorIndex],
|
| 614 | availableThicknesses[currentThicknessIndex],
|
| 615 | currentMouseX, currentMouseY, mouseX,
|
| 616 | mouseY, false);
|
| 617 | // Draw it on the background canvas.
|
| 618 | path.draw(canvasBackgroundCtx);
|
| 619 |
|
| 620 | // Send it to the sever.
|
| 621 | pushPath(path);
|
| 622 |
|
| 623 | // Refresh old coordinates
|
| 624 | currentMouseX = mouseX;
|
| 625 | currentMouseY = mouseY;
|
| 626 |
|
| 627 | } else {
|
| 628 | // Create a new preview path.
|
| 629 | var color = availableColors[currentColorIndex].slice(0);
|
| 630 | color[3] = previewTransparency;
|
| 631 | currentPreviewPath = new Path(drawType.id,
|
| 632 | color,
|
| 633 | availableThicknesses[currentThicknessIndex],
|
| 634 | currentMouseX, currentMouseY, mouseX,
|
| 635 | mouseY, false);
|
| 636 | }
|
| 637 |
|
| 638 | refreshDisplayCanvas();
|
| 639 | } else {
|
| 640 | currentMouseX = mouseX;
|
| 641 | currentMouseY = mouseY;
|
| 642 |
|
| 643 | if (mouseInWindow) {
|
| 644 | refreshDisplayCanvas();
|
| 645 | }
|
| 646 | }
|
| 647 |
|
| 648 | };
|
| 649 | document.addEventListener("mousemove", canvasMouseMoveHandler, false);
|
| 650 |
|
| 651 | document.addEventListener("mouseup", function(e) {
|
| 652 | if (e.button == 0) {
|
| 653 | if (mouseDown) {
|
| 654 | mouseDown = false;
|
| 655 | currentPreviewPath = null;
|
| 656 |
|
| 657 | var mouseX = e.pageX - canvasDisplay.offsetLeft;
|
| 658 | var mouseY = e.pageY - canvasDisplay.offsetTop;
|
| 659 | var drawType = availableDrawTypes[currentDrawTypeIndex];
|
| 660 |
|
| 661 | var path = new Path(drawType.id, availableColors[currentColorIndex],
|
| 662 | availableThicknesses[currentThicknessIndex],
|
| 663 | currentMouseX, currentMouseY, mouseX,
|
| 664 | mouseY, true);
|
| 665 | // Draw it on the background canvas.
|
| 666 | path.draw(canvasBackgroundCtx);
|
| 667 |
|
| 668 | // Send it to the sever.
|
| 669 | pushPath(path);
|
| 670 |
|
| 671 | // Refresh old coordinates
|
| 672 | currentMouseX = mouseX;
|
| 673 | currentMouseY = mouseY;
|
| 674 |
|
| 675 | refreshDisplayCanvas();
|
| 676 | }
|
| 677 | }
|
| 678 | }, false);
|
| 679 |
|
| 680 | canvasDisplay.addEventListener("mouseout", function(e) {
|
| 681 | mouseInWindow = false;
|
| 682 | refreshDisplayCanvas();
|
| 683 | }, false);
|
| 684 |
|
| 685 | canvasDisplay.addEventListener("mousemove", function(e) {
|
| 686 | if (!mouseInWindow) {
|
| 687 | mouseInWindow = true;
|
| 688 | refreshDisplayCanvas();
|
| 689 | }
|
| 690 | }, false);
|
| 691 |
|
| 692 |
|
| 693 | // Create color and thickness controls.
|
| 694 | var colorContainersBox = document.createElement("div");
|
| 695 | colorContainersBox.setAttribute("style",
|
| 696 | "margin: 4px; border: 1px solid #bbb; border-radius: 3px;");
|
| 697 | optionContainer.appendChild(colorContainersBox);
|
| 698 |
|
| 699 | colorContainers = new Array(3 * 3 * 3);
|
| 700 | for (var i = 0; i < colorContainers.length; i++) {
|
| 701 | var colorContainer = colorContainers[i] =
|
| 702 | document.createElement("div");
|
| 703 | var color = availableColors[i] =
|
| 704 | [
|
| 705 | Math.floor((i % 3) * 255 / 2),
|
| 706 | Math.floor((Math.floor(i / 3) % 3) * 255 / 2),
|
| 707 | Math.floor((Math.floor(i / (3 * 3)) % 3) * 255 / 2),
|
| 708 | 1.0
|
| 709 | ];
|
| 710 | colorContainer.setAttribute("style",
|
| 711 | "margin: 3px; width: 18px; height: 18px; "
|
| 712 | + "float: left; background-color: " + rgb(color));
|
| 713 | colorContainer.style.border = '2px solid #000';
|
| 714 | colorContainer.addEventListener("mousedown", (function(ix) {
|
| 715 | return function() {
|
| 716 | setColor(ix);
|
| 717 | };
|
| 718 | })(i), false);
|
| 719 |
|
| 720 | colorContainersBox.appendChild(colorContainer);
|
| 721 | }
|
| 722 |
|
| 723 | var divClearLeft = document.createElement("div");
|
| 724 | divClearLeft.setAttribute("style", "clear: left;");
|
| 725 | colorContainersBox.appendChild(divClearLeft);
|
| 726 |
|
| 727 |
|
| 728 | var drawTypeContainersBox = document.createElement("div");
|
| 729 | drawTypeContainersBox.setAttribute("style",
|
| 730 | "float: right; margin-right: 3px; margin-top: 1px;");
|
| 731 | optionContainer.appendChild(drawTypeContainersBox);
|
| 732 |
|
| 733 | drawTypeContainers = new Array(availableDrawTypes.length);
|
| 734 | for (var i = 0; i < drawTypeContainers.length; i++) {
|
| 735 | var drawTypeContainer = drawTypeContainers[i] =
|
| 736 | document.createElement("div");
|
| 737 | drawTypeContainer.setAttribute("style",
|
| 738 | "text-align: center; margin: 3px; padding: 0 3px;"
|
| 739 | + "height: 18px; float: left;");
|
| 740 | drawTypeContainer.style.border = "2px solid #000";
|
| 741 | drawTypeContainer.appendChild(document.createTextNode(
|
| 742 | String(availableDrawTypes[i].name)));
|
| 743 | drawTypeContainer.addEventListener("mousedown", (function(ix) {
|
| 744 | return function() {
|
| 745 | setDrawType(ix);
|
| 746 | };
|
| 747 | })(i), false);
|
| 748 |
|
| 749 | drawTypeContainersBox.appendChild(drawTypeContainer);
|
| 750 | }
|
| 751 |
|
| 752 |
|
| 753 | var thicknessContainersBox = document.createElement("div");
|
| 754 | thicknessContainersBox.setAttribute("style",
|
| 755 | "margin: 3px; border: 1px solid #bbb; border-radius: 3px;");
|
| 756 | optionContainer.appendChild(thicknessContainersBox);
|
| 757 |
|
| 758 | thicknessContainers = new Array(availableThicknesses.length);
|
| 759 | for (var i = 0; i < thicknessContainers.length; i++) {
|
| 760 | var thicknessContainer = thicknessContainers[i] =
|
| 761 | document.createElement("div");
|
| 762 | thicknessContainer.setAttribute("style",
|
| 763 | "text-align: center; margin: 3px; width: 18px; "
|
| 764 | + "height: 18px; float: left;");
|
| 765 | thicknessContainer.style.border = "2px solid #000";
|
| 766 | thicknessContainer.appendChild(document.createTextNode(
|
| 767 | String(availableThicknesses[i])));
|
| 768 | thicknessContainer.addEventListener("mousedown", (function(ix) {
|
| 769 | return function() {
|
| 770 | setThickness(ix);
|
| 771 | };
|
| 772 | })(i), false);
|
| 773 |
|
| 774 | thicknessContainersBox.appendChild(thicknessContainer);
|
| 775 | }
|
| 776 |
|
| 777 |
|
| 778 | divClearLeft = document.createElement("div");
|
| 779 | divClearLeft.setAttribute("style", "clear: left;");
|
| 780 | thicknessContainersBox.appendChild(divClearLeft);
|
| 781 |
|
| 782 |
|
| 783 | setColor(0);
|
| 784 | setThickness(0);
|
| 785 | setDrawType(0);
|
| 786 |
|
| 787 | }
|
| 788 |
|
| 789 | function disableControls() {
|
| 790 | document.removeEventListener("mousedown", canvasMouseDownHandler);
|
| 791 | document.removeEventListener("mousemove", canvasMouseMoveHandler);
|
| 792 | mouseInWindow = false;
|
| 793 | refreshDisplayCanvas();
|
| 794 |
|
| 795 | isActive = false;
|
| 796 | }
|
| 797 |
|
| 798 | function pushPath(path) {
|
| 799 |
|
| 800 | // Push it into the pathsNotHandled array.
|
| 801 | var container = new PathIdContainer(path, nextMsgId++);
|
| 802 | pathsNotHandled.push(container);
|
| 803 |
|
| 804 | // Send the path to the server.
|
| 805 | var message = container.id + "|" + path.type + ","
|
| 806 | + path.color[0] + "," + path.color[1] + ","
|
| 807 | + path.color[2] + ","
|
| 808 | + Math.round(path.color[3] * 255.0) + ","
|
| 809 | + path.thickness + "," + path.x1 + ","
|
| 810 | + path.y1 + "," + path.x2 + "," + path.y2 + ","
|
| 811 | + (path.lastInChain ? "1" : "0");
|
| 812 |
|
| 813 | socket.send("1" + message);
|
| 814 | }
|
| 815 |
|
| 816 | function setThickness(thicknessIndex) {
|
| 817 | if (typeof currentThicknessIndex !== "undefined")
|
| 818 | thicknessContainers[currentThicknessIndex]
|
| 819 | .style.borderColor = "#000";
|
| 820 | currentThicknessIndex = thicknessIndex;
|
| 821 | thicknessContainers[currentThicknessIndex]
|
| 822 | .style.borderColor = "#d08";
|
| 823 | }
|
| 824 |
|
| 825 | function setColor(colorIndex) {
|
| 826 | if (typeof currentColorIndex !== "undefined")
|
| 827 | colorContainers[currentColorIndex]
|
| 828 | .style.borderColor = "#000";
|
| 829 | currentColorIndex = colorIndex;
|
| 830 | colorContainers[currentColorIndex]
|
| 831 | .style.borderColor = "#d08";
|
| 832 | }
|
| 833 |
|
| 834 | function setDrawType(drawTypeIndex) {
|
| 835 | if (typeof currentDrawTypeIndex !== "undefined")
|
| 836 | drawTypeContainers[currentDrawTypeIndex]
|
| 837 | .style.borderColor = "#000";
|
| 838 | currentDrawTypeIndex = drawTypeIndex;
|
| 839 | drawTypeContainers[currentDrawTypeIndex]
|
| 840 | .style.borderColor = "#d08";
|
| 841 | }
|
| 842 |
|
| 843 |
|
| 844 | connect();
|
| 845 |
|
| 846 | }
|
| 847 |
|
| 848 |
|
| 849 | // Initialize the room
|
| 850 | var room = new Room(document.getElementById("drawContainer"));
|
| 851 |
|
| 852 |
|
| 853 | }, false);
|
| 854 |
|
| 855 | })();
|
| 856 | ]]></script>
|
| 857 | </head>
|
| 858 | <body>
|
| 859 | <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
|
| 860 | Javascript and reload this page!</div></div>
|
| 861 | <div id="labelContainer"/>
|
| 862 | <div id="drawContainer"/>
|
| 863 | <div id="console-container"/>
|
| 864 | <div style="clear: left;"/>
|
| 865 |
|
| 866 | <h1 class="expand" data-content-id="expandContent" style="font-size: 1.3em;"
|
| 867 | >About Drawboard WebSocket Example</h1>
|
| 868 | <div id="expandContent">
|
| 869 | <p>
|
| 870 | This drawboard is a page where you can draw with your mouse or touch input
|
| 871 | (using different colors) and everybody else which has the page open will
|
| 872 | <em>immediately</em> see what you are drawing.<br/>
|
| 873 | If someone opens the page later, they will get the current room image (so they
|
| 874 | can see what was already drawn by other people).
|
| 875 | </p>
|
| 876 | <p>
|
| 877 | It uses asynchronous sending of messages so that it doesn't need separate threads
|
| 878 | for each client to send messages (this needs NIO or APR connector to be used).<br/>
|
| 879 | Each "Room" (where the drawing happens) uses a ReentrantLock to synchronize access
|
| 880 | (currently, only a single Room is implemented).
|
| 881 | </p>
|
| 882 | <p>
|
| 883 | When you open the page, first you will receive a binary websocket message containing
|
| 884 | the current room image as PNG image. After that, you will receive string messages
|
| 885 | that contain the drawing actions (line from x1,y1 to x2,y2).<br/>
|
| 886 | <small>Note that it currently only uses simple string messages instead of JSON because
|
| 887 | I did not want to introduce a dependency on a JSON lib.</small>
|
| 888 | </p>
|
| 889 | <p>
|
| 890 | It uses synchronization mechanisms to ensure that the final image will look the same
|
| 891 | for every user, regardless of what their network latency/speed is – e.g. if two user
|
| 892 | draw at the same time on the same place, the server will decide which line was the
|
| 893 | first one, and that will be reflected on every client.
|
| 894 | </p>
|
| 895 | </div>
|
| 896 | </body>
|
| 897 | </html> |