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