More efficient blocking, leveraging the now-necessary image data copy.
[guacamole-common-js.git] / src / main / resources / layer.js
index c7754c3..48452c5 100644 (file)
@@ -75,6 +75,19 @@ Guacamole.Layer = function(width, height) {
     displayContext.save();
 
     /**
+     * A temporary canvas element whose contents can be relied on only
+     * through the duration of an operation.
+     * @private
+     */
+    var temp = document.createElement("canvas");
+
+    /**
+     * The 2D display context of the temporary canvas element.
+     * @private
+     */
+    var tempContext = temp.getContext("2d");
+
+    /**
      * 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).
@@ -168,6 +181,10 @@ Guacamole.Layer = function(width, height) {
         width = newWidth;
         height = newHeight;
 
+        // Acknowledge reset of stack (happens on resize of canvas)
+        stackSize = 0;
+        displayContext.save();
+
     }
 
     /**
@@ -397,6 +414,15 @@ Guacamole.Layer = function(width, height) {
     };
 
     /**
+     * Returns the display context of the canvas element backing this layer.
+     * @returns {CanvasRenderingContext2D} The display context of the canvas
+     *                                     element backing this layer.
+     */
+    this.getContext = function() {
+        return displayContext;
+    };
+
+    /**
      * Returns whether this Layer is ready. A Layer is ready if it has no
      * pending operations and no operations in-progress.
      * 
@@ -562,27 +588,141 @@ Guacamole.Layer = function(width, height) {
      * @param {Number} y The destination Y coordinate.
      */
     this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
-        scheduleTaskSynced(srcLayer, function() {
+
+        // If we are copying from ourselves, perform simple drawImage() copy.
+        // No other synchronization is necessary.
+        if (srcLayer === this) {
+
+            scheduleTask(function() {
+                if (layer.autosize != 0) fitRect(x, y, srcw, srch);
+                displayContext.drawImage(srcLayer.getCanvas(), srcx, srcy, srcw, srch, x, y, srcw, srch);
+            });
+
+            return;
+        }
+
+        // Note that source image data MUST be retrieved with getImageData()
+        // rather than drawing directly using the source canvas as an
+        // argument to drawImage(). This is because Canvas implementations may
+        // implement drawImage() lazily, which can cause rendering issues if
+        // the source canvas is updated before drawImage() is actually
+        // performed. Retrieving the actual underlying pixel data with
+        // getImageData() ensures that the image data is truly available.
+
+        // Will contain image data once source layer is ready
+        var data;
+
+        // Draw image data from the source layer any time after the
+        // source layer is ready (the copied image data will be stored
+        // such that the source layer can continue unimpeded).
+        var task = scheduleTask(function() {
+
             if (layer.autosize != 0) fitRect(x, y, srcw, srch);
 
             var srcCanvas = srcLayer.getCanvas();
-            if (srcCanvas.width != 0 && srcCanvas.height != 0)
-                displayContext.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch);
+            if (srcCanvas.width != 0 && srcCanvas.height != 0) {
+
+                // Copy image data onto temporary canvas
+                temp.width = srcw;
+                temp.height = srch;
+                tempContext.putImageData(data, 0, 0);
+
+                // Draw from temporary canvas
+                displayContext.drawImage(temp, 0, 0, srcw, srch, x, y, srcw, srch);
+                
+            }
 
+        }, true);
+
+        // When source layer is ready, pull data, and unblock draw task
+        srcLayer.sync(function() {
+            data = srcLayer.getContext().getImageData(srcx, srcy, srcw, srch);
+            task.unblock();
         });
+
     };
 
     /**
-     * Add the specified cubic bezier point to the current path.
+     * Starts a new path at the specified point.
      * 
      * @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(x, y) {
+        scheduleTask(function() {
+            
+            // Start a new path if current path is closed
+            if (pathClosed) {
+                displayContext.beginPath();
+                pathClosed = false;
+            }
+            
+            if (layer.autosize != 0) fitRect(x, y, 0, 0);
+            displayContext.moveTo(x, y);
+            
+        });
+    };
+
+    /**
+     * Add the specified line to the current path.
+     * 
+     * @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(x, y) {
+        scheduleTask(function() {
+            
+            // Start a new path if current path is closed
+            if (pathClosed) {
+                displayContext.beginPath();
+                pathClosed = false;
+            }
+            
+            if (layer.autosize != 0) fitRect(x, y, 0, 0);
+            displayContext.lineTo(x, y);
+            
+        });
+    };
+
+    /**
+     * Add the specified arc to the current path.
+     * 
+     * @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(x, y, radius, startAngle, endAngle, negative) {
+        scheduleTask(function() {
+            
+            // Start a new path if current path is closed
+            if (pathClosed) {
+                displayContext.beginPath();
+                pathClosed = false;
+            }
+            
+            if (layer.autosize != 0) fitRect(x, y, 0, 0);
+            displayContext.arc(x, y, radius, startAngle, endAngle, negative);
+            
+        });
+    };
+
+    /**
+     * Starts a new path at the specified point.
+     * 
      * @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.path = function(x, y, cp1x, cp1y, cp2x, cp2y) {
+    this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
         scheduleTask(function() {
             
             // Start a new path if current path is closed
@@ -592,7 +732,21 @@ Guacamole.Layer = function(width, height) {
             }
             
             if (layer.autosize != 0) fitRect(x, y, 0, 0);
-            displayContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, y);
+            displayContext.bezierCurveTo(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.
+     */
+    this.close = function() {
+        scheduleTask(function() {
+            
+            // Close path
+            displayContext.closePath();
+            pathClosed = true;
             
         });
     };
@@ -809,7 +963,32 @@ Guacamole.Layer = function(width, height) {
     };
 
     /**
-     * Applies the given affine transform (defined with three values from the
+     * Sets the given affine transform (defined with six values from the
+     * transform's matrix).
+     * 
+     * @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(a, b, c, d, e, f) {
+        scheduleTask(function() {
+
+            // Set transform
+            displayContext.setTransform(
+                a, b, c,
+                d, e, f
+              /*0, 0, 1*/
+            );
+
+        });
+    };
+
+
+    /**
+     * Applies the given affine transform (defined with six values from the
      * transform's matrix).
      * 
      * @param {Number} a The first value in the affine transform's matrix.
@@ -822,7 +1001,7 @@ Guacamole.Layer = function(width, height) {
     this.transform = function(a, b, c, d, e, f) {
         scheduleTask(function() {
 
-            // Clear transform
+            // Apply transform
             displayContext.transform(
                 a, b, c,
                 d, e, f