source: OpenRLabs-Git/web2py/applications/rlabs/static/js/guacamole-common-js/modules/Client.js

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100644
File size: 50.4 KB
Line 
1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20var Guacamole = Guacamole || {};
21
22/**
23 * Guacamole protocol client. Given a {@link Guacamole.Tunnel},
24 * automatically handles incoming and outgoing Guacamole instructions via the
25 * provided tunnel, updating its display using one or more canvas elements.
26 *
27 * @constructor
28 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
29 *                                  Guacamole instructions.
30 */
31Guacamole.Client = function(tunnel) {
32
33    var guac_client = this;
34
35    var STATE_IDLE          = 0;
36    var STATE_CONNECTING    = 1;
37    var STATE_WAITING       = 2;
38    var STATE_CONNECTED     = 3;
39    var STATE_DISCONNECTING = 4;
40    var STATE_DISCONNECTED  = 5;
41
42    var currentState = STATE_IDLE;
43   
44    var currentTimestamp = 0;
45    var pingInterval = null;
46
47    /**
48     * Translation from Guacamole protocol line caps to Layer line caps.
49     * @private
50     */
51    var lineCap = {
52        0: "butt",
53        1: "round",
54        2: "square"
55    };
56
57    /**
58     * Translation from Guacamole protocol line caps to Layer line caps.
59     * @private
60     */
61    var lineJoin = {
62        0: "bevel",
63        1: "miter",
64        2: "round"
65    };
66
67    /**
68     * The underlying Guacamole display.
69     *
70     * @private
71     * @type {Guacamole.Display}
72     */
73    var display = new Guacamole.Display();
74
75    /**
76     * All available layers and buffers
77     *
78     * @private
79     * @type {Object.<Number, (Guacamole.Display.VisibleLayer|Guacamole.Layer)>}
80     */
81    var layers = {};
82   
83    /**
84     * All audio players currently in use by the client. Initially, this will
85     * be empty, but audio players may be allocated by the server upon request.
86     *
87     * @private
88     * @type {Object.<Number, Guacamole.AudioPlayer>}
89     */
90    var audioPlayers = {};
91
92    /**
93     * All video players currently in use by the client. Initially, this will
94     * be empty, but video players may be allocated by the server upon request.
95     *
96     * @private
97     * @type {Object.<Number, Guacamole.VideoPlayer>}
98     */
99    var videoPlayers = {};
100
101    // No initial parsers
102    var parsers = [];
103
104    // No initial streams
105    var streams = [];
106
107    /**
108     * All current objects. The index of each object is dictated by the
109     * Guacamole server.
110     *
111     * @private
112     * @type {Guacamole.Object[]}
113     */
114    var objects = [];
115
116    // Pool of available stream indices
117    var stream_indices = new Guacamole.IntegerPool();
118
119    // Array of allocated output streams by index
120    var output_streams = [];
121
122    function setState(state) {
123        if (state != currentState) {
124            currentState = state;
125            if (guac_client.onstatechange)
126                guac_client.onstatechange(currentState);
127        }
128    }
129
130    function isConnected() {
131        return currentState == STATE_CONNECTED
132            || currentState == STATE_WAITING;
133    }
134
135    /**
136     * Produces an opaque representation of Guacamole.Client state which can be
137     * later imported through a call to importState(). This object is
138     * effectively an independent, compressed snapshot of protocol and display
139     * state. Invoking this function implicitly flushes the display.
140     *
141     * @param {function} callback
142     *     Callback which should be invoked once the state object is ready. The
143     *     state object will be passed to the callback as the sole parameter.
144     *     This callback may be invoked immediately, or later as the display
145     *     finishes rendering and becomes ready.
146     */
147    this.exportState = function exportState(callback) {
148
149        // Start with empty state
150        var state = {
151            'currentState' : currentState,
152            'currentTimestamp' : currentTimestamp,
153            'layers' : {}
154        };
155
156        var layersSnapshot = {};
157
158        // Make a copy of all current layers (protocol state)
159        for (var key in layers) {
160            layersSnapshot[key] = layers[key];
161        }
162
163        // Populate layers once data is available (display state, requires flush)
164        display.flush(function populateLayers() {
165
166            // Export each defined layer/buffer
167            for (var key in layersSnapshot) {
168
169                var index = parseInt(key);
170                var layer = layersSnapshot[key];
171                var canvas = layer.toCanvas();
172
173                // Store layer/buffer dimensions
174                var exportLayer = {
175                    'width'  : layer.width,
176                    'height' : layer.height
177                };
178
179                // Store layer/buffer image data, if it can be generated
180                if (layer.width && layer.height)
181                    exportLayer.url = canvas.toDataURL('image/png');
182
183                // Add layer properties if not a buffer nor the default layer
184                if (index > 0) {
185                    exportLayer.x = layer.x;
186                    exportLayer.y = layer.y;
187                    exportLayer.z = layer.z;
188                    exportLayer.alpha = layer.alpha;
189                    exportLayer.matrix = layer.matrix;
190                    exportLayer.parent = getLayerIndex(layer.parent);
191                }
192
193                // Store exported layer
194                state.layers[key] = exportLayer;
195
196            }
197
198            // Invoke callback now that the state is ready
199            callback(state);
200
201        });
202
203    };
204
205    /**
206     * Restores Guacamole.Client protocol and display state based on an opaque
207     * object from a prior call to exportState(). The Guacamole.Client instance
208     * used to export that state need not be the same as this instance.
209     *
210     * @param {Object} state
211     *     An opaque representation of Guacamole.Client state from a prior call
212     *     to exportState().
213     *
214     * @param {function} [callback]
215     *     The function to invoke when state has finished being imported. This
216     *     may happen immediately, or later as images within the provided state
217     *     object are loaded.
218     */
219    this.importState = function importState(state, callback) {
220
221        var key;
222        var index;
223
224        currentState = state.currentState;
225        currentTimestamp = state.currentTimestamp;
226
227        // Dispose of all layers
228        for (key in layers) {
229            index = parseInt(key);
230            if (index > 0)
231                display.dispose(layers[key]);
232        }
233
234        layers = {};
235
236        // Import state of each layer/buffer
237        for (key in state.layers) {
238
239            index = parseInt(key);
240
241            var importLayer = state.layers[key];
242            var layer = getLayer(index);
243
244            // Reset layer size
245            display.resize(layer, importLayer.width, importLayer.height);
246
247            // Initialize new layer if it has associated data
248            if (importLayer.url) {
249                display.setChannelMask(layer, Guacamole.Layer.SRC);
250                display.draw(layer, 0, 0, importLayer.url);
251            }
252
253            // Set layer-specific properties if not a buffer nor the default layer
254            if (index > 0 && importLayer.parent >= 0) {
255
256                // Apply layer position and set parent
257                var parent = getLayer(importLayer.parent);
258                display.move(layer, parent, importLayer.x, importLayer.y, importLayer.z);
259
260                // Set layer transparency
261                display.shade(layer, importLayer.alpha);
262
263                // Apply matrix transform
264                var matrix = importLayer.matrix;
265                display.distort(layer,
266                    matrix[0], matrix[1], matrix[2],
267                    matrix[3], matrix[4], matrix[5]);
268
269            }
270
271        }
272
273        // Flush changes to display
274        display.flush(callback);
275
276    };
277
278    /**
279     * Returns the underlying display of this Guacamole.Client. The display
280     * contains an Element which can be added to the DOM, causing the
281     * display to become visible.
282     *
283     * @return {Guacamole.Display} The underlying display of this
284     *                             Guacamole.Client.
285     */
286    this.getDisplay = function() {
287        return display;
288    };
289
290    /**
291     * Sends the current size of the screen.
292     *
293     * @param {Number} width The width of the screen.
294     * @param {Number} height The height of the screen.
295     */
296    this.sendSize = function(width, height) {
297
298        // Do not send requests if not connected
299        if (!isConnected())
300            return;
301
302        tunnel.sendMessage("size", width, height);
303
304    };
305
306    /**
307     * Sends a key event having the given properties as if the user
308     * pressed or released a key.
309     *
310     * @param {Boolean} pressed Whether the key is pressed (true) or released
311     *                          (false).
312     * @param {Number} keysym The keysym of the key being pressed or released.
313     */
314    this.sendKeyEvent = function(pressed, keysym) {
315        // Do not send requests if not connected
316        if (!isConnected())
317            return;
318
319        tunnel.sendMessage("key", keysym, pressed);
320    };
321
322    /**
323     * Sends a mouse event having the properties provided by the given mouse
324     * state.
325     *
326     * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
327     *                                           in the mouse event.
328     */
329    this.sendMouseState = function(mouseState) {
330
331        // Do not send requests if not connected
332        if (!isConnected())
333            return;
334
335        // Update client-side cursor
336        display.moveCursor(
337            Math.floor(mouseState.x),
338            Math.floor(mouseState.y)
339        );
340
341        // Build mask
342        var buttonMask = 0;
343        if (mouseState.left)   buttonMask |= 1;
344        if (mouseState.middle) buttonMask |= 2;
345        if (mouseState.right)  buttonMask |= 4;
346        if (mouseState.up)     buttonMask |= 8;
347        if (mouseState.down)   buttonMask |= 16;
348
349        // Send message
350        tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask);
351    };
352
353    /**
354     * Sets the clipboard of the remote client to the given text data.
355     *
356     * @deprecated Use createClipboardStream() instead.
357     * @param {String} data The data to send as the clipboard contents.
358     */
359    this.setClipboard = function(data) {
360
361        // Do not send requests if not connected
362        if (!isConnected())
363            return;
364
365        // Open stream
366        var stream = guac_client.createClipboardStream("text/plain");
367        var writer = new Guacamole.StringWriter(stream);
368
369        // Send text chunks
370        for (var i=0; i<data.length; i += 4096)
371            writer.sendText(data.substring(i, i+4096));
372
373        // Close stream
374        writer.sendEnd();
375
376    };
377
378    /**
379     * Allocates an available stream index and creates a new
380     * Guacamole.OutputStream using that index, associating the resulting
381     * stream with this Guacamole.Client. Note that this stream will not yet
382     * exist as far as the other end of the Guacamole connection is concerned.
383     * Streams exist within the Guacamole protocol only when referenced by an
384     * instruction which creates the stream, such as a "clipboard", "file", or
385     * "pipe" instruction.
386     *
387     * @returns {Guacamole.OutputStream}
388     *     A new Guacamole.OutputStream with a newly-allocated index and
389     *     associated with this Guacamole.Client.
390     */
391    this.createOutputStream = function createOutputStream() {
392
393        // Allocate index
394        var index = stream_indices.next();
395
396        // Return new stream
397        var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
398        return stream;
399
400    };
401
402    /**
403     * Opens a new audio stream for writing, where audio data having the give
404     * mimetype will be sent along the returned stream. The instruction
405     * necessary to create this stream will automatically be sent.
406     *
407     * @param {String} mimetype
408     *     The mimetype of the audio data that will be sent along the returned
409     *     stream.
410     *
411     * @return {Guacamole.OutputStream}
412     *     The created audio stream.
413     */
414    this.createAudioStream = function(mimetype) {
415
416        // Allocate and associate stream with audio metadata
417        var stream = guac_client.createOutputStream();
418        tunnel.sendMessage("audio", stream.index, mimetype);
419        return stream;
420
421    };
422
423    /**
424     * Opens a new file for writing, having the given index, mimetype and
425     * filename. The instruction necessary to create this stream will
426     * automatically be sent.
427     *
428     * @param {String} mimetype The mimetype of the file being sent.
429     * @param {String} filename The filename of the file being sent.
430     * @return {Guacamole.OutputStream} The created file stream.
431     */
432    this.createFileStream = function(mimetype, filename) {
433
434        // Allocate and associate stream with file metadata
435        var stream = guac_client.createOutputStream();
436        tunnel.sendMessage("file", stream.index, mimetype, filename);
437        return stream;
438
439    };
440
441    /**
442     * Opens a new pipe for writing, having the given name and mimetype. The
443     * instruction necessary to create this stream will automatically be sent.
444     *
445     * @param {String} mimetype The mimetype of the data being sent.
446     * @param {String} name The name of the pipe.
447     * @return {Guacamole.OutputStream} The created file stream.
448     */
449    this.createPipeStream = function(mimetype, name) {
450
451        // Allocate and associate stream with pipe metadata
452        var stream = guac_client.createOutputStream();
453        tunnel.sendMessage("pipe", stream.index, mimetype, name);
454        return stream;
455
456    };
457
458    /**
459     * Opens a new clipboard object for writing, having the given mimetype. The
460     * instruction necessary to create this stream will automatically be sent.
461     *
462     * @param {String} mimetype The mimetype of the data being sent.
463     * @param {String} name The name of the pipe.
464     * @return {Guacamole.OutputStream} The created file stream.
465     */
466    this.createClipboardStream = function(mimetype) {
467
468        // Allocate and associate stream with clipboard metadata
469        var stream = guac_client.createOutputStream();
470        tunnel.sendMessage("clipboard", stream.index, mimetype);
471        return stream;
472
473    };
474
475    /**
476     * Creates a new output stream associated with the given object and having
477     * the given mimetype and name. The legality of a mimetype and name is
478     * dictated by the object itself. The instruction necessary to create this
479     * stream will automatically be sent.
480     *
481     * @param {Number} index
482     *     The index of the object for which the output stream is being
483     *     created.
484     *
485     * @param {String} mimetype
486     *     The mimetype of the data which will be sent to the output stream.
487     *
488     * @param {String} name
489     *     The defined name of an output stream within the given object.
490     *
491     * @returns {Guacamole.OutputStream}
492     *     An output stream which will write blobs to the named output stream
493     *     of the given object.
494     */
495    this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) {
496
497        // Allocate and ssociate stream with object metadata
498        var stream = guac_client.createOutputStream();
499        tunnel.sendMessage("put", index, stream.index, mimetype, name);
500        return stream;
501
502    };
503
504    /**
505     * Requests read access to the input stream having the given name. If
506     * successful, a new input stream will be created.
507     *
508     * @param {Number} index
509     *     The index of the object from which the input stream is being
510     *     requested.
511     *
512     * @param {String} name
513     *     The name of the input stream to request.
514     */
515    this.requestObjectInputStream = function requestObjectInputStream(index, name) {
516
517        // Do not send requests if not connected
518        if (!isConnected())
519            return;
520
521        tunnel.sendMessage("get", index, name);
522    };
523
524    /**
525     * Acknowledge receipt of a blob on the stream with the given index.
526     *
527     * @param {Number} index The index of the stream associated with the
528     *                       received blob.
529     * @param {String} message A human-readable message describing the error
530     *                         or status.
531     * @param {Number} code The error code, if any, or 0 for success.
532     */
533    this.sendAck = function(index, message, code) {
534
535        // Do not send requests if not connected
536        if (!isConnected())
537            return;
538
539        tunnel.sendMessage("ack", index, message, code);
540    };
541
542    /**
543     * Given the index of a file, writes a blob of data to that file.
544     *
545     * @param {Number} index The index of the file to write to.
546     * @param {String} data Base64-encoded data to write to the file.
547     */
548    this.sendBlob = function(index, data) {
549
550        // Do not send requests if not connected
551        if (!isConnected())
552            return;
553
554        tunnel.sendMessage("blob", index, data);
555    };
556
557    /**
558     * Marks a currently-open stream as complete. The other end of the
559     * Guacamole connection will be notified via an "end" instruction that the
560     * stream is closed, and the index will be made available for reuse in
561     * future streams.
562     *
563     * @param {Number} index
564     *     The index of the stream to end.
565     */
566    this.endStream = function(index) {
567
568        // Do not send requests if not connected
569        if (!isConnected())
570            return;
571
572        // Explicitly close stream by sending "end" instruction
573        tunnel.sendMessage("end", index);
574
575        // Free associated index and stream if they exist
576        if (output_streams[index]) {
577            stream_indices.free(index);
578            delete output_streams[index];
579        }
580
581    };
582
583    /**
584     * Fired whenever the state of this Guacamole.Client changes.
585     *
586     * @event
587     * @param {Number} state The new state of the client.
588     */
589    this.onstatechange = null;
590
591    /**
592     * Fired when the remote client sends a name update.
593     *
594     * @event
595     * @param {String} name The new name of this client.
596     */
597    this.onname = null;
598
599    /**
600     * Fired when an error is reported by the remote client, and the connection
601     * is being closed.
602     *
603     * @event
604     * @param {Guacamole.Status} status A status object which describes the
605     *                                  error.
606     */
607    this.onerror = null;
608
609    /**
610     * Fired when a audio stream is created. The stream provided to this event
611     * handler will contain its own event handlers for received data.
612     *
613     * @event
614     * @param {Guacamole.InputStream} stream
615     *     The stream that will receive audio data from the server.
616     *
617     * @param {String} mimetype
618     *     The mimetype of the audio data which will be received.
619     *
620     * @return {Guacamole.AudioPlayer}
621     *     An object which implements the Guacamole.AudioPlayer interface and
622     *     has been initialied to play the data in the provided stream, or null
623     *     if the built-in audio players of the Guacamole client should be
624     *     used.
625     */
626    this.onaudio = null;
627
628    /**
629     * Fired when a video stream is created. The stream provided to this event
630     * handler will contain its own event handlers for received data.
631     *
632     * @event
633     * @param {Guacamole.InputStream} stream
634     *     The stream that will receive video data from the server.
635     *
636     * @param {Guacamole.Display.VisibleLayer} layer
637     *     The destination layer on which the received video data should be
638     *     played. It is the responsibility of the Guacamole.VideoPlayer
639     *     implementation to play the received data within this layer.
640     *
641     * @param {String} mimetype
642     *     The mimetype of the video data which will be received.
643     *
644     * @return {Guacamole.VideoPlayer}
645     *     An object which implements the Guacamole.VideoPlayer interface and
646     *     has been initialied to play the data in the provided stream, or null
647     *     if the built-in video players of the Guacamole client should be
648     *     used.
649     */
650    this.onvideo = null;
651
652    /**
653     * Fired when the clipboard of the remote client is changing.
654     *
655     * @event
656     * @param {Guacamole.InputStream} stream The stream that will receive
657     *                                       clipboard data from the server.
658     * @param {String} mimetype The mimetype of the data which will be received.
659     */
660    this.onclipboard = null;
661
662    /**
663     * Fired when a file stream is created. The stream provided to this event
664     * handler will contain its own event handlers for received data.
665     *
666     * @event
667     * @param {Guacamole.InputStream} stream The stream that will receive data
668     *                                       from the server.
669     * @param {String} mimetype The mimetype of the file received.
670     * @param {String} filename The name of the file received.
671     */
672    this.onfile = null;
673
674    /**
675     * Fired when a filesystem object is created. The object provided to this
676     * event handler will contain its own event handlers and functions for
677     * requesting and handling data.
678     *
679     * @event
680     * @param {Guacamole.Object} object
681     *     The created filesystem object.
682     *
683     * @param {String} name
684     *     The name of the filesystem.
685     */
686    this.onfilesystem = null;
687
688    /**
689     * Fired when a pipe stream is created. The stream provided to this event
690     * handler will contain its own event handlers for received data;
691     *
692     * @event
693     * @param {Guacamole.InputStream} stream The stream that will receive data
694     *                                       from the server.
695     * @param {String} mimetype The mimetype of the data which will be received.
696     * @param {String} name The name of the pipe.
697     */
698    this.onpipe = null;
699
700    /**
701     * Fired whenever a sync instruction is received from the server, indicating
702     * that the server is finished processing any input from the client and
703     * has sent any results.
704     *
705     * @event
706     * @param {Number} timestamp The timestamp associated with the sync
707     *                           instruction.
708     */
709    this.onsync = null;
710
711    /**
712     * Returns the layer with the given index, creating it if necessary.
713     * Positive indices refer to visible layers, an index of zero refers to
714     * the default layer, and negative indices refer to buffers.
715     *
716     * @private
717     * @param {Number} index
718     *     The index of the layer to retrieve.
719     *
720     * @return {Guacamole.Display.VisibleLayer|Guacamole.Layer}
721     *     The layer having the given index.
722     */
723    var getLayer = function getLayer(index) {
724
725        // Get layer, create if necessary
726        var layer = layers[index];
727        if (!layer) {
728
729            // Create layer based on index
730            if (index === 0)
731                layer = display.getDefaultLayer();
732            else if (index > 0)
733                layer = display.createLayer();
734            else
735                layer = display.createBuffer();
736               
737            // Add new layer
738            layers[index] = layer;
739
740        }
741
742        return layer;
743
744    };
745
746    /**
747     * Returns the index passed to getLayer() when the given layer was created.
748     * Positive indices refer to visible layers, an index of zero refers to the
749     * default layer, and negative indices refer to buffers.
750     *
751     * @param {Guacamole.Display.VisibleLayer|Guacamole.Layer} layer
752     *     The layer whose index should be determined.
753     *
754     * @returns {Number}
755     *     The index of the given layer, or null if no such layer is associated
756     *     with this client.
757     */
758    var getLayerIndex = function getLayerIndex(layer) {
759
760        // Avoid searching if there clearly is no such layer
761        if (!layer)
762            return null;
763
764        // Search through each layer, returning the index of the given layer
765        // once found
766        for (var key in layers) {
767            if (layer === layers[key])
768                return parseInt(key);
769        }
770
771        // Otherwise, no such index
772        return null;
773
774    };
775
776    function getParser(index) {
777
778        var parser = parsers[index];
779
780        // If parser not yet created, create it, and tie to the
781        // oninstruction handler of the tunnel.
782        if (parser == null) {
783            parser = parsers[index] = new Guacamole.Parser();
784            parser.oninstruction = tunnel.oninstruction;
785        }
786
787        return parser;
788
789    }
790
791    /**
792     * Handlers for all defined layer properties.
793     * @private
794     */
795    var layerPropertyHandlers = {
796
797        "miter-limit": function(layer, value) {
798            display.setMiterLimit(layer, parseFloat(value));
799        }
800
801    };
802   
803    /**
804     * Handlers for all instruction opcodes receivable by a Guacamole protocol
805     * client.
806     * @private
807     */
808    var instructionHandlers = {
809
810        "ack": function(parameters) {
811
812            var stream_index = parseInt(parameters[0]);
813            var reason = parameters[1];
814            var code = parseInt(parameters[2]);
815
816            // Get stream
817            var stream = output_streams[stream_index];
818            if (stream) {
819
820                // Signal ack if handler defined
821                if (stream.onack)
822                    stream.onack(new Guacamole.Status(code, reason));
823
824                // If code is an error, invalidate stream if not already
825                // invalidated by onack handler
826                if (code >= 0x0100 && output_streams[stream_index] === stream) {
827                    stream_indices.free(stream_index);
828                    delete output_streams[stream_index];
829                }
830
831            }
832
833        },
834
835        "arc": function(parameters) {
836
837            var layer = getLayer(parseInt(parameters[0]));
838            var x = parseInt(parameters[1]);
839            var y = parseInt(parameters[2]);
840            var radius = parseInt(parameters[3]);
841            var startAngle = parseFloat(parameters[4]);
842            var endAngle = parseFloat(parameters[5]);
843            var negative = parseInt(parameters[6]);
844
845            display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0);
846
847        },
848
849        "audio": function(parameters) {
850
851            var stream_index = parseInt(parameters[0]);
852            var mimetype = parameters[1];
853
854            // Create stream
855            var stream = streams[stream_index] =
856                    new Guacamole.InputStream(guac_client, stream_index);
857
858            // Get player instance via callback
859            var audioPlayer = null;
860            if (guac_client.onaudio)
861                audioPlayer = guac_client.onaudio(stream, mimetype);
862
863            // If unsuccessful, try to use a default implementation
864            if (!audioPlayer)
865                audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype);
866
867            // If we have successfully retrieved an audio player, send success response
868            if (audioPlayer) {
869                audioPlayers[stream_index] = audioPlayer;
870                guac_client.sendAck(stream_index, "OK", 0x0000);
871            }
872
873            // Otherwise, mimetype must be unsupported
874            else
875                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
876
877        },
878
879        "blob": function(parameters) {
880
881            // Get stream
882            var stream_index = parseInt(parameters[0]);
883            var data = parameters[1];
884            var stream = streams[stream_index];
885
886            // Write data
887            if (stream && stream.onblob)
888                stream.onblob(data);
889
890        },
891
892        "body" : function handleBody(parameters) {
893
894            // Get object
895            var objectIndex = parseInt(parameters[0]);
896            var object = objects[objectIndex];
897
898            var streamIndex = parseInt(parameters[1]);
899            var mimetype = parameters[2];
900            var name = parameters[3];
901
902            // Create stream if handler defined
903            if (object && object.onbody) {
904                var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex);
905                object.onbody(stream, mimetype, name);
906            }
907
908            // Otherwise, unsupported
909            else
910                guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100);
911
912        },
913
914        "cfill": function(parameters) {
915
916            var channelMask = parseInt(parameters[0]);
917            var layer = getLayer(parseInt(parameters[1]));
918            var r = parseInt(parameters[2]);
919            var g = parseInt(parameters[3]);
920            var b = parseInt(parameters[4]);
921            var a = parseInt(parameters[5]);
922
923            display.setChannelMask(layer, channelMask);
924            display.fillColor(layer, r, g, b, a);
925
926        },
927
928        "clip": function(parameters) {
929
930            var layer = getLayer(parseInt(parameters[0]));
931
932            display.clip(layer);
933
934        },
935
936        "clipboard": function(parameters) {
937
938            var stream_index = parseInt(parameters[0]);
939            var mimetype = parameters[1];
940
941            // Create stream
942            if (guac_client.onclipboard) {
943                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
944                guac_client.onclipboard(stream, mimetype);
945            }
946
947            // Otherwise, unsupported
948            else
949                guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100);
950
951        },
952
953        "close": function(parameters) {
954
955            var layer = getLayer(parseInt(parameters[0]));
956
957            display.close(layer);
958
959        },
960
961        "copy": function(parameters) {
962
963            var srcL = getLayer(parseInt(parameters[0]));
964            var srcX = parseInt(parameters[1]);
965            var srcY = parseInt(parameters[2]);
966            var srcWidth = parseInt(parameters[3]);
967            var srcHeight = parseInt(parameters[4]);
968            var channelMask = parseInt(parameters[5]);
969            var dstL = getLayer(parseInt(parameters[6]));
970            var dstX = parseInt(parameters[7]);
971            var dstY = parseInt(parameters[8]);
972
973            display.setChannelMask(dstL, channelMask);
974            display.copy(srcL, srcX, srcY, srcWidth, srcHeight,
975                         dstL, dstX, dstY);
976
977        },
978
979        "cstroke": function(parameters) {
980
981            var channelMask = parseInt(parameters[0]);
982            var layer = getLayer(parseInt(parameters[1]));
983            var cap = lineCap[parseInt(parameters[2])];
984            var join = lineJoin[parseInt(parameters[3])];
985            var thickness = parseInt(parameters[4]);
986            var r = parseInt(parameters[5]);
987            var g = parseInt(parameters[6]);
988            var b = parseInt(parameters[7]);
989            var a = parseInt(parameters[8]);
990
991            display.setChannelMask(layer, channelMask);
992            display.strokeColor(layer, cap, join, thickness, r, g, b, a);
993
994        },
995
996        "cursor": function(parameters) {
997
998            var cursorHotspotX = parseInt(parameters[0]);
999            var cursorHotspotY = parseInt(parameters[1]);
1000            var srcL = getLayer(parseInt(parameters[2]));
1001            var srcX = parseInt(parameters[3]);
1002            var srcY = parseInt(parameters[4]);
1003            var srcWidth = parseInt(parameters[5]);
1004            var srcHeight = parseInt(parameters[6]);
1005
1006            display.setCursor(cursorHotspotX, cursorHotspotY,
1007                              srcL, srcX, srcY, srcWidth, srcHeight);
1008
1009        },
1010
1011        "curve": function(parameters) {
1012
1013            var layer = getLayer(parseInt(parameters[0]));
1014            var cp1x = parseInt(parameters[1]);
1015            var cp1y = parseInt(parameters[2]);
1016            var cp2x = parseInt(parameters[3]);
1017            var cp2y = parseInt(parameters[4]);
1018            var x = parseInt(parameters[5]);
1019            var y = parseInt(parameters[6]);
1020
1021            display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y);
1022
1023        },
1024
1025        "disconnect" : function handleDisconnect(parameters) {
1026
1027            // Explicitly tear down connection
1028            guac_client.disconnect();
1029
1030        },
1031
1032        "dispose": function(parameters) {
1033           
1034            var layer_index = parseInt(parameters[0]);
1035
1036            // If visible layer, remove from parent
1037            if (layer_index > 0) {
1038
1039                // Remove from parent
1040                var layer = getLayer(layer_index);
1041                display.dispose(layer);
1042
1043                // Delete reference
1044                delete layers[layer_index];
1045
1046            }
1047
1048            // If buffer, just delete reference
1049            else if (layer_index < 0)
1050                delete layers[layer_index];
1051
1052            // Attempting to dispose the root layer currently has no effect.
1053
1054        },
1055
1056        "distort": function(parameters) {
1057
1058            var layer_index = parseInt(parameters[0]);
1059            var a = parseFloat(parameters[1]);
1060            var b = parseFloat(parameters[2]);
1061            var c = parseFloat(parameters[3]);
1062            var d = parseFloat(parameters[4]);
1063            var e = parseFloat(parameters[5]);
1064            var f = parseFloat(parameters[6]);
1065
1066            // Only valid for visible layers (not buffers)
1067            if (layer_index >= 0) {
1068                var layer = getLayer(layer_index);
1069                display.distort(layer, a, b, c, d, e, f);
1070            }
1071
1072        },
1073 
1074        "error": function(parameters) {
1075
1076            var reason = parameters[0];
1077            var code = parseInt(parameters[1]);
1078
1079            // Call handler if defined
1080            if (guac_client.onerror)
1081                guac_client.onerror(new Guacamole.Status(code, reason));
1082
1083            guac_client.disconnect();
1084
1085        },
1086
1087        "end": function(parameters) {
1088
1089            var stream_index = parseInt(parameters[0]);
1090
1091            // Get stream
1092            var stream = streams[stream_index];
1093            if (stream) {
1094
1095                // Signal end of stream if handler defined
1096                if (stream.onend)
1097                    stream.onend();
1098
1099                // Invalidate stream
1100                delete streams[stream_index];
1101
1102            }
1103
1104        },
1105
1106        "file": function(parameters) {
1107
1108            var stream_index = parseInt(parameters[0]);
1109            var mimetype = parameters[1];
1110            var filename = parameters[2];
1111
1112            // Create stream
1113            if (guac_client.onfile) {
1114                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
1115                guac_client.onfile(stream, mimetype, filename);
1116            }
1117
1118            // Otherwise, unsupported
1119            else
1120                guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100);
1121
1122        },
1123
1124        "filesystem" : function handleFilesystem(parameters) {
1125
1126            var objectIndex = parseInt(parameters[0]);
1127            var name = parameters[1];
1128
1129            // Create object, if supported
1130            if (guac_client.onfilesystem) {
1131                var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex);
1132                guac_client.onfilesystem(object, name);
1133            }
1134
1135            // If unsupported, simply ignore the availability of the filesystem
1136
1137        },
1138
1139        "identity": function(parameters) {
1140
1141            var layer = getLayer(parseInt(parameters[0]));
1142
1143            display.setTransform(layer, 1, 0, 0, 1, 0, 0);
1144
1145        },
1146
1147        "img": function(parameters) {
1148
1149            var stream_index = parseInt(parameters[0]);
1150            var channelMask = parseInt(parameters[1]);
1151            var layer = getLayer(parseInt(parameters[2]));
1152            var mimetype = parameters[3];
1153            var x = parseInt(parameters[4]);
1154            var y = parseInt(parameters[5]);
1155
1156            // Create stream
1157            var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
1158            var reader = new Guacamole.DataURIReader(stream, mimetype);
1159
1160            // Draw image when stream is complete
1161            reader.onend = function drawImageBlob() {
1162                display.setChannelMask(layer, channelMask);
1163                display.draw(layer, x, y, reader.getURI());
1164            };
1165
1166        },
1167
1168        "jpeg": function(parameters) {
1169
1170            var channelMask = parseInt(parameters[0]);
1171            var layer = getLayer(parseInt(parameters[1]));
1172            var x = parseInt(parameters[2]);
1173            var y = parseInt(parameters[3]);
1174            var data = parameters[4];
1175
1176            display.setChannelMask(layer, channelMask);
1177            display.draw(layer, x, y, "data:image/jpeg;base64," + data);
1178
1179        },
1180
1181        "lfill": function(parameters) {
1182
1183            var channelMask = parseInt(parameters[0]);
1184            var layer = getLayer(parseInt(parameters[1]));
1185            var srcLayer = getLayer(parseInt(parameters[2]));
1186
1187            display.setChannelMask(layer, channelMask);
1188            display.fillLayer(layer, srcLayer);
1189
1190        },
1191
1192        "line": function(parameters) {
1193
1194            var layer = getLayer(parseInt(parameters[0]));
1195            var x = parseInt(parameters[1]);
1196            var y = parseInt(parameters[2]);
1197
1198            display.lineTo(layer, x, y);
1199
1200        },
1201
1202        "lstroke": function(parameters) {
1203
1204            var channelMask = parseInt(parameters[0]);
1205            var layer = getLayer(parseInt(parameters[1]));
1206            var srcLayer = getLayer(parseInt(parameters[2]));
1207
1208            display.setChannelMask(layer, channelMask);
1209            display.strokeLayer(layer, srcLayer);
1210
1211        },
1212
1213        "mouse" : function handleMouse(parameters) {
1214
1215            var x = parseInt(parameters[0]);
1216            var y = parseInt(parameters[1]);
1217
1218            // Display and move software cursor to received coordinates
1219            display.showCursor(true);
1220            display.moveCursor(x, y);
1221
1222        },
1223
1224        "move": function(parameters) {
1225           
1226            var layer_index = parseInt(parameters[0]);
1227            var parent_index = parseInt(parameters[1]);
1228            var x = parseInt(parameters[2]);
1229            var y = parseInt(parameters[3]);
1230            var z = parseInt(parameters[4]);
1231
1232            // Only valid for non-default layers
1233            if (layer_index > 0 && parent_index >= 0) {
1234                var layer = getLayer(layer_index);
1235                var parent = getLayer(parent_index);
1236                display.move(layer, parent, x, y, z);
1237            }
1238
1239        },
1240
1241        "name": function(parameters) {
1242            if (guac_client.onname) guac_client.onname(parameters[0]);
1243        },
1244
1245        "nest": function(parameters) {
1246            var parser = getParser(parseInt(parameters[0]));
1247            parser.receive(parameters[1]);
1248        },
1249
1250        "pipe": function(parameters) {
1251
1252            var stream_index = parseInt(parameters[0]);
1253            var mimetype = parameters[1];
1254            var name = parameters[2];
1255
1256            // Create stream
1257            if (guac_client.onpipe) {
1258                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
1259                guac_client.onpipe(stream, mimetype, name);
1260            }
1261
1262            // Otherwise, unsupported
1263            else
1264                guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100);
1265
1266        },
1267
1268        "png": function(parameters) {
1269
1270            var channelMask = parseInt(parameters[0]);
1271            var layer = getLayer(parseInt(parameters[1]));
1272            var x = parseInt(parameters[2]);
1273            var y = parseInt(parameters[3]);
1274            var data = parameters[4];
1275
1276            display.setChannelMask(layer, channelMask);
1277            display.draw(layer, x, y, "data:image/png;base64," + data);
1278
1279        },
1280
1281        "pop": function(parameters) {
1282
1283            var layer = getLayer(parseInt(parameters[0]));
1284
1285            display.pop(layer);
1286
1287        },
1288
1289        "push": function(parameters) {
1290
1291            var layer = getLayer(parseInt(parameters[0]));
1292
1293            display.push(layer);
1294
1295        },
1296 
1297        "rect": function(parameters) {
1298
1299            var layer = getLayer(parseInt(parameters[0]));
1300            var x = parseInt(parameters[1]);
1301            var y = parseInt(parameters[2]);
1302            var w = parseInt(parameters[3]);
1303            var h = parseInt(parameters[4]);
1304
1305            display.rect(layer, x, y, w, h);
1306
1307        },
1308       
1309        "reset": function(parameters) {
1310
1311            var layer = getLayer(parseInt(parameters[0]));
1312
1313            display.reset(layer);
1314
1315        },
1316       
1317        "set": function(parameters) {
1318
1319            var layer = getLayer(parseInt(parameters[0]));
1320            var name = parameters[1];
1321            var value = parameters[2];
1322
1323            // Call property handler if defined
1324            var handler = layerPropertyHandlers[name];
1325            if (handler)
1326                handler(layer, value);
1327
1328        },
1329
1330        "shade": function(parameters) {
1331           
1332            var layer_index = parseInt(parameters[0]);
1333            var a = parseInt(parameters[1]);
1334
1335            // Only valid for visible layers (not buffers)
1336            if (layer_index >= 0) {
1337                var layer = getLayer(layer_index);
1338                display.shade(layer, a);
1339            }
1340
1341        },
1342
1343        "size": function(parameters) {
1344
1345            var layer_index = parseInt(parameters[0]);
1346            var layer = getLayer(layer_index);
1347            var width = parseInt(parameters[1]);
1348            var height = parseInt(parameters[2]);
1349
1350            display.resize(layer, width, height);
1351
1352        },
1353       
1354        "start": function(parameters) {
1355
1356            var layer = getLayer(parseInt(parameters[0]));
1357            var x = parseInt(parameters[1]);
1358            var y = parseInt(parameters[2]);
1359
1360            display.moveTo(layer, x, y);
1361
1362        },
1363
1364        "sync": function(parameters) {
1365
1366            var timestamp = parseInt(parameters[0]);
1367
1368            // Flush display, send sync when done
1369            display.flush(function displaySyncComplete() {
1370
1371                // Synchronize all audio players
1372                for (var index in audioPlayers) {
1373                    var audioPlayer = audioPlayers[index];
1374                    if (audioPlayer)
1375                        audioPlayer.sync();
1376                }
1377
1378                // Send sync response to server
1379                if (timestamp !== currentTimestamp) {
1380                    tunnel.sendMessage("sync", timestamp);
1381                    currentTimestamp = timestamp;
1382                }
1383
1384            });
1385
1386            // If received first update, no longer waiting.
1387            if (currentState === STATE_WAITING)
1388                setState(STATE_CONNECTED);
1389
1390            // Call sync handler if defined
1391            if (guac_client.onsync)
1392                guac_client.onsync(timestamp);
1393
1394        },
1395
1396        "transfer": function(parameters) {
1397
1398            var srcL = getLayer(parseInt(parameters[0]));
1399            var srcX = parseInt(parameters[1]);
1400            var srcY = parseInt(parameters[2]);
1401            var srcWidth = parseInt(parameters[3]);
1402            var srcHeight = parseInt(parameters[4]);
1403            var function_index = parseInt(parameters[5]);
1404            var dstL = getLayer(parseInt(parameters[6]));
1405            var dstX = parseInt(parameters[7]);
1406            var dstY = parseInt(parameters[8]);
1407
1408            /* SRC */
1409            if (function_index === 0x3)
1410                display.put(srcL, srcX, srcY, srcWidth, srcHeight,
1411                    dstL, dstX, dstY);
1412
1413            /* Anything else that isn't a NO-OP */
1414            else if (function_index !== 0x5)
1415                display.transfer(srcL, srcX, srcY, srcWidth, srcHeight,
1416                    dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]);
1417
1418        },
1419
1420        "transform": function(parameters) {
1421
1422            var layer = getLayer(parseInt(parameters[0]));
1423            var a = parseFloat(parameters[1]);
1424            var b = parseFloat(parameters[2]);
1425            var c = parseFloat(parameters[3]);
1426            var d = parseFloat(parameters[4]);
1427            var e = parseFloat(parameters[5]);
1428            var f = parseFloat(parameters[6]);
1429
1430            display.transform(layer, a, b, c, d, e, f);
1431
1432        },
1433
1434        "undefine" : function handleUndefine(parameters) {
1435
1436            // Get object
1437            var objectIndex = parseInt(parameters[0]);
1438            var object = objects[objectIndex];
1439
1440            // Signal end of object definition
1441            if (object && object.onundefine)
1442                object.onundefine();
1443
1444        },
1445
1446        "video": function(parameters) {
1447
1448            var stream_index = parseInt(parameters[0]);
1449            var layer = getLayer(parseInt(parameters[1]));
1450            var mimetype = parameters[2];
1451
1452            // Create stream
1453            var stream = streams[stream_index] =
1454                    new Guacamole.InputStream(guac_client, stream_index);
1455
1456            // Get player instance via callback
1457            var videoPlayer = null;
1458            if (guac_client.onvideo)
1459                videoPlayer = guac_client.onvideo(stream, layer, mimetype);
1460
1461            // If unsuccessful, try to use a default implementation
1462            if (!videoPlayer)
1463                videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype);
1464
1465            // If we have successfully retrieved an video player, send success response
1466            if (videoPlayer) {
1467                videoPlayers[stream_index] = videoPlayer;
1468                guac_client.sendAck(stream_index, "OK", 0x0000);
1469            }
1470
1471            // Otherwise, mimetype must be unsupported
1472            else
1473                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
1474
1475        }
1476
1477    };
1478
1479    tunnel.oninstruction = function(opcode, parameters) {
1480
1481        var handler = instructionHandlers[opcode];
1482        if (handler)
1483            handler(parameters);
1484
1485    };
1486
1487    /**
1488     * Sends a disconnect instruction to the server and closes the tunnel.
1489     */
1490    this.disconnect = function() {
1491
1492        // Only attempt disconnection not disconnected.
1493        if (currentState != STATE_DISCONNECTED
1494                && currentState != STATE_DISCONNECTING) {
1495
1496            setState(STATE_DISCONNECTING);
1497
1498            // Stop ping
1499            if (pingInterval)
1500                window.clearInterval(pingInterval);
1501
1502            // Send disconnect message and disconnect
1503            tunnel.sendMessage("disconnect");
1504            tunnel.disconnect();
1505            setState(STATE_DISCONNECTED);
1506
1507        }
1508
1509    };
1510   
1511    /**
1512     * Connects the underlying tunnel of this Guacamole.Client, passing the
1513     * given arbitrary data to the tunnel during the connection process.
1514     *
1515     * @param data Arbitrary connection data to be sent to the underlying
1516     *             tunnel during the connection process.
1517     * @throws {Guacamole.Status} If an error occurs during connection.
1518     */
1519    this.connect = function(data) {
1520
1521        setState(STATE_CONNECTING);
1522
1523        try {
1524            tunnel.connect(data);
1525        }
1526        catch (status) {
1527            setState(STATE_IDLE);
1528            throw status;
1529        }
1530
1531        // Ping every 5 seconds (ensure connection alive)
1532        pingInterval = window.setInterval(function() {
1533            tunnel.sendMessage("nop");
1534        }, 5000);
1535
1536        setState(STATE_WAITING);
1537    };
1538
1539};
1540
1541/**
1542 * Map of all Guacamole binary raster operations to transfer functions.
1543 * @private
1544 */
1545Guacamole.Client.DefaultTransferFunction = {
1546
1547    /* BLACK */
1548    0x0: function (src, dst) {
1549        dst.red = dst.green = dst.blue = 0x00;
1550    },
1551
1552    /* WHITE */
1553    0xF: function (src, dst) {
1554        dst.red = dst.green = dst.blue = 0xFF;
1555    },
1556
1557    /* SRC */
1558    0x3: function (src, dst) {
1559        dst.red   = src.red;
1560        dst.green = src.green;
1561        dst.blue  = src.blue;
1562        dst.alpha = src.alpha;
1563    },
1564
1565    /* DEST (no-op) */
1566    0x5: function (src, dst) {
1567        // Do nothing
1568    },
1569
1570    /* Invert SRC */
1571    0xC: function (src, dst) {
1572        dst.red   = 0xFF & ~src.red;
1573        dst.green = 0xFF & ~src.green;
1574        dst.blue  = 0xFF & ~src.blue;
1575        dst.alpha =  src.alpha;
1576    },
1577   
1578    /* Invert DEST */
1579    0xA: function (src, dst) {
1580        dst.red   = 0xFF & ~dst.red;
1581        dst.green = 0xFF & ~dst.green;
1582        dst.blue  = 0xFF & ~dst.blue;
1583    },
1584
1585    /* AND */
1586    0x1: function (src, dst) {
1587        dst.red   =  ( src.red   &  dst.red);
1588        dst.green =  ( src.green &  dst.green);
1589        dst.blue  =  ( src.blue  &  dst.blue);
1590    },
1591
1592    /* NAND */
1593    0xE: function (src, dst) {
1594        dst.red   = 0xFF & ~( src.red   &  dst.red);
1595        dst.green = 0xFF & ~( src.green &  dst.green);
1596        dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
1597    },
1598
1599    /* OR */
1600    0x7: function (src, dst) {
1601        dst.red   =  ( src.red   |  dst.red);
1602        dst.green =  ( src.green |  dst.green);
1603        dst.blue  =  ( src.blue  |  dst.blue);
1604    },
1605
1606    /* NOR */
1607    0x8: function (src, dst) {
1608        dst.red   = 0xFF & ~( src.red   |  dst.red);
1609        dst.green = 0xFF & ~( src.green |  dst.green);
1610        dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
1611    },
1612
1613    /* XOR */
1614    0x6: function (src, dst) {
1615        dst.red   =  ( src.red   ^  dst.red);
1616        dst.green =  ( src.green ^  dst.green);
1617        dst.blue  =  ( src.blue  ^  dst.blue);
1618    },
1619
1620    /* XNOR */
1621    0x9: function (src, dst) {
1622        dst.red   = 0xFF & ~( src.red   ^  dst.red);
1623        dst.green = 0xFF & ~( src.green ^  dst.green);
1624        dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
1625    },
1626
1627    /* AND inverted source */
1628    0x4: function (src, dst) {
1629        dst.red   =  0xFF & (~src.red   &  dst.red);
1630        dst.green =  0xFF & (~src.green &  dst.green);
1631        dst.blue  =  0xFF & (~src.blue  &  dst.blue);
1632    },
1633
1634    /* OR inverted source */
1635    0xD: function (src, dst) {
1636        dst.red   =  0xFF & (~src.red   |  dst.red);
1637        dst.green =  0xFF & (~src.green |  dst.green);
1638        dst.blue  =  0xFF & (~src.blue  |  dst.blue);
1639    },
1640
1641    /* AND inverted destination */
1642    0x2: function (src, dst) {
1643        dst.red   =  0xFF & ( src.red   & ~dst.red);
1644        dst.green =  0xFF & ( src.green & ~dst.green);
1645        dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
1646    },
1647
1648    /* OR inverted destination */
1649    0xB: function (src, dst) {
1650        dst.red   =  0xFF & ( src.red   | ~dst.red);
1651        dst.green =  0xFF & ( src.green | ~dst.green);
1652        dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
1653    }
1654
1655};
Note: See TracBrowser for help on using the repository browser.