/* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * The Guacamole display. The display does not deal with the Guacamole * protocol, and instead implements a set of graphical operations which * embody the set of operations present in the protocol. The order operations * are executed is guaranteed to be in the same order as their corresponding * functions are called. * * @constructor */ Guacamole.Display = function() { /** * Reference to this Guacamole.Display. * @private */ var guac_display = this; var displayWidth = 0; var displayHeight = 0; var displayScale = 1; // Create display var display = document.createElement("div"); display.style.position = "relative"; display.style.width = displayWidth + "px"; display.style.height = displayHeight + "px"; // Ensure transformations on display originate at 0,0 display.style.transformOrigin = display.style.webkitTransformOrigin = display.style.MozTransformOrigin = display.style.OTransformOrigin = display.style.msTransformOrigin = "0 0"; // Create default layer var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); // Create cursor layer var cursor = new Guacamole.Display.VisibleLayer(0, 0); cursor.setChannelMask(Guacamole.Layer.SRC); // Add default layer and cursor to display display.appendChild(default_layer.getElement()); display.appendChild(cursor.getElement()); // Create bounding div var bounds = document.createElement("div"); bounds.style.position = "relative"; bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; // Add display to bounds bounds.appendChild(display); /** * The X coordinate of the hotspot of the mouse cursor. The hotspot is * the relative location within the image of the mouse cursor at which * each click occurs. * * @type {Number} */ this.cursorHotspotX = 0; /** * The Y coordinate of the hotspot of the mouse cursor. The hotspot is * the relative location within the image of the mouse cursor at which * each click occurs. * * @type {Number} */ this.cursorHotspotY = 0; /** * The current X coordinate of the local mouse cursor. This is not * necessarily the location of the actual mouse - it refers only to * the location of the cursor image within the Guacamole display, as * last set by moveCursor(). * * @type {Number} */ this.cursorX = 0; /** * The current X coordinate of the local mouse cursor. This is not * necessarily the location of the actual mouse - it refers only to * the location of the cursor image within the Guacamole display, as * last set by moveCursor(). * * @type {Number} */ this.cursorY = 0; /** * Fired when the default layer (and thus the entire Guacamole display) * is resized. * * @event * @param {Number} width The new width of the Guacamole display. * @param {Number} height The new height of the Guacamole display. */ this.onresize = null; /** * Fired whenever the local cursor image is changed. This can be used to * implement special handling of the client-side cursor, or to override * the default use of a software cursor layer. * * @event * @param {HTMLCanvasElement} canvas The cursor image. * @param {Number} x The X-coordinate of the cursor hotspot. * @param {Number} y The Y-coordinate of the cursor hotspot. */ this.oncursor = null; /** * The queue of all pending Tasks. Tasks will be run in order, with new * tasks added at the end of the queue and old tasks removed from the * front of the queue (FIFO). These tasks will eventually be grouped * into a Frame. * @private * @type {Task[]} */ var tasks = []; /** * The queue of all frames. Each frame is a pairing of an array of tasks * and a callback which must be called when the frame is rendered. * @private * @type {Frame[]} */ var frames = []; /** * Flushes all pending frames. * @private */ function __flush_frames() { var rendered_frames = 0; // Draw all pending frames, if ready while (rendered_frames < frames.length) { var frame = frames[rendered_frames]; if (!frame.isReady()) break; frame.flush(); rendered_frames++; } // Remove rendered frames from array frames.splice(0, rendered_frames); } /** * An ordered list of tasks which must be executed atomically. Once * executed, an associated (and optional) callback will be called. * * @private * @constructor * @param {function} callback The function to call when this frame is * rendered. * @param {Task[]} tasks The set of tasks which must be executed to render * this frame. */ function Frame(callback, tasks) { /** * Returns whether this frame is ready to be rendered. This function * returns true if and only if ALL underlying tasks are unblocked. * * @returns {Boolean} true if all underlying tasks are unblocked, * false otherwise. */ this.isReady = function() { // Search for blocked tasks for (var i=0; i < tasks.length; i++) { if (tasks[i].blocked) return false; } // If no blocked tasks, the frame is ready return true; }; /** * Renders this frame, calling the associated callback, if any, after * the frame is complete. This function MUST only be called when no * blocked tasks exist. Calling this function with blocked tasks * will result in undefined behavior. */ this.flush = function() { // Draw all pending tasks. for (var i=0; i < tasks.length; i++) tasks[i].execute(); // Call callback if (callback) callback(); }; } /** * A container for an task handler. Each operation which must be ordered * is associated with a Task that goes into a task queue. Tasks in this * queue are executed in order once their handlers are set, while Tasks * without handlers block themselves and any following Tasks from running. * * @constructor * @private * @param {function} taskHandler The function to call when this task * runs, if any. * @param {boolean} blocked Whether this task should start blocked. */ function Task(taskHandler, blocked) { var task = this; /** * Whether this Task is blocked. * * @type {boolean} */ this.blocked = blocked; /** * Unblocks this Task, allowing it to run. */ this.unblock = function() { if (task.blocked) { task.blocked = false; __flush_frames(); } }; /** * Calls the handler associated with this task IMMEDIATELY. This * function does not track whether this task is marked as blocked. * Enforcing the blocked status of tasks is up to the caller. */ this.execute = function() { if (taskHandler) taskHandler(); }; } /** * Schedules a task for future execution. The given handler will execute * immediately after all previous tasks upon frame flush, unless this * task is blocked. If any tasks is blocked, the entire frame will not * render (and no tasks within will execute) until all tasks are unblocked. * * @private * @param {function} handler The function to call when possible, if any. * @param {boolean} blocked Whether the task should start blocked. * @returns {Task} The Task created and added to the queue for future * running. */ function scheduleTask(handler, blocked) { var task = new Task(handler, blocked); tasks.push(task); return task; } /** * Returns the element which contains the Guacamole display. * * @return {Element} The element containing the Guacamole display. */ this.getElement = function() { return bounds; }; /** * Returns the width of this display. * * @return {Number} The width of this display; */ this.getWidth = function() { return displayWidth; }; /** * Returns the height of this display. * * @return {Number} The height of this display; */ this.getHeight = function() { return displayHeight; }; /** * Returns the default layer of this display. Each Guacamole display always * has at least one layer. Other layers can optionally be created within * this layer, but the default layer cannot be removed and is the absolute * ancestor of all other layers. * * @return {Guacamole.Display.VisibleLayer} The default layer. */ this.getDefaultLayer = function() { return default_layer; }; /** * Returns the cursor layer of this display. Each Guacamole display contains * a layer for the image of the mouse cursor. This layer is a special case * and exists above all other layers, similar to the hardware mouse cursor. * * @return {Guacamole.Display.VisibleLayer} The cursor layer. */ this.getCursorLayer = function() { return cursor; }; /** * Creates a new layer. The new layer will be a direct child of the default * layer, but can be moved to be a child of any other layer. Layers returned * by this function are visible. * * @return {Guacamole.Display.VisibleLayer} The newly-created layer. */ this.createLayer = function() { var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); layer.move(default_layer, 0, 0, 0); return layer; }; /** * Creates a new buffer. Buffers are invisible, off-screen surfaces. They * are implemented in the same manner as layers, but do not provide the * same nesting semantics. * * @return {Guacamole.Layer} The newly-created buffer. */ this.createBuffer = function() { var buffer = new Guacamole.Layer(0, 0); buffer.autosize = 1; return buffer; }; /** * Flush all pending draw tasks, if possible, as a new frame. If the entire * frame is not ready, the flush will wait until all required tasks are * unblocked. * * @param {function} callback The function to call when this frame is * flushed. This may happen immediately, or * later when blocked tasks become unblocked. */ this.flush = function(callback) { // Add frame, reset tasks frames.push(new Frame(callback, tasks)); tasks = []; // Attempt flush __flush_frames(); }; /** * Sets the hotspot and image of the mouse cursor displayed within the * Guacamole display. * * @param {Number} hotspotX The X coordinate of the cursor hotspot. * @param {Number} hotspotY The Y coordinate of the cursor hotspot. * @param {Guacamole.Layer} layer The source layer containing the data which * should be used as the mouse cursor image. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * layer's coordinate space to copy data from. */ this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) { scheduleTask(function __display_set_cursor() { // Set hotspot guac_display.cursorHotspotX = hotspotX; guac_display.cursorHotspotY = hotspotY; // Reset cursor size cursor.resize(srcw, srch); // Draw cursor to cursor layer cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0); guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY); // Fire cursor change event if (guac_display.oncursor) guac_display.oncursor(cursor.getCanvas(), hotspotX, hotspotY); }); }; /** * Sets whether the software-rendered cursor is shown. This cursor differs * from the hardware cursor in that it is built into the Guacamole.Display, * and relies on its own Guacamole layer to render. * * @param {Boolean} [shown=true] Whether to show the software cursor. */ this.showCursor = function(shown) { var element = cursor.getElement(); var parent = element.parentNode; // Remove from DOM if hidden if (shown === false) { if (parent) parent.removeChild(element); } // Otherwise, ensure cursor is child of display else if (parent !== display) display.appendChild(element); }; /** * Sets the location of the local cursor to the given coordinates. For the * sake of responsiveness, this function performs its action immediately. * Cursor motion is not maintained within atomic frames. * * @param {Number} x The X coordinate to move the cursor to. * @param {Number} y The Y coordinate to move the cursor to. */ this.moveCursor = function(x, y) { // Move cursor layer cursor.translate(x - guac_display.cursorHotspotX, y - guac_display.cursorHotspotY); // Update stored position guac_display.cursorX = x; guac_display.cursorY = y; }; /** * Changes the size of the given Layer to the given width and height. * Resizing is only attempted if the new size provided is actually different * from the current size. * * @param {Guacamole.Layer} layer The layer to resize. * @param {Number} width The new width. * @param {Number} height The new height. */ this.resize = function(layer, width, height) { scheduleTask(function __display_resize() { layer.resize(width, height); // Resize display if default layer is resized if (layer === default_layer) { // Update (set) display size displayWidth = width; displayHeight = height; display.style.width = displayWidth + "px"; display.style.height = displayHeight + "px"; // Update bounds size bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; // Notify of resize if (guac_display.onresize) guac_display.onresize(width, height); } }); }; /** * Draws the specified image at the given coordinates. The image specified * must already be loaded. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Image} image The image to draw. Note that this is an Image * object - not a URL. */ this.drawImage = function(layer, x, y, image) { scheduleTask(function __display_drawImage() { layer.drawImage(x, y, image); }); }; /** * Draws the image contained within the specified Blob at the given * coordinates. The Blob specified must already be populated with image * data. * * @param {Guacamole.Layer} layer * The layer to draw upon. * * @param {Number} x * The destination X coordinate. * * @param {Number} y * The destination Y coordinate. * * @param {Blob} blob * The Blob containing the image data to draw. */ this.drawBlob = function(layer, x, y, blob) { // Create URL for blob var url = URL.createObjectURL(blob); // Draw and free blob URL when ready var task = scheduleTask(function __display_drawBlob() { layer.drawImage(x, y, image); URL.revokeObjectURL(url); }, true); // Load image from URL var image = new Image(); image.onload = task.unblock; image.src = url; }; /** * Draws the image at the specified URL at the given coordinates. The image * will be loaded automatically, and this and any future operations will * wait for the image to finish loading. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {String} url The URL of the image to draw. */ this.draw = function(layer, x, y, url) { var task = scheduleTask(function __display_draw() { layer.drawImage(x, y, image); }, true); var image = new Image(); image.onload = task.unblock; image.src = url; }; /** * Plays the video at the specified URL within this layer. The video * will be loaded automatically, and this and any future operations will * wait for the video to finish loading. Future operations will not be * executed until the video finishes playing. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} mimetype The mimetype of the video to play. * @param {Number} duration The duration of the video in milliseconds. * @param {String} url The URL of the video to play. */ this.play = function(layer, mimetype, duration, url) { // Start loading the video var video = document.createElement("video"); video.type = mimetype; video.src = url; // Start copying frames when playing video.addEventListener("play", function() { function render_callback() { layer.drawImage(0, 0, video); if (!video.ended) window.setTimeout(render_callback, 20); } render_callback(); }, false); scheduleTask(video.play); }; /** * Transfer a rectangle of image data from one Layer to this Layer using the * specified transfer function. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Function} transferFunction The transfer function to use to * transfer data from source to * destination. */ this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) { scheduleTask(function __display_transfer() { dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction); }); }; /** * Put a rectangle of image data from one Layer to this Layer directly * without performing any alpha blending. Simply copy the data. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. */ this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { scheduleTask(function __display_put() { dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); }); }; /** * Copy a rectangle of image data from one Layer to this Layer. This * operation will copy exactly the image data that will be drawn once all * operations of the source Layer that were pending at the time this * function was called are complete. This operation will not alter the * size of the source Layer even if its autosize property is set to true. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. */ this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { scheduleTask(function __display_copy() { dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); }); }; /** * Starts a new path at the specified point. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the point to draw. * @param {Number} y The Y coordinate of the point to draw. */ this.moveTo = function(layer, x, y) { scheduleTask(function __display_moveTo() { layer.moveTo(x, y); }); }; /** * Add the specified line to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the endpoint of the line to draw. * @param {Number} y The Y coordinate of the endpoint of the line to draw. */ this.lineTo = function(layer, x, y) { scheduleTask(function __display_lineTo() { layer.lineTo(x, y); }); }; /** * Add the specified arc to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the center of the circle which * will contain the arc. * @param {Number} y The Y coordinate of the center of the circle which * will contain the arc. * @param {Number} radius The radius of the circle. * @param {Number} startAngle The starting angle of the arc, in radians. * @param {Number} endAngle The ending angle of the arc, in radians. * @param {Boolean} negative Whether the arc should be drawn in order of * decreasing angle. */ this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { scheduleTask(function __display_arc() { layer.arc(x, y, radius, startAngle, endAngle, negative); }); }; /** * Starts a new path at the specified point. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} cp1x The X coordinate of the first control point. * @param {Number} cp1y The Y coordinate of the first control point. * @param {Number} cp2x The X coordinate of the second control point. * @param {Number} cp2y The Y coordinate of the second control point. * @param {Number} x The X coordinate of the endpoint of the curve. * @param {Number} y The Y coordinate of the endpoint of the curve. */ this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { scheduleTask(function __display_curveTo() { layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); }); }; /** * Closes the current path by connecting the end point with the start * point (if any) with a straight line. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.close = function(layer) { scheduleTask(function __display_close() { layer.close(); }); }; /** * Add the specified rectangle to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} y The Y coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} w The width of the rectangle to draw. * @param {Number} h The height of the rectangle to draw. */ this.rect = function(layer, x, y, w, h) { scheduleTask(function __display_rect() { layer.rect(x, y, w, h); }); }; /** * Clip all future drawing operations by the current path. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as fillColor()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to affect. */ this.clip = function(layer) { scheduleTask(function __display_clip() { layer.clip(); }); }; /** * Stroke the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { scheduleTask(function __display_strokeColor() { layer.strokeColor(cap, join, thickness, r, g, b, a); }); }; /** * Fills the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.fillColor = function(layer, r, g, b, a) { scheduleTask(function __display_fillColor() { layer.fillColor(r, g, b, a); }); }; /** * Stroke the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the stroke. */ this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { scheduleTask(function __display_strokeLayer() { layer.strokeLayer(cap, join, thickness, srcLayer); }); }; /** * Fills the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the fill. */ this.fillLayer = function(layer, srcLayer) { scheduleTask(function __display_fillLayer() { layer.fillLayer(srcLayer); }); }; /** * Push current layer state onto stack. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.push = function(layer) { scheduleTask(function __display_push() { layer.push(); }); }; /** * Pop layer state off stack. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.pop = function(layer) { scheduleTask(function __display_pop() { layer.pop(); }); }; /** * Reset the layer, clearing the stack, the current path, and any transform * matrix. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.reset = function(layer) { scheduleTask(function __display_reset() { layer.reset(); }); }; /** * Sets the given affine transform (defined with six values from the * transform's matrix). * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.setTransform = function(layer, a, b, c, d, e, f) { scheduleTask(function __display_setTransform() { layer.setTransform(a, b, c, d, e, f); }); }; /** * Applies the given affine transform (defined with six values from the * transform's matrix). * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.transform = function(layer, a, b, c, d, e, f) { scheduleTask(function __display_transform() { layer.transform(a, b, c, d, e, f); }); }; /** * Sets the channel mask for future operations on this Layer. * * The channel mask is a Guacamole-specific compositing operation identifier * with a single bit representing each of four channels (in order): source * image where destination transparent, source where destination opaque, * destination where source transparent, and destination where source * opaque. * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} mask The channel mask for future operations on this * Layer. */ this.setChannelMask = function(layer, mask) { scheduleTask(function __display_setChannelMask() { layer.setChannelMask(mask); }); }; /** * Sets the miter limit for stroke operations using the miter join. This * limit is the maximum ratio of the size of the miter join to the stroke * width. If this ratio is exceeded, the miter will not be drawn for that * joint of the path. * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} limit The miter limit for stroke operations using the * miter join. */ this.setMiterLimit = function(layer, limit) { scheduleTask(function __display_setMiterLimit() { layer.setMiterLimit(limit); }); }; /** * Sets the scale of the client display element such that it renders at * a relatively smaller or larger size, without affecting the true * resolution of the display. * * @param {Number} scale The scale to resize to, where 1.0 is normal * size (1:1 scale). */ this.scale = function(scale) { display.style.transform = display.style.WebkitTransform = display.style.MozTransform = display.style.OTransform = display.style.msTransform = "scale(" + scale + "," + scale + ")"; displayScale = scale; // Update bounds size bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; }; /** * Returns the scale of the display. * * @return {Number} The scale of the display. */ this.getScale = function() { return displayScale; }; /** * Returns a canvas element containing the entire display, with all child * layers composited within. * * @return {HTMLCanvasElement} A new canvas element containing a copy of * the display. */ this.flatten = function() { // Get destination canvas var canvas = document.createElement("canvas"); canvas.width = default_layer.width; canvas.height = default_layer.height; var context = canvas.getContext("2d"); // Returns sorted array of children function get_children(layer) { // Build array of children var children = []; for (var index in layer.children) children.push(layer.children[index]); // Sort children.sort(function children_comparator(a, b) { // Compare based on Z order var diff = a.z - b.z; if (diff !== 0) return diff; // If Z order identical, use document order var a_element = a.getElement(); var b_element = b.getElement(); var position = b_element.compareDocumentPosition(a_element); if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; // Otherwise, assume same return 0; }); // Done return children; } // Draws the contents of the given layer at the given coordinates function draw_layer(layer, x, y) { // Draw layer if (layer.width > 0 && layer.height > 0) { // Save and update alpha var initial_alpha = context.globalAlpha; context.globalAlpha *= layer.alpha / 255.0; // Copy data context.drawImage(layer.getCanvas(), x, y); // Draw all children var children = get_children(layer); for (var i=0; i