More JSDoc, renamed Update to Task, fixed wording.
[guacamole-common-js.git] / src / main / resources / layer.js
1
2 /*
3  *  Guacamole - Clientless Remote Desktop
4  *  Copyright (C) 2010  Michael Jumper
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU Affero General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Affero General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Affero General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 // Guacamole namespace
21 var Guacamole = Guacamole || {};
22
23 /**
24  * Abstract ordered drawing surface. Each Layer contains a canvas element and
25  * provides simple drawing instructions for drawing to that canvas element,
26  * however unlike the canvas element itself, drawing operations on a Layer are
27  * guaranteed to run in order, even if such an operation must wait for an image
28  * to load before completing.
29  * 
30  * @constructor
31  * 
32  * @param {Number} width The width of the Layer, in pixels. The canvas element
33  *                       backing this Layer will be given this width.
34  *                       
35  * @param {Number} height The height of the Layer, in pixels. The canvas element
36  *                        backing this Layer will be given this height.
37  */
38 Guacamole.Layer = function(width, height) {
39
40     /**
41      * Reference to this Layer.
42      * @private
43      */
44     var layer = this;
45
46     /**
47      * The canvas element backing this Layer.
48      * @private
49      */
50     var display = document.createElement("canvas");
51
52     /**
53      * The 2D display context of the canvas element backing this Layer.
54      * @private
55      */
56     var displayContext = display.getContext("2d");
57
58     var tasks = new Array();
59
60     var compositeOperation = {
61      /* 0x0 NOT IMPLEMENTED */
62         0x1: "destination-in",
63         0x2: "destination-out",
64      /* 0x3 NOT IMPLEMENTED */
65         0x4: "source-in",
66      /* 0x5 NOT IMPLEMENTED */
67         0x6: "source-atop",
68      /* 0x7 NOT IMPLEMENTED */
69         0x8: "source-out",
70         0x9: "destination-atop",
71         0xA: "xor",
72         0xB: "destination-over",
73         0xC: "copy",
74      /* 0xD NOT IMPLEMENTED */
75         0xE: "source-over",
76         0xF: "lighter"
77     };
78
79     /**
80      * Returns the canvas element backing this Layer.
81      * @returns {Element} The canvas element backing this Layer.
82      */
83     this.getCanvas = function() {
84         return display;
85     };
86
87     /**
88      * Resizes the canvas element backing this Layer without testing the
89      * new size. This function should only be used internally.
90      * 
91      * @private
92      * @param {Number} newWidth The new width to assign to this Layer.
93      * @param {Number} newHeight The new height to assign to this Layer.
94      */
95     function resize(newWidth, newHeight) {
96         display.style.position = "absolute";
97         display.style.left = "0px";
98         display.style.top = "0px";
99
100         display.width = newWidth;
101         display.height = newHeight;
102
103         width = newWidth;
104         height = newHeight;
105     }
106
107     /**
108      * Changes the size of this Layer to the given width and height. Resizing
109      * is only attempted if the new size provided is actually different from
110      * the current size.
111      * 
112      * @param {Number} newWidth The new width to assign to this Layer.
113      * @param {Number} newHeight The new height to assign to this Layer.
114      */
115     this.resize = function(newWidth, newHeight) {
116         if (newWidth != width || newHeight != height)
117             resize(newWidth, newHeight);
118     };
119
120     /**
121      * Given the X and Y coordinates of the upper-left corner of a rectangle
122      * and the rectangle's width and height, resize the backing canvas element
123      * as necessary to ensure that the rectangle fits within the canvas
124      * element's coordinate space. This function will only make the canvas
125      * larger. If the rectangle already fits within the canvas element's
126      * coordinate space, the canvas is left unchanged.
127      * 
128      * @private
129      * @param {Number} x The X coordinate of the upper-left corner of the
130      *                   rectangle to fit.
131      * @param {Number} y The Y coordinate of the upper-left corner of the
132      *                   rectangle to fit.
133      * @param {Number} w The width of the the rectangle to fit.
134      * @param {Number} h The height of the the rectangle to fit.
135      */
136     function fitRect(x, y, w, h) {
137         
138         // Calculate bounds
139         var opBoundX = w + x;
140         var opBoundY = h + y;
141         
142         // Determine max width
143         var resizeWidth;
144         if (opBoundX > width)
145             resizeWidth = opBoundX;
146         else
147             resizeWidth = width;
148
149         // Determine max height
150         var resizeHeight;
151         if (opBoundY > height)
152             resizeHeight = opBoundY;
153         else
154             resizeHeight = height;
155
156         // Resize if necessary
157         if (resizeWidth != width || resizeHeight != height)
158             resize(resizeWidth, resizeHeight);
159
160     }
161
162     /**
163      * Set to true if this Layer should resize itself to accomodate the
164      * dimensions of any drawing operation, and false (the default) otherwise.
165      * 
166      * Note that setting this property takes effect immediately, and thus may
167      * take effect on operations that were started in the past but have not
168      * yet completed. If you wish the setting of this flag to only modify
169      * future operations, you will need to make the setting of this flag an
170      * operation with sync().
171      * 
172      * @example
173      * // Set autosize to true for all future operations
174      * layer.sync(function() {
175      *     layer.autosize = true;
176      * });
177      * 
178      * @type Boolean
179      * @default false
180      */
181     this.autosize = false;
182
183     /**
184      * A container for an task handler. Each operation which must be ordered
185      * is associated with a Task that goes into a task queue. Tasks in this
186      * queue are executed in order once their handlers are set, while Tasks 
187      * without handlers block themselves and any following Tasks from running.
188      *
189      * @constructor
190      * @private
191      * @param {function} taskHandler The function to call when this task 
192      *                               runs, if any.
193      */
194     function Task(taskHandler) {
195         
196         /**
197          * The handler this Task is associated with, if any.
198          * 
199          * @type function
200          */
201         this.handler = taskHandler;
202         
203     }
204
205     function scheduleTask(handler) {
206         
207         // If no pending tasks, just call (if available) and exit
208         if (layer.isReady() && handler != null) {
209             handler();
210             return null;
211         }
212
213         // If tasks are pending/executing, schedule a pending task
214         // and return a reference to it.
215         var task = new Task(handler);
216         tasks.push(task);
217         return task;
218         
219     }
220
221     function handlePendingTasks() {
222
223         // Draw all pending tasks.
224         var task;
225         while ((task = tasks[0]) != null && task.handler) {
226             task.handler();
227             tasks.shift();
228         }
229
230     }
231
232     /**
233      * Returns whether this Layer is ready. A Layer is ready if it has no
234      * pending operations and no operations in-progress.
235      * 
236      * @returns {Boolean} true if this Layer is ready, false otherwise.
237      */
238     this.isReady = function() {
239         return tasks.length == 0;
240     };
241
242     /**
243      * Draws the specified image at the given coordinates. The image specified
244      * must already be loaded.
245      * 
246      * @param {Number} x The destination X coordinate.
247      * @param {Number} y The destination Y coordinate.
248      * @param {Image} image The image to draw. Note that this is an Image
249      *                      object - not a URL.
250      */
251     this.drawImage = function(x, y, image) {
252         scheduleTask(function() {
253             if (autosize != 0) fitRect(x, y, image.width, image.height);
254             displayContext.drawImage(image, x, y);
255         });
256     };
257
258     /**
259      * Draws the image at the specified URL at the given coordinates. The image
260      * will be loaded automatically, and this and any future operations will
261      * wait for the image to finish loading.
262      * 
263      * @param {Number} x The destination X coordinate.
264      * @param {Number} y The destination Y coordinate.
265      * @param {String} url The URL of the image to draw.
266      */
267     this.draw = function(x, y, url) {
268         var task = scheduleTask(null);
269
270         var image = new Image();
271         image.onload = function() {
272
273             task.handler = function() {
274                 if (autosize != 0) fitRect(x, y, image.width, image.height);
275                 displayContext.drawImage(image, x, y);
276             };
277
278             // As this task originally had no handler and may have blocked
279             // other tasks, handle any blocked tasks.
280             handlePendingTasks();
281
282         };
283         image.src = url;
284
285     };
286
287     /**
288      * Run an arbitrary function as soon as currently pending operations
289      * are complete.
290      * 
291      * @param {function} handler The function to call once all currently
292      *                           pending operations are complete.
293      */
294     this.sync = function(handler) {
295         scheduleTask(handler);
296     };
297
298     /**
299      * Copy a rectangle of image data from one Layer to this Layer. This
300      * operation will copy exactly the image data that will be drawn once all
301      * operations of the source Layer that were pending at the time this
302      * function was called are complete. This operation will not alter the
303      * size of the source Layer even if its autosize property is set to true.
304      * 
305      * @param {Guacamole.Layer} srcLayer The Layer to copy image data from.
306      * @param {Number} srcx The X coordinate of the upper-left corner of the
307      *                      rectangle within the source Layer's coordinate
308      *                      space to copy data from.
309      * @param {Number} srcy The Y coordinate of the upper-left corner of the
310      *                      rectangle within the source Layer's coordinate
311      *                      space to copy data from.
312      * @param {Number} srcw The width of the rectangle within the source Layer's
313      *                      coordinate space to copy data from.
314      * @param {Number} srch The height of the rectangle within the source
315      *                      Layer's coordinate space to copy data from.
316      * @param {Number} x The destination X coordinate.
317      * @param {Number} y The destination Y coordinate.
318      */
319     this.copyRect = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
320
321         function doCopyRect() {
322             if (autosize != 0) fitRect(x, y, srcw, srch);
323             displayContext.drawImage(srcLayer, srcx, srcy, srcw, srch, x, y, srcw, srch);
324         }
325
326         // If we ARE the source layer, no need to sync.
327         // Syncing would result in deadlock.
328         if (layer === srcLayer)
329             scheduleTask(doCopyRect);
330
331         // Otherwise synchronize copy operation with source layer
332         else {
333             var task = scheduleTask(null);
334             srcLayer.sync(function() {
335                 
336                 task.handler = doCopyRect;
337
338                 // As this task originally had no handler and may have blocked
339                 // other tasks, handle any blocked tasks.
340                 handlePendingTasks();
341
342             });
343         }
344
345     };
346
347     this.clearRect = function(x, y, w, h) {
348         scheduleTask(function() {
349             if (autosize != 0) fitRect(x, y, w, h);
350             displayContext.clearRect(x, y, w, h);
351         });
352     };
353
354     this.filter = function(filter) {
355         scheduleTask(function() {
356             var imageData = displayContext.getImageData(0, 0, width, height);
357             filter(imageData.data, width, height);
358             displayContext.putImageData(imageData, 0, 0);
359         });
360     };
361
362     this.setChannelMask = function(mask) {
363         scheduleTask(function() {
364             displayContext.globalCompositeOperation = compositeOperation[mask];
365         });
366     };
367
368     // Initialize canvas dimensions
369     resize(width, height);
370
371 }
372