* @private
*/
var displayContext = display.getContext("2d");
+ 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
/**
* The number of states on the state stack.
+ *
+ * Note that there will ALWAYS be one element on the stack, but that
+ * element is not exposed. It is only used to reset the layer to its
+ * initial state.
*/
var stackSize = 0;
width = newWidth;
height = newHeight;
+ // Acknowledge reset of stack (happens on resize of canvas)
+ stackSize = 0;
+ displayContext.save();
+
}
/**
}
/**
+ * Schedules a task within the current layer just as scheduleTast() does,
+ * except that another specified layer will be blocked until this task
+ * completes, and this task will not start until the other layer is
+ * ready.
+ *
+ * Essentially, a task is scheduled in both layers, and the specified task
+ * will only be performed once both layers are ready, and neither layer may
+ * proceed until this task completes.
+ *
+ * Note that there is no way to specify whether the task starts blocked,
+ * as whether the task is blocked depends completely on whether the
+ * other layer is currently ready.
+ *
+ * @param {Guacamole.Layer} otherLayer The other layer which must be blocked
+ * until this task completes.
+ * @param {function} handler The function to call when possible.
+ */
+ function scheduleTaskSynced(otherLayer, handler) {
+
+ // If we ARE the other layer, no need to sync.
+ // Syncing would result in deadlock.
+ if (layer === otherLayer)
+ scheduleTask(handler);
+
+ // Otherwise synchronize operation with other layer
+ else {
+
+ var drawComplete = false;
+ var layerLock = null;
+
+ function performTask() {
+
+ // Perform task
+ handler();
+
+ // Unblock the other layer now that draw is complete
+ if (layerLock != null)
+ layerLock.unblock();
+
+ // Flag operation as done
+ drawComplete = true;
+
+ }
+
+ // Currently blocked draw task
+ var task = scheduleTask(performTask, true);
+
+ // Unblock draw task once source layer is ready
+ otherLayer.sync(task.unblock);
+
+ // Block other layer until draw completes
+ // Note that the draw MAY have already been performed at this point,
+ // in which case creating a lock on the other layer will lead to
+ // deadlock (the draw task has already run and will thus never
+ // clear the lock)
+ if (!drawComplete)
+ layerLock = otherLayer.sync(null, true);
+
+ }
+ }
+
+ /**
* Set to true if this Layer should resize itself to accomodate the
* dimensions of any drawing operation, and false (the default) otherwise.
*
};
/**
+ * 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.
*
* destination.
*/
this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) {
+ scheduleTaskSynced(srcLayer, function() {
- var drawComplete = false;
- var srcLock = null;
-
- function doTransfer() {
if (layer.autosize != 0) fitRect(x, y, srcw, srch);
var srcCanvas = srcLayer.getCanvas();
}
- // Unblock the source layer now that draw is complete
- if (srcLock != null)
- srcLock.unblock();
-
- // Flag operation as done
- drawComplete = true;
- }
-
- // If we ARE the source layer, no need to sync.
- // Syncing would result in deadlock.
- if (layer === srcLayer)
- scheduleTask(doTransfer);
-
- // Otherwise synchronize copy operation with source layer
- else {
-
- // Currently blocked draw task
- var task = scheduleTask(doTransfer, true);
-
- // Unblock draw task once source layer is ready
- srcLayer.sync(task.unblock);
-
- // Block source layer until draw completes
- // Note that the draw MAY have already been performed at this point,
- // in which case creating a lock on the source layer will lead to
- // deadlock (the draw task has already run and will thus never
- // clear the lock)
- if (!drawComplete)
- srcLock = srcLayer.sync(null, true);
-
- }
-
+ });
};
/**
* @param {Number} y The destination Y coordinate.
*/
this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
-
- var drawComplete = false;
- var srcLock = null;
-
- function doCopy() {
+ scheduleTaskSynced(srcLayer, 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);
-
- // Unblock the source layer now that draw is complete
- if (srcLock != null)
- srcLock.unblock();
+ if (srcCanvas.width != 0 && srcCanvas.height != 0) {
- // Flag operation as done
- drawComplete = true;
- }
+ // Copy source data into temporary canvas (drawing from
+ // source canvas directly can cause the operation to be
+ // performed lazily by the underlying Canvas implementation,
+ // which undermines the sychronization built into these
+ // layers).
+ temp.width = srcw;
+ temp.height = srch;
+ tempContext.putImageData(
+ srcLayer.getContext().getImageData(srcx, srcy, srcw, srch),
+ 0, 0);
+
+ // Draw from temporary canvas
+ displayContext.drawImage(temp, 0, 0, srcw, srch, x, y, srcw, srch);
+
+ }
- // If we ARE the source layer, no need to sync.
- // Syncing would result in deadlock.
- if (layer === srcLayer)
- scheduleTask(doCopy);
+ });
+ };
- // Otherwise synchronize copy operation with source layer
- else {
+ /**
+ * 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() {
- // Currently blocked draw task
- var task = scheduleTask(doCopy, true);
-
- // Unblock draw task once source layer is ready
- srcLayer.sync(task.unblock);
-
- // Block source layer until draw completes
- // Note that the draw MAY have already been performed at this point,
- // in which case creating a lock on the source layer will lead to
- // deadlock (the draw task has already run and will thus never
- // clear the lock)
- if (!drawComplete)
- srcLock = srcLayer.sync(null, true);
+ // 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);
+
+ });
};
/**
- * 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.
* @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
}
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;
});
};
* for other operations (such as clip()) but a new path will be started
* once a path drawing operation (path() or rect()) is used.
*
+ * @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(r, g, b, a) {
+ this.strokeColor = function(cap, join, thickness, r, g, b, a) {
scheduleTask(function() {
// Stroke with color
+ displayContext.lineCap = cap;
+ displayContext.lineJoin = join;
+ displayContext.lineWidth = thickness;
displayContext.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
displayContext.stroke();
});
};
-
/**
* Fills the current path with the specified color. The current path
* is implicitly closed. The current path can continue to be reused
};
/**
+ * 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 {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(cap, join, thickness, srcLayer) {
+ scheduleTaskSynced(srcLayer, function() {
+
+ // Stroke with image data
+ displayContext.lineCap = cap;
+ displayContext.lineJoin = join;
+ displayContext.lineWidth = thickness;
+ displayContext.strokeStyle = displayContext.createPattern(
+ srcLayer.getCanvas(),
+ "repeat"
+ );
+ displayContext.stroke();
+
+ // Path now implicitly closed
+ pathClosed = true;
+
+ });
+ };
+
+ /**
+ * 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} srcLayer The layer to use as a repeating pattern
+ * within the fill.
+ */
+ this.fillLayer = function(srcLayer) {
+ scheduleTask(function() {
+
+ // Fill with image data
+ displayContext.fillStyle = displayContext.createPattern(
+ srcLayer.getCanvas(),
+ "repeat"
+ );
+ displayContext.fill();
+
+ // Path now implicitly closed
+ pathClosed = true;
+
+ });
+ };
+
+ /**
* Push current layer state onto stack.
*/
this.push = function() {
// Clear stack
while (stackSize > 0) {
- displaycontext.restore();
+ displayContext.restore();
stackSize--;
}
- // Clear transform
- displayContext.setTransform(
- 1, 0, 0,
- 0, 1, 0
- /*0, 0, 1*/
- );
+ // Restore to initial state
+ displayContext.restore();
+ displayContext.save();
// Clear path
displayContext.beginPath();
};
/**
+ * 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.
+ * @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(a, b, c, d, e, f) {
+ scheduleTask(function() {
+
+ // Apply transform
+ displayContext.transform(
+ a, b, c,
+ d, e, f
+ /*0, 0, 1*/
+ );
+
+ });
+ };
+
+
+ /**
* Sets the channel mask for future operations on this Layer.
*
* The channel mask is a Guacamole-specific compositing operation identifier
});
};
+ /**
+ * 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 {Number} limit The miter limit for stroke operations using the
+ * miter join.
+ */
+ this.setMiterLimit = function(limit) {
+ scheduleTask(function() {
+ displayContext.miterLimit = limit;
+ });
+ };
+
// Initialize canvas dimensions
display.width = width;
display.height = height;