1 /* 2 * Copyright (C) 2014 Glyptodon LLC 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 var Guacamole = Guacamole || {}; 24 25 /** 26 * The Guacamole display. The display does not deal with the Guacamole 27 * protocol, and instead implements a set of graphical operations which 28 * embody the set of operations present in the protocol. The order operations 29 * are executed is guaranteed to be in the same order as their corresponding 30 * functions are called. 31 * 32 * @constructor 33 */ 34 Guacamole.Display = function() { 35 36 /** 37 * Reference to this Guacamole.Display. 38 * @private 39 */ 40 var guac_display = this; 41 42 var displayWidth = 0; 43 var displayHeight = 0; 44 var displayScale = 1; 45 46 // Create display 47 var display = document.createElement("div"); 48 display.style.position = "relative"; 49 display.style.width = displayWidth + "px"; 50 display.style.height = displayHeight + "px"; 51 52 // Ensure transformations on display originate at 0,0 53 display.style.transformOrigin = 54 display.style.webkitTransformOrigin = 55 display.style.MozTransformOrigin = 56 display.style.OTransformOrigin = 57 display.style.msTransformOrigin = 58 "0 0"; 59 60 // Create default layer 61 var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); 62 63 // Create cursor layer 64 var cursor = new Guacamole.Display.VisibleLayer(0, 0); 65 cursor.setChannelMask(Guacamole.Layer.SRC); 66 67 // Add default layer and cursor to display 68 display.appendChild(default_layer.getElement()); 69 display.appendChild(cursor.getElement()); 70 71 // Create bounding div 72 var bounds = document.createElement("div"); 73 bounds.style.position = "relative"; 74 bounds.style.width = (displayWidth*displayScale) + "px"; 75 bounds.style.height = (displayHeight*displayScale) + "px"; 76 77 // Add display to bounds 78 bounds.appendChild(display); 79 80 /** 81 * The X coordinate of the hotspot of the mouse cursor. The hotspot is 82 * the relative location within the image of the mouse cursor at which 83 * each click occurs. 84 * 85 * @type Number 86 */ 87 this.cursorHotspotX = 0; 88 89 /** 90 * The Y coordinate of the hotspot of the mouse cursor. The hotspot is 91 * the relative location within the image of the mouse cursor at which 92 * each click occurs. 93 * 94 * @type Number 95 */ 96 this.cursorHotspotY = 0; 97 98 /** 99 * The current X coordinate of the local mouse cursor. This is not 100 * necessarily the location of the actual mouse - it refers only to 101 * the location of the cursor image within the Guacamole display, as 102 * last set by moveCursor(). 103 * 104 * @type Number 105 */ 106 this.cursorX = 0; 107 108 /** 109 * The current X coordinate of the local mouse cursor. This is not 110 * necessarily the location of the actual mouse - it refers only to 111 * the location of the cursor image within the Guacamole display, as 112 * last set by moveCursor(). 113 * 114 * @type Number 115 */ 116 this.cursorY = 0; 117 118 /** 119 * Fired when the default layer (and thus the entire Guacamole display) 120 * is resized. 121 * 122 * @event 123 * @param {Number} width The new width of the Guacamole display. 124 * @param {Number} height The new height of the Guacamole display. 125 */ 126 this.onresize = null; 127 128 /** 129 * Fired whenever the local cursor image is changed. This can be used to 130 * implement special handling of the client-side cursor, or to override 131 * the default use of a software cursor layer. 132 * 133 * @event 134 * @param {HTMLCanvasElement} canvas The cursor image. 135 * @param {Number} x The X-coordinate of the cursor hotspot. 136 * @param {Number} y The Y-coordinate of the cursor hotspot. 137 */ 138 this.oncursor = null; 139 140 /** 141 * The queue of all pending Tasks. Tasks will be run in order, with new 142 * tasks added at the end of the queue and old tasks removed from the 143 * front of the queue (FIFO). These tasks will eventually be grouped 144 * into a Frame. 145 * @private 146 * @type Task[] 147 */ 148 var tasks = []; 149 150 /** 151 * The queue of all frames. Each frame is a pairing of an array of tasks 152 * and a callback which must be called when the frame is rendered. 153 * @private 154 * @type Frame[] 155 */ 156 var frames = []; 157 158 /** 159 * Flushes all pending frames. 160 * @private 161 */ 162 function __flush_frames() { 163 164 var rendered_frames = 0; 165 166 // Draw all pending frames, if ready 167 while (rendered_frames < frames.length) { 168 169 var frame = frames[rendered_frames]; 170 if (!frame.isReady()) 171 break; 172 173 frame.flush(); 174 rendered_frames++; 175 176 } 177 178 // Remove rendered frames from array 179 frames.splice(0, rendered_frames); 180 181 } 182 183 /** 184 * An ordered list of tasks which must be executed atomically. Once 185 * executed, an associated (and optional) callback will be called. 186 * 187 * @private 188 * @constructor 189 * @param {function} callback The function to call when this frame is 190 * rendered. 191 * @param {Task[]} tasks The set of tasks which must be executed to render 192 * this frame. 193 */ 194 function Frame(callback, tasks) { 195 196 /** 197 * Returns whether this frame is ready to be rendered. This function 198 * returns true if and only if ALL underlying tasks are unblocked. 199 * 200 * @returns {Boolean} true if all underlying tasks are unblocked, 201 * false otherwise. 202 */ 203 this.isReady = function() { 204 205 // Search for blocked tasks 206 for (var i=0; i < tasks.length; i++) { 207 if (tasks[i].blocked) 208 return false; 209 } 210 211 // If no blocked tasks, the frame is ready 212 return true; 213 214 }; 215 216 /** 217 * Renders this frame, calling the associated callback, if any, after 218 * the frame is complete. This function MUST only be called when no 219 * blocked tasks exist. Calling this function with blocked tasks 220 * will result in undefined behavior. 221 */ 222 this.flush = function() { 223 224 // Draw all pending tasks. 225 for (var i=0; i < tasks.length; i++) 226 tasks[i].execute(); 227 228 // Call callback 229 if (callback) callback(); 230 231 }; 232 233 } 234 235 /** 236 * A container for an task handler. Each operation which must be ordered 237 * is associated with a Task that goes into a task queue. Tasks in this 238 * queue are executed in order once their handlers are set, while Tasks 239 * without handlers block themselves and any following Tasks from running. 240 * 241 * @constructor 242 * @private 243 * @param {function} taskHandler The function to call when this task 244 * runs, if any. 245 * @param {boolean} blocked Whether this task should start blocked. 246 */ 247 function Task(taskHandler, blocked) { 248 249 var task = this; 250 251 /** 252 * Whether this Task is blocked. 253 * 254 * @type boolean 255 */ 256 this.blocked = blocked; 257 258 /** 259 * Unblocks this Task, allowing it to run. 260 */ 261 this.unblock = function() { 262 if (task.blocked) { 263 task.blocked = false; 264 __flush_frames(); 265 } 266 }; 267 268 /** 269 * Calls the handler associated with this task IMMEDIATELY. This 270 * function does not track whether this task is marked as blocked. 271 * Enforcing the blocked status of tasks is up to the caller. 272 */ 273 this.execute = function() { 274 if (taskHandler) taskHandler(); 275 }; 276 277 } 278 279 /** 280 * Schedules a task for future execution. The given handler will execute 281 * immediately after all previous tasks upon frame flush, unless this 282 * task is blocked. If any tasks is blocked, the entire frame will not 283 * render (and no tasks within will execute) until all tasks are unblocked. 284 * 285 * @private 286 * @param {function} handler The function to call when possible, if any. 287 * @param {boolean} blocked Whether the task should start blocked. 288 * @returns {Task} The Task created and added to the queue for future 289 * running. 290 */ 291 function scheduleTask(handler, blocked) { 292 var task = new Task(handler, blocked); 293 tasks.push(task); 294 return task; 295 } 296 297 /** 298 * Returns the element which contains the Guacamole display. 299 * 300 * @return {Element} The element containing the Guacamole display. 301 */ 302 this.getElement = function() { 303 return bounds; 304 }; 305 306 /** 307 * Returns the width of this display. 308 * 309 * @return {Number} The width of this display; 310 */ 311 this.getWidth = function() { 312 return displayWidth; 313 }; 314 315 /** 316 * Returns the height of this display. 317 * 318 * @return {Number} The height of this display; 319 */ 320 this.getHeight = function() { 321 return displayHeight; 322 }; 323 324 /** 325 * Returns the default layer of this display. Each Guacamole display always 326 * has at least one layer. Other layers can optionally be created within 327 * this layer, but the default layer cannot be removed and is the absolute 328 * ancestor of all other layers. 329 * 330 * @return {Guacamole.Display.VisibleLayer} The default layer. 331 */ 332 this.getDefaultLayer = function() { 333 return default_layer; 334 }; 335 336 /** 337 * Returns the cursor layer of this display. Each Guacamole display contains 338 * a layer for the image of the mouse cursor. This layer is a special case 339 * and exists above all other layers, similar to the hardware mouse cursor. 340 * 341 * @return {Guacamole.Display.VisibleLayer} The cursor layer. 342 */ 343 this.getCursorLayer = function() { 344 return cursor; 345 }; 346 347 /** 348 * Creates a new layer. The new layer will be a direct child of the default 349 * layer, but can be moved to be a child of any other layer. Layers returned 350 * by this function are visible. 351 * 352 * @return {Guacamole.Display.VisibleLayer} The newly-created layer. 353 */ 354 this.createLayer = function() { 355 var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); 356 layer.move(default_layer, 0, 0, 0); 357 return layer; 358 }; 359 360 /** 361 * Creates a new buffer. Buffers are invisible, off-screen surfaces. They 362 * are implemented in the same manner as layers, but do not provide the 363 * same nesting semantics. 364 * 365 * @return {Guacamole.Layer} The newly-created buffer. 366 */ 367 this.createBuffer = function() { 368 var buffer = new Guacamole.Layer(0, 0); 369 buffer.autosize = 1; 370 return buffer; 371 }; 372 373 /** 374 * Flush all pending draw tasks, if possible, as a new frame. If the entire 375 * frame is not ready, the flush will wait until all required tasks are 376 * unblocked. 377 * 378 * @param {function} callback The function to call when this frame is 379 * flushed. This may happen immediately, or 380 * later when blocked tasks become unblocked. 381 */ 382 this.flush = function(callback) { 383 384 // Add frame, reset tasks 385 frames.push(new Frame(callback, tasks)); 386 tasks = []; 387 388 // Attempt flush 389 __flush_frames(); 390 391 }; 392 393 /** 394 * Sets the hotspot and image of the mouse cursor displayed within the 395 * Guacamole display. 396 * 397 * @param {Number} hotspotX The X coordinate of the cursor hotspot. 398 * @param {Number} hotspotY The Y coordinate of the cursor hotspot. 399 * @param {Guacamole.Layer} layer The source layer containing the data which 400 * should be used as the mouse cursor image. 401 * @param {Number} srcx The X coordinate of the upper-left corner of the 402 * rectangle within the source layer's coordinate 403 * space to copy data from. 404 * @param {Number} srcy The Y coordinate of the upper-left corner of the 405 * rectangle within the source layer's coordinate 406 * space to copy data from. 407 * @param {Number} srcw The width of the rectangle within the source layer's 408 * coordinate space to copy data from. 409 * @param {Number} srch The height of the rectangle within the source 410 * layer's coordinate space to copy data from. 411 412 */ 413 this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) { 414 scheduleTask(function __display_set_cursor() { 415 416 // Set hotspot 417 guac_display.cursorHotspotX = hotspotX; 418 guac_display.cursorHotspotY = hotspotY; 419 420 // Reset cursor size 421 cursor.resize(srcw, srch); 422 423 // Draw cursor to cursor layer 424 cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0); 425 guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY); 426 427 // Fire cursor change event 428 if (guac_display.oncursor) 429 guac_display.oncursor(cursor.getCanvas(), hotspotX, hotspotY); 430 431 }); 432 }; 433 434 /** 435 * Sets whether the software-rendered cursor is shown. This cursor differs 436 * from the hardware cursor in that it is built into the Guacamole.Display, 437 * and relies on its own Guacamole layer to render. 438 * 439 * @param {Boolean} [shown=true] Whether to show the software cursor. 440 */ 441 this.showCursor = function(shown) { 442 443 var element = cursor.getElement(); 444 var parent = element.parentNode; 445 446 // Remove from DOM if hidden 447 if (shown === false) { 448 if (parent) 449 parent.removeChild(element); 450 } 451 452 // Otherwise, ensure cursor is child of display 453 else if (parent !== display) 454 display.appendChild(element); 455 456 }; 457 458 /** 459 * Sets the location of the local cursor to the given coordinates. For the 460 * sake of responsiveness, this function performs its action immediately. 461 * Cursor motion is not maintained within atomic frames. 462 * 463 * @param {Number} x The X coordinate to move the cursor to. 464 * @param {Number} y The Y coordinate to move the cursor to. 465 */ 466 this.moveCursor = function(x, y) { 467 468 // Move cursor layer 469 cursor.translate(x - guac_display.cursorHotspotX, 470 y - guac_display.cursorHotspotY); 471 472 // Update stored position 473 guac_display.cursorX = x; 474 guac_display.cursorY = y; 475 476 }; 477 478 /** 479 * Changes the size of the given Layer to the given width and height. 480 * Resizing is only attempted if the new size provided is actually different 481 * from the current size. 482 * 483 * @param {Guacamole.Layer} layer The layer to resize. 484 * @param {Number} width The new width. 485 * @param {Number} height The new height. 486 */ 487 this.resize = function(layer, width, height) { 488 scheduleTask(function __display_resize() { 489 490 layer.resize(width, height); 491 492 // Resize display if default layer is resized 493 if (layer === default_layer) { 494 495 // Update (set) display size 496 displayWidth = width; 497 displayHeight = height; 498 display.style.width = displayWidth + "px"; 499 display.style.height = displayHeight + "px"; 500 501 // Update bounds size 502 bounds.style.width = (displayWidth*displayScale) + "px"; 503 bounds.style.height = (displayHeight*displayScale) + "px"; 504 505 // Notify of resize 506 if (guac_display.onresize) 507 guac_display.onresize(width, height); 508 509 } 510 511 }); 512 }; 513 514 /** 515 * Draws the specified image at the given coordinates. The image specified 516 * must already be loaded. 517 * 518 * @param {Guacamole.Layer} layer The layer to draw upon. 519 * @param {Number} x The destination X coordinate. 520 * @param {Number} y The destination Y coordinate. 521 * @param {Image} image The image to draw. Note that this is an Image 522 * object - not a URL. 523 */ 524 this.drawImage = function(layer, x, y, image) { 525 scheduleTask(function __display_drawImage() { 526 layer.drawImage(x, y, image); 527 }); 528 }; 529 530 /** 531 * Draws the image contained within the specified Blob at the given 532 * coordinates. The Blob specified must already be populated with image 533 * data. 534 * 535 * @param {Guacamole.Layer} layer 536 * The layer to draw upon. 537 * 538 * @param {Number} x 539 * The destination X coordinate. 540 * 541 * @param {Number} y 542 * The destination Y coordinate. 543 * 544 * @param {Blob} blob 545 * The Blob containing the image data to draw. 546 */ 547 this.drawBlob = function(layer, x, y, blob) { 548 549 // Create URL for blob 550 var url = URL.createObjectURL(blob); 551 552 // Draw and free blob URL when ready 553 var task = scheduleTask(function __display_drawBlob() { 554 layer.drawImage(x, y, image); 555 URL.revokeObjectURL(url); 556 }, true); 557 558 // Load image from URL 559 var image = new Image(); 560 image.onload = task.unblock; 561 image.src = url; 562 563 }; 564 565 /** 566 * Draws the image at the specified URL at the given coordinates. The image 567 * will be loaded automatically, and this and any future operations will 568 * wait for the image to finish loading. 569 * 570 * @param {Guacamole.Layer} layer The layer to draw upon. 571 * @param {Number} x The destination X coordinate. 572 * @param {Number} y The destination Y coordinate. 573 * @param {String} url The URL of the image to draw. 574 */ 575 this.draw = function(layer, x, y, url) { 576 577 var task = scheduleTask(function __display_draw() { 578 layer.drawImage(x, y, image); 579 }, true); 580 581 var image = new Image(); 582 image.onload = task.unblock; 583 image.src = url; 584 585 }; 586 587 /** 588 * Plays the video at the specified URL within this layer. The video 589 * will be loaded automatically, and this and any future operations will 590 * wait for the video to finish loading. Future operations will not be 591 * executed until the video finishes playing. 592 * 593 * @param {Guacamole.Layer} layer The layer to draw upon. 594 * @param {String} mimetype The mimetype of the video to play. 595 * @param {Number} duration The duration of the video in milliseconds. 596 * @param {String} url The URL of the video to play. 597 */ 598 this.play = function(layer, mimetype, duration, url) { 599 600 // Start loading the video 601 var video = document.createElement("video"); 602 video.type = mimetype; 603 video.src = url; 604 605 // Start copying frames when playing 606 video.addEventListener("play", function() { 607 608 function render_callback() { 609 layer.drawImage(0, 0, video); 610 if (!video.ended) 611 window.setTimeout(render_callback, 20); 612 } 613 614 render_callback(); 615 616 }, false); 617 618 scheduleTask(video.play); 619 620 }; 621 622 /** 623 * Transfer a rectangle of image data from one Layer to this Layer using the 624 * specified transfer function. 625 * 626 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 627 * @param {Number} srcx The X coordinate of the upper-left corner of the 628 * rectangle within the source Layer's coordinate 629 * space to copy data from. 630 * @param {Number} srcy The Y coordinate of the upper-left corner of the 631 * rectangle within the source Layer's coordinate 632 * space to copy data from. 633 * @param {Number} srcw The width of the rectangle within the source Layer's 634 * coordinate space to copy data from. 635 * @param {Number} srch The height of the rectangle within the source 636 * Layer's coordinate space to copy data from. 637 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 638 * @param {Number} x The destination X coordinate. 639 * @param {Number} y The destination Y coordinate. 640 * @param {Function} transferFunction The transfer function to use to 641 * transfer data from source to 642 * destination. 643 */ 644 this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) { 645 scheduleTask(function __display_transfer() { 646 dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction); 647 }); 648 }; 649 650 /** 651 * Put a rectangle of image data from one Layer to this Layer directly 652 * without performing any alpha blending. Simply copy the data. 653 * 654 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 655 * @param {Number} srcx The X coordinate of the upper-left corner of the 656 * rectangle within the source Layer's coordinate 657 * space to copy data from. 658 * @param {Number} srcy The Y coordinate of the upper-left corner of the 659 * rectangle within the source Layer's coordinate 660 * space to copy data from. 661 * @param {Number} srcw The width of the rectangle within the source Layer's 662 * coordinate space to copy data from. 663 * @param {Number} srch The height of the rectangle within the source 664 * Layer's coordinate space to copy data from. 665 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 666 * @param {Number} x The destination X coordinate. 667 * @param {Number} y The destination Y coordinate. 668 */ 669 this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 670 scheduleTask(function __display_put() { 671 dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); 672 }); 673 }; 674 675 /** 676 * Copy a rectangle of image data from one Layer to this Layer. This 677 * operation will copy exactly the image data that will be drawn once all 678 * operations of the source Layer that were pending at the time this 679 * function was called are complete. This operation will not alter the 680 * size of the source Layer even if its autosize property is set to true. 681 * 682 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 683 * @param {Number} srcx The X coordinate of the upper-left corner of the 684 * rectangle within the source Layer's coordinate 685 * space to copy data from. 686 * @param {Number} srcy The Y coordinate of the upper-left corner of the 687 * rectangle within the source Layer's coordinate 688 * space to copy data from. 689 * @param {Number} srcw The width of the rectangle within the source Layer's 690 * coordinate space to copy data from. 691 * @param {Number} srch The height of the rectangle within the source 692 * Layer's coordinate space to copy data from. 693 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 694 * @param {Number} x The destination X coordinate. 695 * @param {Number} y The destination Y coordinate. 696 */ 697 this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 698 scheduleTask(function __display_copy() { 699 dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); 700 }); 701 }; 702 703 /** 704 * Starts a new path at the specified point. 705 * 706 * @param {Guacamole.Layer} layer The layer to draw upon. 707 * @param {Number} x The X coordinate of the point to draw. 708 * @param {Number} y The Y coordinate of the point to draw. 709 */ 710 this.moveTo = function(layer, x, y) { 711 scheduleTask(function __display_moveTo() { 712 layer.moveTo(x, y); 713 }); 714 }; 715 716 /** 717 * Add the specified line to the current path. 718 * 719 * @param {Guacamole.Layer} layer The layer to draw upon. 720 * @param {Number} x The X coordinate of the endpoint of the line to draw. 721 * @param {Number} y The Y coordinate of the endpoint of the line to draw. 722 */ 723 this.lineTo = function(layer, x, y) { 724 scheduleTask(function __display_lineTo() { 725 layer.lineTo(x, y); 726 }); 727 }; 728 729 /** 730 * Add the specified arc to the current path. 731 * 732 * @param {Guacamole.Layer} layer The layer to draw upon. 733 * @param {Number} x The X coordinate of the center of the circle which 734 * will contain the arc. 735 * @param {Number} y The Y coordinate of the center of the circle which 736 * will contain the arc. 737 * @param {Number} radius The radius of the circle. 738 * @param {Number} startAngle The starting angle of the arc, in radians. 739 * @param {Number} endAngle The ending angle of the arc, in radians. 740 * @param {Boolean} negative Whether the arc should be drawn in order of 741 * decreasing angle. 742 */ 743 this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { 744 scheduleTask(function __display_arc() { 745 layer.arc(x, y, radius, startAngle, endAngle, negative); 746 }); 747 }; 748 749 /** 750 * Starts a new path at the specified point. 751 * 752 * @param {Guacamole.Layer} layer The layer to draw upon. 753 * @param {Number} cp1x The X coordinate of the first control point. 754 * @param {Number} cp1y The Y coordinate of the first control point. 755 * @param {Number} cp2x The X coordinate of the second control point. 756 * @param {Number} cp2y The Y coordinate of the second control point. 757 * @param {Number} x The X coordinate of the endpoint of the curve. 758 * @param {Number} y The Y coordinate of the endpoint of the curve. 759 */ 760 this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { 761 scheduleTask(function __display_curveTo() { 762 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); 763 }); 764 }; 765 766 /** 767 * Closes the current path by connecting the end point with the start 768 * point (if any) with a straight line. 769 * 770 * @param {Guacamole.Layer} layer The layer to draw upon. 771 */ 772 this.close = function(layer) { 773 scheduleTask(function __display_close() { 774 layer.close(); 775 }); 776 }; 777 778 /** 779 * Add the specified rectangle to the current path. 780 * 781 * @param {Guacamole.Layer} layer The layer to draw upon. 782 * @param {Number} x The X coordinate of the upper-left corner of the 783 * rectangle to draw. 784 * @param {Number} y The Y coordinate of the upper-left corner of the 785 * rectangle to draw. 786 * @param {Number} w The width of the rectangle to draw. 787 * @param {Number} h The height of the rectangle to draw. 788 */ 789 this.rect = function(layer, x, y, w, h) { 790 scheduleTask(function __display_rect() { 791 layer.rect(x, y, w, h); 792 }); 793 }; 794 795 /** 796 * Clip all future drawing operations by the current path. The current path 797 * is implicitly closed. The current path can continue to be reused 798 * for other operations (such as fillColor()) but a new path will be started 799 * once a path drawing operation (path() or rect()) is used. 800 * 801 * @param {Guacamole.Layer} layer The layer to affect. 802 */ 803 this.clip = function(layer) { 804 scheduleTask(function __display_clip() { 805 layer.clip(); 806 }); 807 }; 808 809 /** 810 * Stroke the current path with the specified color. The current path 811 * is implicitly closed. The current path can continue to be reused 812 * for other operations (such as clip()) but a new path will be started 813 * once a path drawing operation (path() or rect()) is used. 814 * 815 * @param {Guacamole.Layer} layer The layer to draw upon. 816 * @param {String} cap The line cap style. Can be "round", "square", 817 * or "butt". 818 * @param {String} join The line join style. Can be "round", "bevel", 819 * or "miter". 820 * @param {Number} thickness The line thickness in pixels. 821 * @param {Number} r The red component of the color to fill. 822 * @param {Number} g The green component of the color to fill. 823 * @param {Number} b The blue component of the color to fill. 824 * @param {Number} a The alpha component of the color to fill. 825 */ 826 this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { 827 scheduleTask(function __display_strokeColor() { 828 layer.strokeColor(cap, join, thickness, r, g, b, a); 829 }); 830 }; 831 832 /** 833 * Fills the current path with the specified color. The current path 834 * is implicitly closed. The current path can continue to be reused 835 * for other operations (such as clip()) but a new path will be started 836 * once a path drawing operation (path() or rect()) is used. 837 * 838 * @param {Guacamole.Layer} layer The layer to draw upon. 839 * @param {Number} r The red component of the color to fill. 840 * @param {Number} g The green component of the color to fill. 841 * @param {Number} b The blue component of the color to fill. 842 * @param {Number} a The alpha component of the color to fill. 843 */ 844 this.fillColor = function(layer, r, g, b, a) { 845 scheduleTask(function __display_fillColor() { 846 layer.fillColor(r, g, b, a); 847 }); 848 }; 849 850 /** 851 * Stroke the current path with the image within the specified layer. The 852 * image data will be tiled infinitely within the stroke. The current path 853 * is implicitly closed. The current path can continue to be reused 854 * for other operations (such as clip()) but a new path will be started 855 * once a path drawing operation (path() or rect()) is used. 856 * 857 * @param {Guacamole.Layer} layer The layer to draw upon. 858 * @param {String} cap The line cap style. Can be "round", "square", 859 * or "butt". 860 * @param {String} join The line join style. Can be "round", "bevel", 861 * or "miter". 862 * @param {Number} thickness The line thickness in pixels. 863 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 864 * within the stroke. 865 */ 866 this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { 867 scheduleTask(function __display_strokeLayer() { 868 layer.strokeLayer(cap, join, thickness, srcLayer); 869 }); 870 }; 871 872 /** 873 * Fills the current path with the image within the specified layer. The 874 * image data will be tiled infinitely within the stroke. The current path 875 * is implicitly closed. The current path can continue to be reused 876 * for other operations (such as clip()) but a new path will be started 877 * once a path drawing operation (path() or rect()) is used. 878 * 879 * @param {Guacamole.Layer} layer The layer to draw upon. 880 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 881 * within the fill. 882 */ 883 this.fillLayer = function(layer, srcLayer) { 884 scheduleTask(function __display_fillLayer() { 885 layer.fillLayer(srcLayer); 886 }); 887 }; 888 889 /** 890 * Push current layer state onto stack. 891 * 892 * @param {Guacamole.Layer} layer The layer to draw upon. 893 */ 894 this.push = function(layer) { 895 scheduleTask(function __display_push() { 896 layer.push(); 897 }); 898 }; 899 900 /** 901 * Pop layer state off stack. 902 * 903 * @param {Guacamole.Layer} layer The layer to draw upon. 904 */ 905 this.pop = function(layer) { 906 scheduleTask(function __display_pop() { 907 layer.pop(); 908 }); 909 }; 910 911 /** 912 * Reset the layer, clearing the stack, the current path, and any transform 913 * matrix. 914 * 915 * @param {Guacamole.Layer} layer The layer to draw upon. 916 */ 917 this.reset = function(layer) { 918 scheduleTask(function __display_reset() { 919 layer.reset(); 920 }); 921 }; 922 923 /** 924 * Sets the given affine transform (defined with six values from the 925 * transform's matrix). 926 * 927 * @param {Guacamole.Layer} layer The layer to modify. 928 * @param {Number} a The first value in the affine transform's matrix. 929 * @param {Number} b The second value in the affine transform's matrix. 930 * @param {Number} c The third value in the affine transform's matrix. 931 * @param {Number} d The fourth value in the affine transform's matrix. 932 * @param {Number} e The fifth value in the affine transform's matrix. 933 * @param {Number} f The sixth value in the affine transform's matrix. 934 */ 935 this.setTransform = function(layer, a, b, c, d, e, f) { 936 scheduleTask(function __display_setTransform() { 937 layer.setTransform(a, b, c, d, e, f); 938 }); 939 }; 940 941 /** 942 * Applies the given affine transform (defined with six values from the 943 * transform's matrix). 944 * 945 * @param {Guacamole.Layer} layer The layer to modify. 946 * @param {Number} a The first value in the affine transform's matrix. 947 * @param {Number} b The second value in the affine transform's matrix. 948 * @param {Number} c The third value in the affine transform's matrix. 949 * @param {Number} d The fourth value in the affine transform's matrix. 950 * @param {Number} e The fifth value in the affine transform's matrix. 951 * @param {Number} f The sixth value in the affine transform's matrix. 952 */ 953 this.transform = function(layer, a, b, c, d, e, f) { 954 scheduleTask(function __display_transform() { 955 layer.transform(a, b, c, d, e, f); 956 }); 957 }; 958 959 /** 960 * Sets the channel mask for future operations on this Layer. 961 * 962 * The channel mask is a Guacamole-specific compositing operation identifier 963 * with a single bit representing each of four channels (in order): source 964 * image where destination transparent, source where destination opaque, 965 * destination where source transparent, and destination where source 966 * opaque. 967 * 968 * @param {Guacamole.Layer} layer The layer to modify. 969 * @param {Number} mask The channel mask for future operations on this 970 * Layer. 971 */ 972 this.setChannelMask = function(layer, mask) { 973 scheduleTask(function __display_setChannelMask() { 974 layer.setChannelMask(mask); 975 }); 976 }; 977 978 /** 979 * Sets the miter limit for stroke operations using the miter join. This 980 * limit is the maximum ratio of the size of the miter join to the stroke 981 * width. If this ratio is exceeded, the miter will not be drawn for that 982 * joint of the path. 983 * 984 * @param {Guacamole.Layer} layer The layer to modify. 985 * @param {Number} limit The miter limit for stroke operations using the 986 * miter join. 987 */ 988 this.setMiterLimit = function(layer, limit) { 989 scheduleTask(function __display_setMiterLimit() { 990 layer.setMiterLimit(limit); 991 }); 992 }; 993 994 /** 995 * Sets the scale of the client display element such that it renders at 996 * a relatively smaller or larger size, without affecting the true 997 * resolution of the display. 998 * 999 * @param {Number} scale The scale to resize to, where 1.0 is normal 1000 * size (1:1 scale). 1001 */ 1002 this.scale = function(scale) { 1003 1004 display.style.transform = 1005 display.style.WebkitTransform = 1006 display.style.MozTransform = 1007 display.style.OTransform = 1008 display.style.msTransform = 1009 1010 "scale(" + scale + "," + scale + ")"; 1011 1012 displayScale = scale; 1013 1014 // Update bounds size 1015 bounds.style.width = (displayWidth*displayScale) + "px"; 1016 bounds.style.height = (displayHeight*displayScale) + "px"; 1017 1018 }; 1019 1020 /** 1021 * Returns the scale of the display. 1022 * 1023 * @return {Number} The scale of the display. 1024 */ 1025 this.getScale = function() { 1026 return displayScale; 1027 }; 1028 1029 /** 1030 * Returns a canvas element containing the entire display, with all child 1031 * layers composited within. 1032 * 1033 * @return {HTMLCanvasElement} A new canvas element containing a copy of 1034 * the display. 1035 */ 1036 this.flatten = function() { 1037 1038 // Get destination canvas 1039 var canvas = document.createElement("canvas"); 1040 canvas.width = default_layer.width; 1041 canvas.height = default_layer.height; 1042 1043 var context = canvas.getContext("2d"); 1044 1045 // Returns sorted array of children 1046 function get_children(layer) { 1047 1048 // Build array of children 1049 var children = []; 1050 for (var index in layer.children) 1051 children.push(layer.children[index]); 1052 1053 // Sort 1054 children.sort(function children_comparator(a, b) { 1055 1056 // Compare based on Z order 1057 var diff = a.z - b.z; 1058 if (diff !== 0) 1059 return diff; 1060 1061 // If Z order identical, use document order 1062 var a_element = a.getElement(); 1063 var b_element = b.getElement(); 1064 var position = b_element.compareDocumentPosition(a_element); 1065 1066 if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; 1067 if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; 1068 1069 // Otherwise, assume same 1070 return 0; 1071 1072 }); 1073 1074 // Done 1075 return children; 1076 1077 } 1078 1079 // Draws the contents of the given layer at the given coordinates 1080 function draw_layer(layer, x, y) { 1081 1082 // Draw layer 1083 if (layer.width > 0 && layer.height > 0) { 1084 1085 // Save and update alpha 1086 var initial_alpha = context.globalAlpha; 1087 context.globalAlpha *= layer.alpha / 255.0; 1088 1089 // Copy data 1090 context.drawImage(layer.getCanvas(), x, y); 1091 1092 // Draw all children 1093 var children = get_children(layer); 1094 for (var i=0; i<children.length; i++) { 1095 var child = children[i]; 1096 draw_layer(child, x + child.x, y + child.y); 1097 } 1098 1099 // Restore alpha 1100 context.globalAlpha = initial_alpha; 1101 1102 } 1103 1104 } 1105 1106 // Draw default layer and all children 1107 draw_layer(default_layer, 0, 0); 1108 1109 // Return new canvas copy 1110 return canvas; 1111 1112 }; 1113 1114 }; 1115 1116 /** 1117 * Simple container for Guacamole.Layer, allowing layers to be easily 1118 * repositioned and nested. This allows certain operations to be accelerated 1119 * through DOM manipulation, rather than raster operations. 1120 * 1121 * @constructor 1122 * @augments Guacamole.Layer 1123 * @param {Number} width The width of the Layer, in pixels. The canvas element 1124 * backing this Layer will be given this width. 1125 * @param {Number} height The height of the Layer, in pixels. The canvas element 1126 * backing this Layer will be given this height. 1127 */ 1128 Guacamole.Display.VisibleLayer = function(width, height) { 1129 1130 Guacamole.Layer.apply(this, [width, height]); 1131 1132 /** 1133 * Reference to this layer. 1134 * @private 1135 */ 1136 var layer = this; 1137 1138 /** 1139 * Identifier which uniquely identifies this layer. This is COMPLETELY 1140 * UNRELATED to the index of the underlying layer, which is specific 1141 * to the Guacamole protocol, and not relevant at this level. 1142 * 1143 * @private 1144 * @type Number 1145 */ 1146 this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++; 1147 1148 /** 1149 * The opacity of the layer container, where 255 is fully opaque and 0 is 1150 * fully transparent. 1151 */ 1152 this.alpha = 0xFF; 1153 1154 /** 1155 * X coordinate of the upper-left corner of this layer container within 1156 * its parent, in pixels. 1157 * @type Number 1158 */ 1159 this.x = 0; 1160 1161 /** 1162 * Y coordinate of the upper-left corner of this layer container within 1163 * its parent, in pixels. 1164 * @type Number 1165 */ 1166 this.y = 0; 1167 1168 /** 1169 * Z stacking order of this layer relative to other sibling layers. 1170 * @type Number 1171 */ 1172 this.z = 0; 1173 1174 /** 1175 * The affine transformation applied to this layer container. Each element 1176 * corresponds to a value from the transformation matrix, with the first 1177 * three values being the first row, and the last three values being the 1178 * second row. There are six values total. 1179 * 1180 * @type Number[] 1181 */ 1182 this.matrix = [1, 0, 0, 1, 0, 0]; 1183 1184 /** 1185 * The parent layer container of this layer, if any. 1186 * @type Guacamole.Display.LayerContainer 1187 */ 1188 this.parent = null; 1189 1190 /** 1191 * Set of all children of this layer, indexed by layer index. This object 1192 * will have one property per child. 1193 */ 1194 this.children = {}; 1195 1196 // Set layer position 1197 var canvas = layer.getCanvas(); 1198 canvas.style.position = "absolute"; 1199 canvas.style.left = "0px"; 1200 canvas.style.top = "0px"; 1201 1202 // Create div with given size 1203 var div = document.createElement("div"); 1204 div.appendChild(canvas); 1205 div.style.width = width + "px"; 1206 div.style.height = height + "px"; 1207 div.style.position = "absolute"; 1208 div.style.left = "0px"; 1209 div.style.top = "0px"; 1210 div.style.overflow = "hidden"; 1211 1212 /** 1213 * Superclass resize() function. 1214 * @private 1215 */ 1216 var __super_resize = this.resize; 1217 1218 this.resize = function(width, height) { 1219 1220 // Resize containing div 1221 div.style.width = width + "px"; 1222 div.style.height = height + "px"; 1223 1224 __super_resize(width, height); 1225 1226 }; 1227 1228 /** 1229 * Returns the element containing the canvas and any other elements 1230 * associated with this layer. 1231 * @returns {Element} The element containing this layer's canvas. 1232 */ 1233 this.getElement = function() { 1234 return div; 1235 }; 1236 1237 /** 1238 * The translation component of this layer's transform. 1239 * @private 1240 */ 1241 var translate = "translate(0px, 0px)"; // (0, 0) 1242 1243 /** 1244 * The arbitrary matrix component of this layer's transform. 1245 * @private 1246 */ 1247 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity 1248 1249 /** 1250 * Moves the upper-left corner of this layer to the given X and Y 1251 * coordinate. 1252 * 1253 * @param {Number} x The X coordinate to move to. 1254 * @param {Number} y The Y coordinate to move to. 1255 */ 1256 this.translate = function(x, y) { 1257 1258 layer.x = x; 1259 layer.y = y; 1260 1261 // Generate translation 1262 translate = "translate(" 1263 + x + "px," 1264 + y + "px)"; 1265 1266 // Set layer transform 1267 div.style.transform = 1268 div.style.WebkitTransform = 1269 div.style.MozTransform = 1270 div.style.OTransform = 1271 div.style.msTransform = 1272 1273 translate + " " + matrix; 1274 1275 }; 1276 1277 /** 1278 * Moves the upper-left corner of this LayerContainer to the given X and Y 1279 * coordinate, sets the Z stacking order, and reparents this LayerContainer 1280 * to the given LayerContainer. 1281 * 1282 * @param {Guacamole.Display.LayerContainer} parent The parent to set. 1283 * @param {Number} x The X coordinate to move to. 1284 * @param {Number} y The Y coordinate to move to. 1285 * @param {Number} z The Z coordinate to move to. 1286 */ 1287 this.move = function(parent, x, y, z) { 1288 1289 // Set parent if necessary 1290 if (layer.parent !== parent) { 1291 1292 // Maintain relationship 1293 if (layer.parent) 1294 delete layer.parent.children[layer.__unique_id]; 1295 layer.parent = parent; 1296 parent.children[layer.__unique_id] = layer; 1297 1298 // Reparent element 1299 var parent_element = parent.getElement(); 1300 parent_element.appendChild(div); 1301 1302 } 1303 1304 // Set location 1305 layer.translate(x, y); 1306 layer.z = z; 1307 div.style.zIndex = z; 1308 1309 }; 1310 1311 /** 1312 * Sets the opacity of this layer to the given value, where 255 is fully 1313 * opaque and 0 is fully transparent. 1314 * 1315 * @param {Number} a The opacity to set. 1316 */ 1317 this.shade = function(a) { 1318 layer.alpha = a; 1319 div.style.opacity = a/255.0; 1320 }; 1321 1322 /** 1323 * Removes this layer container entirely, such that it is no longer 1324 * contained within its parent layer, if any. 1325 */ 1326 this.dispose = function() { 1327 1328 // Remove from parent container 1329 if (layer.parent) { 1330 delete layer.parent.children[layer.__unique_id]; 1331 layer.parent = null; 1332 } 1333 1334 // Remove from parent element 1335 if (div.parentNode) 1336 div.parentNode.removeChild(div); 1337 1338 }; 1339 1340 /** 1341 * Applies the given affine transform (defined with six values from the 1342 * transform's matrix). 1343 * 1344 * @param {Number} a The first value in the affine transform's matrix. 1345 * @param {Number} b The second value in the affine transform's matrix. 1346 * @param {Number} c The third value in the affine transform's matrix. 1347 * @param {Number} d The fourth value in the affine transform's matrix. 1348 * @param {Number} e The fifth value in the affine transform's matrix. 1349 * @param {Number} f The sixth value in the affine transform's matrix. 1350 */ 1351 this.distort = function(a, b, c, d, e, f) { 1352 1353 // Store matrix 1354 layer.matrix = [a, b, c, d, e, f]; 1355 1356 // Generate matrix transformation 1357 matrix = 1358 1359 /* a c e 1360 * b d f 1361 * 0 0 1 1362 */ 1363 1364 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; 1365 1366 // Set layer transform 1367 div.style.transform = 1368 div.style.WebkitTransform = 1369 div.style.MozTransform = 1370 div.style.OTransform = 1371 div.style.msTransform = 1372 1373 translate + " " + matrix; 1374 1375 }; 1376 1377 }; 1378 1379 /** 1380 * The next identifier to be assigned to the layer container. This identifier 1381 * uniquely identifies each LayerContainer, but is unrelated to the index of 1382 * the layer, which exists at the protocol/client level only. 1383 * 1384 * @private 1385 * @type Number 1386 */ 1387 Guacamole.Display.VisibleLayer.__next_id = 0; 1388