More efficient blocking, leveraging the now-necessary image data copy.
[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
592         // If we are copying from ourselves, perform simple drawImage() copy.
593         // No other synchronization is necessary.
594         if (srcLayer === this) {
595
596             scheduleTask(function() {
597                 if (layer.autosize != 0) fitRect(x, y, srcw, srch);
598                 displayContext.drawImage(srcLayer.getCanvas(), srcx, srcy, srcw, srch, x, y, srcw, srch);
599             });
600
601             return;
602         }
603
604         // Note that source image data MUST be retrieved with getImageData()
605         // rather than drawing directly using the source canvas as an
606         // argument to drawImage(). This is because Canvas implementations may
607         // implement drawImage() lazily, which can cause rendering issues if
608         // the source canvas is updated before drawImage() is actually
609         // performed. Retrieving the actual underlying pixel data with
610         // getImageData() ensures that the image data is truly available.
611
612         // Will contain image data once source layer is ready
613         var data;
614
615         // Draw image data from the source layer any time after the
616         // source layer is ready (the copied image data will be stored
617         // such that the source layer can continue unimpeded).
618         var task = scheduleTask(function() {
619
620             if (layer.autosize != 0) fitRect(x, y, srcw, srch);
621
622             var srcCanvas = srcLayer.getCanvas();
623             if (srcCanvas.width != 0 && srcCanvas.height != 0) {
624
625                 // Copy image data onto temporary canvas
626                 temp.width = srcw;
627                 temp.height = srch;
628                 tempContext.putImageData(data, 0, 0);
629
630                 // Draw from temporary canvas
631                 displayContext.drawImage(temp, 0, 0, srcw, srch, x, y, srcw, srch);
632                 
633             }
634
635         }, true);
636
637         // When source layer is ready, pull data, and unblock draw task
638         srcLayer.sync(function() {
639             data = srcLayer.getContext().getImageData(srcx, srcy, srcw, srch);
640             task.unblock();
641         });
642
643     };
644
645     /**
646      * Starts a new path at the specified point.
647      * 
648      * @param {Number} x The X coordinate of the point to draw.
649      * @param {Number} y The Y coordinate of the point to draw.
650      */
651     this.moveTo = function(x, y) {
652         scheduleTask(function() {
653             
654             // Start a new path if current path is closed
655             if (pathClosed) {
656                 displayContext.beginPath();
657                 pathClosed = false;
658             }
659             
660             if (layer.autosize != 0) fitRect(x, y, 0, 0);
661             displayContext.moveTo(x, y);
662             
663         });
664     };
665
666     /**
667      * Add the specified line to the current path.
668      * 
669      * @param {Number} x The X coordinate of the endpoint of the line to draw.
670      * @param {Number} y The Y coordinate of the endpoint of the line to draw.
671      */
672     this.lineTo = function(x, y) {
673         scheduleTask(function() {
674             
675             // Start a new path if current path is closed
676             if (pathClosed) {
677                 displayContext.beginPath();
678                 pathClosed = false;
679             }
680             
681             if (layer.autosize != 0) fitRect(x, y, 0, 0);
682             displayContext.lineTo(x, y);
683             
684         });
685     };
686
687     /**
688      * Add the specified arc to the current path.
689      * 
690      * @param {Number} x The X coordinate of the center of the circle which
691      *                   will contain the arc.
692      * @param {Number} y The Y coordinate of the center of the circle which
693      *                   will contain the arc.
694      * @param {Number} radius The radius of the circle.
695      * @param {Number} startAngle The starting angle of the arc, in radians.
696      * @param {Number} endAngle The ending angle of the arc, in radians.
697      * @param {Boolean} negative Whether the arc should be drawn in order of
698      *                           decreasing angle.
699      */
700     this.arc = function(x, y, radius, startAngle, endAngle, negative) {
701         scheduleTask(function() {
702             
703             // Start a new path if current path is closed
704             if (pathClosed) {
705                 displayContext.beginPath();
706                 pathClosed = false;
707             }
708             
709             if (layer.autosize != 0) fitRect(x, y, 0, 0);
710             displayContext.arc(x, y, radius, startAngle, endAngle, negative);
711             
712         });
713     };
714
715     /**
716      * Starts a new path at the specified point.
717      * 
718      * @param {Number} cp1x The X coordinate of the first control point.
719      * @param {Number} cp1y The Y coordinate of the first control point.
720      * @param {Number} cp2x The X coordinate of the second control point.
721      * @param {Number} cp2y The Y coordinate of the second control point.
722      * @param {Number} x The X coordinate of the endpoint of the curve.
723      * @param {Number} y The Y coordinate of the endpoint of the curve.
724      */
725     this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
726         scheduleTask(function() {
727             
728             // Start a new path if current path is closed
729             if (pathClosed) {
730                 displayContext.beginPath();
731                 pathClosed = false;
732             }
733             
734             if (layer.autosize != 0) fitRect(x, y, 0, 0);
735             displayContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
736             
737         });
738     };
739
740     /**
741      * Closes the current path by connecting the end point with the start
742      * point (if any) with a straight line.
743      */
744     this.close = function() {
745         scheduleTask(function() {
746             
747             // Close path
748             displayContext.closePath();
749             pathClosed = true;
750             
751         });
752     };
753
754     /**
755      * Add the specified rectangle to the current path.
756      * 
757      * @param {Number} x The X coordinate of the upper-left corner of the
758      *                   rectangle to draw.
759      * @param {Number} y The Y coordinate of the upper-left corner of the
760      *                   rectangle to draw.
761      * @param {Number} w The width of the rectangle to draw.
762      * @param {Number} h The height of the rectangle to draw.
763      */
764     this.rect = function(x, y, w, h) {
765         scheduleTask(function() {
766             
767             // Start a new path if current path is closed
768             if (pathClosed) {
769                 displayContext.beginPath();
770                 pathClosed = false;
771             }
772             
773             if (layer.autosize != 0) fitRect(x, y, w, h);
774             displayContext.rect(x, y, w, h);
775             
776         });
777     };
778
779     /**
780      * Clip all future drawing operations by the current path. The current path
781      * is implicitly closed. The current path can continue to be reused
782      * for other operations (such as fillColor()) but a new path will be started
783      * once a path drawing operation (path() or rect()) is used.
784      */
785     this.clip = function() {
786         scheduleTask(function() {
787
788             // Set new clipping region
789             displayContext.clip();
790
791             // Path now implicitly closed
792             pathClosed = true;
793
794         });
795     };
796
797     /**
798      * Stroke the current path with the specified color. The current path
799      * is implicitly closed. The current path can continue to be reused
800      * for other operations (such as clip()) but a new path will be started
801      * once a path drawing operation (path() or rect()) is used.
802      * 
803      * @param {String} cap The line cap style. Can be "round", "square",
804      *                     or "butt".
805      * @param {String} join The line join style. Can be "round", "bevel",
806      *                      or "miter".
807      * @param {Number} thickness The line thickness in pixels.
808      * @param {Number} r The red component of the color to fill.
809      * @param {Number} g The green component of the color to fill.
810      * @param {Number} b The blue component of the color to fill.
811      * @param {Number} a The alpha component of the color to fill.
812      */
813     this.strokeColor = function(cap, join, thickness, r, g, b, a) {
814         scheduleTask(function() {
815
816             // Stroke with color
817             displayContext.lineCap = cap;
818             displayContext.lineJoin = join;
819             displayContext.lineWidth = thickness;
820             displayContext.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
821             displayContext.stroke();
822
823             // Path now implicitly closed
824             pathClosed = true;
825
826         });
827     };
828
829     /**
830      * Fills the current path with the specified color. The current path
831      * is implicitly closed. The current path can continue to be reused
832      * for other operations (such as clip()) but a new path will be started
833      * once a path drawing operation (path() or rect()) is used.
834      * 
835      * @param {Number} r The red component of the color to fill.
836      * @param {Number} g The green component of the color to fill.
837      * @param {Number} b The blue component of the color to fill.
838      * @param {Number} a The alpha component of the color to fill.
839      */
840     this.fillColor = function(r, g, b, a) {
841         scheduleTask(function() {
842
843             // Fill with color
844             displayContext.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
845             displayContext.fill();
846
847             // Path now implicitly closed
848             pathClosed = true;
849
850         });
851     };
852
853     /**
854      * Stroke the current path with the image within the specified layer. The
855      * image data will be tiled infinitely within the stroke. The current path
856      * is implicitly closed. The current path can continue to be reused
857      * for other operations (such as clip()) but a new path will be started
858      * once a path drawing operation (path() or rect()) is used.
859      * 
860      * @param {String} cap The line cap style. Can be "round", "square",
861      *                     or "butt".
862      * @param {String} join The line join style. Can be "round", "bevel",
863      *                      or "miter".
864      * @param {Number} thickness The line thickness in pixels.
865      * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern
866      *                                   within the stroke.
867      */
868     this.strokeLayer = function(cap, join, thickness, srcLayer) {
869         scheduleTaskSynced(srcLayer, function() {
870
871             // Stroke with image data
872             displayContext.lineCap = cap;
873             displayContext.lineJoin = join;
874             displayContext.lineWidth = thickness;
875             displayContext.strokeStyle = displayContext.createPattern(
876                 srcLayer.getCanvas(),
877                 "repeat"
878             );
879             displayContext.stroke();
880
881             // Path now implicitly closed
882             pathClosed = true;
883
884         });
885     };
886
887     /**
888      * Fills the current path with the image within the specified layer. The
889      * image data will be tiled infinitely within the stroke. The current path
890      * is implicitly closed. The current path can continue to be reused
891      * for other operations (such as clip()) but a new path will be started
892      * once a path drawing operation (path() or rect()) is used.
893      * 
894      * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern
895      *                                   within the fill.
896      */
897     this.fillLayer = function(srcLayer) {
898         scheduleTask(function() {
899
900             // Fill with image data 
901             displayContext.fillStyle = displayContext.createPattern(
902                 srcLayer.getCanvas(),
903                 "repeat"
904             );
905             displayContext.fill();
906
907             // Path now implicitly closed
908             pathClosed = true;
909
910         });
911     };
912
913     /**
914      * Push current layer state onto stack.
915      */
916     this.push = function() {
917         scheduleTask(function() {
918
919             // Save current state onto stack
920             displayContext.save();
921             stackSize++;
922
923         });
924     };
925
926     /**
927      * Pop layer state off stack.
928      */
929     this.pop = function() {
930         scheduleTask(function() {
931
932             // Restore current state from stack
933             if (stackSize > 0) {
934                 displayContext.restore();
935                 stackSize--;
936             }
937
938         });
939     };
940
941     /**
942      * Reset the layer, clearing the stack, the current path, and any transform
943      * matrix.
944      */
945     this.reset = function() {
946         scheduleTask(function() {
947
948             // Clear stack
949             while (stackSize > 0) {
950                 displayContext.restore();
951                 stackSize--;
952             }
953
954             // Restore to initial state
955             displayContext.restore();
956             displayContext.save();
957
958             // Clear path
959             displayContext.beginPath();
960             pathClosed = false;
961
962         });
963     };
964
965     /**
966      * Sets the given affine transform (defined with six values from the
967      * transform's matrix).
968      * 
969      * @param {Number} a The first value in the affine transform's matrix.
970      * @param {Number} b The second value in the affine transform's matrix.
971      * @param {Number} c The third value in the affine transform's matrix.
972      * @param {Number} d The fourth value in the affine transform's matrix.
973      * @param {Number} e The fifth value in the affine transform's matrix.
974      * @param {Number} f The sixth value in the affine transform's matrix.
975      */
976     this.setTransform = function(a, b, c, d, e, f) {
977         scheduleTask(function() {
978
979             // Set transform
980             displayContext.setTransform(
981                 a, b, c,
982                 d, e, f
983               /*0, 0, 1*/
984             );
985
986         });
987     };
988
989
990     /**
991      * Applies the given affine transform (defined with six values from the
992      * transform's matrix).
993      * 
994      * @param {Number} a The first value in the affine transform's matrix.
995      * @param {Number} b The second value in the affine transform's matrix.
996      * @param {Number} c The third value in the affine transform's matrix.
997      * @param {Number} d The fourth value in the affine transform's matrix.
998      * @param {Number} e The fifth value in the affine transform's matrix.
999      * @param {Number} f The sixth value in the affine transform's matrix.
1000      */
1001     this.transform = function(a, b, c, d, e, f) {
1002         scheduleTask(function() {
1003
1004             // Apply transform
1005             displayContext.transform(
1006                 a, b, c,
1007                 d, e, f
1008               /*0, 0, 1*/
1009             );
1010
1011         });
1012     };
1013
1014
1015     /**
1016      * Sets the channel mask for future operations on this Layer.
1017      * 
1018      * The channel mask is a Guacamole-specific compositing operation identifier
1019      * with a single bit representing each of four channels (in order): source
1020      * image where destination transparent, source where destination opaque,
1021      * destination where source transparent, and destination where source
1022      * opaque.
1023      * 
1024      * @param {Number} mask The channel mask for future operations on this
1025      *                      Layer.
1026      */
1027     this.setChannelMask = function(mask) {
1028         scheduleTask(function() {
1029             displayContext.globalCompositeOperation = compositeOperation[mask];
1030         });
1031     };
1032
1033     /**
1034      * Sets the miter limit for stroke operations using the miter join. This
1035      * limit is the maximum ratio of the size of the miter join to the stroke
1036      * width. If this ratio is exceeded, the miter will not be drawn for that
1037      * joint of the path.
1038      * 
1039      * @param {Number} limit The miter limit for stroke operations using the
1040      *                       miter join.
1041      */
1042     this.setMiterLimit = function(limit) {
1043         scheduleTask(function() {
1044             displayContext.miterLimit = limit;
1045         });
1046     };
1047
1048     // Initialize canvas dimensions
1049     display.width = width;
1050     display.height = height;
1051
1052 };
1053
1054 /**
1055  * Channel mask for the composite operation "rout".
1056  */
1057 Guacamole.Layer.ROUT  = 0x2;
1058
1059 /**
1060  * Channel mask for the composite operation "atop".
1061  */
1062 Guacamole.Layer.ATOP  = 0x6;
1063
1064 /**
1065  * Channel mask for the composite operation "xor".
1066  */
1067 Guacamole.Layer.XOR   = 0xA;
1068
1069 /**
1070  * Channel mask for the composite operation "rover".
1071  */
1072 Guacamole.Layer.ROVER = 0xB;
1073
1074 /**
1075  * Channel mask for the composite operation "over".
1076  */
1077 Guacamole.Layer.OVER  = 0xE;
1078
1079 /**
1080  * Channel mask for the composite operation "plus".
1081  */
1082 Guacamole.Layer.PLUS  = 0xF;
1083
1084 /**
1085  * Channel mask for the composite operation "rin".
1086  * Beware that WebKit-based browsers may leave the contents of the destionation
1087  * layer where the source layer is transparent, despite the definition of this
1088  * operation.
1089  */
1090 Guacamole.Layer.RIN   = 0x1;
1091
1092 /**
1093  * Channel mask for the composite operation "in".
1094  * Beware that WebKit-based browsers may leave the contents of the destionation
1095  * layer where the source layer is transparent, despite the definition of this
1096  * operation.
1097  */
1098 Guacamole.Layer.IN    = 0x4;
1099
1100 /**
1101  * Channel mask for the composite operation "out".
1102  * Beware that WebKit-based browsers may leave the contents of the destionation
1103  * layer where the source layer is transparent, despite the definition of this
1104  * operation.
1105  */
1106 Guacamole.Layer.OUT   = 0x8;
1107
1108 /**
1109  * Channel mask for the composite operation "ratop".
1110  * Beware that WebKit-based browsers may leave the contents of the destionation
1111  * layer where the source layer is transparent, despite the definition of this
1112  * operation.
1113  */
1114 Guacamole.Layer.RATOP = 0x9;
1115
1116 /**
1117  * Channel mask for the composite operation "src".
1118  * Beware that WebKit-based browsers may leave the contents of the destionation
1119  * layer where the source layer is transparent, despite the definition of this
1120  * operation.
1121  */
1122 Guacamole.Layer.SRC   = 0xC;
1123
1124
1125 /**
1126  * Represents a single pixel of image data. All components have a minimum value
1127  * of 0 and a maximum value of 255.
1128  * 
1129  * @constructor
1130  * 
1131  * @param {Number} r The red component of this pixel.
1132  * @param {Number} g The green component of this pixel.
1133  * @param {Number} b The blue component of this pixel.
1134  * @param {Number} a The alpha component of this pixel.
1135  */
1136 Guacamole.Layer.Pixel = function(r, g, b, a) {
1137
1138     /**
1139      * The red component of this pixel, where 0 is the minimum value,
1140      * and 255 is the maximum.
1141      */
1142     this.red   = r;
1143
1144     /**
1145      * The green component of this pixel, where 0 is the minimum value,
1146      * and 255 is the maximum.
1147      */
1148     this.green = g;
1149
1150     /**
1151      * The blue component of this pixel, where 0 is the minimum value,
1152      * and 255 is the maximum.
1153      */
1154     this.blue  = b;
1155
1156     /**
1157      * The alpha component of this pixel, where 0 is the minimum value,
1158      * and 255 is the maximum.
1159      */
1160     this.alpha = a;
1161
1162 };