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