/**
 * @license jCanvas v22.0.1
 * Copyright 2017-2024 Caleb Evans
 * Released under the MIT license
 */


// Define local aliases to frequently used properties
var defaults,
	// Aliases to jQuery methods
	extendObject = $.extend,
	inArray = $.inArray,
	typeOf = function (operand) {
		return Object.prototype.toString.call(operand)
			.slice(8, -1).toLowerCase();
	},
	isPlainObject = $.isPlainObject,
	// Math constants and functions
	PI = Math.PI,
	round = Math.round,
	abs = Math.abs,
	sin = Math.sin,
	cos = Math.cos,
	atan2 = Math.atan2,
	// The Array slice() method
	arraySlice = Array.prototype.slice,
	// jQuery's internal event normalization function
	jQueryEventFix = $.event.fix,
	// Object for storing a number of internal property maps
	maps = {},
	// jQuery internal caches
	caches = {
		dataCache: {},
		propCache: {},
		imageCache: {}
	},
	// Base transformations
	baseTransforms = {
		rotate: 0,
		scaleX: 1,
		scaleY: 1,
		translateX: 0,
		translateY: 0,
		// Store all previous masks
		masks: []
	},
	// Object for storing CSS-related properties
	css = {},
	tangibleEvents = [
		'mousedown',
		'mousemove',
		'mouseup',
		'mouseover',
		'mouseout',
		'touchstart',
		'touchmove',
		'touchend'
	];

// Constructor for creating objects that inherit from jCanvas preferences and defaults
function jCanvasObject(args) {
	var params = this,
		propName;
	// Copy the given parameters into new object
	for (propName in args) {
		// Do not merge defaults into parameters
		if (Object.prototype.hasOwnProperty.call(args, propName)) {
			params[propName] = args[propName];
		}
	}
	return params;
}

// jCanvas object in which global settings are other data are stored
var jCanvas = {
	// Events object for storing jCanvas event initiation functions
	events: {},
	// Object containing all jCanvas event hooks
	eventHooks: {},
	// Settings for enabling future jCanvas features
	future: {}
};

// jCanvas default property values
function jCanvasDefaults() {
	extendObject(this, jCanvasDefaults.baseDefaults);
}
jCanvasDefaults.baseDefaults = {
	align: 'center',
	arrowAngle: 90,
	arrowRadius: 0,
	autosave: true,
	baseline: 'middle',
	bringToFront: false,
	ccw: false,
	closed: false,
	compositing: 'source-over',
	concavity: 0,
	cornerRadius: 0,
	count: 1,
	cropFromCenter: true,
	crossOrigin: null,
	cursors: null,
	disableEvents: false,
	draggable: false,
	dragGroups: null,
	groups: null,
	data: null,
	dx: null,
	dy: null,
	end: 360,
	eventX: null,
	eventY: null,
	fillStyle: 'transparent',
	fontStyle: 'normal',
	fontSize: '12pt',
	fontFamily: 'sans-serif',
	fromCenter: true,
	height: null,
	imageSmoothing: true,
	inDegrees: true,
	intangible: false,
	index: null,
	letterSpacing: null,
	lineHeight: 1,
	layer: false,
	mask: false,
	maxWidth: null,
	miterLimit: 10,
	name: null,
	opacity: 1,
	r1: null,
	r2: null,
	radius: 0,
	repeat: 'repeat',
	respectAlign: false,
	restrictDragToAxis: null,
	rotate: 0,
	rounded: false,
	scale: 1,
	scaleX: 1,
	scaleY: 1,
	shadowBlur: 0,
	shadowColor: 'transparent',
	shadowStroke: false,
	shadowX: 0,
	shadowY: 0,
	sHeight: null,
	sides: 0,
	source: '',
	spread: 0,
	start: 0,
	strokeCap: 'butt',
	strokeDash: null,
	strokeDashOffset: 0,
	strokeJoin: 'miter',
	strokeStyle: 'transparent',
	strokeWidth: 1,
	sWidth: null,
	sx: null,
	sy: null,
	text: '',
	translate: 0,
	translateX: 0,
	translateY: 0,
	type: null,
	visible: true,
	width: null,
	x: 0,
	y: 0
};
defaults = new jCanvasDefaults();
jCanvasObject.prototype = defaults;

/* Internal helper methods */

// Determines if the given operand is a string
function isString(operand) {
	return (typeOf(operand) === 'string');
}

// Determines if the given operand is a function
function isFunction(operand) {
	return (typeOf(operand) === 'function');
}

// Determines if the given operand is numeric
function isNumeric(operand) {
	return !isNaN(Number(operand)) && !isNaN(parseFloat(operand));
}

// Get 2D context for the given canvas
function _getContext(canvas) {
	return (canvas && canvas.getContext ? canvas.getContext('2d') : null);
}

// Coerce designated number properties from strings to numbers
function _coerceNumericProps(props) {
	var propName, propType, propValue;
	// Loop through all properties in given property map
	for (propName in props) {
		if (Object.prototype.hasOwnProperty.call(props, propName)) {
			propValue = props[propName];
			propType = typeOf(propValue);
			// If property is non-empty string and value is numeric
			if (propType === 'string' && isNumeric(propValue) && propName !== 'text') {
				// Convert value to number
				props[propName] = parseFloat(propValue);
			}
		}
	}
	// Ensure value of text property is always a string
	if (props.text !== undefined) {
		props.text = String(props.text);
	}
}

// Clone the given transformations object
function _cloneTransforms(transforms) {
	// Clone the object itself
	transforms = extendObject({}, transforms);
	// Clone the object's masks array
	transforms.masks = transforms.masks.slice(0);
	return transforms;
}

// Save canvas context and update transformation stack
function _saveCanvas(ctx, data) {
	var transforms;
	ctx.save();
	transforms = _cloneTransforms(data.transforms);
	data.savedTransforms.push(transforms);
}

// Restore canvas context update transformation stack
function _restoreCanvas(ctx, data) {
	if (data.savedTransforms.length === 0) {
		// Reset transformation state if it can't be restored any more
		data.transforms = _cloneTransforms(baseTransforms);
	} else {
		// Restore canvas context
		ctx.restore();
		// Restore current transform state to the last saved state
		data.transforms = data.savedTransforms.pop();
	}
}

// Set the style with the given name
function _setStyle(canvas, ctx, params, styleName) {
	if (params[styleName]) {
		if (isFunction(params[styleName])) {
			// Handle functions
			ctx[styleName] = params[styleName].call(canvas, params);
		} else {
			// Handle string values
			ctx[styleName] = params[styleName];
		}
	}
}

// Set canvas context properties
function _setGlobalProps(canvas, ctx, params) {
	_setStyle(canvas, ctx, params, 'fillStyle');
	_setStyle(canvas, ctx, params, 'strokeStyle');
	ctx.lineWidth = params.strokeWidth;
	// Optionally round corners for paths
	if (params.rounded) {
		ctx.lineCap = ctx.lineJoin = 'round';
	} else {
		ctx.lineCap = params.strokeCap;
		ctx.lineJoin = params.strokeJoin;
		ctx.miterLimit = params.miterLimit;
	}
	// Reset strokeDash if null
	if (!params.strokeDash) {
		params.strokeDash = [];
	}
	// Dashed lines
	if (ctx.setLineDash) {
		ctx.setLineDash(params.strokeDash);
	}
	ctx.webkitLineDash = params.strokeDash;
	ctx.lineDashOffset = ctx.webkitLineDashOffset = ctx.mozDashOffset = params.strokeDashOffset;
	// Drop shadow
	ctx.shadowOffsetX = params.shadowX;
	ctx.shadowOffsetY = params.shadowY;
	ctx.shadowBlur = params.shadowBlur;
	ctx.shadowColor = params.shadowColor;
	// Opacity and composite operation
	ctx.globalAlpha = params.opacity;
	ctx.globalCompositeOperation = params.compositing;
	// Support cross-browser toggling of image smoothing
	if (params.imageSmoothing) {
		ctx.imageSmoothingEnabled = params.imageSmoothing;
	}
}

// Optionally enable masking support for this path
function _enableMasking(ctx, data, params) {
	if (params.mask) {
		// If jCanvas autosave is enabled
		if (params.autosave) {
			// Automatically save transformation state by default
			_saveCanvas(ctx, data);
		}
		// Clip the current path
		ctx.clip();
		// Keep track of current masks
		data.transforms.masks.push(params._args);
	}
}

// Restore individual shape transformation
function _restoreTransform(ctx, params) {
	// If shape has been transformed by jCanvas
	if (params._transformed) {
		// Restore canvas context
		ctx.restore();
	}
}

// Close current canvas path
function _closePath(canvas, ctx, params) {
	var data;

	// Optionally close path
	if (params.closed) {
		ctx.closePath();
	}

	if (params.shadowStroke && params.strokeWidth !== 0) {
		// Extend the shadow to include the stroke of a drawing

		// Add a stroke shadow by stroking before filling
		ctx.stroke();
		ctx.fill();
		// Ensure the below stroking does not inherit a shadow
		ctx.shadowColor = 'transparent';
		ctx.shadowBlur = 0;
		// Stroke over fill as usual
		ctx.stroke();

	} else {
		// If shadowStroke is not enabled, stroke & fill as usual

		ctx.fill();
		// Prevent extra shadow created by stroke (but only when fill is present)
		if (params.fillStyle !== 'transparent') {
			ctx.shadowColor = 'transparent';
		}
		if (params.strokeWidth !== 0) {
			// Only stroke if the stroke is not 0
			ctx.stroke();
		}

	}

	// Optionally close path
	if (!params.closed) {
		ctx.closePath();
	}

	// Restore individual shape transformation
	_restoreTransform(ctx, params);

	// Mask shape if chosen
	if (params.mask) {
		// Retrieve canvas data
		data = _getCanvasData(canvas);
		_enableMasking(ctx, data, params);
	}

}

// Transform (translate, scale, or rotate) shape
function _transformShape(canvas, ctx, params, width, height) {

	// Get conversion factor for radians
	params._toRad = (params.inDegrees ? (PI / 180) : 1);

	params._transformed = true;
	ctx.save();

	// Optionally measure (x, y) position from top-left corner
	if (!params.fromCenter && !params._centered && width !== undefined) {
		// Always draw from center unless otherwise specified
		if (height === undefined) {
			height = width;
		}
		params.x += width / 2;
		params.y += height / 2;
		params._centered = true;
	}
	// Optionally rotate shape
	if (params.rotate) {
		_rotateCanvas(ctx, params, null);
	}
	// Optionally scale shape
	if (params.scale !== 1 || params.scaleX !== 1 || params.scaleY !== 1) {
		_scaleCanvas(ctx, params, null);
	}
	// Optionally translate shape
	if (params.translate || params.translateX || params.translateY) {
		_translateCanvas(ctx, params, null);
	}

}

/* Plugin API */

// Extend jCanvas with a user-defined method
jCanvas.extend = function extend(plugin) {

	// Create plugin
	if (plugin.name) {
		// Merge properties with defaults
		if (plugin.props) {
			extendObject(defaults, plugin.props);
		}
		// Define plugin method
		$.fn[plugin.name] = function self(args) {
			var $canvases = this, canvas, e, ctx,
				params;

			for (e = 0; e < $canvases.length; e += 1) {
				canvas = $canvases[e];
				ctx = _getContext(canvas);
				if (ctx) {

					params = new jCanvasObject(args);
					_addLayer(canvas, params, args, self);

					_setGlobalProps(canvas, ctx, params);
					plugin.fn.call(canvas, ctx, params);

				}
			}
			return $canvases;
		};
		// Add drawing type to drawing map
		if (plugin.type) {
			maps.drawings[plugin.type] = plugin.name;
		}
	}
	return $.fn[plugin.name];
};

/* Layer API */

// Retrieved the stored jCanvas data for a canvas element
function _getCanvasData(canvas) {
	var dataCache = caches.dataCache, data;
	if (dataCache._canvas === canvas && dataCache._data) {

		// Retrieve canvas data from cache if possible
		data = dataCache._data;

	} else {

		// Retrieve canvas data from jQuery's internal data storage
		data = $.data(canvas, 'jCanvas');
		if (!data) {

			// Create canvas data object if it does not already exist
			data = {
				// The associated canvas element
				canvas: canvas,
				// Layers array
				layers: [],
				// Layer maps
				layer: {
					names: {},
					groups: {}
				},
				eventHooks: {},
				// All layers that intersect with the event coordinates (regardless of visibility)
				intersecting: [],
				// The topmost layer whose area contains the event coordinates
				lastIntersected: null,
				cursor: $(canvas).css('cursor'),
				// Properties for the current drag event
				drag: {
					layer: null,
					dragging: false
				},
				// Data for the current event
				event: {
					type: null,
					x: null,
					y: null
				},
				// Events which already have been bound to the canvas
				events: {},
				// The canvas's current transformation state
				transforms: _cloneTransforms(baseTransforms),
				savedTransforms: [],
				// Whether a layer is being animated or not
				animating: false,
				// The layer currently being animated
				animated: null,
				// The device pixel ratio
				pixelRatio: 1,
				// Whether pixel ratio transformations have been applied
				scaled: false,
				// Whether the canvas should be redrawn when a layer mousemove
				// event triggers (either directly, or indirectly via dragging)
				redrawOnMousemove: false
			};
			// Use jQuery to store canvas data
			$.data(canvas, 'jCanvas', data);

		}
		// Cache canvas data for faster retrieval
		dataCache._canvas = canvas;
		dataCache._data = data;

	}
	return data;
}

// Initialize all of a layer's associated jCanvas events
function _addLayerEvents($canvas, data, layer) {
	var eventName;
	// Determine which jCanvas events need to be bound to this layer
	for (eventName in jCanvas.events) {
		if (Object.prototype.hasOwnProperty.call(jCanvas.events, eventName)) {
			// If layer has callback function to complement it
			if (layer[eventName] || (layer.cursors && layer.cursors[eventName])) {
				// Bind event to layer
				_addExplicitLayerEvent($canvas, data, layer, eventName);
			}
		}
	}
	if (!data.events.mouseout) {
		$canvas.bind('mouseout.jCanvas', function () {
			// Retrieve the layer whose drag event was canceled
			var layer = data.drag.layer, l;
			// If cursor mouses out of canvas while dragging
			if (layer) {
				// Cancel drag
				data.drag = {};
				_triggerLayerEvent($canvas, data, layer, 'dragcancel');
			}
			// Loop through all layers
			for (l = 0; l < data.layers.length; l += 1) {
				layer = data.layers[l];
				// If layer thinks it's still being moused over
				if (layer._hovered) {
					// Trigger mouseout on layer
					$canvas.triggerLayerEvent(data.layers[l], 'mouseout');
				}
			}
			// Redraw layers
			$canvas.drawLayers();
		});
		// Indicate that an event handler has been bound
		data.events.mouseout = true;
	}
}

// Initialize the given event on the given layer
function _addLayerEvent($canvas, data, layer, eventName) {
	// Use touch events if appropriate
	// eventName = _getMouseEventName(eventName);
	// Bind event to layer
	jCanvas.events[eventName]($canvas, data);
	layer._event = true;
}

// Add a layer event that was explicitly declared in the layer's parameter map,
// excluding events added implicitly (e.g. mousemove event required by draggable
// layers)
function _addExplicitLayerEvent($canvas, data, layer, eventName) {
	_addLayerEvent($canvas, data, layer, eventName);
	if (eventName === 'mouseover' || eventName === 'mouseout' || eventName === 'mousemove') {
		data.redrawOnMousemove = true;
	}
}

// Enable drag support for this layer
function _enableDrag($canvas, data, layer) {
	var dragHelperEvents, eventName, i;
	// Only make layer draggable if necessary
	if (layer.draggable || layer.cursors) {

		// Organize helper events which enable drag support
		dragHelperEvents = ['mousedown', 'mousemove', 'mouseup'];

		// Bind each helper event to the canvas
		for (i = 0; i < dragHelperEvents.length; i += 1) {
			// Use touch events if appropriate
			eventName = dragHelperEvents[i];
			// Bind event
			_addLayerEvent($canvas, data, layer, eventName);
		}
		// Indicate that this layer has events bound to it
		layer._event = true;

	}
}

// Update a layer property map if property is changed
function _updateLayerName($canvas, data, layer, props) {
	var nameMap = data.layer.names;

	// If layer name is being added, not changed
	if (!props) {

		props = layer;

	} else {

		// Remove old layer name entry because layer name has changed
		if (props.name !== undefined && isString(layer.name) && layer.name !== props.name) {
			delete nameMap[layer.name];
		}

	}

	// Add new entry to layer name map with new name
	if (isString(props.name)) {
		nameMap[props.name] = layer;
	}
}

// Create or update the data map for the given layer and group type
function _updateLayerGroups($canvas, data, layer, props) {
	var groupMap = data.layer.groups,
		group, groupName, g,
		index, l;

	// If group name is not changing
	if (!props) {

		props = layer;

	} else {

		// Remove layer from all of its associated groups
		if (props.groups !== undefined && layer.groups !== null) {
			for (g = 0; g < layer.groups.length; g += 1) {
				groupName = layer.groups[g];
				group = groupMap[groupName];
				if (group) {
					// Remove layer from its old layer group entry
					for (l = 0; l < group.length; l += 1) {
						if (group[l] === layer) {
							// Keep track of the layer's initial index
							index = l;
							// Remove layer once found
							group.splice(l, 1);
							break;
						}
					}
					// Remove layer group entry if group is empty
					if (group.length === 0) {
						delete groupMap[groupName];
					}
				}
			}
		}

	}

	// Add layer to new group if a new group name is given
	if (props.groups !== undefined && props.groups !== null) {

		for (g = 0; g < props.groups.length; g += 1) {

			groupName = props.groups[g];

			group = groupMap[groupName];
			if (!group) {
				// Create new group entry if it doesn't exist
				group = groupMap[groupName] = [];
				group.name = groupName;
			}
			if (index === undefined) {
				// Add layer to end of group unless otherwise stated
				index = group.length;
			}
			// Add layer to its new layer group
			group.splice(index, 0, layer);

		}

	}
}

// Get event hooks object for the first selected canvas
$.fn.getEventHooks = function getEventHooks() {
	var $canvases = this, canvas, data,
		eventHooks = {};

	if ($canvases.length !== 0) {
		canvas = $canvases[0];
		data = _getCanvasData(canvas);
		eventHooks = data.eventHooks;
	}
	return eventHooks;
};

// Set event hooks for the selected canvases
$.fn.setEventHooks = function setEventHooks(eventHooks) {
	var $canvases = this, e,
		data;
	for (e = 0; e < $canvases.length; e += 1) {
		data = _getCanvasData($canvases[e]);
		extendObject(data.eventHooks, eventHooks);
	}
	return $canvases;
};

// Get jCanvas layers array
$.fn.getLayers = function getLayers(callback) {
	var $canvases = this, canvas, data,
		layers, layer, l,
		matching = [];

	if ($canvases.length !== 0) {

		canvas = $canvases[0];
		data = _getCanvasData(canvas);
		// Retrieve layers array for this canvas
		layers = data.layers;

		// If a callback function is given
		if (isFunction(callback)) {

			// Filter the layers array using the callback
			for (l = 0; l < layers.length; l += 1) {
				layer = layers[l];
				if (callback.call(canvas, layer)) {
					// Add layer to array of matching layers if test passes
					matching.push(layer);
				}
			}

		} else {
			// Otherwise, get all layers

			matching = layers;

		}

	}
	return matching;
};

// Get a single jCanvas layer object
$.fn.getLayer = function getLayer(layerId) {
	var $canvases = this, canvas,
		data, layers, layer, l,
		idType;

	if ($canvases.length !== 0) {

		canvas = $canvases[0];
		data = _getCanvasData(canvas);
		layers = data.layers;
		idType = typeOf(layerId);

		if (layerId && layerId.layer) {

			// Return the actual layer object if given
			layer = layerId;

		} else if (idType === 'number') {

			// Retrieve the layer using the given index

			// Allow for negative indices
			if (layerId < 0) {
				layerId = layers.length + layerId;
			}
			// Get layer with the given index
			layer = layers[layerId];

		} else if (idType === 'regexp') {

			// Get layer with the name that matches the given regex
			for (l = 0; l < layers.length; l += 1) {
				// Check if layer matches name
				if (isString(layers[l].name) && layers[l].name.match(layerId)) {
					layer = layers[l];
					break;
				}
			}

		} else {

			// Get layer with the given name
			layer = data.layer.names[layerId];

		}

	}
	return layer;
};

// Get all layers in the given group
$.fn.getLayerGroup = function getLayerGroup(groupId) {
	var $canvases = this, canvas, data,
		groups, groupName, group,
		idType = typeOf(groupId);

	if ($canvases.length !== 0) {

		canvas = $canvases[0];

		if (idType === 'array') {

			// Return layer group if given
			group = groupId;

		} else if (idType === 'regexp') {

			// Get canvas data
			data = _getCanvasData(canvas);
			groups = data.layer.groups;
			// Loop through all layers groups for this canvas
			for (groupName in groups) {
				// Find a group whose name matches the given regex
				if (groupName.match(groupId)) {
					group = groups[groupName];
					// Stop after finding the first matching group
					break;
				}
			}

		} else {

			// Find layer group with the given group name
			data = _getCanvasData(canvas);
			group = data.layer.groups[groupId];
		}

	}
	return group;
};

// Get index of layer in layers array
$.fn.getLayerIndex = function getLayerIndex(layerId) {
	var $canvases = this,
		layers = $canvases.getLayers(),
		layer = $canvases.getLayer(layerId);

	return inArray(layer, layers);
};

// Set properties of a layer
$.fn.setLayer = function setLayer(layerId, props) {
	var $canvases = this, $canvas, e,
		data, layer,
		propName, propValue, propType;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);

		layer = $($canvases[e]).getLayer(layerId);
		if (layer) {

			// Update layer property maps
			_updateLayerName($canvas, data, layer, props);
			_updateLayerGroups($canvas, data, layer, props);

			_coerceNumericProps(props);

			// Merge properties with layer
			for (propName in props) {
				if (Object.prototype.hasOwnProperty.call(props, propName)) {
					propValue = props[propName];
					propType = typeOf(propValue);
					if (propType === 'object' && isPlainObject(propValue)) {
						// Clone objects
						layer[propName] = extendObject({}, propValue);
						_coerceNumericProps(layer[propName]);
					} else if (propType === 'array') {
						// Clone arrays
						layer[propName] = propValue.slice(0);
					} else if (propType === 'string') {
						if (propValue.indexOf('+=') === 0) {
							// Increment numbers prefixed with +=
							layer[propName] += parseFloat(propValue.substr(2));
						} else if (propValue.indexOf('-=') === 0) {
							// Decrement numbers prefixed with -=
							layer[propName] -= parseFloat(propValue.substr(2));
						} else if (!isNaN(propValue) && isNumeric(propValue) && propName !== 'text') {
							// Convert numeric values as strings to numbers
							layer[propName] = parseFloat(propValue);
						} else {
							// Otherwise, set given string value
							layer[propName] = propValue;
						}
					} else {
						// Otherwise, set given value
						layer[propName] = propValue;
					}
				}
			}

			// Update layer events
			_addLayerEvents($canvas, data, layer);
			_enableDrag($canvas, data, layer);

			// If layer's properties were changed
			if ($.isEmptyObject(props) === false) {
				_triggerLayerEvent($canvas, data, layer, 'change', props);
			}

		}
	}
	return $canvases;
};

// Set properties of all layers (optionally filtered by a callback)
$.fn.setLayers = function setLayers(props, callback) {
	var $canvases = this, $canvas, e,
		layers, l;
	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);

		layers = $canvas.getLayers(callback);
		// Loop through all layers
		for (l = 0; l < layers.length; l += 1) {
			// Set properties of each layer
			$canvas.setLayer(layers[l], props);
		}
	}
	return $canvases;
};

// Set properties of all layers in the given group
$.fn.setLayerGroup = function setLayerGroup(groupId, props) {
	var $canvases = this, $canvas, e,
		group, l;

	for (e = 0; e < $canvases.length; e += 1) {
		// Get layer group
		$canvas = $($canvases[e]);

		group = $canvas.getLayerGroup(groupId);
		// If group exists
		if (group) {

			// Loop through layers in group
			for (l = 0; l < group.length; l += 1) {
				// Merge given properties with layer
				$canvas.setLayer(group[l], props);
			}

		}
	}
	return $canvases;
};

// Move a layer to the given index in the layers array
$.fn.moveLayer = function moveLayer(layerId, index) {
	var $canvases = this, $canvas, e,
		data, layers, layer;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);

		// Retrieve layers array and desired layer
		layers = data.layers;
		layer = $canvas.getLayer(layerId);
		if (layer) {

			// Ensure layer index is accurate
			layer.index = inArray(layer, layers);

			// Remove layer from its current placement
			layers.splice(layer.index, 1);
			// Add layer in its new placement
			layers.splice(index, 0, layer);

			// Handle negative indices
			if (index < 0) {
				index = layers.length + index;
			}
			// Update layer's stored index
			layer.index = index;

			_triggerLayerEvent($canvas, data, layer, 'move');

		}
	}
	return $canvases;
};

// Remove a jCanvas layer
$.fn.removeLayer = function removeLayer(layerId) {
	var $canvases = this, $canvas, e, data,
		layers, layer;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);

		// Retrieve layers array and desired layer
		layers = $canvas.getLayers();
		layer = $canvas.getLayer(layerId);
		// Remove layer if found
		if (layer) {

			// Ensure layer index is accurate
			layer.index = inArray(layer, layers);
			// Remove layer and allow it to be re-added later
			layers.splice(layer.index, 1);
			delete layer._layer;

			// Update layer name map
			_updateLayerName($canvas, data, layer, {
				name: null
			});
			// Update layer group map
			_updateLayerGroups($canvas, data, layer, {
				groups: null
			});

			// Trigger 'remove' event
			_triggerLayerEvent($canvas, data, layer, 'remove');

		}
	}
	return $canvases;
};

// Remove all layers
$.fn.removeLayers = function removeLayers(callback) {
	var $canvases = this, $canvas, e,
		data, layers, layer, l;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);
		layers = $canvas.getLayers(callback).slice(0);
		// Remove all layers individually
		for (l = 0; l < layers.length; l += 1) {
			layer = layers[l];
			$canvas.removeLayer(layer);
		}
		// Update layer maps
		data.layer.names = {};
		data.layer.groups = {};
	}
	return $canvases;
};

// Remove all layers in the group with the given ID
$.fn.removeLayerGroup = function removeLayerGroup(groupId) {
	var $canvases = this, $canvas, e, group, l;

	if (groupId !== undefined) {
		for (e = 0; e < $canvases.length; e += 1) {
			$canvas = $($canvases[e]);

			group = $canvas.getLayerGroup(groupId);
			// Remove layer group using given group name
			if (group) {

				// Clone groups array
				group = group.slice(0);

				// Loop through layers in group
				for (l = 0; l < group.length; l += 1) {
					$canvas.removeLayer(group[l]);
				}

			}
		}
	}
	return $canvases;
};

// Add an existing layer to a layer group
$.fn.addLayerToGroup = function addLayerToGroup(layerId, groupName) {
	var $canvases = this, $canvas, e,
		layer, groups = [groupName];

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		layer = $canvas.getLayer(layerId);

		// If layer is not already in group
		if (layer.groups) {
			// Clone groups list
			groups = layer.groups.slice(0);
			// If layer is not already in group
			if (inArray(groupName, layer.groups) === -1) {
				// Add layer to group
				groups.push(groupName);
			}
		}
		// Update layer group maps
		$canvas.setLayer(layer, {
			groups: groups
		});

	}
	return $canvases;
};

// Remove an existing layer from a layer group
$.fn.removeLayerFromGroup = function removeLayerFromGroup(layerId, groupName) {
	var $canvases = this, $canvas, e,
		layer, groups = [],
		index;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		layer = $canvas.getLayer(layerId);

		if (layer.groups) {

			// Find index of layer in group
			index = inArray(groupName, layer.groups);

			// If layer is in group
			if (index !== -1) {

				// Clone groups list
				groups = layer.groups.slice(0);

				// Remove layer from group
				groups.splice(index, 1);

				// Update layer group maps
				$canvas.setLayer(layer, {
					groups: groups
				});

			}

		}

	}
	return $canvases;
};

// Get topmost layer that intersects with event coordinates
function _getIntersectingLayer(data) {
	var layer, i,
		mask, m;

	// Store the topmost layer
	layer = null;

	// Get the topmost layer whose visible area intersects event coordinates
	for (i = data.intersecting.length - 1; i >= 0; i -= 1) {

		// Get current layer
		layer = data.intersecting[i];

		// If layer has previous masks
		if (layer._masks) {

			// Search previous masks to ensure
			// layer is visible at event coordinates
			for (m = layer._masks.length - 1; m >= 0; m -= 1) {
				mask = layer._masks[m];
				// If mask does not intersect event coordinates
				if (!mask.intersects) {
					// Indicate that the mask does not
					// intersect event coordinates
					layer.intersects = false;
					// Stop searching previous masks
					break;
				}

			}

			// If event coordinates intersect all previous masks
			// and layer is not intangible
			if (layer.intersects && !layer.intangible) {
				// Stop searching for topmost layer
				break;
			}

		}

	}
	// If resulting layer is intangible
	if (layer && layer.intangible) {
		// Cursor does not intersect this layer
		layer = null;
	}
	return layer;
}

// Draw individual layer (internal)
function _drawLayer($canvas, ctx, layer, nextLayerIndex) {
	if (layer && layer.visible && layer._method) {
		if (nextLayerIndex) {
			layer._next = nextLayerIndex;
		} else {
			layer._next = null;
		}
		// If layer is an object, call its respective method
		if (layer._method) {
			layer._method.call($canvas, layer);
		}
	}
}

// Handle dragging of the currently-dragged layer
function _handleLayerDrag($canvas, data, eventType) {
	var layers, layer, l,
		drag, dragGroups,
		group, groupName, g,
		newX, newY;

	drag = data.drag;
	layer = drag.layer;
	dragGroups = (layer && layer.dragGroups) || [];
	layers = data.layers;

	if (eventType === 'mousemove' || eventType === 'touchmove') {
		// Detect when user is currently dragging layer

		if (!drag.dragging) {
			// Detect when user starts dragging layer

			// Signify that a layer on the canvas is being dragged
			drag.dragging = true;
			layer.dragging = true;

			// Optionally bring layer to front when drag starts
			if (layer.bringToFront) {
				// Remove layer from its original position
				layers.splice(layer.index, 1);
				// Bring layer to front
				// push() returns the new array length
				layer.index = layers.push(layer);
			}

			// Set drag properties for this layer
			layer._startX = layer.x;
			layer._startY = layer.y;
			layer._endX = layer._eventX;
			layer._endY = layer._eventY;

			// Trigger dragstart event
			_triggerLayerEvent($canvas, data, layer, 'dragstart');

		}

		if (drag.dragging) {

			// Calculate position after drag
			newX = layer._eventX - (layer._endX - layer._startX);
			newY = layer._eventY - (layer._endY - layer._startY);
			if (layer.updateDragX) {
				newX = layer.updateDragX.call($canvas[0], layer, newX);
			}
			if (layer.updateDragY) {
				newY = layer.updateDragY.call($canvas[0], layer, newY);
			}
			layer.dx = newX - layer.x;
			layer.dy = newY - layer.y;
			if (layer.restrictDragToAxis !== 'y') {
				layer.x = newX;
			}
			if (layer.restrictDragToAxis !== 'x') {
				layer.y = newY;
			}

			// Trigger drag event
			_triggerLayerEvent($canvas, data, layer, 'drag');

			// Move groups with layer on drag
			for (g = 0; g < dragGroups.length; g += 1) {

				groupName = dragGroups[g];
				group = data.layer.groups[groupName];
				if (layer.groups && group) {

					for (l = 0; l < group.length; l += 1) {
						if (group[l] !== layer) {
							if (layer.restrictDragToAxis !== 'y' && group[l].restrictDragToAxis !== 'y') {
								group[l].x += layer.dx;
							}
							if (layer.restrictDragToAxis !== 'x' && group[l].restrictDragToAxis !== 'x') {
								group[l].y += layer.dy;
							}
						}
					}

				}

			}

		}

	} else if (eventType === 'mouseup' || eventType === 'touchend') {
		// Detect when user stops dragging layer

		if (drag.dragging) {
			layer.dragging = false;
			drag.dragging = false;
			data.redrawOnMousemove = data.originalRedrawOnMousemove;
			// Trigger dragstop event
			_triggerLayerEvent($canvas, data, layer, 'dragstop');
		}

		// Cancel dragging
		data.drag = {};

	}
}


// List of CSS3 cursors that need to be prefixed
css.cursors = ['grab', 'grabbing', 'zoom-in', 'zoom-out'];

// Function to detect vendor prefix
// Modified version of David Walsh's implementation
// https://davidwalsh.name/vendor-prefix
css.prefix = (function () {
	var styles = getComputedStyle(document.documentElement, ''),
		pre = (arraySlice
			.call(styles)
			.join('')
			.match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o'])
		)[1];
	return '-' + pre + '-';
})();

// Set cursor on canvas
function _setCursor($canvas, layer, eventType) {
	var cursor;
	if (layer.cursors) {
		// Retrieve cursor from cursors object if it exists
		cursor = layer.cursors[eventType];
	}
	// Prefix any CSS3 cursor
	if ($.inArray(cursor, css.cursors) !== -1) {
		cursor = css.prefix + cursor;
	}
	// If cursor is defined
	if (cursor) {
		// Set canvas cursor
		$canvas.css({
			cursor: cursor
		});
	}
}

// Reset cursor on canvas
function _resetCursor($canvas, data) {
	$canvas.css({
		cursor: data.cursor
	});
}

// Run the given event callback with the given arguments
function _runEventCallback($canvas, layer, eventType, callbacks, arg) {
	// Prevent callback from firing recursively
	if (callbacks[eventType] && layer._running && !layer._running[eventType]) {
		// Signify the start of callback execution for this event
		layer._running[eventType] = true;
		// Run event callback with the given arguments
		callbacks[eventType].call($canvas[0], layer, arg);
		// Signify the end of callback execution for this event
		layer._running[eventType] = false;
	}
}

// Determine if the given layer can "legally" fire the given event
function _layerCanFireEvent(layer, eventType) {
	// If events are disable and if
	// layer is tangible or event is not tangible
	return (!layer.disableEvents &&
		(!layer.intangible || $.inArray(eventType, tangibleEvents) === -1));
}

// Trigger the given event on the given layer
function _triggerLayerEvent($canvas, data, layer, eventType, arg) {
	// If layer can legally fire this event type
	if (_layerCanFireEvent(layer, eventType)) {

		// Do not set a custom cursor on layer mouseout
		if (eventType !== 'mouseout') {
			// Update cursor if one is defined for this event
			_setCursor($canvas, layer, eventType);
		}

		// Trigger the user-defined event callback
		_runEventCallback($canvas, layer, eventType, layer, arg);
		// Trigger the canvas-bound event hook
		_runEventCallback($canvas, layer, eventType, data.eventHooks, arg);
		// Trigger the global event hook
		_runEventCallback($canvas, layer, eventType, jCanvas.eventHooks, arg);

	}
}

// Manually trigger a layer event
$.fn.triggerLayerEvent = function (layer, eventType) {
	var $canvases = this, $canvas, e,
		data;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);
		layer = $canvas.getLayer(layer);
		if (layer) {
			_triggerLayerEvent($canvas, data, layer, eventType);
		}
	}
	return $canvases;
};

// Draw layer with the given ID
$.fn.drawLayer = function drawLayer(layerId) {
	var $canvases = this, e, ctx,
		$canvas, layer;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		ctx = _getContext($canvases[e]);
		if (ctx) {
			layer = $canvas.getLayer(layerId);
			_drawLayer($canvas, ctx, layer);
		}
	}
	return $canvases;
};

// Draw all layers (or, if given, only layers starting at an index)
$.fn.drawLayers = function drawLayers(args) {
	var $canvases = this, $canvas, e, ctx,
		// Internal parameters for redrawing the canvas
		params = args || {},
		// Other variables
		layers, layer, lastLayer, l, index, lastIndex,
		data, eventCache, eventType, isImageLayer;

	// The layer index from which to start redrawing the canvas
	index = params.index;
	if (!index) {
		index = 0;
	}

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			// Clear canvas first unless otherwise directed
			if (params.clear !== false) {
				$canvas.clearCanvas();
			}

			// If a completion callback was provided, save it to the canvas data
			// store so that the function can be passed to drawLayers() again
			// after any image layers have loaded
			if (params.complete) {
				data.drawLayersComplete = params.complete;
			}

			// Cache the layers array
			layers = data.layers;

			// Draw layers from first to last (bottom to top)
			for (l = index; l < layers.length; l += 1) {
				layer = layers[l];

				// Ensure layer index is up-to-date
				layer.index = l;

				// Prevent any one event from firing excessively
				if (params.resetFire) {
					layer._fired = false;
				}
				// Draw layer
				_drawLayer($canvas, ctx, layer, l + 1);
				// Store list of previous masks for each layer
				layer._masks = data.transforms.masks.slice(0);

				// Allow image layers to load before drawing successive layers
				if (layer._method === $.fn.drawImage && layer.visible) {
					isImageLayer = true;
					break;
				}

			}

			// If layer is an image layer
			if (isImageLayer) {
				// Stop and wait for drawImage() to resume drawLayers()
				continue;
			}

			// Store the latest
			lastIndex = l;

			// Run completion callback (if provided) once all layers have drawn
			if (params.complete) {
				params.complete.call($canvases[e]);
				delete data.drawLayersComplete;
			}

			// Get first layer that intersects with event coordinates
			layer = _getIntersectingLayer(data);

			eventCache = data.event;
			eventType = eventCache.type;

			// If jCanvas has detected a dragstart
			if (data.drag.layer) {
				// Handle dragging of layer
				_handleLayerDrag($canvas, data, eventType);
			}

			// Manage mouseout event
			lastLayer = data.lastIntersected;
			if (lastLayer !== null && layer !== lastLayer && lastLayer._hovered && !lastLayer._fired && !data.drag.dragging) {

				data.lastIntersected = null;
				lastLayer._fired = true;
				lastLayer._hovered = false;
				_triggerLayerEvent($canvas, data, lastLayer, 'mouseout');
				_resetCursor($canvas, data);

			}

			if (layer) {

				// Use mouse event callbacks if no touch event callbacks are given
				if (!layer[eventType]) {
					eventType = _getMouseEventName(eventType);
				}

				// Check events for intersecting layer
				if (layer._event && layer.intersects) {

					data.lastIntersected = layer;

					// Detect mouseover events
					if ((layer.mouseover || layer.mouseout || layer.cursors) && !data.drag.dragging) {

						if (!layer._hovered && !layer._fired) {

							// Prevent events from firing excessively
							layer._fired = true;
							layer._hovered = true;
							_triggerLayerEvent($canvas, data, layer, 'mouseover');

						}

					}

					// Detect any other mouse event
					if (!layer._fired) {

						// Prevent event from firing twice unintentionally
						layer._fired = true;
						eventCache.type = null;

						_triggerLayerEvent($canvas, data, layer, eventType);

					}

					// Use the mousedown event to start drag
					if (layer.draggable && !layer.disableEvents && (eventType === 'mousedown' || eventType === 'touchstart')) {

						// Keep track of drag state
						data.drag.layer = layer;
						data.originalRedrawOnMousemove = data.redrawOnMousemove;
						data.redrawOnMousemove = true;

					}

				}

			}

			// If cursor is not intersecting with any layer
			if (layer === null && !data.drag.dragging) {
				// Reset cursor to previous state
				_resetCursor($canvas, data);
			}

			// If the last layer has been drawn
			if (lastIndex === layers.length) {

				// Reset list of intersecting layers
				data.intersecting.length = 0;
				// Reset transformation stack
				data.transforms = _cloneTransforms(baseTransforms);
				data.savedTransforms.length = 0;

			}

		}
	}
	return $canvases;
};

// Add a jCanvas layer (internal)
function _addLayer(canvas, params, args, method) {
	var $canvas, data,
		layers, layer = (params._layer ? args : params);

	// Store arguments object for later use
	params._args = args;

	// Convert all draggable drawings into jCanvas layers
	if (params.draggable || params.dragGroups) {
		params.layer = true;
		params.draggable = true;
	}

	// Determine the layer's type using the available information
	if (!params._method) {
		if (method) {
			params._method = method;
		} else if (params.method) {
			params._method = $.fn[params.method];
		} else if (params.type) {
			params._method = $.fn[maps.drawings[params.type]];
		}
	}

	// If layer hasn't been added yet
	if (params.layer && !params._layer) {
		// Add layer to canvas

		$canvas = $(canvas);

		data = _getCanvasData(canvas);
		layers = data.layers;

		// Do not add duplicate layers of same name
		if (layer.name === null || (isString(layer.name) && data.layer.names[layer.name] === undefined)) {

			// Convert number properties to numbers
			_coerceNumericProps(params);

			// Ensure layers are unique across canvases by cloning them
			layer = new jCanvasObject(params);
			layer.canvas = canvas;
			// Indicate that this is a layer for future checks
			layer.layer = true;
			layer._layer = true;
			layer._running = {};
			// If layer stores user-defined data
			if (layer.data !== null) {
				// Clone object
				layer.data = extendObject({}, layer.data);
			} else {
				// Otherwise, create data object
				layer.data = {};
			}
			// If layer stores a list of associated groups
			if (layer.groups !== null) {
				// Clone list
				layer.groups = layer.groups.slice(0);
			} else {
				// Otherwise, create empty list
				layer.groups = [];
			}

			// Update layer group maps
			_updateLayerName($canvas, data, layer);
			_updateLayerGroups($canvas, data, layer);

			// Check for any associated jCanvas events and enable them
			_addLayerEvents($canvas, data, layer);

			// Optionally enable drag-and-drop support and cursor support
			_enableDrag($canvas, data, layer);

			// Copy _event property to parameters object
			params._event = layer._event;

			// Calculate width/height for text layers
			if (layer._method === $.fn.drawText) {
				$canvas.measureText(layer);
			}

			// Add layer to end of array if no index is specified
			if (layer.index === null) {
				layer.index = layers.length;
			}

			// Add layer to layers array at specified index
			layers.splice(layer.index, 0, layer);

			// Store layer on parameters object
			params._args = layer;

			// Trigger an 'add' event
			_triggerLayerEvent($canvas, data, layer, 'add');

		}

	} else if (!params.layer) {
		_coerceNumericProps(params);
	}

	return layer;
}

// Add a jCanvas layer
$.fn.addLayer = function addLayer(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			params.layer = true;
			_addLayer($canvases[e], params, args);

		}
	}
	return $canvases;
};

/* Animation API */

// Define properties used in both CSS and jCanvas
css.props = [
	'width',
	'height',
	'opacity',
	'lineHeight'
];
css.propsObj = {};

// Hide/show jCanvas/CSS properties so they can be animated using jQuery
function _showProps(obj) {
	var cssProp, p;
	for (p = 0; p < css.props.length; p += 1) {
		cssProp = css.props[p];
		obj[cssProp] = obj['_' + cssProp];
	}
}
function _hideProps(obj, reset) {
	var cssProp, p;
	for (p = 0; p < css.props.length; p += 1) {
		cssProp = css.props[p];
		// Hide property using same name with leading underscore
		if (obj[cssProp] !== undefined) {
			obj['_' + cssProp] = obj[cssProp];
			css.propsObj[cssProp] = true;
			if (reset) {
				delete obj[cssProp];
			}
		}
	}
}

// Evaluate property values that are functions
function _parseEndValues(canvas, layer, endValues) {
	var propName, propValue,
		subPropName, subPropValue;
	// Loop through all properties in map of end values
	for (propName in endValues) {
		if (Object.prototype.hasOwnProperty.call(endValues, propName)) {
			propValue = endValues[propName];
			// If end value is function
			if (isFunction(propValue)) {
				// Call function and use its value as the end value
				endValues[propName] = propValue.call(canvas, layer, propName);
			}
			// If end value is an object
			if (typeOf(propValue) === 'object' && isPlainObject(propValue)) {
				// Prepare to animate properties in object
				for (subPropName in propValue) {
					if (Object.prototype.hasOwnProperty.call(propValue, subPropName)) {
						subPropValue = propValue[subPropName];
						// Store property's start value at top-level of layer
						if (layer[propName] !== undefined) {
							layer[propName + '.' + subPropName] = layer[propName][subPropName];
							// Store property's end value at top-level of end values map
							endValues[propName + '.' + subPropName] = subPropValue;
						}
					}
				}
				// Delete sub-property of object as it's no longer needed
				delete endValues[propName];
			}
		}
	}
	return endValues;
}

// Remove sub-property aliases from layer object
function _removeSubPropAliases(layer) {
	var propName;
	for (propName in layer) {
		if (Object.prototype.hasOwnProperty.call(layer, propName)) {
			if (propName.indexOf('.') !== -1) {
				delete layer[propName];
			}
		}
	}
}

// Convert a color value to an array of RGB values
function _colorToRgbArray(color) {
	var originalColor, elem,
		rgb = [],
		multiple = 1;

	// Deal with complete transparency
	if (color === 'transparent') {
		color = 'rgba(0, 0, 0, 0)';
	} else if (color.match(/^([a-z]+|#[0-9a-f]+)$/gi)) {
		// Deal with hexadecimal colors and color names
		elem = document.head;
		originalColor = elem.style.color;
		elem.style.color = color;
		color = $.css(elem, 'color');
		elem.style.color = originalColor;
	}
	// Parse RGB string
	if (color.match(/^rgb/gi)) {
		rgb = color.match(/(\d+(\.\d+)?)/gi);
		// Deal with RGB percentages
		if (color.match(/%/gi)) {
			multiple = 2.55;
		}
		rgb[0] *= multiple;
		rgb[1] *= multiple;
		rgb[2] *= multiple;
		// Ad alpha channel if given
		if (rgb[3] !== undefined) {
			rgb[3] = parseFloat(rgb[3]);
		} else {
			rgb[3] = 1;
		}
	}
	return rgb;
}

// Animate a hex or RGB color
function _animateColor(fx) {
	var n = 3,
		i;
	// Only parse start and end colors once
	if (typeOf(fx.start) !== 'array') {
		fx.start = _colorToRgbArray(fx.start);
		fx.end = _colorToRgbArray(fx.end);
	}
	fx.now = [];

	// If colors are RGBA, animate transparency
	if (fx.start[3] !== 1 || fx.end[3] !== 1) {
		n = 4;
	}

	// Calculate current frame for red, green, blue, and alpha
	for (i = 0; i < n; i += 1) {
		fx.now[i] = fx.start[i] + ((fx.end[i] - fx.start[i]) * fx.pos);
		// Only the red, green, and blue values must be integers
		if (i < 3) {
			fx.now[i] = round(fx.now[i]);
		}
	}
	if (fx.start[3] !== 1 || fx.end[3] !== 1) {
		// Only use RGBA if RGBA colors are given
		fx.now = 'rgba(' + fx.now.join(',') + ')';
	} else {
		// Otherwise, animate as solid colors
		fx.now.slice(0, 3);
		fx.now = 'rgb(' + fx.now.join(',') + ')';
	}
	// Animate colors for both canvas layers and DOM elements
	if (fx.elem.nodeName) {
		fx.elem.style[fx.prop] = fx.now;
	} else {
		fx.elem[fx.prop] = fx.now;
	}
}

// Animate jCanvas layer
$.fn.animateLayer = function animateLayer() {
	var $canvases = this, $canvas, e, ctx,
		args = arraySlice.call(arguments, 0),
		data, layer, props;

	// Deal with all cases of argument placement
	/*
		0. layer name/index
		1. properties
		2. duration/options
		3. easing
		4. complete function
		5. step function
	*/

	if (typeOf(args[2]) === 'object') {

		// Accept an options object for animation
		args.splice(2, 0, args[2].duration || null);
		args.splice(3, 0, args[3].easing || null);
		args.splice(4, 0, args[4].complete || null);
		args.splice(5, 0, args[5].step || null);

	} else {

		if (args[2] === undefined) {
			// If object is the last argument
			args.splice(2, 0, null);
			args.splice(3, 0, null);
			args.splice(4, 0, null);
		} else if (isFunction(args[2])) {
			// If callback comes after object
			args.splice(2, 0, null);
			args.splice(3, 0, null);
		}
		if (args[3] === undefined) {
			// If duration is the last argument
			args[3] = null;
			args.splice(4, 0, null);
		} else if (isFunction(args[3])) {
			// If callback comes after duration
			args.splice(3, 0, null);
		}

	}

	// Run callback function when animation completes
	function complete($canvas, data, layer) {

		return function () {

			_showProps(layer);
			_removeSubPropAliases(layer);

			// Prevent multiple redraw loops
			if (!data.animating || data.animated === layer) {
				// Redraw layers on last frame
				$canvas.drawLayers();
			}

			// Signify the end of an animation loop
			layer._animating = false;
			data.animating = false;
			data.animated = null;

			// If callback is defined
			if (args[4]) {
				// Run callback at the end of the animation
				args[4].call($canvas[0], layer);
			}

			_triggerLayerEvent($canvas, data, layer, 'animateend');

		};

	}

	// Redraw layers on every frame of the animation
	function step($canvas, data, layer) {

		return function (now, fx) {
			var parts, propName, subPropName,
				hidden = false;

			// If animated property has been hidden
			if (fx.prop[0] === '_') {
				hidden = true;
				// Unhide property temporarily
				fx.prop = fx.prop.replace('_', '');
				layer[fx.prop] = layer['_' + fx.prop];
			}

			// If animating property of sub-object
			if (fx.prop.indexOf('.') !== -1) {
				parts = fx.prop.split('.');
				propName = parts[0];
				subPropName = parts[1];
				if (layer[propName]) {
					layer[propName][subPropName] = fx.now;
				}
			}

			// Throttle animation to improve efficiency
			if (layer._pos !== fx.pos) {

				layer._pos = fx.pos;

				// Signify the start of an animation loop
				if (!layer._animating && !data.animating) {
					layer._animating = true;
					data.animating = true;
					data.animated = layer;
				}

				// Prevent multiple redraw loops
				if (!data.animating || data.animated === layer) {
					// Redraw layers for every frame
					$canvas.drawLayers();
				}

			}

			// If callback is defined
			if (args[5]) {
				// Run callback for each step of animation
				args[5].call($canvas[0], now, fx, layer);
			}

			_triggerLayerEvent($canvas, data, layer, 'animate', fx);

			// If property should be hidden during animation
			if (hidden) {
				// Hide property again
				fx.prop = '_' + fx.prop;
			}

		};

	}

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			// If a layer object was passed, use it the layer to be animated
			layer = $canvas.getLayer(args[0]);

			// Ignore layers that are functions
			if (layer && layer._method !== $.fn.draw) {

				// Do not modify original object
				props = extendObject({}, args[1]);

				props = _parseEndValues($canvases[e], layer, props);

				// Bypass jQuery CSS Hooks for CSS properties (width, opacity, etc.)
				_hideProps(props, true);
				_hideProps(layer);

				// Fix for jQuery's vendor prefixing support, which affects how width/height/opacity are animated
				layer.style = css.propsObj;

				// Animate layer
				$(layer).animate(props, {
					duration: args[2],
					easing: ($.easing[args[3]] ? args[3] : null),
					// When animation completes
					complete: complete($canvas, data, layer),
					// Redraw canvas for every animation frame
					step: step($canvas, data, layer)
				});
				_triggerLayerEvent($canvas, data, layer, 'animatestart');
			}

		}
	}
	return $canvases;
};

// Animate all layers in a layer group
$.fn.animateLayerGroup = function animateLayerGroup(groupId) {
	var $canvases = this, $canvas, e,
		args = arraySlice.call(arguments, 0),
		group, l;
	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		group = $canvas.getLayerGroup(groupId);
		if (group) {

			// Animate all layers in the group
			for (l = 0; l < group.length; l += 1) {

				// Replace first argument with layer
				args[0] = group[l];
				$canvas.animateLayer.apply($canvas, args);

			}

		}
	}
	return $canvases;
};

// Delay layer animation by a given number of milliseconds
$.fn.delayLayer = function delayLayer(layerId, duration) {
	var $canvases = this, $canvas, e,
		data, layer;
	duration = duration || 0;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);
		layer = $canvas.getLayer(layerId);
		// If layer exists
		if (layer) {
			// Delay animation
			$(layer).delay(duration);
			_triggerLayerEvent($canvas, data, layer, 'delay');
		}
	}
	return $canvases;
};

// Delay animation all layers in a layer group
$.fn.delayLayerGroup = function delayLayerGroup(groupId, duration) {
	var $canvases = this, $canvas, e,
		group, layer, l;
	duration = duration || 0;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);

		group = $canvas.getLayerGroup(groupId);
		// Delay all layers in the group
		if (group) {

			for (l = 0; l < group.length; l += 1) {
				// Delay each layer in the group
				layer = group[l];
				$canvas.delayLayer(layer, duration);
			}

		}
	}
	return $canvases;
};

// Stop layer animation
$.fn.stopLayer = function stopLayer(layerId, clearQueue) {
	var $canvases = this, $canvas, e,
		data, layer;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);
		data = _getCanvasData($canvases[e]);
		layer = $canvas.getLayer(layerId);
		// If layer exists
		if (layer) {
			// Stop animation
			$(layer).stop(clearQueue);
			_triggerLayerEvent($canvas, data, layer, 'stop');
		}
	}
	return $canvases;
};

// Stop animation of all layers in a layer group
$.fn.stopLayerGroup = function stopLayerGroup(groupId, clearQueue) {
	var $canvases = this, $canvas, e,
		group, layer, l;

	for (e = 0; e < $canvases.length; e += 1) {
		$canvas = $($canvases[e]);

		group = $canvas.getLayerGroup(groupId);
		// Stop all layers in the group
		if (group) {

			for (l = 0; l < group.length; l += 1) {
				// Stop each layer in the group
				layer = group[l];
				$canvas.stopLayer(layer, clearQueue);
			}

		}
	}
	return $canvases;
};

// Enable animation for color properties
function _supportColorProps(props) {
	var p;
	for (p = 0; p < props.length; p += 1) {
		$.fx.step[props[p]] = _animateColor;
	}
}

// Enable animation for color properties
_supportColorProps([
	'color',
	'backgroundColor',
	'borderColor',
	'borderTopColor',
	'borderRightColor',
	'borderBottomColor',
	'borderLeftColor',
	'fillStyle',
	'outlineColor',
	'strokeStyle',
	'shadowColor'
]);

/* Event API */

// Map standard mouse events to touch events
maps.touchEvents = {
	'mousedown': 'touchstart',
	'mouseup': 'touchend',
	'mousemove': 'touchmove'
};
// Map standard touch events to mouse events
maps.mouseEvents = {
	'touchstart': 'mousedown',
	'touchend': 'mouseup',
	'touchmove': 'mousemove'
};

// Convert mouse event name to a corresponding touch event name (if possible)
function _getTouchEventName(eventName) {
	// Detect touch event support
	if (maps.touchEvents[eventName]) {
		eventName = maps.touchEvents[eventName];
	}
	return eventName;
}
// Convert touch event name to a corresponding mouse event name
function _getMouseEventName(eventName) {
	if (maps.mouseEvents[eventName]) {
		eventName = maps.mouseEvents[eventName];
	}
	return eventName;
}

// Bind event to jCanvas layer using standard jQuery events
function _createEvent(eventName) {

	jCanvas.events[eventName] = function ($canvas, data) {
		var helperEventName, touchEventName, eventCache;

		// Retrieve canvas's event cache
		eventCache = data.event;

		// Both mouseover/mouseout events will be managed by a single mousemove event
		helperEventName = (eventName === 'mouseover' || eventName === 'mouseout') ? 'mousemove' : eventName;
		touchEventName = _getTouchEventName(helperEventName);

		function eventCallback(event) {
			// Cache current mouse position and redraw layers
			eventCache.x = event.offsetX;
			eventCache.y = event.offsetY;
			eventCache.type = helperEventName;
			eventCache.event = event;
			// Redraw layers on every trigger of the event; don't redraw if at
			// least one layer is draggable and there are no layers with
			// explicit mouseover/mouseout/mousemove events
			if (event.type !== 'mousemove' || data.redrawOnMousemove || data.drag.dragging) {
				$canvas.drawLayers({
					resetFire: true
				});
			}
			// Prevent default event behavior
			event.preventDefault();
		}

		// Ensure the event is not bound more than once
		if (!data.events[helperEventName]) {
			// Bind one canvas event which handles all layer events of that type
			if (touchEventName !== helperEventName) {
				$canvas.bind(helperEventName + '.jCanvas ' + touchEventName + '.jCanvas', eventCallback);
			} else {
				$canvas.bind(helperEventName + '.jCanvas', eventCallback);
			}
			// Prevent this event from being bound twice
			data.events[helperEventName] = true;
		}
	};
}
function _createEvents(eventNames) {
	var n;
	for (n = 0; n < eventNames.length; n += 1) {
		_createEvent(eventNames[n]);
	}
}
// Populate jCanvas events object with some standard events
_createEvents([
	'click',
	'dblclick',
	'mousedown',
	'mouseup',
	'mousemove',
	'mouseover',
	'mouseout',
	'touchstart',
	'touchmove',
	'touchend',
	'pointerdown',
	'pointermove',
	'pointerup',
	'contextmenu'
]);

// Check if event fires when a drawing is drawn
function _detectEvents(canvas, ctx, params) {
	var layer, data, eventCache, intersects,
		transforms, x, y, angle;

	// Use the layer object stored by the given parameters object
	layer = params._args;
	// Canvas must have event bindings
	if (layer) {

		data = _getCanvasData(canvas);
		eventCache = data.event;
		if (eventCache.x !== null && eventCache.y !== null) {
			// Respect user-defined pixel ratio
			x = eventCache.x * data.pixelRatio;
			y = eventCache.y * data.pixelRatio;
			// Determine if the given coordinates are in the current path
			intersects = ctx.isPointInPath(x, y) || (ctx.isPointInStroke && ctx.isPointInStroke(x, y));
		}
		transforms = data.transforms;

		// Allow callback functions to retrieve the mouse coordinates
		layer.eventX = eventCache.x;
		layer.eventY = eventCache.y;
		layer.event = eventCache.event;

		// Adjust coordinates to match current canvas transformation

		// Keep track of some transformation values
		angle = data.transforms.rotate;
		x = layer.eventX;
		y = layer.eventY;

		if (angle !== 0) {
			// Rotate coordinates if coordinate space has been rotated
			layer._eventX = (x * cos(-angle)) - (y * sin(-angle));
			layer._eventY = (y * cos(-angle)) + (x * sin(-angle));
		} else {
			// Otherwise, no calculations need to be made
			layer._eventX = x;
			layer._eventY = y;
		}

		// Scale coordinates
		layer._eventX /= transforms.scaleX;
		layer._eventY /= transforms.scaleY;

		// If layer intersects with cursor
		if (intersects) {
			// Add it to a list of layers that intersect with cursor
			data.intersecting.push(layer);
		}
		layer.intersects = Boolean(intersects);
	}
}

// Normalize offsetX and offsetY for all browsers
$.event.fix = function (event) {
	var offset, originalEvent, touches;

	event = jQueryEventFix.call($.event, event);
	originalEvent = event.originalEvent;

	// originalEvent does not exist for manually-triggered events
	if (originalEvent) {

		touches = originalEvent.changedTouches;

		// If offsetX and offsetY are not supported, define them
		if (event.pageX !== undefined && event.offsetX === undefined) {
			try {
				offset = $(event.currentTarget).offset();
				if (offset) {
					event.offsetX = event.pageX - offset.left;
					event.offsetY = event.pageY - offset.top;
				}
			} catch (error) {
				// Fail silently
			}
		} else if (touches) {
			try {
				// Enable offsetX and offsetY for mobile devices
				offset = $(event.currentTarget).offset();
				if (offset) {
					event.offsetX = touches[0].pageX - offset.left;
					event.offsetY = touches[0].pageY - offset.top;
				}
			} catch (error) {
				// Fail silently
			}
		}

	}
	return event;
};

/* Drawing API */

// Map drawing names with their respective method names
maps.drawings = {
	'arc': 'drawArc',
	'bezier': 'drawBezier',
	'ellipse': 'drawEllipse',
	'function': 'draw',
	'image': 'drawImage',
	'line': 'drawLine',
	'path': 'drawPath',
	'polygon': 'drawPolygon',
	'slice': 'drawSlice',
	'quadratic': 'drawQuadratic',
	'rectangle': 'drawRect',
	'text': 'drawText',
	'vector': 'drawVector',
	'save': 'saveCanvas',
	'restore': 'restoreCanvas',
	'rotate': 'rotateCanvas',
	'scale': 'scaleCanvas',
	'translate': 'translateCanvas'
};

// Draws on canvas using a function
$.fn.draw = function draw(args) {
	var $canvases = this, e, ctx,
		params = new jCanvasObject(args);

	// Draw using any other method
	if (maps.drawings[params.type] && params.type !== 'function') {

		$canvases[maps.drawings[params.type]](args);

	} else {

		for (e = 0; e < $canvases.length; e += 1) {
			ctx = _getContext($canvases[e]);
			if (ctx) {

				params = new jCanvasObject(args);
				_addLayer($canvases[e], params, args, draw);
				if (params.visible) {

					if (params.fn) {
						// Call the given user-defined function
						params.fn.call($canvases[e], ctx, params);
					}

				}

			}
		}

	}
	return $canvases;
};

// Clears canvas
$.fn.clearCanvas = function clearCanvas(args) {
	var $canvases = this, e, ctx,
		params = new jCanvasObject(args);

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			if (params.width === null || params.height === null) {
				// Clear entire canvas if width/height is not given

				// Reset current transformation temporarily to ensure that the entire canvas is cleared
				ctx.save();
				ctx.setTransform(1, 0, 0, 1, 0, 0);
				ctx.clearRect(0, 0, $canvases[e].width, $canvases[e].height);
				ctx.restore();

			} else {
				// Otherwise, clear the defined section of the canvas

				// Transform clear rectangle
				_addLayer($canvases[e], params, args, clearCanvas);
				_transformShape($canvases[e], ctx, params, params.width, params.height);
				ctx.clearRect(params.x - (params.width / 2), params.y - (params.height / 2), params.width, params.height);
				// Restore previous transformation
				_restoreTransform(ctx, params);

			}

		}
	}
	return $canvases;
};

/* Transformation API */

// Restores canvas
$.fn.saveCanvas = function saveCanvas(args) {
	var $canvases = this, e, ctx,
		params, data, i;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, saveCanvas);

			// Restore a number of times using the given count
			for (i = 0; i < params.count; i += 1) {
				_saveCanvas(ctx, data);
			}

		}
	}
	return $canvases;
};

// Restores canvas
$.fn.restoreCanvas = function restoreCanvas(args) {
	var $canvases = this, e, ctx,
		params, data, i;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, restoreCanvas);

			// Restore a number of times using the given count
			for (i = 0; i < params.count; i += 1) {
				_restoreCanvas(ctx, data);
			}

		}
	}
	return $canvases;
};

// Rotates canvas (internal)
function _rotateCanvas(ctx, params, transforms) {

	// Get conversion factor for radians
	params._toRad = (params.inDegrees ? (PI / 180) : 1);

	// Rotate canvas using shape as center of rotation
	ctx.translate(params.x, params.y);
	ctx.rotate(params.rotate * params._toRad);
	ctx.translate(-params.x, -params.y);

	// If transformation data was given
	if (transforms) {
		// Update transformation data
		transforms.rotate += (params.rotate * params._toRad);
	}
}

// Scales canvas (internal)
function _scaleCanvas(ctx, params, transforms) {

	// Scale both the x- and y- axis using the 'scale' property
	if (params.scale !== 1) {
		params.scaleX = params.scaleY = params.scale;
	}

	// Scale canvas using shape as center of rotation
	ctx.translate(params.x, params.y);
	ctx.scale(params.scaleX, params.scaleY);
	ctx.translate(-params.x, -params.y);

	// If transformation data was given
	if (transforms) {
		// Update transformation data
		transforms.scaleX *= params.scaleX;
		transforms.scaleY *= params.scaleY;
	}
}

// Translates canvas (internal)
function _translateCanvas(ctx, params, transforms) {

	// Translate both the x- and y-axis using the 'translate' property
	if (params.translate) {
		params.translateX = params.translateY = params.translate;
	}

	// Translate canvas
	ctx.translate(params.translateX, params.translateY);

	// If transformation data was given
	if (transforms) {
		// Update transformation data
		transforms.translateX += params.translateX;
		transforms.translateY += params.translateY;
	}
}

// Rotates canvas
$.fn.rotateCanvas = function rotateCanvas(args) {
	var $canvases = this, e, ctx,
		params, data;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, rotateCanvas);

			// Autosave transformation state by default
			if (params.autosave) {
				// Automatically save transformation state by default
				_saveCanvas(ctx, data);
			}
			_rotateCanvas(ctx, params, data.transforms);
		}

	}
	return $canvases;
};

// Scales canvas
$.fn.scaleCanvas = function scaleCanvas(args) {
	var $canvases = this, e, ctx,
		params, data;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, scaleCanvas);

			// Autosave transformation state by default
			if (params.autosave) {
				// Automatically save transformation state by default
				_saveCanvas(ctx, data);
			}
			_scaleCanvas(ctx, params, data.transforms);

		}
	}
	return $canvases;
};

// Translates canvas
$.fn.translateCanvas = function translateCanvas(args) {
	var $canvases = this, e, ctx,
		params, data;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, translateCanvas);

			// Autosave transformation state by default
			if (params.autosave) {
				// Automatically save transformation state by default
				_saveCanvas(ctx, data);
			}
			_translateCanvas(ctx, params, data.transforms);

		}
	}
	return $canvases;
};

/* Shape API */

// Draws rectangle
$.fn.drawRect = function drawRect(args) {
	var $canvases = this, e, ctx,
		params,
		x1, y1,
		x2, y2,
		r, temp;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawRect);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params, params.width, params.height);
				_setGlobalProps($canvases[e], ctx, params);

				ctx.beginPath();
				if (params.width && params.height) {
					x1 = params.x - (params.width / 2);
					y1 = params.y - (params.height / 2);
					r = abs(params.cornerRadius);
					// If corner radius is defined and is not zero
					if (r) {
						// Draw rectangle with rounded corners if cornerRadius is defined

						x2 = params.x + (params.width / 2);
						y2 = params.y + (params.height / 2);

						// Handle negative width
						if (params.width < 0) {
							temp = x1;
							x1 = x2;
							x2 = temp;
						}
						// Handle negative height
						if (params.height < 0) {
							temp = y1;
							y1 = y2;
							y2 = temp;
						}

						// Prevent over-rounded corners
						if ((x2 - x1) - (2 * r) < 0) {
							r = (x2 - x1) / 2;
						}
						if ((y2 - y1) - (2 * r) < 0) {
							r = (y2 - y1) / 2;
						}

						// Draw rectangle
						ctx.moveTo(x1 + r, y1);
						ctx.lineTo(x2 - r, y1);
						ctx.arc(x2 - r, y1 + r, r, 3 * PI / 2, PI * 2, false);
						ctx.lineTo(x2, y2 - r);
						ctx.arc(x2 - r, y2 - r, r, 0, PI / 2, false);
						ctx.lineTo(x1 + r, y2);
						ctx.arc(x1 + r, y2 - r, r, PI / 2, PI, false);
						ctx.lineTo(x1, y1 + r);
						ctx.arc(x1 + r, y1 + r, r, PI, 3 * PI / 2, false);
						// Always close path
						params.closed = true;

					} else {

						// Otherwise, draw rectangle with square corners
						ctx.rect(x1, y1, params.width, params.height);

					}
				}
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Close rectangle path
				_closePath($canvases[e], ctx, params);
			}
		}
	}
	return $canvases;
};

// Retrieves a coterminal angle between 0 and 2pi for the given angle
function _getCoterminal(angle) {
	while (angle < 0) {
		angle += (2 * PI);
	}
	return angle;
}

// Retrieves the x-coordinate for the given angle in a circle
function _getArcX(params, angle) {
	return params.x + (params.radius * cos(angle));
}
// Retrieves the y-coordinate for the given angle in a circle
function _getArcY(params, angle) {
	return params.y + (params.radius * sin(angle));
}

// Draws arc (internal)
function _drawArc(canvas, ctx, params, path) {
	var x1, y1, x2, y2,
		x3, y3, x4, y4,
		offsetX, offsetY,
		diff;

	// Determine offset from dragging
	if (params === path) {
		offsetX = 0;
		offsetY = 0;
	} else {
		offsetX = params.x;
		offsetY = params.y;
	}

	// Convert default end angle to radians
	if (!path.inDegrees && path.end === 360) {
		path.end = PI * 2;
	}

	// Convert angles to radians
	path.start *= params._toRad;
	path.end *= params._toRad;
	// Consider 0deg due north of arc
	path.start -= (PI / 2);
	path.end -= (PI / 2);

	// Ensure arrows are pointed correctly for CCW arcs
	diff = PI / 180;
	if (path.ccw) {
		diff *= -1;
	}

	// Calculate coordinates for start arrow
	x1 = _getArcX(path, path.start + diff);
	y1 = _getArcY(path, path.start + diff);
	x2 = _getArcX(path, path.start);
	y2 = _getArcY(path, path.start);

	_addStartArrow(
		canvas, ctx,
		params, path,
		x1, y1,
		x2, y2
	);

	// Draw arc
	ctx.arc(path.x + offsetX, path.y + offsetY, path.radius, path.start, path.end, path.ccw);

	// Calculate coordinates for end arrow
	x3 = _getArcX(path, path.end + diff);
	y3 = _getArcY(path, path.end + diff);
	x4 = _getArcX(path, path.end);
	y4 = _getArcY(path, path.end);

	_addEndArrow(
		canvas, ctx,
		params, path,
		x4, y4,
		x3, y3
	);
}

// Draws arc or circle
$.fn.drawArc = function drawArc(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawArc);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params, params.radius * 2);
				_setGlobalProps($canvases[e], ctx, params);

				ctx.beginPath();
				_drawArc($canvases[e], ctx, params, params);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}

		}
	}
	return $canvases;
};

// Draws ellipse
$.fn.drawEllipse = function drawEllipse(args) {
	var $canvases = this, e, ctx,
		params,
		controlW,
		controlH;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawEllipse);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params, params.width, params.height);
				_setGlobalProps($canvases[e], ctx, params);

				// Calculate control width and height
				controlW = params.width * (4 / 3);
				controlH = params.height;

				// Create ellipse using curves
				ctx.beginPath();
				ctx.moveTo(params.x, params.y - (controlH / 2));
				// Left side
				ctx.bezierCurveTo(params.x - (controlW / 2), params.y - (controlH / 2), params.x - (controlW / 2), params.y + (controlH / 2), params.x, params.y + (controlH / 2));
				// Right side
				ctx.bezierCurveTo(params.x + (controlW / 2), params.y + (controlH / 2), params.x + (controlW / 2), params.y - (controlH / 2), params.x, params.y - (controlH / 2));
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Always close path
				params.closed = true;
				_closePath($canvases[e], ctx, params);

			}
		}
	}
	return $canvases;
};

// Draws a regular (equal-angled) polygon
$.fn.drawPolygon = function drawPolygon(args) {
	var $canvases = this, e, ctx,
		params,
		theta, dtheta, hdtheta,
		apothem,
		x, y, i;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawPolygon);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params, params.radius * 2);
				_setGlobalProps($canvases[e], ctx, params);

				// Polygon's central angle
				dtheta = (2 * PI) / params.sides;
				// Half of dtheta
				hdtheta = dtheta / 2;
				// Polygon's starting angle
				theta = hdtheta + (PI / 2);
				// Distance from polygon's center to the middle of its side
				apothem = params.radius * cos(hdtheta);

				// Calculate path and draw
				ctx.beginPath();
				for (i = 0; i < params.sides; i += 1) {

					// Draw side of polygon
					x = params.x + (params.radius * cos(theta));
					y = params.y + (params.radius * sin(theta));

					// Plot point on polygon
					ctx.lineTo(x, y);

					// Project side if chosen
					if (params.concavity) {
						// Sides are projected from the polygon's apothem
						x = params.x + ((apothem + (-apothem * params.concavity)) * cos(theta + hdtheta));
						y = params.y + ((apothem + (-apothem * params.concavity)) * sin(theta + hdtheta));
						ctx.lineTo(x, y);
					}

					// Increment theta by delta theta
					theta += dtheta;

				}
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Always close path
				params.closed = true;
				_closePath($canvases[e], ctx, params);

			}
		}
	}
	return $canvases;
};

// Draws pie-shaped slice
$.fn.drawSlice = function drawSlice(args) {
	var $canvases = this, e, ctx,
		params,
		angle, dx, dy;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawSlice);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params, params.radius * 2);
				_setGlobalProps($canvases[e], ctx, params);

				// Perform extra calculations

				// Convert angles to radians
				params.start *= params._toRad;
				params.end *= params._toRad;
				// Consider 0deg at north of arc
				params.start -= (PI / 2);
				params.end -= (PI / 2);

				// Find positive equivalents of angles
				params.start = _getCoterminal(params.start);
				params.end = _getCoterminal(params.end);
				// Ensure start angle is less than end angle
				if (params.end < params.start) {
					params.end += (2 * PI);
				}

				// Calculate angular position of slice
				angle = ((params.start + params.end) / 2);

				// Calculate ratios for slice's angle
				dx = (params.radius * params.spread * cos(angle));
				dy = (params.radius * params.spread * sin(angle));

				// Adjust position of slice
				params.x += dx;
				params.y += dy;

				// Draw slice
				ctx.beginPath();
				ctx.arc(params.x, params.y, params.radius, params.start, params.end, params.ccw);
				ctx.lineTo(params.x, params.y);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Always close path
				params.closed = true;
				_closePath($canvases[e], ctx, params);

			}

		}
	}
	return $canvases;
};

/* Path API */

// Adds arrow to path using the given properties
function _addArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
	var leftX, leftY,
		rightX, rightY,
		offsetX, offsetY,
		angle;

	// If arrow radius is given and path is not closed
	if (path.arrowRadius && !params.closed) {

		// Calculate angle
		angle = atan2((y2 - y1), (x2 - x1));
		// Adjust angle correctly
		angle -= PI;
		// Calculate offset to place arrow at edge of path
		offsetX = (params.strokeWidth * cos(angle));
		offsetY = (params.strokeWidth * sin(angle));

		// Calculate coordinates for left half of arrow
		leftX = x2 + (path.arrowRadius * cos(angle + (path.arrowAngle / 2)));
		leftY = y2 + (path.arrowRadius * sin(angle + (path.arrowAngle / 2)));
		// Calculate coordinates for right half of arrow
		rightX = x2 + (path.arrowRadius * cos(angle - (path.arrowAngle / 2)));
		rightY = y2 + (path.arrowRadius * sin(angle - (path.arrowAngle / 2)));

		// Draw left half of arrow
		ctx.moveTo(leftX - offsetX, leftY - offsetY);
		ctx.lineTo(x2 - offsetX, y2 - offsetY);
		// Draw right half of arrow
		ctx.lineTo(rightX - offsetX, rightY - offsetY);

		// Visually connect arrow to path
		ctx.moveTo(x2 - offsetX, y2 - offsetY);
		ctx.lineTo(x2 + offsetX, y2 + offsetY);
		// Move back to end of path
		ctx.moveTo(x2, y2);

	}
}

// Optionally adds arrow to start of path
function _addStartArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
	if (!path._arrowAngleConverted) {
		path.arrowAngle *= params._toRad;
		path._arrowAngleConverted = true;
	}
	if (path.startArrow) {
		_addArrow(canvas, ctx, params, path, x1, y1, x2, y2);
	}
}

// Optionally adds arrow to end of path
function _addEndArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
	if (!path._arrowAngleConverted) {
		path.arrowAngle *= params._toRad;
		path._arrowAngleConverted = true;
	}
	if (path.endArrow) {
		_addArrow(canvas, ctx, params, path, x1, y1, x2, y2);
	}
}

// Draws line (internal)
function _drawLine(canvas, ctx, params, path) {
	var l,
		lx, ly;
	l = 2;
	_addStartArrow(
		canvas, ctx,
		params, path,
		path.x2 + params.x,
		path.y2 + params.y,
		path.x1 + params.x,
		path.y1 + params.y
	);
	if (path.x1 !== undefined && path.y1 !== undefined) {
		ctx.moveTo(path.x1 + params.x, path.y1 + params.y);
	}
	while (true) {
		// Calculate next coordinates
		lx = path['x' + l];
		ly = path['y' + l];
		// If coordinates are given
		if (lx !== undefined && ly !== undefined) {
			// Draw next line
			ctx.lineTo(lx + params.x, ly + params.y);
			l += 1;
		} else {
			// Otherwise, stop drawing
			break;
		}
	}
	l -= 1;
	// Optionally add arrows to path
	_addEndArrow(
		canvas, ctx,
		params,
		path,
		path['x' + (l - 1)] + params.x,
		path['y' + (l - 1)] + params.y,
		path['x' + l] + params.x,
		path['y' + l] + params.y
	);
}

// Draws line
$.fn.drawLine = function drawLine(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawLine);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params);
				_setGlobalProps($canvases[e], ctx, params);

				// Draw each point
				ctx.beginPath();
				_drawLine($canvases[e], ctx, params, params);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}

		}
	}
	return $canvases;
};

// Draws quadratic curve (internal)
function _drawQuadratic(canvas, ctx, params, path) {
	var l,
		lx, ly,
		lcx, lcy;

	l = 2;

	_addStartArrow(
		canvas,
		ctx,
		params,
		path,
		path.cx1 + params.x,
		path.cy1 + params.y,
		path.x1 + params.x,
		path.y1 + params.y
	);

	if (path.x1 !== undefined && path.y1 !== undefined) {
		ctx.moveTo(path.x1 + params.x, path.y1 + params.y);
	}
	while (true) {
		// Calculate next coordinates
		lx = path['x' + l];
		ly = path['y' + l];
		lcx = path['cx' + (l - 1)];
		lcy = path['cy' + (l - 1)];
		// If coordinates are given
		if (lx !== undefined && ly !== undefined && lcx !== undefined && lcy !== undefined) {
			// Draw next curve
			ctx.quadraticCurveTo(lcx + params.x, lcy + params.y, lx + params.x, ly + params.y);
			l += 1;
		} else {
			// Otherwise, stop drawing
			break;
		}
	}
	l -= 1;
	_addEndArrow(
		canvas,
		ctx,
		params,
		path,
		path['cx' + (l - 1)] + params.x,
		path['cy' + (l - 1)] + params.y,
		path['x' + l] + params.x,
		path['y' + l] + params.y
	);
}

// Draws quadratic curve
$.fn.drawQuadratic = function drawQuadratic(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawQuadratic);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params);
				_setGlobalProps($canvases[e], ctx, params);

				// Draw each point
				ctx.beginPath();
				_drawQuadratic($canvases[e], ctx, params, params);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}
		}
	}
	return $canvases;
};

// Draws Bezier curve (internal)
function _drawBezier(canvas, ctx, params, path) {
	var l, lc,
		lx, ly,
		lcx1, lcy1,
		lcx2, lcy2;

	l = 2;
	lc = 1;

	_addStartArrow(
		canvas,
		ctx,
		params,
		path,
		path.cx1 + params.x,
		path.cy1 + params.y,
		path.x1 + params.x,
		path.y1 + params.y
	);

	if (path.x1 !== undefined && path.y1 !== undefined) {
		ctx.moveTo(path.x1 + params.x, path.y1 + params.y);
	}
	while (true) {
		// Calculate next coordinates
		lx = path['x' + l];
		ly = path['y' + l];
		lcx1 = path['cx' + lc];
		lcy1 = path['cy' + lc];
		lcx2 = path['cx' + (lc + 1)];
		lcy2 = path['cy' + (lc + 1)];
		// If next coordinates are given
		if (lx !== undefined && ly !== undefined && lcx1 !== undefined && lcy1 !== undefined && lcx2 !== undefined && lcy2 !== undefined) {
			// Draw next curve
			ctx.bezierCurveTo(lcx1 + params.x, lcy1 + params.y, lcx2 + params.x, lcy2 + params.y, lx + params.x, ly + params.y);
			l += 1;
			lc += 2;
		} else {
			// Otherwise, stop drawing
			break;
		}
	}
	l -= 1;
	lc -= 2;
	_addEndArrow(
		canvas,
		ctx,
		params,
		path,
		path['cx' + (lc + 1)] + params.x,
		path['cy' + (lc + 1)] + params.y,
		path['x' + l] + params.x,
		path['y' + l] + params.y
	);
}

// Draws Bezier curve
$.fn.drawBezier = function drawBezier(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawBezier);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params);
				_setGlobalProps($canvases[e], ctx, params);

				// Draw each point
				ctx.beginPath();
				_drawBezier($canvases[e], ctx, params, params);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}
		}
	}
	return $canvases;
};

// Retrieves the x-coordinate for the given vector angle and length
function _getVectorX(params, angle, length) {
	angle *= params._toRad;
	angle -= (PI / 2);
	return (length * cos(angle));
}
// Retrieves the y-coordinate for the given vector angle and length
function _getVectorY(params, angle, length) {
	angle *= params._toRad;
	angle -= (PI / 2);
	return (length * sin(angle));
}

// Draws vector (internal) #2
function _drawVector(canvas, ctx, params, path) {
	var l, angle, length,
		offsetX, offsetY,
		x, y,
		x3, y3,
		x4, y4;

	// Determine offset from dragging
	if (params === path) {
		offsetX = 0;
		offsetY = 0;
	} else {
		offsetX = params.x;
		offsetY = params.y;
	}

	l = 1;
	x = x3 = x4 = path.x + offsetX;
	y = y3 = y4 = path.y + offsetY;

	_addStartArrow(
		canvas, ctx,
		params, path,
		x + _getVectorX(params, path.a1, path.l1),
		y + _getVectorY(params, path.a1, path.l1),
		x,
		y
	);

	// The vector starts at the given (x, y) coordinates
	if (path.x !== undefined && path.y !== undefined) {
		ctx.moveTo(x, y);
	}
	while (true) {

		angle = path['a' + l];
		length = path['l' + l];

		if (angle !== undefined && length !== undefined) {
			// Convert the angle to radians with 0 degrees starting at north
			// Keep track of last two coordinates
			x3 = x4;
			y3 = y4;
			// Compute (x, y) coordinates from angle and length
			x4 += _getVectorX(params, angle, length);
			y4 += _getVectorY(params, angle, length);
			ctx.lineTo(x4, y4);
			l += 1;
		} else {
			// Otherwise, stop drawing
			break;
		}

	}
	_addEndArrow(
		canvas, ctx,
		params, path,
		x3, y3,
		x4, y4
	);
}

// Draws vector
$.fn.drawVector = function drawVector(args) {
	var $canvases = this, e, ctx,
		params;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawVector);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params);
				_setGlobalProps($canvases[e], ctx, params);

				// Draw each point
				ctx.beginPath();
				_drawVector($canvases[e], ctx, params, params);
				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}
		}
	}
	return $canvases;
};

// Draws a path consisting of one or more subpaths
$.fn.drawPath = function drawPath(args) {
	var $canvases = this, e, ctx,
		params,
		l, lp;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawPath);
			if (params.visible) {

				_transformShape($canvases[e], ctx, params);
				_setGlobalProps($canvases[e], ctx, params);

				ctx.beginPath();
				l = 1;
				while (true) {
					lp = params['p' + l];
					if (lp !== undefined) {
						lp = new jCanvasObject(lp);
						if (lp.type === 'line') {
							_drawLine($canvases[e], ctx, params, lp);
						} else if (lp.type === 'quadratic') {
							_drawQuadratic($canvases[e], ctx, params, lp);
						} else if (lp.type === 'bezier') {
							_drawBezier($canvases[e], ctx, params, lp);
						} else if (lp.type === 'vector') {
							_drawVector($canvases[e], ctx, params, lp);
						} else if (lp.type === 'arc') {
							_drawArc($canvases[e], ctx, params, lp);
						}
						l += 1;
					} else {
						break;
					}
				}

				// Check for jCanvas events
				_detectEvents($canvases[e], ctx, params);
				// Optionally close path
				_closePath($canvases[e], ctx, params);

			}

		}
	}
	return $canvases;
};

/* Text API */

// Calculates font string and set it as the canvas font
function _setCanvasFont(canvas, ctx, params) {
	// Otherwise, use the given font attributes
	if (!isNaN(Number(params.fontSize))) {
		// Give font size units if it doesn't have any
		params.fontSize += 'px';
	}
	// Set font using given font properties
	ctx.font = params.fontStyle + ' ' + params.fontSize + ' ' + params.fontFamily;
}

// Measures canvas text
function _measureText(canvas, ctx, params, lines) {
	var originalSize, curWidth, l,
		propCache = caches.propCache;

	// Used cached width/height if possible
	if (propCache.text === params.text && propCache.fontStyle === params.fontStyle && propCache.fontSize === params.fontSize && propCache.fontFamily === params.fontFamily && propCache.maxWidth === params.maxWidth && propCache.lineHeight === params.lineHeight) {

		params.width = propCache.width;
		params.height = propCache.height;

	} else {
		// Calculate text dimensions only once

		// Calculate width of first line (for comparison)
		params.width = ctx.measureText(lines[0]).width;

		// Get width of longest line
		for (l = 1; l < lines.length; l += 1) {

			curWidth = ctx.measureText(lines[l]).width;
			// Ensure text's width is the width of its longest line
			if (curWidth > params.width) {
				params.width = curWidth;
			}

		}

		// Save original font size
		originalSize = canvas.style.fontSize;
		// Temporarily set canvas font size to retrieve size in pixels
		canvas.style.fontSize = params.fontSize;
		// Save text width and height in parameters object
		params.height = parseFloat($.css(canvas, 'fontSize')) * lines.length * params.lineHeight;
		// Reset font size to original size
		canvas.style.fontSize = originalSize;
	}
}

// Wraps a string of text within a defined width
function _wrapText(ctx, params) {
	var allText = String(params.text),
		// Maximum line width (optional)
		maxWidth = params.maxWidth,
		// Lines created by manual line breaks (\n)
		manualLines = allText.split('\n'),
		// All lines created manually and by wrapping
		allLines = [],
		// Other variables
		lines, line, l,
		text, words, w;

	// Loop through manually-broken lines
	for (l = 0; l < manualLines.length; l += 1) {

		text = manualLines[l];
		// Split line into list of words
		words = text.split(' ');
		lines = [];
		line = '';

		// If text is short enough initially
		// Or, if the text consists of only one word
		if (words.length === 1 || ctx.measureText(text).width < maxWidth) {

			// No need to wrap text
			lines = [text];

		} else {

			// Wrap lines
			for (w = 0; w < words.length; w += 1) {

				// Once line gets too wide, push word to next line
				if (ctx.measureText(line + words[w]).width > maxWidth) {
					// This check prevents empty lines from being created
					if (line !== '') {
						lines.push(line);
					}
					// Start new line and repeat process
					line = '';
				}
				// Add words to line until the line is too wide
				line += words[w];
				// Do not add a space after the last word
				if (w !== (words.length - 1)) {
					line += ' ';
				}
			}
			// The last word should always be pushed
			lines.push(line);

		}
		// Remove extra space at the end of each line
		allLines = allLines.concat(
			lines
			.join('\n')
			.replace(/((\n))|($)/gi, '$2')
			.split('\n')
		);

	}

	return allLines;
}

// Draws text on canvas
$.fn.drawText = function drawText(args) {
	var $canvases = this, e, ctx,
		params, layer,
		lines, line, l,
		fontSize, constantCloseness = 500,
		nchars, chars, ch, c,
		x, y;

	for (e = 0; e < $canvases.length; e += 1) {
		ctx = _getContext($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer($canvases[e], params, args, drawText);
			if (params.visible) {

				// Set text-specific properties
				ctx.textBaseline = params.baseline;
				ctx.textAlign = params.align;

				// Set canvas font using given properties
				_setCanvasFont($canvases[e], ctx, params);

				if (params.maxWidth !== null) {
					// Wrap text using an internal function
					lines = _wrapText(ctx, params);
				} else {
					// Convert string of text to list of lines
					lines = params.text
					.toString()
					.split('\n');
				}

				// Calculate text's width and height
				_measureText($canvases[e], ctx, params, lines);

				// If text is a layer
				if (layer) {
					// Copy calculated width/height to layer object
					layer.width = params.width;
					layer.height = params.height;
				}

				_transformShape($canvases[e], ctx, params, params.width, params.height);
				_setGlobalProps($canvases[e], ctx, params);

				// Adjust text position to accomodate different horizontal alignments
				x = params.x;
				if (params.align === 'left') {
					if (params.respectAlign) {
						// Realign text to the left if chosen
						params.x += params.width / 2;
					} else {
						// Center text block by default
						x -= params.width / 2;
					}
				} else if (params.align === 'right') {
					if (params.respectAlign) {
						// Realign text to the right if chosen
						params.x -= params.width / 2;
					} else {
						// Center text block by default
						x += params.width / 2;
					}
				}

				if (params.radius) {

					fontSize = parseFloat(params.fontSize);

					// Greater values move clockwise
					if (params.letterSpacing === null) {
						params.letterSpacing = fontSize / constantCloseness;
					}

					// Loop through each line of text
					for (l = 0; l < lines.length; l += 1) {
						ctx.save();
						ctx.translate(params.x, params.y);
						line = lines[l];
						if (params.flipArcText) {
							chars = line.split('');
							chars.reverse();
							line = chars.join('');
						}
						nchars = line.length;
						ctx.rotate(-(PI * params.letterSpacing * (nchars - 1)) / 2);
						// Loop through characters on each line
						for (c = 0; c < nchars; c += 1) {
							ch = line[c];
							// If character is not the first character
							if (c !== 0) {
								// Rotate character onto arc
								ctx.rotate(PI * params.letterSpacing);
							}
							ctx.save();
							ctx.translate(0, -params.radius);
							if (params.flipArcText) {
								ctx.scale(-1, -1);
							}
							ctx.fillText(ch, 0, 0);
							// Prevent extra shadow created by stroke (but only when fill is present)
							if (params.fillStyle !== 'transparent') {
								ctx.shadowColor = 'transparent';
							}
							if (params.strokeWidth !== 0) {
								// Only stroke if the stroke is not 0
								ctx.strokeText(ch, 0, 0);
							}
							ctx.restore();
						}
						params.radius -= fontSize;
						params.letterSpacing += fontSize / (constantCloseness * 2 * PI);
						ctx.restore();
					}

				} else {

					// Draw each line of text separately
					for (l = 0; l < lines.length; l += 1) {
						line = lines[l];
						// Add line offset to center point, but subtract some to center everything
						y = params.y + (l * params.height / lines.length) - (((lines.length - 1) * params.height / lines.length) / 2);

						ctx.shadowColor = params.shadowColor;

						// Fill & stroke text
						ctx.fillText(line, x, y);
						// Prevent extra shadow created by stroke (but only when fill is present)
						if (params.fillStyle !== 'transparent') {
							ctx.shadowColor = 'transparent';
						}
						if (params.strokeWidth !== 0) {
							// Only stroke if the stroke is not 0
							ctx.strokeText(line, x, y);
						}

					}

				}

				// Adjust bounding box according to text baseline
				y = 0;
				if (params.baseline === 'top') {
					y += params.height / 2;
				} else if (params.baseline === 'bottom') {
					y -= params.height / 2;
				}

				// Detect jCanvas events
				if (params._event) {
					ctx.beginPath();
					ctx.rect(
						params.x - (params.width / 2),
						params.y - (params.height / 2) + y,
						params.width,
						params.height
					);
					_detectEvents($canvases[e], ctx, params);
					// Close path and configure masking
					ctx.closePath();
				}
				_restoreTransform(ctx, params);

			}
		}
	}
	// Cache jCanvas parameters object for efficiency
	caches.propCache = params;
	return $canvases;
};

// Measures text width/height using the given parameters
$.fn.measureText = function measureText(args) {
	var $canvases = this, ctx,
		params, lines;

	// Attempt to retrieve layer
	params = $canvases.getLayer(args);
	// If layer does not exist or if returned object is not a jCanvas layer
	if (!params || (params && !params._layer)) {
		params = new jCanvasObject(args);
	}

	ctx = _getContext($canvases[0]);
	if (ctx) {

		// Set canvas font using given properties
		_setCanvasFont($canvases[0], ctx, params);
		// Calculate width and height of text
		if (params.maxWidth !== null) {
			lines = _wrapText(ctx, params);
		} else {
			lines = params.text.split('\n');
		}
		_measureText($canvases[0], ctx, params, lines);


	}

	return params;
};

/* Image API */

// Draws image on canvas
$.fn.drawImage = function drawImage(args) {
	var $canvases = this, canvas, e, ctx, data,
		params, layer,
		img, imgCtx, source,
		imageCache = caches.imageCache;

	// Draw image function
	function draw(canvas, ctx, data, params, layer) {

		// If width and sWidth are not defined, use image width
		if (params.width === null && params.sWidth === null) {
			params.width = params.sWidth = img.width;
		}
		// If width and sHeight are not defined, use image height
		if (params.height === null && params.sHeight === null) {
			params.height = params.sHeight = img.height;
		}

		// Ensure image layer's width and height are accurate
		if (layer) {
			layer.width = params.width;
			layer.height = params.height;
		}

		// Only crop image if all cropping properties are given
		if (params.sWidth !== null && params.sHeight !== null && params.sx !== null && params.sy !== null) {

			// If width is not defined, use the given sWidth
			if (params.width === null) {
				params.width = params.sWidth;
			}
			// If height is not defined, use the given sHeight
			if (params.height === null) {
				params.height = params.sHeight;
			}

			// Optionally crop from top-left corner of region
			if (params.cropFromCenter) {
				params.sx += params.sWidth / 2;
				params.sy += params.sHeight / 2;
			}

			// Ensure cropped region does not escape image boundaries

			// Top
			if ((params.sy - (params.sHeight / 2)) < 0) {
				params.sy = (params.sHeight / 2);
			}
			// Bottom
			if ((params.sy + (params.sHeight / 2)) > img.height) {
				params.sy = img.height - (params.sHeight / 2);
			}
			// Left
			if ((params.sx - (params.sWidth / 2)) < 0) {
				params.sx = (params.sWidth / 2);
			}
			// Right
			if ((params.sx + (params.sWidth / 2)) > img.width) {
				params.sx = img.width - (params.sWidth / 2);
			}

			_transformShape(canvas, ctx, params, params.width, params.height);
			_setGlobalProps(canvas, ctx, params);

			// Draw image
			ctx.drawImage(
				img,
				params.sx - (params.sWidth / 2),
				params.sy - (params.sHeight / 2),
				params.sWidth,
				params.sHeight,
				params.x - (params.width / 2),
				params.y - (params.height / 2),
				params.width,
				params.height
			);

		} else {
			// Show entire image if no crop region is defined

			_transformShape(canvas, ctx, params, params.width, params.height);
			_setGlobalProps(canvas, ctx, params);

			// Draw image on canvas
			ctx.drawImage(
				img,
				params.x - (params.width / 2),
				params.y - (params.height / 2),
				params.width,
				params.height
			);

		}

		// Draw invisible rectangle to allow for events and masking
		ctx.beginPath();
		ctx.rect(
			params.x - (params.width / 2),
			params.y - (params.height / 2),
			params.width,
			params.height
		);
		// Check for jCanvas events
		_detectEvents(canvas, ctx, params);
		// Close path and configure masking
		ctx.closePath();
		_restoreTransform(ctx, params);
		_enableMasking(ctx, data, params);
	}
	// On load function
	function onload(canvas, ctx, data, params, layer) {
		return function () {
			var $canvas = $(canvas);
			draw(canvas, ctx, data, params, layer);
			if (params.layer) {
				// Trigger 'load' event for layers
				_triggerLayerEvent($canvas, data, layer, 'load');
			} else if (params.load) {
				// Run 'load' callback for non-layers
				params.load.call($canvas[0], layer);
			}
			// Continue drawing successive layers after this image layer has loaded
			if (params.layer) {
				// Store list of previous masks for each layer
				layer._masks = data.transforms.masks.slice(0);
				if (params._next) {
					// Draw successive layers
					var complete = data.drawLayersComplete;
					delete data.drawLayersComplete;
					$canvas.drawLayers({
						clear: false,
						resetFire: true,
						index: params._next,
						complete: complete
					});
				}
			}
		};
	}
	for (e = 0; e < $canvases.length; e += 1) {
		canvas = $canvases[e];
		ctx = _getContext($canvases[e]);
		if (ctx) {

			data = _getCanvasData($canvases[e]);
			params = new jCanvasObject(args);
			layer = _addLayer($canvases[e], params, args, drawImage);
			if (params.visible) {

				// Cache the given source
				source = params.source;

				imgCtx = source.getContext;
				if (source.src || imgCtx) {
					// Use image or canvas element if given
					img = source;
				} else if (source) {
					if (imageCache[source] && imageCache[source].complete) {
						// Get the image element from the cache if possible
						img = imageCache[source];
					} else {
						// Otherwise, get the image from the given source URL
						img = new Image();
						// If source URL is not a data URL
						if (!source.match(/^data:/i)) {
							// Set crossOrigin for this image
							img.crossOrigin = params.crossOrigin;
						}
						img.src = source;
						// Save image in cache for improved performance
						imageCache[source] = img;
					}
				}

				if (img) {
					if (img.complete || imgCtx) {
						// Draw image if already loaded
						onload(canvas, ctx, data, params, layer)();
					} else {
						// Otherwise, draw image when it loads
						img.onload = onload(canvas, ctx, data, params, layer);
					}
				}

			}
		}
	}
	return $canvases;
};

// Creates a canvas pattern object
$.fn.createPattern = function createPattern(args) {
	var $canvases = this, ctx,
		params,
		img, imgCtx,
		pattern, source;

	// Function to be called when pattern loads
	function onload() {
		// Create pattern
		pattern = ctx.createPattern(img, params.repeat);
		// Run callback function if defined
		if (params.load) {
			params.load.call($canvases[0], pattern);
		}
	}

	ctx = _getContext($canvases[0]);
	if (ctx) {

		params = new jCanvasObject(args);

		// Cache the given source
		source = params.source;

		// Draw when image is loaded (if load() callback function is defined)

		if (isFunction(source)) {
			// Draw pattern using function if given

			img = $('<canvas />')[0];
			img.width = params.width;
			img.height = params.height;
			imgCtx = _getContext(img);
			source.call(img, imgCtx);
			onload();

		} else {
			// Otherwise, draw pattern using source image

			imgCtx = source.getContext;
			if (source.src || imgCtx) {
				// Use image element if given
				img = source;
			} else {
				// Use URL if given to get the image
				img = new Image();
				// If source URL is not a data URL
				if (!source.match(/^data:/i)) {
					// Set crossOrigin for this image
					img.crossOrigin = params.crossOrigin;
				}
				img.src = source;
			}

			// Create pattern if already loaded
			if (img.complete || imgCtx) {
				onload();
			} else {
				img.onload = onload;
			}

		}

	} else {

		pattern = null;

	}
	return pattern;
};

// Creates a canvas gradient object
$.fn.createGradient = function createGradient(args) {
	var $canvases = this, ctx,
		params,
		gradient,
		stops = [], nstops,
		start, end,
		i, a, n, p;

	params = new jCanvasObject(args);
	ctx = _getContext($canvases[0]);
	if (ctx) {

		// Gradient coordinates must be defined
		params.x1 = params.x1 || 0;
		params.y1 = params.y1 || 0;
		params.x2 = params.x2 || 0;
		params.y2 = params.y2 || 0;

		if (params.r1 !== null && params.r2 !== null) {
			// Create radial gradient if chosen
			gradient = ctx.createRadialGradient(params.x1, params.y1, params.r1, params.x2, params.y2, params.r2);
		} else {
			// Otherwise, create a linear gradient by default
			gradient = ctx.createLinearGradient(params.x1, params.y1, params.x2, params.y2);
		}

		// Count number of color stops
		for (i = 1; params['c' + i] !== undefined; i += 1) {
			if (params['s' + i] !== undefined) {
				stops.push(params['s' + i]);
			} else {
				stops.push(null);
			}
		}
		nstops = stops.length;

		// Define start stop if not already defined
		if (stops[0] === null) {
			stops[0] = 0;
		}
		// Define end stop if not already defined
		if (stops[nstops - 1] === null) {
			stops[nstops - 1] = 1;
		}

		// Loop through color stops to fill in the blanks
		for (i = 0; i < nstops; i += 1) {
			// A progression, in this context, is defined as all of the color stops between and including two known color stops

			if (stops[i] !== null) {
				// Start a new progression if stop is a number

				// Number of stops in current progression
				n = 1;
				// Current iteration in current progression
				p = 0;
				start = stops[i];

				// Look ahead to find end stop
				for (a = (i + 1); a < nstops; a += 1) {
					if (stops[a] !== null) {
						// If this future stop is a number, make it the end stop for this progression
						end = stops[a];
						break;
					} else {
						// Otherwise, keep looking ahead
						n += 1;
					}
				}

				// Ensure start stop is not greater than end stop
				if (start > end) {
					stops[a] = stops[i];
				}

			} else if (stops[i] === null) {
				// Calculate stop if not initially given
				p += 1;
				stops[i] = start + (p * ((end - start) / n));
			}
			// Add color stop to gradient object
			gradient.addColorStop(stops[i], params['c' + (i + 1)]);
		}

	} else {
		gradient = null;
	}
	return gradient;
};

// Manipulates pixels on the canvas
$.fn.setPixels = function setPixels(args) {
	var $canvases = this,
		canvas, e, ctx, canvasData,
		params,
		px,
		imgData, pixelData, i, len;

	for (e = 0; e < $canvases.length; e += 1) {
		canvas = $canvases[e];
		ctx = _getContext(canvas);
		canvasData = _getCanvasData($canvases[e]);
		if (ctx) {

			params = new jCanvasObject(args);
			_addLayer(canvas, params, args, setPixels);
			_transformShape($canvases[e], ctx, params, params.width, params.height);

			// Use entire canvas of x, y, width, or height is not defined
			if (params.width === null || params.height === null) {
				params.width = canvas.width;
				params.height = canvas.height;
				params.x = params.width / 2;
				params.y = params.height / 2;
			}

			if (params.width !== 0 && params.height !== 0) {
				// Only set pixels if width and height are not zero

				imgData = ctx.getImageData(
					(params.x - (params.width / 2)) * canvasData.pixelRatio,
					(params.y - (params.height / 2)) * canvasData.pixelRatio,
					params.width * canvasData.pixelRatio,
					params.height * canvasData.pixelRatio
				);
				pixelData = imgData.data;
				len = pixelData.length;

				// Loop through pixels with the "each" callback function
				if (params.each) {
					for (i = 0; i < len; i += 4) {
						px = {
							r: pixelData[i],
							g: pixelData[i + 1],
							b: pixelData[i + 2],
							a: pixelData[i + 3]
						};
						params.each.call(canvas, px, params);
						pixelData[i] = px.r;
						pixelData[i + 1] = px.g;
						pixelData[i + 2] = px.b;
						pixelData[i + 3] = px.a;
					}
				}
				// Put pixels on canvas
				ctx.putImageData(
					imgData,
					(params.x - (params.width / 2)) * canvasData.pixelRatio,
					(params.y - (params.height / 2)) * canvasData.pixelRatio
				);
				// Restore transformation
				ctx.restore();

			}

		}
	}
	return $canvases;
};

// Retrieves canvas image as data URL
$.fn.getCanvasImage = function getCanvasImage(type, quality) {
	var $canvases = this, canvas,
		dataURL = null;
	if ($canvases.length !== 0) {
		canvas = $canvases[0];
		if (canvas.toDataURL) {
			// JPEG quality defaults to 1
			if (quality === undefined) {
				quality = 1;
			}
			dataURL = canvas.toDataURL('image/' + type, quality);
		}
	}
	return dataURL;
};

// Scales canvas based on the device's pixel ratio
$.fn.detectPixelRatio = function detectPixelRatio(callback) {
	var $canvases = this,
		canvas, e, ctx,
		devicePixelRatio, backingStoreRatio, ratio,
		oldWidth, oldHeight,
		data;

	for (e = 0; e < $canvases.length; e += 1) {
		// Get canvas and its associated data
		canvas = $canvases[e];
		ctx = _getContext(canvas);
		data = _getCanvasData($canvases[e]);

		// If canvas has not already been scaled with this method
		if (!data.scaled) {

			// Determine device pixel ratios
			devicePixelRatio = window.devicePixelRatio || 1;
			backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
				ctx.mozBackingStorePixelRatio ||
				ctx.msBackingStorePixelRatio ||
				ctx.oBackingStorePixelRatio ||
				ctx.backingStorePixelRatio || 1;

			// Calculate general ratio based on the two given ratios
			ratio = devicePixelRatio / backingStoreRatio;

			if (ratio !== 1) {
				// Scale canvas relative to ratio

				// Get the current canvas dimensions for future use
				oldWidth = canvas.width;
				oldHeight = canvas.height;

				// Resize canvas relative to the determined ratio
				canvas.width = oldWidth * ratio;
				canvas.height = oldHeight * ratio;

				// Scale canvas back to original dimensions via CSS
				canvas.style.width = oldWidth + 'px';
				canvas.style.height = oldHeight + 'px';

				// Scale context to counter the manual scaling of canvas
				ctx.scale(ratio, ratio);

			}

			// Set pixel ratio on canvas data object
			data.pixelRatio = ratio;
			// Ensure that this method can only be called once for any given canvas
			data.scaled = true;

			// Call the given callback function with the ratio as its only argument
			if (callback) {
				callback.call(canvas, ratio);
			}

		}

	}
	return $canvases;
};

// Clears the jCanvas cache
jCanvas.clearCache = function clearCache() {
	var cacheName;
	for (cacheName in caches) {
		if (Object.prototype.hasOwnProperty.call(caches, cacheName)) {
			caches[cacheName] = {};
		}
	}
};

// Enable canvas feature detection with $.support
$.support.canvas = ($('<canvas />')[0].getContext !== undefined);

// Export jCanvas functions
extendObject(jCanvas, {
	defaults: defaults,
	setGlobalProps: _setGlobalProps,
	transformShape: _transformShape,
	detectEvents: _detectEvents,
	closePath: _closePath,
	setCanvasFont: _setCanvasFont,
	measureText: _measureText
});
$.jCanvas = jCanvas;
$.jCanvasObject = jCanvasObject;
