01acafbe0dddd4ab583f53cbf5f3bc3b9f203d06
[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     // Reference to this Layer
41     var layer = this;
42
43     // Off-screen buffer (canvas element) and corresponding
44     // context.
45     var display = document.createElement("canvas");
46     var displayContext = display.getContext("2d");
47
48     /**
49      * Returns the canvas element backing this Layer.
50      * @returns {Element} The canvas element backing this Layer.
51      */
52     this.getCanvas = function() {
53         return display;
54     };
55
56     /**
57      * Resizes the canvas element backing this Layer without testing the
58      * new size. This function should only be used internally.
59      * 
60      * @private
61      * @param {Number} newWidth The new width to assign to this Layer.
62      * @param {Number} newHeight The new height to assign to this Layer.
63      */
64     function resize(newWidth, newHeight) {
65         display.style.position = "absolute";
66         display.style.left = "0px";
67         display.style.top = "0px";
68
69         display.width = newWidth;
70         display.height = newHeight;
71
72         width = newWidth;
73         height = newHeight;
74     }
75
76     /**
77      * Changes the size of this Layer to the given width and height. Resizing
78      * is only attempted if the new size provided is actually different from
79      * the current size.
80      * 
81      * @param {Number} newWidth The new width to assign to this Layer.
82      * @param {Number} newHeight The new height to assign to this Layer.
83      */
84     this.resize = function(newWidth, newHeight) {
85         if (newWidth != width || newHeight != height)
86             resize(newWidth, newHeight);
87     };
88
89     /**
90      * Given the X and Y coordinates of the upper-left corner of a rectangle
91      * and the rectangle's width and height, resize the backing canvas element
92      * as necessary to ensure that the rectangle fits within the canvas
93      * element's coordinate space. This function will only make the canvas
94      * larger. If the rectangle already fits within the canvas element's
95      * coordinate space, the canvas is left unchanged.
96      * 
97      * @private
98      * @param {Number} x The X coordinate of the upper-left corner of the
99      *                   rectangle to fit.
100      * @param {Number} y The Y coordinate of the upper-left corner of the
101      *                   rectangle to fit.
102      * @param {Number} w The width of the the rectangle to fit.
103      * @param {Number} h The height of the the rectangle to fit.
104      */
105     function fitRect(x, y, w, h) {
106         
107         // Calculate bounds
108         var opBoundX = w + x;
109         var opBoundY = h + y;
110         
111         // Determine max width
112         var resizeWidth;
113         if (opBoundX > width)
114             resizeWidth = opBoundX;
115         else
116             resizeWidth = width;
117
118         // Determine max height
119         var resizeHeight;
120         if (opBoundY > height)
121             resizeHeight = opBoundY;
122         else
123             resizeHeight = height;
124
125         // Resize if necessary
126         if (resizeWidth != width || resizeHeight != height)
127             resize(resizeWidth, resizeHeight);
128
129     }
130
131     // Initialize canvas dimensions
132     resize(width, height);
133
134     /**
135      * Set to true if this Layer should resize itself to accomodate the
136      * dimensions of any drawing operation, and false (the default) otherwise.
137      * 
138      * Note that setting this property takes effect immediately, and thus may
139      * take effect on operations that were started in the past but have not
140      * yet completed. If you wish the setting of this flag to only modify
141      * future operations, you will need to make the setting of this flag an
142      * operation with sync().
143      * 
144      * @example
145      * // Set autosize to true for all future operations
146      * layer.sync(function() {
147      *     layer.autosize = true;
148      * });
149      * 
150      * @type Boolean
151      * @default false
152      */
153     this.autosize = false;
154
155     var updates = new Array();
156
157     function Update(updateHandler) {
158
159         this.setHandler = function(handler) {
160             updateHandler = handler;
161         };
162
163         this.hasHandler = function() {
164             return updateHandler != null;
165         };
166
167         this.handle = function() {
168             updateHandler();
169         };
170
171     }
172
173     function reserveJob(handler) {
174         
175         // If no pending updates, just call (if available) and exit
176         if (layer.isReady() && handler != null) {
177             handler();
178             return null;
179         }
180
181         // If updates are pending/executing, schedule a pending update
182         // and return a reference to it.
183         var update = new Update(handler);
184         updates.push(update);
185         return update;
186         
187     }
188
189     function handlePendingUpdates() {
190
191         // Draw all pending updates.
192         var update;
193         while ((update = updates[0]) != null && update.hasHandler()) {
194             update.handle();
195             updates.shift();
196         }
197
198     }
199
200     /**
201      * Returns whether this Layer is ready. A Layer is ready if it has no
202      * pending operations and no operations in-progress.
203      * 
204      * @returns {Boolean} true if this Layer is ready, false otherwise.
205      */
206     this.isReady = function() {
207         return updates.length == 0;
208     };
209
210     /**
211      * Draws the specified image at the given coordinates. The image specified
212      * must already be loaded.
213      * 
214      * @param {Number} x The destination X coordinate.
215      * @param {Number} y The destination Y coordinate.
216      * @param {Image} image The image to draw. Note that this is an Image
217      *                      object - not a URL.
218      */
219     this.drawImage = function(x, y, image) {
220         reserveJob(function() {
221             if (autosize != 0) fitRect(x, y, image.width, image.height);
222             displayContext.drawImage(image, x, y);
223         });
224     };
225
226     /**
227      * Draws the image at the specified URL at the given coordinates. The image
228      * will be loaded automatically, and this and any future operations will
229      * wait for the image to finish loading.
230      * 
231      * @param {Number} x The destination X coordinate.
232      * @param {Number} y The destination Y coordinate.
233      * @param {String} url The URL of the image to draw.
234      */
235     this.draw = function(x, y, url) {
236         var update = reserveJob(null);
237
238         var image = new Image();
239         image.onload = function() {
240
241             update.setHandler(function() {
242                 if (autosize != 0) fitRect(x, y, image.width, image.height);
243                 displayContext.drawImage(image, x, y);
244             });
245
246             // As this update originally had no handler and may have blocked
247             // other updates, handle any blocked updates.
248             handlePendingUpdates();
249
250         };
251         image.src = url;
252
253     };
254
255     /**
256      * Run an arbitrary function as soon as currently pending operations
257      * are complete.
258      * 
259      * @param {function} handler The function to call once all currently
260      *                           pending operations are complete.
261      */
262     this.sync = function(handler) {
263         reserveJob(handler);
264     };
265
266     /**
267      * Copy a rectangle of image data from one Layer to this Layer. This
268      * operation will copy exactly the image data that will be drawn once all
269      * operations of the source Layer that were pending at the time this
270      * function was called are complete. This operation will not alter the
271      * size of the source Layer even if its autosize property is set to true.
272      * 
273      * @param {Guacamole.Layer} srcLayer The Layer to copy image data from.
274      * @param {Number} srcx The X coordinate of the upper-left corner of the
275      *                      rectangle within the source Layer's coordinate
276      *                      space to copy data from.
277      * @param {Number} srcy The Y coordinate of the upper-left corner of the
278      *                      rectangle within the source Layer's coordinate
279      *                      space to copy data from.
280      * @param {Number} srcw The width of the rectangle within the source Layer's
281      *                      coordinate space to copy data from.
282      * @param {Number} srch The height of the rectangle within the source
283      *                      Layer's coordinate space to copy data from.
284      * @param {Number} x The destination X coordinate.
285      * @param {Number} y The destination Y coordinate.
286      */
287     this.copyRect = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
288
289         function doCopyRect() {
290             if (autosize != 0) fitRect(x, y, srcw, srch);
291             displayContext.drawImage(srcLayer, srcx, srcy, srcw, srch, x, y, srcw, srch);
292         }
293
294         // If we ARE the source layer, no need to sync.
295         // Syncing would result in deadlock.
296         if (layer === srcLayer)
297             reserveJob(doCopyRect);
298
299         // Otherwise synchronize copy operation with source layer
300         else {
301             var update = reserveJob(null);
302             srcLayer.sync(function() {
303                 
304                 update.setHandler(doCopyRect);
305
306                 // As this update originally had no handler and may have blocked
307                 // other updates, handle any blocked updates.
308                 handlePendingUpdates();
309
310             });
311         }
312
313     };
314
315     this.clearRect = function(x, y, w, h) {
316         reserveJob(function() {
317             if (autosize != 0) fitRect(x, y, w, h);
318             displayContext.clearRect(x, y, w, h);
319         });
320     };
321
322     this.filter = function(filter) {
323         reserveJob(function() {
324             var imageData = displayContext.getImageData(0, 0, width, height);
325             filter(imageData.data, width, height);
326             displayContext.putImageData(imageData, 0, 0);
327         });
328     };
329
330     var compositeOperation = {
331      /* 0x0 NOT IMPLEMENTED */
332         0x1: "destination-in",
333         0x2: "destination-out",
334      /* 0x3 NOT IMPLEMENTED */
335         0x4: "source-in",
336      /* 0x5 NOT IMPLEMENTED */
337         0x6: "source-atop",
338      /* 0x7 NOT IMPLEMENTED */
339         0x8: "source-out",
340         0x9: "destination-atop",
341         0xA: "xor",
342         0xB: "destination-over",
343         0xC: "copy",
344      /* 0xD NOT IMPLEMENTED */
345         0xE: "source-over",
346         0xF: "lighter"
347     };
348
349     this.setChannelMask = function(mask) {
350         reserveJob(function() {
351             displayContext.globalCompositeOperation = compositeOperation[mask];
352         });
353     };
354
355 }
356