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 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel}, 27 * automatically handles incoming and outgoing Guacamole instructions via the 28 * provided tunnel, updating the display using one or more canvas elements. 29 * 30 * @constructor 31 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive 32 * Guacamole instructions. 33 */ 34 Guacamole.Client = function(tunnel) { 35 36 var guac_client = this; 37 38 var STATE_IDLE = 0; 39 var STATE_CONNECTING = 1; 40 var STATE_WAITING = 2; 41 var STATE_CONNECTED = 3; 42 var STATE_DISCONNECTING = 4; 43 var STATE_DISCONNECTED = 5; 44 45 var currentState = STATE_IDLE; 46 47 var currentTimestamp = 0; 48 var pingInterval = null; 49 50 var displayWidth = 0; 51 var displayHeight = 0; 52 var displayScale = 1; 53 54 /** 55 * Translation from Guacamole protocol line caps to Layer line caps. 56 * @private 57 */ 58 var lineCap = { 59 0: "butt", 60 1: "round", 61 2: "square" 62 }; 63 64 /** 65 * Translation from Guacamole protocol line caps to Layer line caps. 66 * @private 67 */ 68 var lineJoin = { 69 0: "bevel", 70 1: "miter", 71 2: "round" 72 }; 73 74 // Create bounding div 75 var bounds = document.createElement("div"); 76 bounds.style.position = "relative"; 77 bounds.style.width = (displayWidth*displayScale) + "px"; 78 bounds.style.height = (displayHeight*displayScale) + "px"; 79 80 // Create display 81 var display = document.createElement("div"); 82 display.style.position = "relative"; 83 display.style.width = displayWidth + "px"; 84 display.style.height = displayHeight + "px"; 85 86 // Ensure transformations on display originate at 0,0 87 display.style.transformOrigin = 88 display.style.webkitTransformOrigin = 89 display.style.MozTransformOrigin = 90 display.style.OTransformOrigin = 91 display.style.msTransformOrigin = 92 "0 0"; 93 94 // Create default layer 95 var default_layer_container = new Guacamole.Client.LayerContainer(0, displayWidth, displayHeight); 96 97 // Position default layer 98 var default_layer_container_element = default_layer_container.getElement(); 99 default_layer_container_element.style.position = "absolute"; 100 default_layer_container_element.style.left = "0px"; 101 default_layer_container_element.style.top = "0px"; 102 default_layer_container_element.style.overflow = "hidden"; 103 default_layer_container_element.style.zIndex = "0"; 104 105 // Create cursor layer 106 var cursor = new Guacamole.Client.LayerContainer(null, 0, 0); 107 cursor.getLayer().setChannelMask(Guacamole.Layer.SRC); 108 cursor.getLayer().autoflush = true; 109 110 // Position cursor layer 111 var cursor_element = cursor.getElement(); 112 cursor_element.style.position = "absolute"; 113 cursor_element.style.left = "0px"; 114 cursor_element.style.top = "0px"; 115 cursor_element.style.zIndex = "1"; 116 117 // Add default layer and cursor to display 118 display.appendChild(default_layer_container.getElement()); 119 display.appendChild(cursor.getElement()); 120 121 // Add display to bounds 122 bounds.appendChild(display); 123 124 // Initially, only default layer exists 125 var layers = [default_layer_container]; 126 127 // No initial buffers 128 var buffers = []; 129 130 // No initial parsers 131 var parsers = []; 132 133 // No initial audio channels 134 var audio_channels = []; 135 136 // No initial streams 137 var streams = []; 138 139 // Pool of available stream indices 140 var stream_indices = new Guacamole.IntegerPool(); 141 142 // Array of allocated output streams by index 143 var output_streams = []; 144 145 function setState(state) { 146 if (state != currentState) { 147 currentState = state; 148 if (guac_client.onstatechange) 149 guac_client.onstatechange(currentState); 150 } 151 } 152 153 function isConnected() { 154 return currentState == STATE_CONNECTED 155 || currentState == STATE_WAITING; 156 } 157 158 var cursorHotspotX = 0; 159 var cursorHotspotY = 0; 160 161 var cursorX = 0; 162 var cursorY = 0; 163 164 function moveCursor(x, y) { 165 166 // Move cursor layer 167 cursor.translate(x - cursorHotspotX, y - cursorHotspotY); 168 169 // Update stored position 170 cursorX = x; 171 cursorY = y; 172 173 } 174 175 /** 176 * Returns an element containing the display of this Guacamole.Client. 177 * Adding the element returned by this function to an element in the body 178 * of a document will cause the client's display to be visible. 179 * 180 * @return {Element} An element containing ths display of this 181 * Guacamole.Client. 182 */ 183 this.getDisplay = function() { 184 return bounds; 185 }; 186 187 /** 188 * Sends the current size of the screen. 189 * 190 * @param {Number} width The width of the screen. 191 * @param {Number} height The height of the screen. 192 */ 193 this.sendSize = function(width, height) { 194 195 // Do not send requests if not connected 196 if (!isConnected()) 197 return; 198 199 tunnel.sendMessage("size", width, height); 200 201 }; 202 203 /** 204 * Sends a key event having the given properties as if the user 205 * pressed or released a key. 206 * 207 * @param {Boolean} pressed Whether the key is pressed (true) or released 208 * (false). 209 * @param {Number} keysym The keysym of the key being pressed or released. 210 */ 211 this.sendKeyEvent = function(pressed, keysym) { 212 // Do not send requests if not connected 213 if (!isConnected()) 214 return; 215 216 tunnel.sendMessage("key", keysym, pressed); 217 }; 218 219 /** 220 * Sends a mouse event having the properties provided by the given mouse 221 * state. 222 * 223 * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send 224 * in the mouse event. 225 */ 226 this.sendMouseState = function(mouseState) { 227 228 // Do not send requests if not connected 229 if (!isConnected()) 230 return; 231 232 // Update client-side cursor 233 moveCursor( 234 Math.floor(mouseState.x), 235 Math.floor(mouseState.y) 236 ); 237 238 // Build mask 239 var buttonMask = 0; 240 if (mouseState.left) buttonMask |= 1; 241 if (mouseState.middle) buttonMask |= 2; 242 if (mouseState.right) buttonMask |= 4; 243 if (mouseState.up) buttonMask |= 8; 244 if (mouseState.down) buttonMask |= 16; 245 246 // Send message 247 tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); 248 }; 249 250 /** 251 * Sets the clipboard of the remote client to the given text data. 252 * 253 * @param {String} data The data to send as the clipboard contents. 254 */ 255 this.setClipboard = function(data) { 256 257 // Do not send requests if not connected 258 if (!isConnected()) 259 return; 260 261 tunnel.sendMessage("clipboard", data); 262 }; 263 264 /** 265 * Opens a new file for writing, having the given index, mimetype and 266 * filename. The stream to associate with this file must already exist. 267 * 268 * @param {String} mimetype The mimetype of the file being sent. 269 * @param {String} filename The filename of the file being sent. 270 * @return {Guacamole.OutputStream} The created file stream. 271 */ 272 this.createFileStream = function(mimetype, filename) { 273 274 // Allocate index 275 var index = stream_indices.next(); 276 277 // Create new stream 278 tunnel.sendMessage("file", index, mimetype, filename); 279 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 280 281 // Override sendEnd() of stream to automatically free index 282 var old_end = stream.sendEnd; 283 stream.sendEnd = function() { 284 old_end(); 285 stream_indices.free(index); 286 delete output_streams[index]; 287 }; 288 289 // Return new, overridden stream 290 return stream; 291 292 }; 293 294 /** 295 * Opens a new pipe for writing, having the given name and mimetype. The 296 * stream to associate with this pipe must already exist. 297 * 298 * @param {String} mimetype The mimetype of the data being sent. 299 * @param {String} name The name of the pipe. 300 * @return {Guacamole.OutputStream} The created file stream. 301 */ 302 this.createPipeStream = function(mimetype, name) { 303 304 // Allocate index 305 var index = stream_indices.next(); 306 307 // Create new stream 308 tunnel.sendMessage("pipe", index, mimetype, name); 309 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 310 311 // Override sendEnd() of stream to automatically free index 312 var old_end = stream.sendEnd; 313 stream.sendEnd = function() { 314 old_end(); 315 stream_indices.free(index); 316 delete output_streams[index]; 317 }; 318 319 // Return new, overridden stream 320 return stream; 321 322 }; 323 324 /** 325 * Acknowledge receipt of a blob on the stream with the given index. 326 * 327 * @param {Number} index The index of the stream associated with the 328 * received blob. 329 * @param {String} message A human-readable message describing the error 330 * or status. 331 * @param {Number} code The error code, if any, or 0 for success. 332 */ 333 this.sendAck = function(index, message, code) { 334 335 // Do not send requests if not connected 336 if (!isConnected()) 337 return; 338 339 tunnel.sendMessage("ack", index, message, code); 340 }; 341 342 /** 343 * Given the index of a file, writes a blob of data to that file. 344 * 345 * @param {Number} index The index of the file to write to. 346 * @param {String} data Base64-encoded data to write to the file. 347 */ 348 this.sendBlob = function(index, data) { 349 350 // Do not send requests if not connected 351 if (!isConnected()) 352 return; 353 354 tunnel.sendMessage("blob", index, data); 355 }; 356 357 /** 358 * Marks a currently-open stream as complete. 359 * 360 * @param {Number} index The index of the stream to end. 361 */ 362 this.endStream = function(index) { 363 364 // Do not send requests if not connected 365 if (!isConnected()) 366 return; 367 368 tunnel.sendMessage("end", index); 369 }; 370 371 /** 372 * Fired whenever the state of this Guacamole.Client changes. 373 * 374 * @event 375 * @param {Number} state The new state of the client. 376 */ 377 this.onstatechange = null; 378 379 /** 380 * Fired when the remote client sends a name update. 381 * 382 * @event 383 * @param {String} name The new name of this client. 384 */ 385 this.onname = null; 386 387 /** 388 * Fired when an error is reported by the remote client, and the connection 389 * is being closed. 390 * 391 * @event 392 * @param {Guacamole.Status} status A status object which describes the 393 * error. 394 */ 395 this.onerror = null; 396 397 /** 398 * Fired when the clipboard of the remote client is changing. 399 * 400 * @event 401 * @param {String} data The new text data of the remote clipboard. 402 */ 403 this.onclipboard = null; 404 405 /** 406 * Fired when the default layer (and thus the entire Guacamole display) 407 * is resized. 408 * 409 * @event 410 * @param {Number} width The new width of the Guacamole display. 411 * @param {Number} height The new height of the Guacamole display. 412 */ 413 this.onresize = null; 414 415 /** 416 * Fired when a file stream is created. The stream provided to this event 417 * handler will contain its own event handlers for received data. 418 * 419 * @event 420 * @param {Guacamole.InputStream} stream The stream that will receive data 421 * from the server. 422 * @param {String} mimetype The mimetype of the file received. 423 * @param {String} filename The name of the file received. 424 */ 425 this.onfile = null; 426 427 /** 428 * Fired when a pipe stream is created. The stream provided to this event 429 * handler will contain its own event handlers for received data; 430 * 431 * @event 432 * @param {Guacamole.InputStream} stream The stream that will receive data 433 * from the server. 434 * @param {String} mimetype The mimetype of the data which will be received. 435 * @param {String} name The name of the pipe. 436 */ 437 this.onpipe = null; 438 439 /** 440 * Fired whenever a sync instruction is received from the server, indicating 441 * that the server is finished processing any input from the client and 442 * has sent any results. 443 * 444 * @event 445 * @param {Number} timestamp The timestamp associated with the sync 446 * instruction. 447 */ 448 this.onsync = null; 449 450 // Layers 451 function getBufferLayer(index) { 452 453 index = -1 - index; 454 var buffer = buffers[index]; 455 456 // Create buffer if necessary 457 if (buffer == null) { 458 buffer = new Guacamole.Layer(0, 0); 459 buffer.autoflush = 1; 460 buffer.autosize = 1; 461 buffers[index] = buffer; 462 } 463 464 return buffer; 465 466 } 467 468 function getLayerContainer(index) { 469 470 var layer = layers[index]; 471 if (layer == null) { 472 473 // Add new layer 474 layer = new Guacamole.Client.LayerContainer(index, displayWidth, displayHeight); 475 layers[index] = layer; 476 477 // Get and position layer 478 var layer_element = layer.getElement(); 479 layer_element.style.position = "absolute"; 480 layer_element.style.left = "0px"; 481 layer_element.style.top = "0px"; 482 layer_element.style.overflow = "hidden"; 483 484 // Add to default layer container 485 layer.move(default_layer_container, 0, 0, 0); 486 487 } 488 489 return layer; 490 491 } 492 493 function getLayer(index) { 494 495 // If buffer, just get layer 496 if (index < 0) 497 return getBufferLayer(index); 498 499 // Otherwise, retrieve layer from layer container 500 return getLayerContainer(index).getLayer(); 501 502 } 503 504 function getParser(index) { 505 506 var parser = parsers[index]; 507 508 // If parser not yet created, create it, and tie to the 509 // oninstruction handler of the tunnel. 510 if (parser == null) { 511 parser = parsers[index] = new Guacamole.Parser(); 512 parser.oninstruction = tunnel.oninstruction; 513 } 514 515 return parser; 516 517 } 518 519 function getAudioChannel(index) { 520 521 var audio_channel = audio_channels[index]; 522 523 // If audio channel not yet created, create it 524 if (audio_channel == null) 525 audio_channel = audio_channels[index] = new Guacamole.AudioChannel(); 526 527 return audio_channel; 528 529 } 530 531 /** 532 * Handlers for all defined layer properties. 533 * @private 534 */ 535 var layerPropertyHandlers = { 536 537 "miter-limit": function(layer, value) { 538 layer.setMiterLimit(parseFloat(value)); 539 } 540 541 }; 542 543 /** 544 * Handlers for all instruction opcodes receivable by a Guacamole protocol 545 * client. 546 * @private 547 */ 548 var instructionHandlers = { 549 550 "ack": function(parameters) { 551 552 var stream_index = parseInt(parameters[0]); 553 var reason = parameters[1]; 554 var code = parameters[2]; 555 556 // Get stream 557 var stream = output_streams[stream_index]; 558 if (stream) { 559 560 // Signal ack if handler defined 561 if (stream.onack) 562 stream.onack(new Guacamole.Status(code, reason)); 563 564 // If code is an error, invalidate stream 565 if (code >= 0x0100) { 566 stream_indices.free(stream_index); 567 delete output_streams[stream_index]; 568 } 569 570 } 571 572 }, 573 574 "arc": function(parameters) { 575 576 var layer = getLayer(parseInt(parameters[0])); 577 var x = parseInt(parameters[1]); 578 var y = parseInt(parameters[2]); 579 var radius = parseInt(parameters[3]); 580 var startAngle = parseFloat(parameters[4]); 581 var endAngle = parseFloat(parameters[5]); 582 var negative = parseInt(parameters[6]); 583 584 layer.arc(x, y, radius, startAngle, endAngle, negative != 0); 585 586 }, 587 588 "audio": function(parameters) { 589 590 var stream_index = parseInt(parameters[0]); 591 var channel = getAudioChannel(parseInt(parameters[1])); 592 var mimetype = parameters[2]; 593 var duration = parseFloat(parameters[3]); 594 595 // Create stream 596 var stream = streams[stream_index] = 597 new Guacamole.InputStream(guac_client, stream_index); 598 599 // Assemble entire stream as a blob 600 var blob_reader = new Guacamole.BlobReader(stream, mimetype); 601 602 // Play blob as audio 603 blob_reader.onend = function() { 604 channel.play(mimetype, duration, blob_reader.getBlob()); 605 }; 606 607 // Send success response 608 guac_client.sendAck(stream_index, "OK", 0x0000); 609 610 }, 611 612 "blob": function(parameters) { 613 614 // Get stream 615 var stream_index = parseInt(parameters[0]); 616 var data = parameters[1]; 617 var stream = streams[stream_index]; 618 619 // Write data 620 stream.onblob(data); 621 622 }, 623 624 "cfill": function(parameters) { 625 626 var channelMask = parseInt(parameters[0]); 627 var layer = getLayer(parseInt(parameters[1])); 628 var r = parseInt(parameters[2]); 629 var g = parseInt(parameters[3]); 630 var b = parseInt(parameters[4]); 631 var a = parseInt(parameters[5]); 632 633 layer.setChannelMask(channelMask); 634 635 layer.fillColor(r, g, b, a); 636 637 }, 638 639 "clip": function(parameters) { 640 641 var layer = getLayer(parseInt(parameters[0])); 642 643 layer.clip(); 644 645 }, 646 647 "clipboard": function(parameters) { 648 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]); 649 }, 650 651 "close": function(parameters) { 652 653 var layer = getLayer(parseInt(parameters[0])); 654 655 layer.close(); 656 657 }, 658 659 "copy": function(parameters) { 660 661 var srcL = getLayer(parseInt(parameters[0])); 662 var srcX = parseInt(parameters[1]); 663 var srcY = parseInt(parameters[2]); 664 var srcWidth = parseInt(parameters[3]); 665 var srcHeight = parseInt(parameters[4]); 666 var channelMask = parseInt(parameters[5]); 667 var dstL = getLayer(parseInt(parameters[6])); 668 var dstX = parseInt(parameters[7]); 669 var dstY = parseInt(parameters[8]); 670 671 dstL.setChannelMask(channelMask); 672 673 dstL.copy( 674 srcL, 675 srcX, 676 srcY, 677 srcWidth, 678 srcHeight, 679 dstX, 680 dstY 681 ); 682 683 }, 684 685 "cstroke": function(parameters) { 686 687 var channelMask = parseInt(parameters[0]); 688 var layer = getLayer(parseInt(parameters[1])); 689 var cap = lineCap[parseInt(parameters[2])]; 690 var join = lineJoin[parseInt(parameters[3])]; 691 var thickness = parseInt(parameters[4]); 692 var r = parseInt(parameters[5]); 693 var g = parseInt(parameters[6]); 694 var b = parseInt(parameters[7]); 695 var a = parseInt(parameters[8]); 696 697 layer.setChannelMask(channelMask); 698 699 layer.strokeColor(cap, join, thickness, r, g, b, a); 700 701 }, 702 703 "cursor": function(parameters) { 704 705 cursorHotspotX = parseInt(parameters[0]); 706 cursorHotspotY = parseInt(parameters[1]); 707 var srcL = getLayer(parseInt(parameters[2])); 708 var srcX = parseInt(parameters[3]); 709 var srcY = parseInt(parameters[4]); 710 var srcWidth = parseInt(parameters[5]); 711 var srcHeight = parseInt(parameters[6]); 712 713 // Reset cursor size 714 cursor.resize(srcWidth, srcHeight); 715 716 // Draw cursor to cursor layer 717 cursor.getLayer().copy( 718 srcL, 719 srcX, 720 srcY, 721 srcWidth, 722 srcHeight, 723 0, 724 0 725 ); 726 727 // Update cursor position (hotspot may have changed) 728 moveCursor(cursorX, cursorY); 729 730 }, 731 732 "curve": function(parameters) { 733 734 var layer = getLayer(parseInt(parameters[0])); 735 var cp1x = parseInt(parameters[1]); 736 var cp1y = parseInt(parameters[2]); 737 var cp2x = parseInt(parameters[3]); 738 var cp2y = parseInt(parameters[4]); 739 var x = parseInt(parameters[5]); 740 var y = parseInt(parameters[6]); 741 742 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); 743 744 }, 745 746 "dispose": function(parameters) { 747 748 var layer_index = parseInt(parameters[0]); 749 750 // If visible layer, remove from parent 751 if (layer_index > 0) { 752 753 // Remove from parent 754 var layer_container = getLayerContainer(layer_index); 755 layer_container.dispose(); 756 757 // Delete reference 758 delete layers[layer_index]; 759 760 } 761 762 // If buffer, just delete reference 763 else if (layer_index < 0) 764 delete buffers[-1 - layer_index]; 765 766 // Attempting to dispose the root layer currently has no effect. 767 768 }, 769 770 "distort": function(parameters) { 771 772 var layer_index = parseInt(parameters[0]); 773 var a = parseFloat(parameters[1]); 774 var b = parseFloat(parameters[2]); 775 var c = parseFloat(parameters[3]); 776 var d = parseFloat(parameters[4]); 777 var e = parseFloat(parameters[5]); 778 var f = parseFloat(parameters[6]); 779 780 // Only valid for visible layers (not buffers) 781 if (layer_index >= 0) { 782 783 // Set layer transform 784 var layer_container = getLayerContainer(layer_index); 785 layer_container.transform(a, b, c, d, e, f); 786 787 } 788 789 }, 790 791 "error": function(parameters) { 792 793 var reason = parameters[0]; 794 var code = parameters[1]; 795 796 // Call handler if defined 797 if (guac_client.onerror) 798 guac_client.onerror(new Guacamole.Status(code, reason)); 799 800 guac_client.disconnect(); 801 802 }, 803 804 "end": function(parameters) { 805 806 // Get stream 807 var stream_index = parseInt(parameters[0]); 808 var stream = streams[stream_index]; 809 810 // Signal end of stream 811 if (stream.onend) 812 stream.onend(); 813 814 }, 815 816 "file": function(parameters) { 817 818 var stream_index = parseInt(parameters[0]); 819 var mimetype = parameters[1]; 820 var filename = parameters[2]; 821 822 // Create stream 823 if (guac_client.onfile) { 824 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 825 guac_client.onfile(stream, mimetype, filename); 826 } 827 828 // Otherwise, unsupported 829 else 830 guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); 831 832 }, 833 834 "identity": function(parameters) { 835 836 var layer = getLayer(parseInt(parameters[0])); 837 838 layer.setTransform(1, 0, 0, 1, 0, 0); 839 840 }, 841 842 "lfill": function(parameters) { 843 844 var channelMask = parseInt(parameters[0]); 845 var layer = getLayer(parseInt(parameters[1])); 846 var srcLayer = getLayer(parseInt(parameters[2])); 847 848 layer.setChannelMask(channelMask); 849 850 layer.fillLayer(srcLayer); 851 852 }, 853 854 "line": function(parameters) { 855 856 var layer = getLayer(parseInt(parameters[0])); 857 var x = parseInt(parameters[1]); 858 var y = parseInt(parameters[2]); 859 860 layer.lineTo(x, y); 861 862 }, 863 864 "lstroke": function(parameters) { 865 866 var channelMask = parseInt(parameters[0]); 867 var layer = getLayer(parseInt(parameters[1])); 868 var srcLayer = getLayer(parseInt(parameters[2])); 869 870 layer.setChannelMask(channelMask); 871 872 layer.strokeLayer(srcLayer); 873 874 }, 875 876 "move": function(parameters) { 877 878 var layer_index = parseInt(parameters[0]); 879 var parent_index = parseInt(parameters[1]); 880 var x = parseInt(parameters[2]); 881 var y = parseInt(parameters[3]); 882 var z = parseInt(parameters[4]); 883 884 // Only valid for non-default layers 885 if (layer_index > 0 && parent_index >= 0) { 886 887 // Get container element 888 var layer_container = getLayerContainer(layer_index); 889 var parent = getLayerContainer(parent_index); 890 891 // Move layer 892 layer_container.move(parent, x, y, z); 893 894 } 895 896 }, 897 898 "name": function(parameters) { 899 if (guac_client.onname) guac_client.onname(parameters[0]); 900 }, 901 902 "nest": function(parameters) { 903 var parser = getParser(parseInt(parameters[0])); 904 parser.receive(parameters[1]); 905 }, 906 907 "pipe": function(parameters) { 908 909 var stream_index = parseInt(parameters[0]); 910 var mimetype = parameters[1]; 911 var name = parameters[2]; 912 913 // Create stream 914 if (guac_client.onpipe) { 915 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 916 guac_client.onpipe(stream, mimetype, name); 917 } 918 919 // Otherwise, unsupported 920 else 921 guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100); 922 923 }, 924 925 "png": function(parameters) { 926 927 var channelMask = parseInt(parameters[0]); 928 var layer = getLayer(parseInt(parameters[1])); 929 var x = parseInt(parameters[2]); 930 var y = parseInt(parameters[3]); 931 var data = parameters[4]; 932 933 layer.setChannelMask(channelMask); 934 935 layer.draw( 936 x, 937 y, 938 "data:image/png;base64," + data 939 ); 940 941 }, 942 943 "pop": function(parameters) { 944 945 var layer = getLayer(parseInt(parameters[0])); 946 947 layer.pop(); 948 949 }, 950 951 "push": function(parameters) { 952 953 var layer = getLayer(parseInt(parameters[0])); 954 955 layer.push(); 956 957 }, 958 959 "rect": function(parameters) { 960 961 var layer = getLayer(parseInt(parameters[0])); 962 var x = parseInt(parameters[1]); 963 var y = parseInt(parameters[2]); 964 var w = parseInt(parameters[3]); 965 var h = parseInt(parameters[4]); 966 967 layer.rect(x, y, w, h); 968 969 }, 970 971 "reset": function(parameters) { 972 973 var layer = getLayer(parseInt(parameters[0])); 974 975 layer.reset(); 976 977 }, 978 979 "set": function(parameters) { 980 981 var layer = getLayer(parseInt(parameters[0])); 982 var name = parameters[1]; 983 var value = parameters[2]; 984 985 // Call property handler if defined 986 var handler = layerPropertyHandlers[name]; 987 if (handler) 988 handler(layer, value); 989 990 }, 991 992 "shade": function(parameters) { 993 994 var layer_index = parseInt(parameters[0]); 995 var a = parseInt(parameters[1]); 996 997 // Only valid for visible layers (not buffers) 998 if (layer_index >= 0) { 999 var layer_container = getLayerContainer(layer_index); 1000 layer_container.shade(a); 1001 } 1002 1003 }, 1004 1005 "size": function(parameters) { 1006 1007 var layer_index = parseInt(parameters[0]); 1008 var width = parseInt(parameters[1]); 1009 var height = parseInt(parameters[2]); 1010 1011 // If not buffer, resize layer and container 1012 if (layer_index >= 0) { 1013 1014 // Resize layer 1015 var layer_container = getLayerContainer(layer_index); 1016 layer_container.resize(width, height); 1017 1018 // If layer is default, resize display 1019 if (layer_index == 0) { 1020 1021 displayWidth = width; 1022 displayHeight = height; 1023 1024 // Update (set) display size 1025 display.style.width = displayWidth + "px"; 1026 display.style.height = displayHeight + "px"; 1027 1028 // Update bounds size 1029 bounds.style.width = (displayWidth*displayScale) + "px"; 1030 bounds.style.height = (displayHeight*displayScale) + "px"; 1031 1032 // Call resize event handler if defined 1033 if (guac_client.onresize) 1034 guac_client.onresize(width, height); 1035 1036 } 1037 1038 } 1039 1040 // If buffer, resize layer only 1041 else { 1042 var layer = getBufferLayer(parseInt(parameters[0])); 1043 layer.resize(width, height); 1044 } 1045 1046 }, 1047 1048 "start": function(parameters) { 1049 1050 var layer = getLayer(parseInt(parameters[0])); 1051 var x = parseInt(parameters[1]); 1052 var y = parseInt(parameters[2]); 1053 1054 layer.moveTo(x, y); 1055 1056 }, 1057 1058 "sync": function(parameters) { 1059 1060 var timestamp = parameters[0]; 1061 1062 // When all layers have finished rendering all instructions 1063 // UP TO THIS POINT IN TIME, send sync response. 1064 1065 var layersToSync = 0; 1066 function syncLayer() { 1067 1068 layersToSync--; 1069 1070 // Send sync response when layers are finished 1071 if (layersToSync == 0) { 1072 if (timestamp != currentTimestamp) { 1073 tunnel.sendMessage("sync", timestamp); 1074 currentTimestamp = timestamp; 1075 } 1076 } 1077 1078 } 1079 1080 // Count active, not-ready layers and install sync tracking hooks 1081 for (var i=0; i<layers.length; i++) { 1082 1083 var layer_container = layers[i]; 1084 if (layer_container) { 1085 1086 var layer = layer_container.getLayer(); 1087 if (layer) { 1088 1089 // Flush layer 1090 layer.flush(); 1091 1092 // If still not ready, sync later 1093 if (!layer.isReady()) { 1094 layersToSync++; 1095 layer.sync(syncLayer); 1096 } 1097 1098 } 1099 1100 } // end if layer exists 1101 1102 } // end for each layer 1103 1104 // If all layers are ready, then we didn't install any hooks. 1105 // Send sync message now, 1106 if (layersToSync == 0) { 1107 if (timestamp != currentTimestamp) { 1108 tunnel.sendMessage("sync", timestamp); 1109 currentTimestamp = timestamp; 1110 } 1111 } 1112 1113 // If received first update, no longer waiting. 1114 if (currentState == STATE_WAITING) 1115 setState(STATE_CONNECTED); 1116 1117 // Call sync handler if defined 1118 if (guac_client.onsync) 1119 guac_client.onsync(timestamp); 1120 1121 }, 1122 1123 "transfer": function(parameters) { 1124 1125 var srcL = getLayer(parseInt(parameters[0])); 1126 var srcX = parseInt(parameters[1]); 1127 var srcY = parseInt(parameters[2]); 1128 var srcWidth = parseInt(parameters[3]); 1129 var srcHeight = parseInt(parameters[4]); 1130 var function_index = parseInt(parameters[5]); 1131 var dstL = getLayer(parseInt(parameters[6])); 1132 var dstX = parseInt(parameters[7]); 1133 var dstY = parseInt(parameters[8]); 1134 1135 /* SRC */ 1136 if (function_index === 0x3) 1137 dstL.put( 1138 srcL, 1139 srcX, 1140 srcY, 1141 srcWidth, 1142 srcHeight, 1143 dstX, 1144 dstY 1145 ); 1146 1147 /* Anything else that isn't a NO-OP */ 1148 else if (function_index !== 0x5) 1149 dstL.transfer( 1150 srcL, 1151 srcX, 1152 srcY, 1153 srcWidth, 1154 srcHeight, 1155 dstX, 1156 dstY, 1157 Guacamole.Client.DefaultTransferFunction[function_index] 1158 ); 1159 1160 }, 1161 1162 "transform": function(parameters) { 1163 1164 var layer = getLayer(parseInt(parameters[0])); 1165 var a = parseFloat(parameters[1]); 1166 var b = parseFloat(parameters[2]); 1167 var c = parseFloat(parameters[3]); 1168 var d = parseFloat(parameters[4]); 1169 var e = parseFloat(parameters[5]); 1170 var f = parseFloat(parameters[6]); 1171 1172 layer.transform(a, b, c, d, e, f); 1173 1174 }, 1175 1176 "video": function(parameters) { 1177 1178 var stream_index = parseInt(parameters[0]); 1179 var layer = getLayer(parseInt(parameters[1])); 1180 var mimetype = parameters[2]; 1181 var duration = parseFloat(parameters[3]); 1182 1183 // Create stream 1184 var stream = streams[stream_index] = 1185 new Guacamole.InputStream(guac_client, stream_index); 1186 1187 // Assemble entire stream as a blob 1188 var blob_reader = new Guacamole.BlobReader(stream, mimetype); 1189 1190 // Play video once finished 1191 blob_reader.onend = function() { 1192 1193 // Read data from blob from stream 1194 var reader = new FileReader(); 1195 reader.onload = function() { 1196 1197 var binary = ""; 1198 var bytes = new Uint8Array(reader.result); 1199 1200 // Produce binary string from bytes in buffer 1201 for (var i=0; i<bytes.byteLength; i++) 1202 binary += String.fromCharCode(bytes[i]); 1203 1204 // Play video 1205 layer.play(mimetype, duration, "data:" + mimetype + ";base64," + window.btoa(binary)); 1206 1207 }; 1208 reader.readAsArrayBuffer(blob_reader.getBlob()); 1209 1210 }; 1211 1212 // Send success response 1213 tunnel.sendMessage("ack", stream_index, "OK", 0x0000); 1214 1215 } 1216 1217 }; 1218 1219 tunnel.oninstruction = function(opcode, parameters) { 1220 1221 var handler = instructionHandlers[opcode]; 1222 if (handler) 1223 handler(parameters); 1224 1225 }; 1226 1227 /** 1228 * Sends a disconnect instruction to the server and closes the tunnel. 1229 */ 1230 this.disconnect = function() { 1231 1232 // Only attempt disconnection not disconnected. 1233 if (currentState != STATE_DISCONNECTED 1234 && currentState != STATE_DISCONNECTING) { 1235 1236 setState(STATE_DISCONNECTING); 1237 1238 // Stop ping 1239 if (pingInterval) 1240 window.clearInterval(pingInterval); 1241 1242 // Send disconnect message and disconnect 1243 tunnel.sendMessage("disconnect"); 1244 tunnel.disconnect(); 1245 setState(STATE_DISCONNECTED); 1246 1247 } 1248 1249 }; 1250 1251 /** 1252 * Connects the underlying tunnel of this Guacamole.Client, passing the 1253 * given arbitrary data to the tunnel during the connection process. 1254 * 1255 * @param data Arbitrary connection data to be sent to the underlying 1256 * tunnel during the connection process. 1257 * @throws {Guacamole.Status} If an error occurs during connection. 1258 */ 1259 this.connect = function(data) { 1260 1261 setState(STATE_CONNECTING); 1262 1263 try { 1264 tunnel.connect(data); 1265 } 1266 catch (status) { 1267 setState(STATE_IDLE); 1268 throw status; 1269 } 1270 1271 // Ping every 5 seconds (ensure connection alive) 1272 pingInterval = window.setInterval(function() { 1273 tunnel.sendMessage("sync", currentTimestamp); 1274 }, 5000); 1275 1276 setState(STATE_WAITING); 1277 }; 1278 1279 /** 1280 * Sets the scale of the client display element such that it renders at 1281 * a relatively smaller or larger size, without affecting the true 1282 * resolution of the display. 1283 * 1284 * @param {Number} scale The scale to resize to, where 1.0 is normal 1285 * size (1:1 scale). 1286 */ 1287 this.scale = function(scale) { 1288 1289 display.style.transform = 1290 display.style.WebkitTransform = 1291 display.style.MozTransform = 1292 display.style.OTransform = 1293 display.style.msTransform = 1294 1295 "scale(" + scale + "," + scale + ")"; 1296 1297 displayScale = scale; 1298 1299 // Update bounds size 1300 bounds.style.width = (displayWidth*displayScale) + "px"; 1301 bounds.style.height = (displayHeight*displayScale) + "px"; 1302 1303 }; 1304 1305 /** 1306 * Returns the width of the display. 1307 * 1308 * @return {Number} The width of the display. 1309 */ 1310 this.getWidth = function() { 1311 return displayWidth; 1312 }; 1313 1314 /** 1315 * Returns the height of the display. 1316 * 1317 * @return {Number} The height of the display. 1318 */ 1319 this.getHeight = function() { 1320 return displayHeight; 1321 }; 1322 1323 /** 1324 * Returns the scale of the display. 1325 * 1326 * @return {Number} The scale of the display. 1327 */ 1328 this.getScale = function() { 1329 return displayScale; 1330 }; 1331 1332 /** 1333 * Returns a canvas element containing the entire display, with all child 1334 * layers composited within. 1335 * 1336 * @return {HTMLCanvasElement} A new canvas element containing a copy of 1337 * the display. 1338 */ 1339 this.flatten = function() { 1340 1341 // Get default layer 1342 var default_layer = getLayerContainer(0); 1343 1344 // Get destination canvas 1345 var canvas = document.createElement("canvas"); 1346 canvas.width = default_layer.width; 1347 canvas.height = default_layer.height; 1348 1349 var context = canvas.getContext("2d"); 1350 1351 // Returns sorted array of children 1352 function get_children(layer) { 1353 1354 // Build array of children 1355 var children = []; 1356 for (var index in layer.children) 1357 children.push(layer.children[index]); 1358 1359 // Sort 1360 children.sort(function children_comparator(a, b) { 1361 1362 // Compare based on Z order 1363 var diff = a.z - b.z; 1364 if (diff !== 0) 1365 return diff; 1366 1367 // If Z order identical, use document order 1368 var a_element = a.getElement(); 1369 var b_element = b.getElement(); 1370 var position = b_element.compareDocumentPosition(a_element); 1371 1372 if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; 1373 if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; 1374 1375 // Otherwise, assume same 1376 return 0; 1377 1378 }); 1379 1380 // Done 1381 return children; 1382 1383 } 1384 1385 // Draws the contents of the given layer at the given coordinates 1386 function draw_layer(layer, x, y) { 1387 1388 // Draw layer 1389 if (layer.width > 0 && layer.height > 0) { 1390 1391 // Save and update alpha 1392 var initial_alpha = context.globalAlpha; 1393 context.globalAlpha *= layer.alpha / 255.0; 1394 1395 // Copy data 1396 context.drawImage(layer.getLayer().getCanvas(), x, y); 1397 1398 // Draw all children 1399 var children = get_children(layer); 1400 for (var i=0; i<children.length; i++) { 1401 var child = children[i]; 1402 draw_layer(child, x + child.x, y + child.y); 1403 } 1404 1405 // Restore alpha 1406 context.globalAlpha = initial_alpha; 1407 1408 } 1409 1410 } 1411 1412 // Draw default layer and all children 1413 draw_layer(default_layer, 0, 0); 1414 1415 // Return new canvas copy 1416 return canvas; 1417 1418 }; 1419 1420 }; 1421 1422 /** 1423 * Simple container for Guacamole.Layer, allowing layers to be easily 1424 * repositioned and nested. This allows certain operations to be accelerated 1425 * through DOM manipulation, rather than raster operations. 1426 * 1427 * @constructor 1428 * 1429 * @param {Number} index The unique integer which identifies this layer. 1430 * 1431 * @param {Number} width The width of the Layer, in pixels. The canvas element 1432 * backing this Layer will be given this width. 1433 * 1434 * @param {Number} height The height of the Layer, in pixels. The canvas element 1435 * backing this Layer will be given this height. 1436 */ 1437 Guacamole.Client.LayerContainer = function(index, width, height) { 1438 1439 /** 1440 * Reference to this LayerContainer. 1441 * @private 1442 */ 1443 var layer_container = this; 1444 1445 /** 1446 * Unique integer identifying this layer container. 1447 * @type Number 1448 */ 1449 this.index = index; 1450 1451 /** 1452 * The opacity of the layer container, where 255 is fully opaque and 0 is 1453 * fully transparent. 1454 */ 1455 this.alpha = 0xFF; 1456 1457 /** 1458 * X coordinate of the upper-left corner of this layer container within 1459 * its parent, in pixels. 1460 * @type Number 1461 */ 1462 this.x = 0; 1463 1464 /** 1465 * Y coordinate of the upper-left corner of this layer container within 1466 * its parent, in pixels. 1467 * @type Number 1468 */ 1469 this.y = 0; 1470 1471 /** 1472 * Z stacking order of this layer relative to other sibling layers. 1473 * @type Number 1474 */ 1475 this.z = 0; 1476 1477 /** 1478 * The affine transformation applied to this layer container. Each element 1479 * corresponds to a value from the transformation matrix, with the first 1480 * three values being the first row, and the last three values being the 1481 * second row. There are six values total. 1482 * 1483 * @type Number[] 1484 */ 1485 this.matrix = [1, 0, 0, 1, 0, 0]; 1486 1487 /** 1488 * The width of this layer in pixels. 1489 * @type Number 1490 */ 1491 this.width = width; 1492 1493 /** 1494 * The height of this layer in pixels. 1495 * @type Number 1496 */ 1497 this.height = height; 1498 1499 /** 1500 * The parent layer container of this layer, if any. 1501 * @type Guacamole.Client.LayerContainer 1502 */ 1503 this.parent = null; 1504 1505 /** 1506 * Set of all children of this layer, indexed by layer index. This object 1507 * will have one property per child. 1508 */ 1509 this.children = {}; 1510 1511 // Create layer with given size 1512 var layer = new Guacamole.Layer(width, height); 1513 1514 // Set layer position 1515 var canvas = layer.getCanvas(); 1516 canvas.style.position = "absolute"; 1517 canvas.style.left = "0px"; 1518 canvas.style.top = "0px"; 1519 1520 // Create div with given size 1521 var div = document.createElement("div"); 1522 div.appendChild(canvas); 1523 div.style.width = width + "px"; 1524 div.style.height = height + "px"; 1525 1526 /** 1527 * Changes the size of this LayerContainer and the contained Layer to the 1528 * given width and height. 1529 * 1530 * @param {Number} width The new width to assign to this Layer. 1531 * @param {Number} height The new height to assign to this Layer. 1532 */ 1533 this.resize = function(width, height) { 1534 1535 layer_container.width = width; 1536 layer_container.height = height; 1537 1538 // Resize layer 1539 layer.resize(width, height); 1540 1541 // Resize containing div 1542 div.style.width = width + "px"; 1543 div.style.height = height + "px"; 1544 1545 }; 1546 1547 /** 1548 * Returns the Layer contained within this LayerContainer. 1549 * @returns {Guacamole.Layer} The Layer contained within this 1550 * LayerContainer. 1551 */ 1552 this.getLayer = function() { 1553 return layer; 1554 }; 1555 1556 /** 1557 * Returns the element containing the Layer within this LayerContainer. 1558 * @returns {Element} The element containing the Layer within this 1559 * LayerContainer. 1560 */ 1561 this.getElement = function() { 1562 return div; 1563 }; 1564 1565 /** 1566 * The translation component of this LayerContainer's transform. 1567 * @private 1568 */ 1569 var translate = "translate(0px, 0px)"; // (0, 0) 1570 1571 /** 1572 * The arbitrary matrix component of this LayerContainer's transform. 1573 * @private 1574 */ 1575 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity 1576 1577 /** 1578 * Moves the upper-left corner of this LayerContainer to the given X and Y 1579 * coordinate. 1580 * 1581 * @param {Number} x The X coordinate to move to. 1582 * @param {Number} y The Y coordinate to move to. 1583 */ 1584 this.translate = function(x, y) { 1585 1586 layer_container.x = x; 1587 layer_container.y = y; 1588 1589 // Generate translation 1590 translate = "translate(" 1591 + x + "px," 1592 + y + "px)"; 1593 1594 // Set layer transform 1595 div.style.transform = 1596 div.style.WebkitTransform = 1597 div.style.MozTransform = 1598 div.style.OTransform = 1599 div.style.msTransform = 1600 1601 translate + " " + matrix; 1602 1603 }; 1604 1605 /** 1606 * Moves the upper-left corner of this LayerContainer to the given X and Y 1607 * coordinate, sets the Z stacking order, and reparents this LayerContainer 1608 * to the given LayerContainer. 1609 * 1610 * @param {Guacamole.Client.LayerContainer} parent The parent to set. 1611 * @param {Number} x The X coordinate to move to. 1612 * @param {Number} y The Y coordinate to move to. 1613 * @param {Number} z The Z coordinate to move to. 1614 */ 1615 this.move = function(parent, x, y, z) { 1616 1617 // Set parent if necessary 1618 if (layer_container.parent !== parent) { 1619 1620 // Maintain relationship 1621 if (layer_container.parent) 1622 delete layer_container.parent.children[layer_container.index]; 1623 layer_container.parent = parent; 1624 parent.children[layer_container.index] = layer_container; 1625 1626 // Reparent element 1627 var parent_element = parent.getElement(); 1628 parent_element.appendChild(div); 1629 1630 } 1631 1632 // Set location 1633 layer_container.translate(x, y); 1634 layer_container.z = z; 1635 div.style.zIndex = z; 1636 1637 }; 1638 1639 /** 1640 * Sets the opacity of this layer to the given value, where 255 is fully 1641 * opaque and 0 is fully transparent. 1642 * 1643 * @param {Number} a The opacity to set. 1644 */ 1645 this.shade = function(a) { 1646 layer_container.alpha = a; 1647 div.style.opacity = a/255.0; 1648 }; 1649 1650 /** 1651 * Removes this layer container entirely, such that it is no longer 1652 * contained within its parent layer, if any. 1653 */ 1654 this.dispose = function() { 1655 1656 // Remove from parent container 1657 if (layer_container.parent) { 1658 delete layer_container.parent.children[layer_container.index]; 1659 layer_container.parent = null; 1660 } 1661 1662 // Remove from parent element 1663 if (div.parentNode) 1664 div.parentNode.removeChild(div); 1665 1666 }; 1667 1668 /** 1669 * Applies the given affine transform (defined with six values from the 1670 * transform's matrix). 1671 * 1672 * @param {Number} a The first value in the affine transform's matrix. 1673 * @param {Number} b The second value in the affine transform's matrix. 1674 * @param {Number} c The third value in the affine transform's matrix. 1675 * @param {Number} d The fourth value in the affine transform's matrix. 1676 * @param {Number} e The fifth value in the affine transform's matrix. 1677 * @param {Number} f The sixth value in the affine transform's matrix. 1678 */ 1679 this.transform = function(a, b, c, d, e, f) { 1680 1681 // Store matrix 1682 layer_container.matrix = [a, b, c, d, e, f]; 1683 1684 // Generate matrix transformation 1685 matrix = 1686 1687 /* a c e 1688 * b d f 1689 * 0 0 1 1690 */ 1691 1692 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; 1693 1694 // Set layer transform 1695 div.style.transform = 1696 div.style.WebkitTransform = 1697 div.style.MozTransform = 1698 div.style.OTransform = 1699 div.style.msTransform = 1700 1701 translate + " " + matrix; 1702 1703 }; 1704 1705 }; 1706 1707 /** 1708 * Map of all Guacamole binary raster operations to transfer functions. 1709 * @private 1710 */ 1711 Guacamole.Client.DefaultTransferFunction = { 1712 1713 /* BLACK */ 1714 0x0: function (src, dst) { 1715 dst.red = dst.green = dst.blue = 0x00; 1716 }, 1717 1718 /* WHITE */ 1719 0xF: function (src, dst) { 1720 dst.red = dst.green = dst.blue = 0xFF; 1721 }, 1722 1723 /* SRC */ 1724 0x3: function (src, dst) { 1725 dst.red = src.red; 1726 dst.green = src.green; 1727 dst.blue = src.blue; 1728 dst.alpha = src.alpha; 1729 }, 1730 1731 /* DEST (no-op) */ 1732 0x5: function (src, dst) { 1733 // Do nothing 1734 }, 1735 1736 /* Invert SRC */ 1737 0xC: function (src, dst) { 1738 dst.red = 0xFF & ~src.red; 1739 dst.green = 0xFF & ~src.green; 1740 dst.blue = 0xFF & ~src.blue; 1741 dst.alpha = src.alpha; 1742 }, 1743 1744 /* Invert DEST */ 1745 0xA: function (src, dst) { 1746 dst.red = 0xFF & ~dst.red; 1747 dst.green = 0xFF & ~dst.green; 1748 dst.blue = 0xFF & ~dst.blue; 1749 }, 1750 1751 /* AND */ 1752 0x1: function (src, dst) { 1753 dst.red = ( src.red & dst.red); 1754 dst.green = ( src.green & dst.green); 1755 dst.blue = ( src.blue & dst.blue); 1756 }, 1757 1758 /* NAND */ 1759 0xE: function (src, dst) { 1760 dst.red = 0xFF & ~( src.red & dst.red); 1761 dst.green = 0xFF & ~( src.green & dst.green); 1762 dst.blue = 0xFF & ~( src.blue & dst.blue); 1763 }, 1764 1765 /* OR */ 1766 0x7: function (src, dst) { 1767 dst.red = ( src.red | dst.red); 1768 dst.green = ( src.green | dst.green); 1769 dst.blue = ( src.blue | dst.blue); 1770 }, 1771 1772 /* NOR */ 1773 0x8: function (src, dst) { 1774 dst.red = 0xFF & ~( src.red | dst.red); 1775 dst.green = 0xFF & ~( src.green | dst.green); 1776 dst.blue = 0xFF & ~( src.blue | dst.blue); 1777 }, 1778 1779 /* XOR */ 1780 0x6: function (src, dst) { 1781 dst.red = ( src.red ^ dst.red); 1782 dst.green = ( src.green ^ dst.green); 1783 dst.blue = ( src.blue ^ dst.blue); 1784 }, 1785 1786 /* XNOR */ 1787 0x9: function (src, dst) { 1788 dst.red = 0xFF & ~( src.red ^ dst.red); 1789 dst.green = 0xFF & ~( src.green ^ dst.green); 1790 dst.blue = 0xFF & ~( src.blue ^ dst.blue); 1791 }, 1792 1793 /* AND inverted source */ 1794 0x4: function (src, dst) { 1795 dst.red = 0xFF & (~src.red & dst.red); 1796 dst.green = 0xFF & (~src.green & dst.green); 1797 dst.blue = 0xFF & (~src.blue & dst.blue); 1798 }, 1799 1800 /* OR inverted source */ 1801 0xD: function (src, dst) { 1802 dst.red = 0xFF & (~src.red | dst.red); 1803 dst.green = 0xFF & (~src.green | dst.green); 1804 dst.blue = 0xFF & (~src.blue | dst.blue); 1805 }, 1806 1807 /* AND inverted destination */ 1808 0x2: function (src, dst) { 1809 dst.red = 0xFF & ( src.red & ~dst.red); 1810 dst.green = 0xFF & ( src.green & ~dst.green); 1811 dst.blue = 0xFF & ( src.blue & ~dst.blue); 1812 }, 1813 1814 /* OR inverted destination */ 1815 0xB: function (src, dst) { 1816 dst.red = 0xFF & ( src.red | ~dst.red); 1817 dst.green = 0xFF & ( src.green | ~dst.green); 1818 dst.blue = 0xFF & ( src.blue | ~dst.blue); 1819 } 1820 1821 }; 1822