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