3 * Guacamole - Clientless Remote Desktop
4 * Copyright (C) 2010 Michael Jumper
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.
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.
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/>.
20 // Guacamole namespace
21 var Guacamole = Guacamole || {};
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.
32 * @param {Number} width The width of the Layer, in pixels. The canvas element
33 * backing this Layer will be given this width.
35 * @param {Number} height The height of the Layer, in pixels. The canvas element
36 * backing this Layer will be given this height.
38 Guacamole.Layer = function(width, height) {
40 // Reference to this Layer
43 // Off-screen buffer (canvas element) and corresponding
45 var display = document.createElement("canvas");
46 var displayContext = display.getContext("2d");
49 * Returns the canvas element backing this Layer.
50 * @returns {Element} The canvas element backing this Layer.
52 this.getCanvas = function() {
57 * Resizes the canvas element backing this Layer without testing the
58 * new size. This function should only be used internally.
61 * @param {Number} newWidth The new width to assign to this Layer.
62 * @param {Number} newHeight The new height to assign to this Layer.
64 function resize(newWidth, newHeight) {
65 display.style.position = "absolute";
66 display.style.left = "0px";
67 display.style.top = "0px";
69 display.width = newWidth;
70 display.height = newHeight;
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
81 * @param {Number} newWidth The new width to assign to this Layer.
82 * @param {Number} newHeight The new height to assign to this Layer.
84 this.resize = function(newWidth, newHeight) {
85 if (newWidth != width || newHeight != height)
86 resize(newWidth, newHeight);
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.
98 * @param {Number} x The X coordinate of the upper-left corner of the
100 * @param {Number} y The Y coordinate of the upper-left corner of the
102 * @param {Number} w The width of the the rectangle to fit.
103 * @param {Number} h The height of the the rectangle to fit.
105 function fitRect(x, y, w, h) {
108 var opBoundX = w + x;
109 var opBoundY = h + y;
111 // Determine max width
113 if (opBoundX > width)
114 resizeWidth = opBoundX;
118 // Determine max height
120 if (opBoundY > height)
121 resizeHeight = opBoundY;
123 resizeHeight = height;
125 // Resize if necessary
126 if (resizeWidth != width || resizeHeight != height)
127 resize(resizeWidth, resizeHeight);
131 // Initialize canvas dimensions
132 resize(width, height);
135 * Set to true if this Layer should resize itself to accomodate the
136 * dimensions of any drawing operation, and false (the default) otherwise.
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().
145 * // Set autosize to true for all future operations
146 * layer.sync(function() {
147 * layer.autosize = true;
153 this.autosize = false;
155 var updates = new Array();
157 function Update(updateHandler) {
159 this.setHandler = function(handler) {
160 updateHandler = handler;
163 this.hasHandler = function() {
164 return updateHandler != null;
167 this.handle = function() {
173 function reserveJob(handler) {
175 // If no pending updates, just call (if available) and exit
176 if (layer.isReady() && handler != null) {
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);
189 function handlePendingUpdates() {
191 // Draw all pending updates.
193 while ((update = updates[0]) != null && update.hasHandler()) {
201 * Returns whether this Layer is ready. A Layer is ready if it has no
202 * pending operations and no operations in-progress.
204 * @returns {Boolean} true if this Layer is ready, false otherwise.
206 this.isReady = function() {
207 return updates.length == 0;
211 * Draws the specified image at the given coordinates. The image specified
212 * must already be loaded.
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.
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);
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.
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.
235 this.draw = function(x, y, url) {
236 var update = reserveJob(null);
238 var image = new Image();
239 image.onload = function() {
241 update.setHandler(function() {
242 if (autosize != 0) fitRect(x, y, image.width, image.height);
243 displayContext.drawImage(image, x, y);
246 // As this update originally had no handler and may have blocked
247 // other updates, handle any blocked updates.
248 handlePendingUpdates();
256 * Run an arbitrary function as soon as currently pending operations
259 * @param {function} handler The function to call once all currently
260 * pending operations are complete.
262 this.sync = function(handler) {
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.
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.
287 this.copyRect = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
289 function doCopyRect() {
290 if (autosize != 0) fitRect(x, y, srcw, srch);
291 displayContext.drawImage(srcLayer, srcx, srcy, srcw, srch, x, y, srcw, srch);
294 // If we ARE the source layer, no need to sync.
295 // Syncing would result in deadlock.
296 if (layer === srcLayer)
297 reserveJob(doCopyRect);
299 // Otherwise synchronize copy operation with source layer
301 var update = reserveJob(null);
302 srcLayer.sync(function() {
304 update.setHandler(doCopyRect);
306 // As this update originally had no handler and may have blocked
307 // other updates, handle any blocked updates.
308 handlePendingUpdates();
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);
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);
330 var compositeOperation = {
331 /* 0x0 NOT IMPLEMENTED */
332 0x1: "destination-in",
333 0x2: "destination-out",
334 /* 0x3 NOT IMPLEMENTED */
336 /* 0x5 NOT IMPLEMENTED */
338 /* 0x7 NOT IMPLEMENTED */
340 0x9: "destination-atop",
342 0xB: "destination-over",
344 /* 0xD NOT IMPLEMENTED */
349 this.setChannelMask = function(mask) {
350 reserveJob(function() {
351 displayContext.globalCompositeOperation = compositeOperation[mask];