1 2 /* ***** BEGIN LICENSE BLOCK ***** 3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 4 * 5 * The contents of this file are subject to the Mozilla Public License Version 6 * 1.1 (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * http://www.mozilla.org/MPL/ 9 * 10 * Software distributed under the License is distributed on an "AS IS" basis, 11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 * for the specific language governing rights and limitations under the 13 * License. 14 * 15 * The Original Code is guacamole-common-js. 16 * 17 * The Initial Developer of the Original Code is 18 * Michael Jumper. 19 * Portions created by the Initial Developer are Copyright (C) 2010 20 * the Initial Developer. All Rights Reserved. 21 * 22 * Contributor(s): 23 * 24 * Alternatively, the contents of this file may be used under the terms of 25 * either the GNU General Public License Version 2 or later (the "GPL"), or 26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 * in which case the provisions of the GPL or the LGPL are applicable instead 28 * of those above. If you wish to allow use of your version of this file only 29 * under the terms of either the GPL or the LGPL, and not to allow others to 30 * use your version of this file under the terms of the MPL, indicate your 31 * decision by deleting the provisions above and replace them with the notice 32 * and other provisions required by the GPL or the LGPL. If you do not delete 33 * the provisions above, a recipient may use your version of this file under 34 * the terms of any one of the MPL, the GPL or the LGPL. 35 * 36 * ***** END LICENSE BLOCK ***** */ 37 38 /** 39 * Namespace for all Guacamole JavaScript objects. 40 * @namespace 41 */ 42 var Guacamole = Guacamole || {}; 43 44 /** 45 * Abstract ordered drawing surface. Each Layer contains a canvas element and 46 * provides simple drawing instructions for drawing to that canvas element, 47 * however unlike the canvas element itself, drawing operations on a Layer are 48 * guaranteed to run in order, even if such an operation must wait for an image 49 * to load before completing. 50 * 51 * @constructor 52 * 53 * @param {Number} width The width of the Layer, in pixels. The canvas element 54 * backing this Layer will be given this width. 55 * 56 * @param {Number} height The height of the Layer, in pixels. The canvas element 57 * backing this Layer will be given this height. 58 */ 59 Guacamole.Layer = function(width, height) { 60 61 /** 62 * Reference to this Layer. 63 * @private 64 */ 65 var layer = this; 66 67 /** 68 * The canvas element backing this Layer. 69 * @private 70 */ 71 var display = document.createElement("canvas"); 72 73 /** 74 * The 2D display context of the canvas element backing this Layer. 75 * @private 76 */ 77 var displayContext = display.getContext("2d"); 78 displayContext.save(); 79 80 /** 81 * The queue of all pending Tasks. Tasks will be run in order, with new 82 * tasks added at the end of the queue and old tasks removed from the 83 * front of the queue (FIFO). 84 * @private 85 */ 86 var tasks = new Array(); 87 88 /** 89 * Whether a new path should be started with the next path drawing 90 * operations. 91 * @private 92 */ 93 var pathClosed = true; 94 95 /** 96 * The number of states on the state stack. 97 * 98 * Note that there will ALWAYS be one element on the stack, but that 99 * element is not exposed. It is only used to reset the layer to its 100 * initial state. 101 * 102 * @private 103 */ 104 var stackSize = 0; 105 106 /** 107 * Map of all Guacamole channel masks to HTML5 canvas composite operation 108 * names. Not all channel mask combinations are currently implemented. 109 * @private 110 */ 111 var compositeOperation = { 112 /* 0x0 NOT IMPLEMENTED */ 113 0x1: "destination-in", 114 0x2: "destination-out", 115 /* 0x3 NOT IMPLEMENTED */ 116 0x4: "source-in", 117 /* 0x5 NOT IMPLEMENTED */ 118 0x6: "source-atop", 119 /* 0x7 NOT IMPLEMENTED */ 120 0x8: "source-out", 121 0x9: "destination-atop", 122 0xA: "xor", 123 0xB: "destination-over", 124 0xC: "copy", 125 /* 0xD NOT IMPLEMENTED */ 126 0xE: "source-over", 127 0xF: "lighter" 128 }; 129 130 /** 131 * Resizes the canvas element backing this Layer without testing the 132 * new size. This function should only be used internally. 133 * 134 * @private 135 * @param {Number} newWidth The new width to assign to this Layer. 136 * @param {Number} newHeight The new height to assign to this Layer. 137 */ 138 function resize(newWidth, newHeight) { 139 140 // Only preserve old data if width/height are both non-zero 141 var oldData = null; 142 if (width != 0 && height != 0) { 143 144 // Create canvas and context for holding old data 145 oldData = document.createElement("canvas"); 146 oldData.width = width; 147 oldData.height = height; 148 149 var oldDataContext = oldData.getContext("2d"); 150 151 // Copy image data from current 152 oldDataContext.drawImage(display, 153 0, 0, width, height, 154 0, 0, width, height); 155 156 } 157 158 // Preserve composite operation 159 var oldCompositeOperation = displayContext.globalCompositeOperation; 160 161 // Resize canvas 162 display.width = newWidth; 163 display.height = newHeight; 164 165 // Redraw old data, if any 166 if (oldData) 167 displayContext.drawImage(oldData, 168 0, 0, width, height, 169 0, 0, width, height); 170 171 // Restore composite operation 172 displayContext.globalCompositeOperation = oldCompositeOperation; 173 174 width = newWidth; 175 height = newHeight; 176 177 // Acknowledge reset of stack (happens on resize of canvas) 178 stackSize = 0; 179 displayContext.save(); 180 181 } 182 183 /** 184 * Given the X and Y coordinates of the upper-left corner of a rectangle 185 * and the rectangle's width and height, resize the backing canvas element 186 * as necessary to ensure that the rectangle fits within the canvas 187 * element's coordinate space. This function will only make the canvas 188 * larger. If the rectangle already fits within the canvas element's 189 * coordinate space, the canvas is left unchanged. 190 * 191 * @private 192 * @param {Number} x The X coordinate of the upper-left corner of the 193 * rectangle to fit. 194 * @param {Number} y The Y coordinate of the upper-left corner of the 195 * rectangle to fit. 196 * @param {Number} w The width of the the rectangle to fit. 197 * @param {Number} h The height of the the rectangle to fit. 198 */ 199 function fitRect(x, y, w, h) { 200 201 // Calculate bounds 202 var opBoundX = w + x; 203 var opBoundY = h + y; 204 205 // Determine max width 206 var resizeWidth; 207 if (opBoundX > width) 208 resizeWidth = opBoundX; 209 else 210 resizeWidth = width; 211 212 // Determine max height 213 var resizeHeight; 214 if (opBoundY > height) 215 resizeHeight = opBoundY; 216 else 217 resizeHeight = height; 218 219 // Resize if necessary 220 if (resizeWidth != width || resizeHeight != height) 221 resize(resizeWidth, resizeHeight); 222 223 } 224 225 /** 226 * A container for an task handler. Each operation which must be ordered 227 * is associated with a Task that goes into a task queue. Tasks in this 228 * queue are executed in order once their handlers are set, while Tasks 229 * without handlers block themselves and any following Tasks from running. 230 * 231 * @constructor 232 * @private 233 * @param {function} taskHandler The function to call when this task 234 * runs, if any. 235 * @param {boolean} blocked Whether this task should start blocked. 236 */ 237 function Task(taskHandler, blocked) { 238 239 var task = this; 240 241 /** 242 * Whether this Task is blocked. 243 * 244 * @type boolean 245 */ 246 this.blocked = blocked; 247 248 /** 249 * The handler this Task is associated with, if any. 250 * 251 * @type function 252 */ 253 this.handler = taskHandler; 254 255 /** 256 * Unblocks this Task, allowing it to run. 257 */ 258 this.unblock = function() { 259 if (task.blocked) { 260 task.blocked = false; 261 262 // Flush automatically if enabled 263 if (layer.autoflush || !flushComplete) 264 layer.flush(); 265 266 } 267 } 268 269 } 270 271 /** 272 * If no tasks are pending or running, run the provided handler immediately, 273 * if any. Otherwise, schedule a task to run immediately after all currently 274 * running or pending tasks are complete. 275 * 276 * @private 277 * @param {function} handler The function to call when possible, if any. 278 * @param {boolean} blocked Whether the task should start blocked. 279 * @returns {Task} The Task created and added to the queue for future 280 * running, if any, or null if the handler was run 281 * immediately and no Task needed to be created. 282 */ 283 function scheduleTask(handler, blocked) { 284 285 // If no pending tasks, just call (if available) and exit 286 if (layer.autoflush && layer.isReady() && !blocked) { 287 if (handler) handler(); 288 return null; 289 } 290 291 // If tasks are pending/executing, schedule a pending task 292 // and return a reference to it. 293 var task = new Task(handler, blocked); 294 tasks.push(task); 295 return task; 296 297 } 298 299 /** 300 * Whether all previous calls to flush() have completed. If a task was 301 * waiting in the queue when flush() was called but still blocked, the 302 * queue will continue to flush outside the original flush() call until 303 * the queue is empty. 304 * 305 * @private 306 */ 307 var flushComplete = true; 308 309 /** 310 * Whether tasks are currently being actively flushed. As flush() is not 311 * reentrant, this flag prevents calls of flush() from overlapping. 312 * @private 313 */ 314 var tasksInProgress = false; 315 316 /** 317 * Run any Tasks which were pending but are now ready to run and are not 318 * blocked by other Tasks. 319 */ 320 this.flush = function() { 321 322 if (tasksInProgress) 323 return; 324 325 tasksInProgress = true; 326 flushComplete = false; 327 328 // Draw all pending tasks. 329 var task; 330 while ((task = tasks[0]) != null && !task.blocked) { 331 tasks.shift(); 332 if (task.handler) task.handler(); 333 } 334 335 // If all pending draws have been flushed 336 if (layer.isReady()) 337 flushComplete = true; 338 339 tasksInProgress = false; 340 341 }; 342 343 /** 344 * Schedules a task within the current layer just as scheduleTast() does, 345 * except that another specified layer will be blocked until this task 346 * completes, and this task will not start until the other layer is 347 * ready. 348 * 349 * Essentially, a task is scheduled in both layers, and the specified task 350 * will only be performed once both layers are ready, and neither layer may 351 * proceed until this task completes. 352 * 353 * Note that there is no way to specify whether the task starts blocked, 354 * as whether the task is blocked depends completely on whether the 355 * other layer is currently ready. 356 * 357 * @private 358 * @param {Guacamole.Layer} otherLayer The other layer which must be blocked 359 * until this task completes. 360 * @param {function} handler The function to call when possible. 361 */ 362 function scheduleTaskSynced(otherLayer, handler) { 363 364 // If we ARE the other layer, no need to sync. 365 // Syncing would result in deadlock. 366 if (layer === otherLayer) 367 scheduleTask(handler); 368 369 // Otherwise synchronize operation with other layer 370 else { 371 372 var drawComplete = false; 373 var layerLock = null; 374 375 function performTask() { 376 377 // Perform task 378 handler(); 379 380 // Unblock the other layer now that draw is complete 381 if (layerLock != null) 382 layerLock.unblock(); 383 384 // Flag operation as done 385 drawComplete = true; 386 387 } 388 389 // Currently blocked draw task 390 var task = scheduleTask(performTask, true); 391 392 // Unblock draw task once source layer is ready 393 otherLayer.sync(task.unblock); 394 395 // Block other layer until draw completes 396 // Note that the draw MAY have already been performed at this point, 397 // in which case creating a lock on the other layer will lead to 398 // deadlock (the draw task has already run and will thus never 399 // clear the lock) 400 if (!drawComplete) 401 layerLock = otherLayer.sync(null, true); 402 403 } 404 } 405 406 /** 407 * Set to true if this Layer should resize itself to accomodate the 408 * dimensions of any drawing operation, and false (the default) otherwise. 409 * 410 * Note that setting this property takes effect immediately, and thus may 411 * take effect on operations that were started in the past but have not 412 * yet completed. If you wish the setting of this flag to only modify 413 * future operations, you will need to make the setting of this flag an 414 * operation with sync(). 415 * 416 * @example 417 * // Set autosize to true for all future operations 418 * layer.sync(function() { 419 * layer.autosize = true; 420 * }); 421 * 422 * @type Boolean 423 * @default false 424 */ 425 this.autosize = false; 426 427 /** 428 * Set to true to allow operations to flush automatically, instantly 429 * affecting the layer. By default, operations are buffered and only 430 * drawn when flush() is called. 431 * 432 * @type Boolean 433 * @default false 434 */ 435 this.autoflush = false; 436 437 /** 438 * Returns the canvas element backing this Layer. 439 * @returns {Element} The canvas element backing this Layer. 440 */ 441 this.getCanvas = function() { 442 return display; 443 }; 444 445 /** 446 * Returns whether this Layer is ready. A Layer is ready if it has no 447 * pending operations and no operations in-progress. 448 * 449 * @returns {Boolean} true if this Layer is ready, false otherwise. 450 */ 451 this.isReady = function() { 452 return tasks.length == 0; 453 }; 454 455 /** 456 * Changes the size of this Layer to the given width and height. Resizing 457 * is only attempted if the new size provided is actually different from 458 * the current size. 459 * 460 * @param {Number} newWidth The new width to assign to this Layer. 461 * @param {Number} newHeight The new height to assign to this Layer. 462 */ 463 this.resize = function(newWidth, newHeight) { 464 scheduleTask(function() { 465 if (newWidth != width || newHeight != height) 466 resize(newWidth, newHeight); 467 }); 468 }; 469 470 /** 471 * Draws the specified image at the given coordinates. The image specified 472 * must already be loaded. 473 * 474 * @param {Number} x The destination X coordinate. 475 * @param {Number} y The destination Y coordinate. 476 * @param {Image} image The image to draw. Note that this is an Image 477 * object - not a URL. 478 */ 479 this.drawImage = function(x, y, image) { 480 scheduleTask(function() { 481 if (layer.autosize != 0) fitRect(x, y, image.width, image.height); 482 displayContext.drawImage(image, x, y); 483 }); 484 }; 485 486 /** 487 * Draws the image at the specified URL at the given coordinates. The image 488 * will be loaded automatically, and this and any future operations will 489 * wait for the image to finish loading. 490 * 491 * @param {Number} x The destination X coordinate. 492 * @param {Number} y The destination Y coordinate. 493 * @param {String} url The URL of the image to draw. 494 */ 495 this.draw = function(x, y, url) { 496 497 var task = scheduleTask(function() { 498 if (layer.autosize != 0) fitRect(x, y, image.width, image.height); 499 displayContext.drawImage(image, x, y); 500 }, true); 501 502 var image = new Image(); 503 image.onload = task.unblock; 504 image.src = url; 505 506 }; 507 508 /** 509 * Plays the video at the specified URL within this layer. The video 510 * will be loaded automatically, and this and any future operations will 511 * wait for the video to finish loading. Future operations will not be 512 * executed until the video finishes playing. 513 * 514 * @param {String} mimetype The mimetype of the video to play. 515 * @param {Number} duration The duration of the video in milliseconds. 516 * @param {String} url The URL of the video to play. 517 */ 518 this.play = function(mimetype, duration, url) { 519 520 // Start loading the video 521 var video = document.createElement("video"); 522 video.type = mimetype; 523 video.src = url; 524 525 // Main task - playing the video 526 var task = scheduleTask(function() { 527 video.play(); 528 }, true); 529 530 // Lock which will be cleared after video ends 531 var lock = scheduleTask(null, true); 532 533 // Start copying frames when playing 534 video.addEventListener("play", function() { 535 536 function render_callback() { 537 displayContext.drawImage(video, 0, 0, width, height); 538 if (!video.ended) 539 window.setTimeout(render_callback, 20); 540 else 541 lock.unblock(); 542 } 543 544 render_callback(); 545 546 }, false); 547 548 // Unblock future operations after an error 549 video.addEventListener("error", lock.unblock, false); 550 551 // Play video as soon as current tasks are complete, now that the 552 // lock has been set up. 553 task.unblock(); 554 555 }; 556 557 /** 558 * Run an arbitrary function as soon as currently pending operations 559 * are complete. 560 * 561 * @param {function} handler The function to call once all currently 562 * pending operations are complete. 563 * @param {boolean} blocked Whether the task should start blocked. 564 */ 565 this.sync = scheduleTask; 566 567 /** 568 * Transfer a rectangle of image data from one Layer to this Layer using the 569 * specified transfer function. 570 * 571 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 572 * @param {Number} srcx The X coordinate of the upper-left corner of the 573 * rectangle within the source Layer's coordinate 574 * space to copy data from. 575 * @param {Number} srcy The Y coordinate of the upper-left corner of the 576 * rectangle within the source Layer's coordinate 577 * space to copy data from. 578 * @param {Number} srcw The width of the rectangle within the source Layer's 579 * coordinate space to copy data from. 580 * @param {Number} srch The height of the rectangle within the source 581 * Layer's coordinate space to copy data from. 582 * @param {Number} x The destination X coordinate. 583 * @param {Number} y The destination Y coordinate. 584 * @param {Function} transferFunction The transfer function to use to 585 * transfer data from source to 586 * destination. 587 */ 588 this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) { 589 scheduleTaskSynced(srcLayer, function() { 590 591 var srcCanvas = srcLayer.getCanvas(); 592 593 // If entire rectangle outside source canvas, stop 594 if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 595 596 // Otherwise, clip rectangle to area 597 if (srcx + srcw > srcCanvas.width) 598 srcw = srcCanvas.width - srcx; 599 600 if (srcy + srch > srcCanvas.height) 601 srch = srcCanvas.height - srcy; 602 603 // Stop if nothing to draw. 604 if (srcw == 0 || srch == 0) return; 605 606 if (layer.autosize != 0) fitRect(x, y, srcw, srch); 607 608 // Get image data from src and dst 609 var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); 610 var dst = displayContext.getImageData(x , y, srcw, srch); 611 612 // Apply transfer for each pixel 613 for (var i=0; i<srcw*srch*4; i+=4) { 614 615 // Get source pixel environment 616 var src_pixel = new Guacamole.Layer.Pixel( 617 src.data[i], 618 src.data[i+1], 619 src.data[i+2], 620 src.data[i+3] 621 ); 622 623 // Get destination pixel environment 624 var dst_pixel = new Guacamole.Layer.Pixel( 625 dst.data[i], 626 dst.data[i+1], 627 dst.data[i+2], 628 dst.data[i+3] 629 ); 630 631 // Apply transfer function 632 transferFunction(src_pixel, dst_pixel); 633 634 // Save pixel data 635 dst.data[i ] = dst_pixel.red; 636 dst.data[i+1] = dst_pixel.green; 637 dst.data[i+2] = dst_pixel.blue; 638 dst.data[i+3] = dst_pixel.alpha; 639 640 } 641 642 // Draw image data 643 displayContext.putImageData(dst, x, y); 644 645 }); 646 }; 647 648 /** 649 * Copy a rectangle of image data from one Layer to this Layer. This 650 * operation will copy exactly the image data that will be drawn once all 651 * operations of the source Layer that were pending at the time this 652 * function was called are complete. This operation will not alter the 653 * size of the source Layer even if its autosize property is set to true. 654 * 655 * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 656 * @param {Number} srcx The X coordinate of the upper-left corner of the 657 * rectangle within the source Layer's coordinate 658 * space to copy data from. 659 * @param {Number} srcy The Y coordinate of the upper-left corner of the 660 * rectangle within the source Layer's coordinate 661 * space to copy data from. 662 * @param {Number} srcw The width of the rectangle within the source Layer's 663 * coordinate space to copy data from. 664 * @param {Number} srch The height of the rectangle within the source 665 * Layer's coordinate space to copy data from. 666 * @param {Number} x The destination X coordinate. 667 * @param {Number} y The destination Y coordinate. 668 */ 669 this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) { 670 scheduleTaskSynced(srcLayer, function() { 671 672 var srcCanvas = srcLayer.getCanvas(); 673 674 // If entire rectangle outside source canvas, stop 675 if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 676 677 // Otherwise, clip rectangle to area 678 if (srcx + srcw > srcCanvas.width) 679 srcw = srcCanvas.width - srcx; 680 681 if (srcy + srch > srcCanvas.height) 682 srch = srcCanvas.height - srcy; 683 684 // Stop if nothing to draw. 685 if (srcw == 0 || srch == 0) return; 686 687 if (layer.autosize != 0) fitRect(x, y, srcw, srch); 688 displayContext.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch); 689 690 }); 691 }; 692 693 /** 694 * Starts a new path at the specified point. 695 * 696 * @param {Number} x The X coordinate of the point to draw. 697 * @param {Number} y The Y coordinate of the point to draw. 698 */ 699 this.moveTo = function(x, y) { 700 scheduleTask(function() { 701 702 // Start a new path if current path is closed 703 if (pathClosed) { 704 displayContext.beginPath(); 705 pathClosed = false; 706 } 707 708 if (layer.autosize != 0) fitRect(x, y, 0, 0); 709 displayContext.moveTo(x, y); 710 711 }); 712 }; 713 714 /** 715 * Add the specified line to the current path. 716 * 717 * @param {Number} x The X coordinate of the endpoint of the line to draw. 718 * @param {Number} y The Y coordinate of the endpoint of the line to draw. 719 */ 720 this.lineTo = function(x, y) { 721 scheduleTask(function() { 722 723 // Start a new path if current path is closed 724 if (pathClosed) { 725 displayContext.beginPath(); 726 pathClosed = false; 727 } 728 729 if (layer.autosize != 0) fitRect(x, y, 0, 0); 730 displayContext.lineTo(x, y); 731 732 }); 733 }; 734 735 /** 736 * Add the specified arc to the current path. 737 * 738 * @param {Number} x The X coordinate of the center of the circle which 739 * will contain the arc. 740 * @param {Number} y The Y coordinate of the center of the circle which 741 * will contain the arc. 742 * @param {Number} radius The radius of the circle. 743 * @param {Number} startAngle The starting angle of the arc, in radians. 744 * @param {Number} endAngle The ending angle of the arc, in radians. 745 * @param {Boolean} negative Whether the arc should be drawn in order of 746 * decreasing angle. 747 */ 748 this.arc = function(x, y, radius, startAngle, endAngle, negative) { 749 scheduleTask(function() { 750 751 // Start a new path if current path is closed 752 if (pathClosed) { 753 displayContext.beginPath(); 754 pathClosed = false; 755 } 756 757 if (layer.autosize != 0) fitRect(x, y, 0, 0); 758 displayContext.arc(x, y, radius, startAngle, endAngle, negative); 759 760 }); 761 }; 762 763 /** 764 * Starts a new path at the specified point. 765 * 766 * @param {Number} cp1x The X coordinate of the first control point. 767 * @param {Number} cp1y The Y coordinate of the first control point. 768 * @param {Number} cp2x The X coordinate of the second control point. 769 * @param {Number} cp2y The Y coordinate of the second control point. 770 * @param {Number} x The X coordinate of the endpoint of the curve. 771 * @param {Number} y The Y coordinate of the endpoint of the curve. 772 */ 773 this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { 774 scheduleTask(function() { 775 776 // Start a new path if current path is closed 777 if (pathClosed) { 778 displayContext.beginPath(); 779 pathClosed = false; 780 } 781 782 if (layer.autosize != 0) fitRect(x, y, 0, 0); 783 displayContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); 784 785 }); 786 }; 787 788 /** 789 * Closes the current path by connecting the end point with the start 790 * point (if any) with a straight line. 791 */ 792 this.close = function() { 793 scheduleTask(function() { 794 795 // Close path 796 displayContext.closePath(); 797 pathClosed = true; 798 799 }); 800 }; 801 802 /** 803 * Add the specified rectangle to the current path. 804 * 805 * @param {Number} x The X coordinate of the upper-left corner of the 806 * rectangle to draw. 807 * @param {Number} y The Y coordinate of the upper-left corner of the 808 * rectangle to draw. 809 * @param {Number} w The width of the rectangle to draw. 810 * @param {Number} h The height of the rectangle to draw. 811 */ 812 this.rect = function(x, y, w, h) { 813 scheduleTask(function() { 814 815 // Start a new path if current path is closed 816 if (pathClosed) { 817 displayContext.beginPath(); 818 pathClosed = false; 819 } 820 821 if (layer.autosize != 0) fitRect(x, y, w, h); 822 displayContext.rect(x, y, w, h); 823 824 }); 825 }; 826 827 /** 828 * Clip all future drawing operations by the current path. The current path 829 * is implicitly closed. The current path can continue to be reused 830 * for other operations (such as fillColor()) but a new path will be started 831 * once a path drawing operation (path() or rect()) is used. 832 */ 833 this.clip = function() { 834 scheduleTask(function() { 835 836 // Set new clipping region 837 displayContext.clip(); 838 839 // Path now implicitly closed 840 pathClosed = true; 841 842 }); 843 }; 844 845 /** 846 * Stroke the current path with the specified color. The current path 847 * is implicitly closed. The current path can continue to be reused 848 * for other operations (such as clip()) but a new path will be started 849 * once a path drawing operation (path() or rect()) is used. 850 * 851 * @param {String} cap The line cap style. Can be "round", "square", 852 * or "butt". 853 * @param {String} join The line join style. Can be "round", "bevel", 854 * or "miter". 855 * @param {Number} thickness The line thickness in pixels. 856 * @param {Number} r The red component of the color to fill. 857 * @param {Number} g The green component of the color to fill. 858 * @param {Number} b The blue component of the color to fill. 859 * @param {Number} a The alpha component of the color to fill. 860 */ 861 this.strokeColor = function(cap, join, thickness, r, g, b, a) { 862 scheduleTask(function() { 863 864 // Stroke with color 865 displayContext.lineCap = cap; 866 displayContext.lineJoin = join; 867 displayContext.lineWidth = thickness; 868 displayContext.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 869 displayContext.stroke(); 870 871 // Path now implicitly closed 872 pathClosed = true; 873 874 }); 875 }; 876 877 /** 878 * Fills the current path with the specified color. The current path 879 * is implicitly closed. The current path can continue to be reused 880 * for other operations (such as clip()) but a new path will be started 881 * once a path drawing operation (path() or rect()) is used. 882 * 883 * @param {Number} r The red component of the color to fill. 884 * @param {Number} g The green component of the color to fill. 885 * @param {Number} b The blue component of the color to fill. 886 * @param {Number} a The alpha component of the color to fill. 887 */ 888 this.fillColor = function(r, g, b, a) { 889 scheduleTask(function() { 890 891 // Fill with color 892 displayContext.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 893 displayContext.fill(); 894 895 // Path now implicitly closed 896 pathClosed = true; 897 898 }); 899 }; 900 901 /** 902 * Stroke the current path with the image within the specified layer. The 903 * image data will be tiled infinitely within the stroke. The current path 904 * is implicitly closed. The current path can continue to be reused 905 * for other operations (such as clip()) but a new path will be started 906 * once a path drawing operation (path() or rect()) is used. 907 * 908 * @param {String} cap The line cap style. Can be "round", "square", 909 * or "butt". 910 * @param {String} join The line join style. Can be "round", "bevel", 911 * or "miter". 912 * @param {Number} thickness The line thickness in pixels. 913 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 914 * within the stroke. 915 */ 916 this.strokeLayer = function(cap, join, thickness, srcLayer) { 917 scheduleTaskSynced(srcLayer, function() { 918 919 // Stroke with image data 920 displayContext.lineCap = cap; 921 displayContext.lineJoin = join; 922 displayContext.lineWidth = thickness; 923 displayContext.strokeStyle = displayContext.createPattern( 924 srcLayer.getCanvas(), 925 "repeat" 926 ); 927 displayContext.stroke(); 928 929 // Path now implicitly closed 930 pathClosed = true; 931 932 }); 933 }; 934 935 /** 936 * Fills the current path with the image within the specified layer. The 937 * image data will be tiled infinitely within the stroke. The current path 938 * is implicitly closed. The current path can continue to be reused 939 * for other operations (such as clip()) but a new path will be started 940 * once a path drawing operation (path() or rect()) is used. 941 * 942 * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 943 * within the fill. 944 */ 945 this.fillLayer = function(srcLayer) { 946 scheduleTask(function() { 947 948 // Fill with image data 949 displayContext.fillStyle = displayContext.createPattern( 950 srcLayer.getCanvas(), 951 "repeat" 952 ); 953 displayContext.fill(); 954 955 // Path now implicitly closed 956 pathClosed = true; 957 958 }); 959 }; 960 961 /** 962 * Push current layer state onto stack. 963 */ 964 this.push = function() { 965 scheduleTask(function() { 966 967 // Save current state onto stack 968 displayContext.save(); 969 stackSize++; 970 971 }); 972 }; 973 974 /** 975 * Pop layer state off stack. 976 */ 977 this.pop = function() { 978 scheduleTask(function() { 979 980 // Restore current state from stack 981 if (stackSize > 0) { 982 displayContext.restore(); 983 stackSize--; 984 } 985 986 }); 987 }; 988 989 /** 990 * Reset the layer, clearing the stack, the current path, and any transform 991 * matrix. 992 */ 993 this.reset = function() { 994 scheduleTask(function() { 995 996 // Clear stack 997 while (stackSize > 0) { 998 displayContext.restore(); 999 stackSize--; 1000 } 1001 1002 // Restore to initial state 1003 displayContext.restore(); 1004 displayContext.save(); 1005 1006 // Clear path 1007 displayContext.beginPath(); 1008 pathClosed = false; 1009 1010 }); 1011 }; 1012 1013 /** 1014 * Sets the given affine transform (defined with six values from the 1015 * transform's matrix). 1016 * 1017 * @param {Number} a The first value in the affine transform's matrix. 1018 * @param {Number} b The second value in the affine transform's matrix. 1019 * @param {Number} c The third value in the affine transform's matrix. 1020 * @param {Number} d The fourth value in the affine transform's matrix. 1021 * @param {Number} e The fifth value in the affine transform's matrix. 1022 * @param {Number} f The sixth value in the affine transform's matrix. 1023 */ 1024 this.setTransform = function(a, b, c, d, e, f) { 1025 scheduleTask(function() { 1026 1027 // Set transform 1028 displayContext.setTransform( 1029 a, b, c, 1030 d, e, f 1031 /*0, 0, 1*/ 1032 ); 1033 1034 }); 1035 }; 1036 1037 1038 /** 1039 * Applies the given affine transform (defined with six values from the 1040 * transform's matrix). 1041 * 1042 * @param {Number} a The first value in the affine transform's matrix. 1043 * @param {Number} b The second value in the affine transform's matrix. 1044 * @param {Number} c The third value in the affine transform's matrix. 1045 * @param {Number} d The fourth value in the affine transform's matrix. 1046 * @param {Number} e The fifth value in the affine transform's matrix. 1047 * @param {Number} f The sixth value in the affine transform's matrix. 1048 */ 1049 this.transform = function(a, b, c, d, e, f) { 1050 scheduleTask(function() { 1051 1052 // Apply transform 1053 displayContext.transform( 1054 a, b, c, 1055 d, e, f 1056 /*0, 0, 1*/ 1057 ); 1058 1059 }); 1060 }; 1061 1062 1063 /** 1064 * Sets the channel mask for future operations on this Layer. 1065 * 1066 * The channel mask is a Guacamole-specific compositing operation identifier 1067 * with a single bit representing each of four channels (in order): source 1068 * image where destination transparent, source where destination opaque, 1069 * destination where source transparent, and destination where source 1070 * opaque. 1071 * 1072 * @param {Number} mask The channel mask for future operations on this 1073 * Layer. 1074 */ 1075 this.setChannelMask = function(mask) { 1076 scheduleTask(function() { 1077 displayContext.globalCompositeOperation = compositeOperation[mask]; 1078 }); 1079 }; 1080 1081 /** 1082 * Sets the miter limit for stroke operations using the miter join. This 1083 * limit is the maximum ratio of the size of the miter join to the stroke 1084 * width. If this ratio is exceeded, the miter will not be drawn for that 1085 * joint of the path. 1086 * 1087 * @param {Number} limit The miter limit for stroke operations using the 1088 * miter join. 1089 */ 1090 this.setMiterLimit = function(limit) { 1091 scheduleTask(function() { 1092 displayContext.miterLimit = limit; 1093 }); 1094 }; 1095 1096 // Initialize canvas dimensions 1097 display.width = width; 1098 display.height = height; 1099 1100 }; 1101 1102 /** 1103 * Channel mask for the composite operation "rout". 1104 */ 1105 Guacamole.Layer.ROUT = 0x2; 1106 1107 /** 1108 * Channel mask for the composite operation "atop". 1109 */ 1110 Guacamole.Layer.ATOP = 0x6; 1111 1112 /** 1113 * Channel mask for the composite operation "xor". 1114 */ 1115 Guacamole.Layer.XOR = 0xA; 1116 1117 /** 1118 * Channel mask for the composite operation "rover". 1119 */ 1120 Guacamole.Layer.ROVER = 0xB; 1121 1122 /** 1123 * Channel mask for the composite operation "over". 1124 */ 1125 Guacamole.Layer.OVER = 0xE; 1126 1127 /** 1128 * Channel mask for the composite operation "plus". 1129 */ 1130 Guacamole.Layer.PLUS = 0xF; 1131 1132 /** 1133 * Channel mask for the composite operation "rin". 1134 * Beware that WebKit-based browsers may leave the contents of the destionation 1135 * layer where the source layer is transparent, despite the definition of this 1136 * operation. 1137 */ 1138 Guacamole.Layer.RIN = 0x1; 1139 1140 /** 1141 * Channel mask for the composite operation "in". 1142 * Beware that WebKit-based browsers may leave the contents of the destionation 1143 * layer where the source layer is transparent, despite the definition of this 1144 * operation. 1145 */ 1146 Guacamole.Layer.IN = 0x4; 1147 1148 /** 1149 * Channel mask for the composite operation "out". 1150 * Beware that WebKit-based browsers may leave the contents of the destionation 1151 * layer where the source layer is transparent, despite the definition of this 1152 * operation. 1153 */ 1154 Guacamole.Layer.OUT = 0x8; 1155 1156 /** 1157 * Channel mask for the composite operation "ratop". 1158 * Beware that WebKit-based browsers may leave the contents of the destionation 1159 * layer where the source layer is transparent, despite the definition of this 1160 * operation. 1161 */ 1162 Guacamole.Layer.RATOP = 0x9; 1163 1164 /** 1165 * Channel mask for the composite operation "src". 1166 * Beware that WebKit-based browsers may leave the contents of the destionation 1167 * layer where the source layer is transparent, despite the definition of this 1168 * operation. 1169 */ 1170 Guacamole.Layer.SRC = 0xC; 1171 1172 1173 /** 1174 * Represents a single pixel of image data. All components have a minimum value 1175 * of 0 and a maximum value of 255. 1176 * 1177 * @constructor 1178 * 1179 * @param {Number} r The red component of this pixel. 1180 * @param {Number} g The green component of this pixel. 1181 * @param {Number} b The blue component of this pixel. 1182 * @param {Number} a The alpha component of this pixel. 1183 */ 1184 Guacamole.Layer.Pixel = function(r, g, b, a) { 1185 1186 /** 1187 * The red component of this pixel, where 0 is the minimum value, 1188 * and 255 is the maximum. 1189 */ 1190 this.red = r; 1191 1192 /** 1193 * The green component of this pixel, where 0 is the minimum value, 1194 * and 255 is the maximum. 1195 */ 1196 this.green = g; 1197 1198 /** 1199 * The blue component of this pixel, where 0 is the minimum value, 1200 * and 255 is the maximum. 1201 */ 1202 this.blue = b; 1203 1204 /** 1205 * The alpha component of this pixel, where 0 is the minimum value, 1206 * and 255 is the maximum. 1207 */ 1208 this.alpha = a; 1209 1210 }; 1211