55b71ba68d24b5eff59256676678641be0f2c5f4
[guacamole-common-js.git] / src / main / resources / guacamole.js
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  * Matt Hortman
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
38
39 // Guacamole namespace
40 var Guacamole = Guacamole || {};
41
42
43 /**
44  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
45  * automatically handles incoming and outgoing Guacamole instructions via the
46  * provided tunnel, updating the display using one or more canvas elements.
47  * 
48  * @constructor
49  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
50  *                                  Guacamole instructions.
51  */
52 Guacamole.Client = function(tunnel) {
53
54     var guac_client = this;
55
56     var STATE_IDLE          = 0;
57     var STATE_CONNECTING    = 1;
58     var STATE_WAITING       = 2;
59     var STATE_CONNECTED     = 3;
60     var STATE_DISCONNECTING = 4;
61     var STATE_DISCONNECTED  = 5;
62
63     var currentState = STATE_IDLE;
64     
65     var currentTimestamp = 0;
66     var pingInterval = null;
67
68     var displayWidth = 0;
69     var displayHeight = 0;
70
71     /**
72      * Translation from Guacamole protocol line caps to Layer line caps.
73      */
74     var lineCap = {
75         0: "butt",
76         1: "round",
77         2: "square"
78     };
79
80     /**
81      * Translation from Guacamole protocol line caps to Layer line caps.
82      */
83     var lineJoin = {
84         0: "bevel",
85         1: "miter",
86         2: "round"
87     };
88
89     // Create display
90     var display = document.createElement("div");
91     display.style.position = "relative";
92     display.style.width = displayWidth + "px";
93     display.style.height = displayHeight + "px";
94
95     // Create default layer
96     var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
97
98     // Position default layer
99     var default_layer_container_element = default_layer_container.getElement();
100     default_layer_container_element.style.position = "absolute";
101     default_layer_container_element.style.left = "0px";
102     default_layer_container_element.style.top  = "0px";
103     default_layer_container_element.style.overflow = "hidden";
104
105     // Create cursor layer
106     var cursor = new Guacamole.Client.LayerContainer(0, 0);
107     cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
108
109     // Position cursor layer
110     var cursor_element = cursor.getElement();
111     cursor_element.style.position = "absolute";
112     cursor_element.style.left = "0px";
113     cursor_element.style.top  = "0px";
114
115     // Add default layer and cursor to display
116     display.appendChild(default_layer_container.getElement());
117     display.appendChild(cursor.getElement());
118
119     // Initially, only default layer exists
120     var layers =  [default_layer_container];
121
122     // No initial buffers
123     var buffers = [];
124
125     tunnel.onerror = function(message) {
126         if (guac_client.onerror)
127             guac_client.onerror(message);
128     };
129
130     function setState(state) {
131         if (state != currentState) {
132             currentState = state;
133             if (guac_client.onstatechange)
134                 guac_client.onstatechange(currentState);
135         }
136     }
137
138     function isConnected() {
139         return currentState == STATE_CONNECTED
140             || currentState == STATE_WAITING;
141     }
142
143     var cursorHotspotX = 0;
144     var cursorHotspotY = 0;
145
146     var cursorX = 0;
147     var cursorY = 0;
148
149     function moveCursor(x, y) {
150
151         var element = cursor.getElement();
152
153         // Update rect
154         element.style.left = (x - cursorHotspotX) + "px";
155         element.style.top  = (y - cursorHotspotY) + "px";
156
157         // Update stored position
158         cursorX = x;
159         cursorY = y;
160
161     }
162
163     guac_client.getDisplay = function() {
164         return display;
165     };
166
167     guac_client.sendKeyEvent = function(pressed, keysym) {
168         // Do not send requests if not connected
169         if (!isConnected())
170             return;
171
172         tunnel.sendMessage("key", keysym, pressed);
173     };
174
175     guac_client.sendMouseState = function(mouseState) {
176
177         // Do not send requests if not connected
178         if (!isConnected())
179             return;
180
181         // Update client-side cursor
182         moveCursor(
183             mouseState.x,
184             mouseState.y
185         );
186
187         // Build mask
188         var buttonMask = 0;
189         if (mouseState.left)   buttonMask |= 1;
190         if (mouseState.middle) buttonMask |= 2;
191         if (mouseState.right)  buttonMask |= 4;
192         if (mouseState.up)     buttonMask |= 8;
193         if (mouseState.down)   buttonMask |= 16;
194
195         // Send message
196         tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
197     };
198
199     guac_client.setClipboard = function(data) {
200
201         // Do not send requests if not connected
202         if (!isConnected())
203             return;
204
205         tunnel.sendMessage("clipboard", data);
206     };
207
208     // Handlers
209     guac_client.onstatechange = null;
210     guac_client.onname = null;
211     guac_client.onerror = null;
212     guac_client.onclipboard = null;
213
214     // Layers
215     function getBufferLayer(index) {
216
217         index = -1 - index;
218         var buffer = buffers[index];
219
220         // Create buffer if necessary
221         if (buffer == null) {
222             buffer = new Guacamole.Layer(0, 0);
223             buffer.autosize = 1;
224             buffers[index] = buffer;
225         }
226
227         return buffer;
228
229     }
230
231     function getLayerContainer(index) {
232
233         var layer = layers[index];
234         if (layer == null) {
235
236             // Add new layer
237             layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
238             layers[index] = layer;
239
240             // Get and position layer
241             var layer_element = layer.getElement();
242             layer_element.style.position = "absolute";
243             layer_element.style.left = "0px";
244             layer_element.style.top = "0px";
245             layer_element.style.overflow = "hidden";
246
247             // Add to default layer container
248             default_layer_container.getElement().appendChild(layer_element);
249
250         }
251
252         return layer;
253
254     }
255
256     function getLayer(index) {
257        
258         // If buffer, just get layer
259         if (index < 0)
260             return getBufferLayer(index);
261
262         // Otherwise, retrieve layer from layer container
263         return getLayerContainer(index).getLayer();
264
265     }
266
267     var instructionHandlers = {
268
269         "arc": function(parameters) {
270
271             var layer = getLayer(parseInt(parameters[0]));
272             var x = parseInt(parameters[1]);
273             var y = parseInt(parameters[2]);
274             var radius = parseInt(parameters[3]);
275             var startAngle = parseFloat(parameters[4]);
276             var endAngle = parseFloat(parameters[5]);
277
278             layer.arc(x, y, radius, startAngle, endAngle);
279
280         },
281
282         "cfill": function(parameters) {
283
284             var channelMask = parseInt(parameters[0]);
285             var layer = getLayer(parseInt(parameters[1]));
286             var r = parseInt(parameters[2]);
287             var g = parseInt(parameters[3]);
288             var b = parseInt(parameters[4]);
289             var a = parseInt(parameters[5]);
290
291             layer.setChannelMask(channelMask);
292
293             layer.fillColor(r, g, b, a);
294
295         },
296
297         "clip": function(parameters) {
298
299             var layer = getLayer(parseInt(parameters[0]));
300
301             layer.clip();
302
303         },
304
305         "clipboard": function(parameters) {
306             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
307         },
308
309         "close": function(parameters) {
310
311             var layer = getLayer(parseInt(parameters[0]));
312
313             layer.close();
314
315         },
316
317         "copy": function(parameters) {
318
319             var srcL = getLayer(parseInt(parameters[0]));
320             var srcX = parseInt(parameters[1]);
321             var srcY = parseInt(parameters[2]);
322             var srcWidth = parseInt(parameters[3]);
323             var srcHeight = parseInt(parameters[4]);
324             var channelMask = parseInt(parameters[5]);
325             var dstL = getLayer(parseInt(parameters[6]));
326             var dstX = parseInt(parameters[7]);
327             var dstY = parseInt(parameters[8]);
328
329             dstL.setChannelMask(channelMask);
330
331             dstL.copy(
332                 srcL,
333                 srcX,
334                 srcY,
335                 srcWidth, 
336                 srcHeight, 
337                 dstX,
338                 dstY 
339             );
340
341         },
342
343         "cstroke": function(parameters) {
344
345             var channelMask = parseInt(parameters[0]);
346             var layer = getLayer(parseInt(parameters[1]));
347             var cap = lineCap[parseInt(parameters[2])];
348             var join = lineJoin[parseInt(parameters[3])];
349             var thickness = parseInt(parameters[4]);
350             var r = parseInt(parameters[5]);
351             var g = parseInt(parameters[6]);
352             var b = parseInt(parameters[7]);
353             var a = parseInt(parameters[8]);
354
355             layer.setChannelMask(channelMask);
356
357             layer.strokeColor(cap, join, thickness, r, g, b, a);
358
359         },
360
361         "cursor": function(parameters) {
362
363             cursorHotspotX = parseInt(parameters[0]);
364             cursorHotspotY = parseInt(parameters[1]);
365             var srcL = getLayer(parseInt(parameters[2]));
366             var srcX = parseInt(parameters[3]);
367             var srcY = parseInt(parameters[4]);
368             var srcWidth = parseInt(parameters[5]);
369             var srcHeight = parseInt(parameters[6]);
370
371             // Reset cursor size
372             cursor.resize(srcWidth, srcHeight);
373
374             // Draw cursor to cursor layer
375             cursor.getLayer().copy(
376                 srcL,
377                 srcX,
378                 srcY,
379                 srcWidth, 
380                 srcHeight, 
381                 0,
382                 0 
383             );
384
385             // Update cursor position (hotspot may have changed)
386             moveCursor(cursorX, cursorY);
387
388         },
389
390         "curve": function(parameters) {
391
392             var layer = getLayer(parseInt(parameters[0]));
393             var cp1x = parseInt(parameters[1]);
394             var cp1y = parseInt(parameters[2]);
395             var cp2x = parseInt(parameters[3]);
396             var cp2y = parseInt(parameters[4]);
397             var x = parseInt(parameters[5]);
398             var y = parseInt(parameters[6]);
399
400             layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
401
402         },
403
404         "dispose": function(parameters) {
405             
406             var layer_index = parseInt(parameters[0]);
407
408             // If visible layer, remove from parent
409             if (layer_index > 0) {
410
411                 // Get container element
412                 var layer_container = getLayerContainer(layer_index).getElement();
413
414                 // Remove from parent
415                 layer_container.parentNode.removeChild(layer_container);
416
417                 // Delete reference
418                 delete layers[layer_index];
419
420             }
421
422             // If buffer, just delete reference
423             else if (layer_index < 0)
424                 delete buffers[-1 - layer_index];
425
426             // Attempting to dispose the root layer currently has no effect.
427
428         },
429
430         "distort": function(parameters) {
431
432             var layer_index = parseInt(parameters[0]);
433             var a = parseFloat(parameters[1]);
434             var b = parseFloat(parameters[2]);
435             var c = parseFloat(parameters[3]);
436             var d = parseFloat(parameters[4]);
437             var e = parseFloat(parameters[5]);
438             var f = parseFloat(parameters[6]);
439
440             // Only valid for visible layers (not buffers)
441             if (layer_index >= 0) {
442
443                 // Get container element
444                 var layer_container = getLayerContainer(layer_index).getElement();
445
446                 // Set layer transform 
447                 layer_container.style.transform =
448                 layer_container.style.WebkitTransform =
449                 layer_container.style.MozTransform =
450                 layer_container.style.OTransform =
451                 layer_container.style.msTransform =
452
453                     /* a c e
454                      * b d f
455                      * 0 0 1
456                      */
457             
458                     "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
459
460              }
461
462         },
463  
464         "error": function(parameters) {
465             if (guac_client.onerror) guac_client.onerror(parameters[0]);
466             guac_client.disconnect();
467         },
468
469         "identity": function(parameters) {
470
471             var layer = getLayer(parseInt(parameters[0]));
472
473             layer.setTransform(1, 0, 0, 1, 0, 0);
474
475         },
476
477         "lfill": function(parameters) {
478
479             var channelMask = parseInt(parameters[0]);
480             var layer = getLayer(parseInt(parameters[1]));
481             var srcLayer = getLayer(parseInt(parameters[2]));
482
483             layer.setChannelMask(channelMask);
484
485             layer.fillLayer(srcLayer);
486
487         },
488
489         "line": function(parameters) {
490
491             var layer = getLayer(parseInt(parameters[0]));
492             var x = parseInt(parameters[1]);
493             var y = parseInt(parameters[2]);
494
495             layer.lineTo(x, y);
496
497         },
498
499         "lstroke": function(parameters) {
500
501             var channelMask = parseInt(parameters[0]);
502             var layer = getLayer(parseInt(parameters[1]));
503             var srcLayer = getLayer(parseInt(parameters[2]));
504
505             layer.setChannelMask(channelMask);
506
507             layer.strokeLayer(srcLayer);
508
509         },
510
511         "move": function(parameters) {
512             
513             var layer_index = parseInt(parameters[0]);
514             var parent_index = parseInt(parameters[1]);
515             var x = parseInt(parameters[2]);
516             var y = parseInt(parameters[3]);
517             var z = parseInt(parameters[4]);
518
519             // Only valid for non-default layers
520             if (layer_index > 0 && parent_index >= 0) {
521
522                 // Get container element
523                 var layer_container = getLayerContainer(layer_index).getElement();
524                 var parent = getLayerContainer(parent_index).getElement();
525
526                 // Set parent if necessary
527                 if (!(layer_container.parentNode === parent))
528                     parent.appendChild(layer_container);
529
530                 // Move layer
531                 layer_container.style.left   = x + "px";
532                 layer_container.style.top    = y + "px";
533                 layer_container.style.zIndex = z;
534
535             }
536
537         },
538
539         "name": function(parameters) {
540             if (guac_client.onname) guac_client.onname(parameters[0]);
541         },
542
543         "png": function(parameters) {
544
545             var channelMask = parseInt(parameters[0]);
546             var layer = getLayer(parseInt(parameters[1]));
547             var x = parseInt(parameters[2]);
548             var y = parseInt(parameters[3]);
549             var data = parameters[4];
550
551             layer.setChannelMask(channelMask);
552
553             layer.draw(
554                 x,
555                 y,
556                 "data:image/png;base64," + data
557             );
558
559             // If received first update, no longer waiting.
560             if (currentState == STATE_WAITING)
561                 setState(STATE_CONNECTED);
562
563         },
564
565         "pop": function(parameters) {
566
567             var layer = getLayer(parseInt(parameters[0]));
568
569             layer.pop();
570
571         },
572
573         "push": function(parameters) {
574
575             var layer = getLayer(parseInt(parameters[0]));
576
577             layer.push();
578
579         },
580  
581         "rect": function(parameters) {
582
583             var layer = getLayer(parseInt(parameters[0]));
584             var x = parseInt(parameters[1]);
585             var y = parseInt(parameters[2]);
586             var w = parseInt(parameters[3]);
587             var h = parseInt(parameters[4]);
588
589             layer.rect(x, y, w, h);
590
591         },
592         
593         "reset": function(parameters) {
594
595             var layer = getLayer(parseInt(parameters[0]));
596
597             layer.reset();
598
599         },
600
601         "shade": function(parameters) {
602             
603             var layer_index = parseInt(parameters[0]);
604             var a = parseInt(parameters[1]);
605
606             // Only valid for visible layers (not buffers)
607             if (layer_index >= 0) {
608
609                 // Get container element
610                 var layer_container = getLayerContainer(layer_index).getElement();
611
612                 // Set layer opacity
613                 layer_container.style.opacity = a/255.0;
614
615             }
616
617         },
618
619         "size": function(parameters) {
620
621             var layer_index = parseInt(parameters[0]);
622             var width = parseInt(parameters[1]);
623             var height = parseInt(parameters[2]);
624
625             // Resize layer
626             var layer_container = getLayerContainer(layer_index);
627             layer_container.resize(width, height);
628
629             // If layer is default, resize display
630             if (layer_index == 0) {
631
632                 displayWidth = width;
633                 displayHeight = height;
634
635                 // Update (set) display size
636                 display.style.width = displayWidth + "px";
637                 display.style.height = displayHeight + "px";
638
639             }
640
641         },
642         
643         "start": function(parameters) {
644
645             var layer = getLayer(parseInt(parameters[0]));
646             var x = parseInt(parameters[1]);
647             var y = parseInt(parameters[2]);
648
649             layer.moveTo(x, y);
650
651         },
652
653         "sync": function(parameters) {
654
655             var timestamp = parameters[0];
656
657             // When all layers have finished rendering all instructions
658             // UP TO THIS POINT IN TIME, send sync response.
659
660             var layersToSync = 0;
661             function syncLayer() {
662
663                 layersToSync--;
664
665                 // Send sync response when layers are finished
666                 if (layersToSync == 0) {
667                     if (timestamp != currentTimestamp) {
668                         tunnel.sendMessage("sync", timestamp);
669                         currentTimestamp = timestamp;
670                     }
671                 }
672
673             }
674
675             // Count active, not-ready layers and install sync tracking hooks
676             for (var i=0; i<layers.length; i++) {
677
678                 var layer = layers[i].getLayer();
679                 if (layer && !layer.isReady()) {
680                     layersToSync++;
681                     layer.sync(syncLayer);
682                 }
683
684             }
685
686             // If all layers are ready, then we didn't install any hooks.
687             // Send sync message now,
688             if (layersToSync == 0) {
689                 if (timestamp != currentTimestamp) {
690                     tunnel.sendMessage("sync", timestamp);
691                     currentTimestamp = timestamp;
692                 }
693             }
694
695         },
696
697         "transfer": function(parameters) {
698
699             var srcL = getLayer(parseInt(parameters[0]));
700             var srcX = parseInt(parameters[1]);
701             var srcY = parseInt(parameters[2]);
702             var srcWidth = parseInt(parameters[3]);
703             var srcHeight = parseInt(parameters[4]);
704             var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]];
705             var dstL = getLayer(parseInt(parameters[6]));
706             var dstX = parseInt(parameters[7]);
707             var dstY = parseInt(parameters[8]);
708
709             dstL.transfer(
710                 srcL,
711                 srcX,
712                 srcY,
713                 srcWidth, 
714                 srcHeight, 
715                 dstX,
716                 dstY,
717                 transferFunction
718             );
719
720         },
721
722         "transform": function(parameters) {
723
724             var layer = getLayer(parseInt(parameters[0]));
725             var a = parseFloat(parameters[1]);
726             var b = parseFloat(parameters[2]);
727             var c = parseFloat(parameters[3]);
728             var d = parseFloat(parameters[4]);
729             var e = parseFloat(parameters[5]);
730             var f = parseFloat(parameters[6]);
731
732             layer.transform(a, b, c, d, e, f);
733
734         }
735       
736     };
737
738
739     tunnel.oninstruction = function(opcode, parameters) {
740
741         var handler = instructionHandlers[opcode];
742         if (handler)
743             handler(parameters);
744
745     };
746
747
748     guac_client.disconnect = function() {
749
750         // Only attempt disconnection not disconnected.
751         if (currentState != STATE_DISCONNECTED
752                 && currentState != STATE_DISCONNECTING) {
753
754             setState(STATE_DISCONNECTING);
755
756             // Stop ping
757             if (pingInterval)
758                 window.clearInterval(pingInterval);
759
760             // Send disconnect message and disconnect
761             tunnel.sendMessage("disconnect");
762             tunnel.disconnect();
763             setState(STATE_DISCONNECTED);
764
765         }
766
767     };
768     
769     guac_client.connect = function(data) {
770
771         setState(STATE_CONNECTING);
772
773         try {
774             tunnel.connect(data);
775         }
776         catch (e) {
777             setState(STATE_IDLE);
778             throw e;
779         }
780
781         // Ping every 5 seconds (ensure connection alive)
782         pingInterval = window.setInterval(function() {
783             tunnel.sendMessage("sync", currentTimestamp);
784         }, 5000);
785
786         setState(STATE_WAITING);
787     };
788
789 };
790
791
792 /**
793  * Simple container for Guacamole.Layer, allowing layers to be easily
794  * repositioned and nested. This allows certain operations to be accelerated
795  * through DOM manipulation, rather than raster operations.
796  * 
797  * @constructor
798  * 
799  * @param {Number} width The width of the Layer, in pixels. The canvas element
800  *                       backing this Layer will be given this width.
801  *                       
802  * @param {Number} height The height of the Layer, in pixels. The canvas element
803  *                        backing this Layer will be given this height.
804  */
805 Guacamole.Client.LayerContainer = function(width, height) {
806
807     /**
808      * Reference to this LayerContainer.
809      * @private
810      */
811     var layer_container = this;
812
813     // Create layer with given size
814     var layer = new Guacamole.Layer(width, height);
815
816     // Set layer position
817     var canvas = layer.getCanvas();
818     canvas.style.position = "absolute";
819     canvas.style.left = "0px";
820     canvas.style.top = "0px";
821
822     // Create div with given size
823     var div = document.createElement("div");
824     div.appendChild(canvas);
825     div.style.width = width + "px";
826     div.style.height = height + "px";
827
828     /**
829      * Changes the size of this LayerContainer and the contained Layer to the
830      * given width and height.
831      * 
832      * @param {Number} width The new width to assign to this Layer.
833      * @param {Number} height The new height to assign to this Layer.
834      */
835     layer_container.resize = function(width, height) {
836
837         // Resize layer
838         layer.resize(width, height);
839
840         // Resize containing div
841         div.style.width = width + "px";
842         div.style.height = height + "px";
843
844     };
845   
846     /**
847      * Returns the Layer contained within this LayerContainer.
848      * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
849      */
850     layer_container.getLayer = function() {
851         return layer;
852     };
853
854     /**
855      * Returns the element containing the Layer within this LayerContainer.
856      * @returns {Element} The element containing the Layer within this LayerContainer.
857      */
858     layer_container.getElement = function() {
859         return div;
860     };
861
862 };
863
864 /**
865  * Map of all Guacamole binary raster operations to transfer functions.
866  * @private
867  */
868 Guacamole.Client.DefaultTransferFunction = {
869
870     /* BLACK */
871     0x0: function (src, dst) {
872         dst.red = dst.green = dst.blue = 0x00;
873     },
874
875     /* WHITE */
876     0xF: function (src, dst) {
877         dst.red = dst.green = dst.blue = 0xFF;
878     },
879
880     /* SRC */
881     0x3: function (src, dst) {
882         dst.red   = src.red;
883         dst.green = src.green;
884         dst.blue  = src.blue;
885         dst.alpha = src.alpha;
886     },
887
888     /* DEST (no-op) */
889     0x5: function (src, dst) {
890         // Do nothing
891     },
892
893     /* Invert SRC */
894     0xC: function (src, dst) {
895         dst.red   = 0xFF & ~src.red;
896         dst.green = 0xFF & ~src.green;
897         dst.blue  = 0xFF & ~src.blue;
898         dst.alpha =  src.alpha;
899     },
900     
901     /* Invert DEST */
902     0xA: function (src, dst) {
903         dst.red   = 0xFF & ~dst.red;
904         dst.green = 0xFF & ~dst.green;
905         dst.blue  = 0xFF & ~dst.blue;
906     },
907
908     /* AND */
909     0x1: function (src, dst) {
910         dst.red   =  ( src.red   &  dst.red);
911         dst.green =  ( src.green &  dst.green);
912         dst.blue  =  ( src.blue  &  dst.blue);
913     },
914
915     /* NAND */
916     0xE: function (src, dst) {
917         dst.red   = 0xFF & ~( src.red   &  dst.red);
918         dst.green = 0xFF & ~( src.green &  dst.green);
919         dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
920     },
921
922     /* OR */
923     0x7: function (src, dst) {
924         dst.red   =  ( src.red   |  dst.red);
925         dst.green =  ( src.green |  dst.green);
926         dst.blue  =  ( src.blue  |  dst.blue);
927     },
928
929     /* NOR */
930     0x8: function (src, dst) {
931         dst.red   = 0xFF & ~( src.red   |  dst.red);
932         dst.green = 0xFF & ~( src.green |  dst.green);
933         dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
934     },
935
936     /* XOR */
937     0x6: function (src, dst) {
938         dst.red   =  ( src.red   ^  dst.red);
939         dst.green =  ( src.green ^  dst.green);
940         dst.blue  =  ( src.blue  ^  dst.blue);
941     },
942
943     /* XNOR */
944     0x9: function (src, dst) {
945         dst.red   = 0xFF & ~( src.red   ^  dst.red);
946         dst.green = 0xFF & ~( src.green ^  dst.green);
947         dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
948     },
949
950     /* AND inverted source */
951     0x4: function (src, dst) {
952         dst.red   =  0xFF & (~src.red   &  dst.red);
953         dst.green =  0xFF & (~src.green &  dst.green);
954         dst.blue  =  0xFF & (~src.blue  &  dst.blue);
955     },
956
957     /* OR inverted source */
958     0xD: function (src, dst) {
959         dst.red   =  0xFF & (~src.red   |  dst.red);
960         dst.green =  0xFF & (~src.green |  dst.green);
961         dst.blue  =  0xFF & (~src.blue  |  dst.blue);
962     },
963
964     /* AND inverted destination */
965     0x2: function (src, dst) {
966         dst.red   =  0xFF & ( src.red   & ~dst.red);
967         dst.green =  0xFF & ( src.green & ~dst.green);
968         dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
969     },
970
971     /* OR inverted destination */
972     0xB: function (src, dst) {
973         dst.red   =  0xFF & ( src.red   | ~dst.red);
974         dst.green =  0xFF & ( src.green | ~dst.green);
975         dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
976     }
977
978 };