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 at the specified URL at the given coordinates. The image 532 * will be loaded automatically, and this and any future operations will 533 * wait for the image to finish loading. 534 * 535 * @param {Guacamole.Layer} layer The layer to draw upon. 536 * @param {Number} x The destination X coordinate. 537 * @param {Number} y The destination Y coordinate. 538 * @param {String} url The URL of the image to draw. 539 */ 540 this.draw = function(layer, x, y, url) { 541 542 var task = scheduleTask(function __display_draw() { 543 layer.drawImage(x, y, image); 544 }, true); 545 546 var image = new Image(); 547 image.onload = task.unblock; 548 image.src = url; 549 550 }; 551 552 /** 553 * Plays the video at the specified URL within this layer. The video 554 * will be loaded automatically, and this and any future operations will 555 * wait for the video to finish loading. Future operations will not be 556 * executed until the video finishes playing. 557 * 558 * @param {Guacamole.Layer} layer The layer to draw upon. 559 * @param {String} mimetype The mimetype of the video to play. 560 * @param {Number} duration The duration of the video in milliseconds. 561 * @param {String} url The URL of the video to play. 562 */ 563 this.play = function(layer, mimetype, duration, url) { 564 565 // Start loading the video 566 var video = document.createElement("video"); 567 video.type = mimetype; 568 video.src = url; 569 570 // Start copying frames when playing 571 video.addEventListener("play", function() { 572 573 function render_callback() { 574 layer.drawImage(0, 0, video); 575 if (!video.ended) 576 window.setTimeout(render_callback, 20); 577 } 578 579 render_callback(); 580 581 }, false); 582 583 scheduleTask(video.play); 584 585 }; 586 587 /** 588 * Transfer a rectangle of image data from one Layer to this Layer using the 589 * specified transfer function. 590 * 591 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 592 * @param {Number} srcx The X coordinate of the upper-left corner of the 593 * rectangle within the source Layer's coordinate 594 * space to copy data from. 595 * @param {Number} srcy The Y coordinate of the upper-left corner of the 596 * rectangle within the source Layer's coordinate 597 * space to copy data from. 598 * @param {Number} srcw The width of the rectangle within the source Layer's 599 * coordinate space to copy data from. 600 * @param {Number} srch The height of the rectangle within the source 601 * Layer's coordinate space to copy data from. 602 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 603 * @param {Number} x The destination X coordinate. 604 * @param {Number} y The destination Y coordinate. 605 * @param {Function} transferFunction The transfer function to use to 606 * transfer data from source to 607 * destination. 608 */ 609 this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) { 610 scheduleTask(function __display_transfer() { 611 dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction); 612 }); 613 }; 614 615 /** 616 * Put a rectangle of image data from one Layer to this Layer directly 617 * without performing any alpha blending. Simply copy the data. 618 * 619 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 620 * @param {Number} srcx The X coordinate of the upper-left corner of the 621 * rectangle within the source Layer's coordinate 622 * space to copy data from. 623 * @param {Number} srcy The Y coordinate of the upper-left corner of the 624 * rectangle within the source Layer's coordinate 625 * space to copy data from. 626 * @param {Number} srcw The width of the rectangle within the source Layer's 627 * coordinate space to copy data from. 628 * @param {Number} srch The height of the rectangle within the source 629 * Layer's coordinate space to copy data from. 630 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 631 * @param {Number} x The destination X coordinate. 632 * @param {Number} y The destination Y coordinate. 633 */ 634 this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 635 scheduleTask(function __display_put() { 636 dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); 637 }); 638 }; 639 640 /** 641 * Copy a rectangle of image data from one Layer to this Layer. This 642 * operation will copy exactly the image data that will be drawn once all 643 * operations of the source Layer that were pending at the time this 644 * function was called are complete. This operation will not alter the 645 * size of the source Layer even if its autosize property is set to true. 646 * 647 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 648 * @param {Number} srcx The X coordinate of the upper-left corner of the 649 * rectangle within the source Layer's coordinate 650 * space to copy data from. 651 * @param {Number} srcy The Y coordinate of the upper-left corner of the 652 * rectangle within the source Layer's coordinate 653 * space to copy data from. 654 * @param {Number} srcw The width of the rectangle within the source Layer's 655 * coordinate space to copy data from. 656 * @param {Number} srch The height of the rectangle within the source 657 * Layer's coordinate space to copy data from. 658 * @param {Guacamole.Layer} dstLayer The layer to draw upon. 659 * @param {Number} x The destination X coordinate. 660 * @param {Number} y The destination Y coordinate. 661 */ 662 this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 663 scheduleTask(function __display_copy() { 664 dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); 665 }); 666 }; 667 668 /** 669 * Starts a new path at the specified point. 670 * 671 * @param {Guacamole.Layer} layer The layer to draw upon. 672 * @param {Number} x The X coordinate of the point to draw. 673 * @param {Number} y The Y coordinate of the point to draw. 674 */ 675 this.moveTo = function(layer, x, y) { 676 scheduleTask(function __display_moveTo() { 677 layer.moveTo(x, y); 678 }); 679 }; 680 681 /** 682 * Add the specified line to the current path. 683 * 684 * @param {Guacamole.Layer} layer The layer to draw upon. 685 * @param {Number} x The X coordinate of the endpoint of the line to draw. 686 * @param {Number} y The Y coordinate of the endpoint of the line to draw. 687 */ 688 this.lineTo = function(layer, x, y) { 689 scheduleTask(function __display_lineTo() { 690 layer.lineTo(x, y); 691 }); 692 }; 693 694 /** 695 * Add the specified arc to the current path. 696 * 697 * @param {Guacamole.Layer} layer The layer to draw upon. 698 * @param {Number} x The X coordinate of the center of the circle which 699 * will contain the arc. 700 * @param {Number} y The Y coordinate of the center of the circle which 701 * will contain the arc. 702 * @param {Number} radius The radius of the circle. 703 * @param {Number} startAngle The starting angle of the arc, in radians. 704 * @param {Number} endAngle The ending angle of the arc, in radians. 705 * @param {Boolean} negative Whether the arc should be drawn in order of 706 * decreasing angle. 707 */ 708 this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { 709 scheduleTask(function __display_arc() { 710 layer.arc(x, y, radius, startAngle, endAngle, negative); 711 }); 712 }; 713 714 /** 715 * Starts a new path at the specified point. 716 * 717 * @param {Guacamole.Layer} layer The layer to draw upon. 718 * @param {Number} cp1x The X coordinate of the first control point. 719 * @param {Number} cp1y The Y coordinate of the first control point. 720 * @param {Number} cp2x The X coordinate of the second control point. 721 * @param {Number} cp2y The Y coordinate of the second control point. 722 * @param {Number} x The X coordinate of the endpoint of the curve. 723 * @param {Number} y The Y coordinate of the endpoint of the curve. 724 */ 725 this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { 726 scheduleTask(function __display_curveTo() { 727 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); 728 }); 729 }; 730 731 /** 732 * Closes the current path by connecting the end point with the start 733 * point (if any) with a straight line. 734 * 735 * @param {Guacamole.Layer} layer The layer to draw upon. 736 */ 737 this.close = function(layer) { 738 scheduleTask(function __display_close() { 739 layer.close(); 740 }); 741 }; 742 743 /** 744 * Add the specified rectangle to the current path. 745 * 746 * @param {Guacamole.Layer} layer The layer to draw upon. 747 * @param {Number} x The X coordinate of the upper-left corner of the 748 * rectangle to draw. 749 * @param {Number} y The Y coordinate of the upper-left corner of the 750 * rectangle to draw. 751 * @param {Number} w The width of the rectangle to draw. 752 * @param {Number} h The height of the rectangle to draw. 753 */ 754 this.rect = function(layer, x, y, w, h) { 755 scheduleTask(function __display_rect() { 756 layer.rect(x, y, w, h); 757 }); 758 }; 759 760 /** 761 * Clip all future drawing operations by the current path. The current path 762 * is implicitly closed. The current path can continue to be reused 763 * for other operations (such as fillColor()) but a new path will be started 764 * once a path drawing operation (path() or rect()) is used. 765 * 766 * @param {Guacamole.Layer} layer The layer to affect. 767 */ 768 this.clip = function(layer) { 769 scheduleTask(function __display_clip() { 770 layer.clip(); 771 }); 772 }; 773 774 /** 775 * Stroke the current path with the specified color. The current path 776 * is implicitly closed. The current path can continue to be reused 777 * for other operations (such as clip()) but a new path will be started 778 * once a path drawing operation (path() or rect()) is used. 779 * 780 * @param {Guacamole.Layer} layer The layer to draw upon. 781 * @param {String} cap The line cap style. Can be "round", "square", 782 * or "butt". 783 * @param {String} join The line join style. Can be "round", "bevel", 784 * or "miter". 785 * @param {Number} thickness The line thickness in pixels. 786 * @param {Number} r The red component of the color to fill. 787 * @param {Number} g The green component of the color to fill. 788 * @param {Number} b The blue component of the color to fill. 789 * @param {Number} a The alpha component of the color to fill. 790 */ 791 this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { 792 scheduleTask(function __display_strokeColor() { 793 layer.strokeColor(cap, join, thickness, r, g, b, a); 794 }); 795 }; 796 797 /** 798 * Fills the current path with the specified color. The current path 799 * is implicitly closed. The current path can continue to be reused 800 * for other operations (such as clip()) but a new path will be started 801 * once a path drawing operation (path() or rect()) is used. 802 * 803 * @param {Guacamole.Layer} layer The layer to draw upon. 804 * @param {Number} r The red component of the color to fill. 805 * @param {Number} g The green component of the color to fill. 806 * @param {Number} b The blue component of the color to fill. 807 * @param {Number} a The alpha component of the color to fill. 808 */ 809 this.fillColor = function(layer, r, g, b, a) { 810 scheduleTask(function __display_fillColor() { 811 layer.fillColor(r, g, b, a); 812 }); 813 }; 814 815 /** 816 * Stroke the current path with the image within the specified layer. The 817 * image data will be tiled infinitely within the stroke. The current path 818 * is implicitly closed. The current path can continue to be reused 819 * for other operations (such as clip()) but a new path will be started 820 * once a path drawing operation (path() or rect()) is used. 821 * 822 * @param {Guacamole.Layer} layer The layer to draw upon. 823 * @param {String} cap The line cap style. Can be "round", "square", 824 * or "butt". 825 * @param {String} join The line join style. Can be "round", "bevel", 826 * or "miter". 827 * @param {Number} thickness The line thickness in pixels. 828 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 829 * within the stroke. 830 */ 831 this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { 832 scheduleTask(function __display_strokeLayer() { 833 layer.strokeLayer(cap, join, thickness, srcLayer); 834 }); 835 }; 836 837 /** 838 * Fills the current path with the image within the specified layer. The 839 * image data will be tiled infinitely within the stroke. The current path 840 * is implicitly closed. The current path can continue to be reused 841 * for other operations (such as clip()) but a new path will be started 842 * once a path drawing operation (path() or rect()) is used. 843 * 844 * @param {Guacamole.Layer} layer The layer to draw upon. 845 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 846 * within the fill. 847 */ 848 this.fillLayer = function(layer, srcLayer) { 849 scheduleTask(function __display_fillLayer() { 850 layer.fillLayer(srcLayer); 851 }); 852 }; 853 854 /** 855 * Push current layer state onto stack. 856 * 857 * @param {Guacamole.Layer} layer The layer to draw upon. 858 */ 859 this.push = function(layer) { 860 scheduleTask(function __display_push() { 861 layer.push(); 862 }); 863 }; 864 865 /** 866 * Pop layer state off stack. 867 * 868 * @param {Guacamole.Layer} layer The layer to draw upon. 869 */ 870 this.pop = function(layer) { 871 scheduleTask(function __display_pop() { 872 layer.pop(); 873 }); 874 }; 875 876 /** 877 * Reset the layer, clearing the stack, the current path, and any transform 878 * matrix. 879 * 880 * @param {Guacamole.Layer} layer The layer to draw upon. 881 */ 882 this.reset = function(layer) { 883 scheduleTask(function __display_reset() { 884 layer.reset(); 885 }); 886 }; 887 888 /** 889 * Sets the given affine transform (defined with six values from the 890 * transform's matrix). 891 * 892 * @param {Guacamole.Layer} layer The layer to modify. 893 * @param {Number} a The first value in the affine transform's matrix. 894 * @param {Number} b The second value in the affine transform's matrix. 895 * @param {Number} c The third value in the affine transform's matrix. 896 * @param {Number} d The fourth value in the affine transform's matrix. 897 * @param {Number} e The fifth value in the affine transform's matrix. 898 * @param {Number} f The sixth value in the affine transform's matrix. 899 */ 900 this.setTransform = function(layer, a, b, c, d, e, f) { 901 scheduleTask(function __display_setTransform() { 902 layer.setTransform(a, b, c, d, e, f); 903 }); 904 }; 905 906 /** 907 * Applies the given affine transform (defined with six values from the 908 * transform's matrix). 909 * 910 * @param {Guacamole.Layer} layer The layer to modify. 911 * @param {Number} a The first value in the affine transform's matrix. 912 * @param {Number} b The second value in the affine transform's matrix. 913 * @param {Number} c The third value in the affine transform's matrix. 914 * @param {Number} d The fourth value in the affine transform's matrix. 915 * @param {Number} e The fifth value in the affine transform's matrix. 916 * @param {Number} f The sixth value in the affine transform's matrix. 917 */ 918 this.transform = function(layer, a, b, c, d, e, f) { 919 scheduleTask(function __display_transform() { 920 layer.transform(a, b, c, d, e, f); 921 }); 922 }; 923 924 /** 925 * Sets the channel mask for future operations on this Layer. 926 * 927 * The channel mask is a Guacamole-specific compositing operation identifier 928 * with a single bit representing each of four channels (in order): source 929 * image where destination transparent, source where destination opaque, 930 * destination where source transparent, and destination where source 931 * opaque. 932 * 933 * @param {Guacamole.Layer} layer The layer to modify. 934 * @param {Number} mask The channel mask for future operations on this 935 * Layer. 936 */ 937 this.setChannelMask = function(layer, mask) { 938 scheduleTask(function __display_setChannelMask() { 939 layer.setChannelMask(mask); 940 }); 941 }; 942 943 /** 944 * Sets the miter limit for stroke operations using the miter join. This 945 * limit is the maximum ratio of the size of the miter join to the stroke 946 * width. If this ratio is exceeded, the miter will not be drawn for that 947 * joint of the path. 948 * 949 * @param {Guacamole.Layer} layer The layer to modify. 950 * @param {Number} limit The miter limit for stroke operations using the 951 * miter join. 952 */ 953 this.setMiterLimit = function(layer, limit) { 954 scheduleTask(function __display_setMiterLimit() { 955 layer.setMiterLimit(limit); 956 }); 957 }; 958 959 /** 960 * Sets the scale of the client display element such that it renders at 961 * a relatively smaller or larger size, without affecting the true 962 * resolution of the display. 963 * 964 * @param {Number} scale The scale to resize to, where 1.0 is normal 965 * size (1:1 scale). 966 */ 967 this.scale = function(scale) { 968 969 display.style.transform = 970 display.style.WebkitTransform = 971 display.style.MozTransform = 972 display.style.OTransform = 973 display.style.msTransform = 974 975 "scale(" + scale + "," + scale + ")"; 976 977 displayScale = scale; 978 979 // Update bounds size 980 bounds.style.width = (displayWidth*displayScale) + "px"; 981 bounds.style.height = (displayHeight*displayScale) + "px"; 982 983 }; 984 985 /** 986 * Returns the scale of the display. 987 * 988 * @return {Number} The scale of the display. 989 */ 990 this.getScale = function() { 991 return displayScale; 992 }; 993 994 /** 995 * Returns a canvas element containing the entire display, with all child 996 * layers composited within. 997 * 998 * @return {HTMLCanvasElement} A new canvas element containing a copy of 999 * the display. 1000 */ 1001 this.flatten = function() { 1002 1003 // Get destination canvas 1004 var canvas = document.createElement("canvas"); 1005 canvas.width = default_layer.width; 1006 canvas.height = default_layer.height; 1007 1008 var context = canvas.getContext("2d"); 1009 1010 // Returns sorted array of children 1011 function get_children(layer) { 1012 1013 // Build array of children 1014 var children = []; 1015 for (var index in layer.children) 1016 children.push(layer.children[index]); 1017 1018 // Sort 1019 children.sort(function children_comparator(a, b) { 1020 1021 // Compare based on Z order 1022 var diff = a.z - b.z; 1023 if (diff !== 0) 1024 return diff; 1025 1026 // If Z order identical, use document order 1027 var a_element = a.getElement(); 1028 var b_element = b.getElement(); 1029 var position = b_element.compareDocumentPosition(a_element); 1030 1031 if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; 1032 if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; 1033 1034 // Otherwise, assume same 1035 return 0; 1036 1037 }); 1038 1039 // Done 1040 return children; 1041 1042 } 1043 1044 // Draws the contents of the given layer at the given coordinates 1045 function draw_layer(layer, x, y) { 1046 1047 // Draw layer 1048 if (layer.width > 0 && layer.height > 0) { 1049 1050 // Save and update alpha 1051 var initial_alpha = context.globalAlpha; 1052 context.globalAlpha *= layer.alpha / 255.0; 1053 1054 // Copy data 1055 context.drawImage(layer.getCanvas(), x, y); 1056 1057 // Draw all children 1058 var children = get_children(layer); 1059 for (var i=0; i<children.length; i++) { 1060 var child = children[i]; 1061 draw_layer(child, x + child.x, y + child.y); 1062 } 1063 1064 // Restore alpha 1065 context.globalAlpha = initial_alpha; 1066 1067 } 1068 1069 } 1070 1071 // Draw default layer and all children 1072 draw_layer(default_layer, 0, 0); 1073 1074 // Return new canvas copy 1075 return canvas; 1076 1077 }; 1078 1079 }; 1080 1081 /** 1082 * Simple container for Guacamole.Layer, allowing layers to be easily 1083 * repositioned and nested. This allows certain operations to be accelerated 1084 * through DOM manipulation, rather than raster operations. 1085 * 1086 * @constructor 1087 * @augments Guacamole.Layer 1088 * @param {Number} width The width of the Layer, in pixels. The canvas element 1089 * backing this Layer will be given this width. 1090 * @param {Number} height The height of the Layer, in pixels. The canvas element 1091 * backing this Layer will be given this height. 1092 */ 1093 Guacamole.Display.VisibleLayer = function(width, height) { 1094 1095 Guacamole.Layer.apply(this, [width, height]); 1096 1097 /** 1098 * Reference to this layer. 1099 * @private 1100 */ 1101 var layer = this; 1102 1103 /** 1104 * Identifier which uniquely identifies this layer. This is COMPLETELY 1105 * UNRELATED to the index of the underlying layer, which is specific 1106 * to the Guacamole protocol, and not relevant at this level. 1107 * 1108 * @private 1109 * @type Number 1110 */ 1111 this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++; 1112 1113 /** 1114 * The opacity of the layer container, where 255 is fully opaque and 0 is 1115 * fully transparent. 1116 */ 1117 this.alpha = 0xFF; 1118 1119 /** 1120 * X coordinate of the upper-left corner of this layer container within 1121 * its parent, in pixels. 1122 * @type Number 1123 */ 1124 this.x = 0; 1125 1126 /** 1127 * Y coordinate of the upper-left corner of this layer container within 1128 * its parent, in pixels. 1129 * @type Number 1130 */ 1131 this.y = 0; 1132 1133 /** 1134 * Z stacking order of this layer relative to other sibling layers. 1135 * @type Number 1136 */ 1137 this.z = 0; 1138 1139 /** 1140 * The affine transformation applied to this layer container. Each element 1141 * corresponds to a value from the transformation matrix, with the first 1142 * three values being the first row, and the last three values being the 1143 * second row. There are six values total. 1144 * 1145 * @type Number[] 1146 */ 1147 this.matrix = [1, 0, 0, 1, 0, 0]; 1148 1149 /** 1150 * The parent layer container of this layer, if any. 1151 * @type Guacamole.Display.LayerContainer 1152 */ 1153 this.parent = null; 1154 1155 /** 1156 * Set of all children of this layer, indexed by layer index. This object 1157 * will have one property per child. 1158 */ 1159 this.children = {}; 1160 1161 // Set layer position 1162 var canvas = layer.getCanvas(); 1163 canvas.style.position = "absolute"; 1164 canvas.style.left = "0px"; 1165 canvas.style.top = "0px"; 1166 1167 // Create div with given size 1168 var div = document.createElement("div"); 1169 div.appendChild(canvas); 1170 div.style.width = width + "px"; 1171 div.style.height = height + "px"; 1172 div.style.position = "absolute"; 1173 div.style.left = "0px"; 1174 div.style.top = "0px"; 1175 div.style.overflow = "hidden"; 1176 1177 /** 1178 * Superclass resize() function. 1179 * @private 1180 */ 1181 var __super_resize = this.resize; 1182 1183 this.resize = function(width, height) { 1184 1185 // Resize containing div 1186 div.style.width = width + "px"; 1187 div.style.height = height + "px"; 1188 1189 __super_resize(width, height); 1190 1191 }; 1192 1193 /** 1194 * Returns the element containing the canvas and any other elements 1195 * associated with this layer. 1196 * @returns {Element} The element containing this layer's canvas. 1197 */ 1198 this.getElement = function() { 1199 return div; 1200 }; 1201 1202 /** 1203 * The translation component of this layer's transform. 1204 * @private 1205 */ 1206 var translate = "translate(0px, 0px)"; // (0, 0) 1207 1208 /** 1209 * The arbitrary matrix component of this layer's transform. 1210 * @private 1211 */ 1212 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity 1213 1214 /** 1215 * Moves the upper-left corner of this layer to the given X and Y 1216 * coordinate. 1217 * 1218 * @param {Number} x The X coordinate to move to. 1219 * @param {Number} y The Y coordinate to move to. 1220 */ 1221 this.translate = function(x, y) { 1222 1223 layer.x = x; 1224 layer.y = y; 1225 1226 // Generate translation 1227 translate = "translate(" 1228 + x + "px," 1229 + y + "px)"; 1230 1231 // Set layer transform 1232 div.style.transform = 1233 div.style.WebkitTransform = 1234 div.style.MozTransform = 1235 div.style.OTransform = 1236 div.style.msTransform = 1237 1238 translate + " " + matrix; 1239 1240 }; 1241 1242 /** 1243 * Moves the upper-left corner of this LayerContainer to the given X and Y 1244 * coordinate, sets the Z stacking order, and reparents this LayerContainer 1245 * to the given LayerContainer. 1246 * 1247 * @param {Guacamole.Display.LayerContainer} parent The parent to set. 1248 * @param {Number} x The X coordinate to move to. 1249 * @param {Number} y The Y coordinate to move to. 1250 * @param {Number} z The Z coordinate to move to. 1251 */ 1252 this.move = function(parent, x, y, z) { 1253 1254 // Set parent if necessary 1255 if (layer.parent !== parent) { 1256 1257 // Maintain relationship 1258 if (layer.parent) 1259 delete layer.parent.children[layer.__unique_id]; 1260 layer.parent = parent; 1261 parent.children[layer.__unique_id] = layer; 1262 1263 // Reparent element 1264 var parent_element = parent.getElement(); 1265 parent_element.appendChild(div); 1266 1267 } 1268 1269 // Set location 1270 layer.translate(x, y); 1271 layer.z = z; 1272 div.style.zIndex = z; 1273 1274 }; 1275 1276 /** 1277 * Sets the opacity of this layer to the given value, where 255 is fully 1278 * opaque and 0 is fully transparent. 1279 * 1280 * @param {Number} a The opacity to set. 1281 */ 1282 this.shade = function(a) { 1283 layer.alpha = a; 1284 div.style.opacity = a/255.0; 1285 }; 1286 1287 /** 1288 * Removes this layer container entirely, such that it is no longer 1289 * contained within its parent layer, if any. 1290 */ 1291 this.dispose = function() { 1292 1293 // Remove from parent container 1294 if (layer.parent) { 1295 delete layer.parent.children[layer.__unique_id]; 1296 layer.parent = null; 1297 } 1298 1299 // Remove from parent element 1300 if (div.parentNode) 1301 div.parentNode.removeChild(div); 1302 1303 }; 1304 1305 /** 1306 * Applies the given affine transform (defined with six values from the 1307 * transform's matrix). 1308 * 1309 * @param {Number} a The first value in the affine transform's matrix. 1310 * @param {Number} b The second value in the affine transform's matrix. 1311 * @param {Number} c The third value in the affine transform's matrix. 1312 * @param {Number} d The fourth value in the affine transform's matrix. 1313 * @param {Number} e The fifth value in the affine transform's matrix. 1314 * @param {Number} f The sixth value in the affine transform's matrix. 1315 */ 1316 this.distort = function(a, b, c, d, e, f) { 1317 1318 // Store matrix 1319 layer.matrix = [a, b, c, d, e, f]; 1320 1321 // Generate matrix transformation 1322 matrix = 1323 1324 /* a c e 1325 * b d f 1326 * 0 0 1 1327 */ 1328 1329 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; 1330 1331 // Set layer transform 1332 div.style.transform = 1333 div.style.WebkitTransform = 1334 div.style.MozTransform = 1335 div.style.OTransform = 1336 div.style.msTransform = 1337 1338 translate + " " + matrix; 1339 1340 }; 1341 1342 }; 1343 1344 /** 1345 * The next identifier to be assigned to the layer container. This identifier 1346 * uniquely identifies each LayerContainer, but is unrelated to the index of 1347 * the layer, which exists at the protocol/client level only. 1348 * 1349 * @private 1350 * @type Number 1351 */ 1352 Guacamole.Display.VisibleLayer.__next_id = 0; 1353