/**
 * @license jCanvas v16.07.03
 * Copyright 2016 Caleb Evans
 * Released under the MIT license
 */
(function( jQuery, global, factory ) {

	if ( typeof module === 'object' && typeof module.exports === 'object' ) {
		module.exports = function( jQuery, w ) {
			return factory( jQuery, w );
		};
	} else {
		factory( jQuery, global );
	}

// Pass this if window is not defined yet
}( typeof window !== 'undefined' ? window.jQuery : {}, typeof window !== 'undefined' ? window : this, function( $, window ) {

var document = window.document,
	Image = window.Image,
	Array = window.Array,
	getComputedStyle = window.getComputedStyle,
	Math = window.Math,
	Number = window.Number,
	parseFloat = window.parseFloat,
	TRUE = true,
	FALSE = false,
	NULL = null,
	// jshint -W080
	UNDEFINED = undefined;

// 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();
	},
	isFunction = $.isFunction,
	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 ( args.hasOwnProperty( 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 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 ( props.hasOwnProperty( 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 = ctx.mozImageSmoothingEnabled = params.imageSmoothingEnabled;
	}
}

// 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, layer;

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

					params = new jCanvasObject( args );
					layer = _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
			};
			// 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 ( jCanvas.events.hasOwnProperty( eventName ) ) {
			// If layer has callback function to complement it
			if ( layer[ eventName ] || ( layer.cursors && layer.cursors[ eventName ] ) ) {
				// Bind event to layer
				_addLayerEvent( $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;
}

// 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, $canvas, e,
		data;
	for ( e = 0; e < $canvases.length; e += 1 ) {
		$canvas = $( $canvases[ e ] );
		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 ( props.hasOwnProperty( 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 );
		// Remove all layers individually
		for ( l = 0; l < layers.length; l += 1 ) {
			layer = layers[ l ];
			$canvas.removeLayer( layer );
			// Ensure no layer is skipped over
			l -= 1;
		}
		// 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, data,
		layers, group, l;

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

			layers = $canvas.getLayers();
			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
		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 );
			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;
			// 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();
			}

			// 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()
				break;
			}

			// Store the latest
			lastIndex = l;

			// 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;

					}

				}

			}

			// 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 ] ];
		} else {
			params._method = function () {};
		}
	}

	// 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 ( endValues.hasOwnProperty( 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 ( propValue.hasOwnProperty( 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 ( layer.hasOwnProperty( 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
			$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',
	'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 = !!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 ) {
			offset = $( event.currentTarget ).offset();
			if ( offset ) {
				event.offsetX = event.pageX - offset.left;
				event.offsetY = event.pageY - offset.top;
			}
		} else if ( touches ) {
			// 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;
			}
		}

	}
	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, $canvas, e, ctx,
		params = new jCanvasObject( args ),
		layer;

	// 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 ) {
			$canvas = $( $canvases[ e ] );
			ctx = _getContext( $canvases[ e ] );
			if ( ctx ) {

				params = new jCanvasObject( args );
				layer = _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 ),
		layer;

	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
				layer = _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, layer,
		data, i;

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

			data = _getCanvasData( $canvases[ e ] );

			params = new jCanvasObject( args );
			layer = _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, layer,
		data, i;

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

			data = _getCanvasData( $canvases[ e ] );

			params = new jCanvasObject( args );
			layer = _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, layer,
		data;

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

			data = _getCanvasData( $canvases[ e ] );

			params = new jCanvasObject( args );
			layer = _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, layer,
		data;

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

			data = _getCanvasData( $canvases[ e ] );

			params = new jCanvasObject( args );
			layer = _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, layer,
		data;

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

			data = _getCanvasData( $canvases[ e ] );

			params = new jCanvasObject( args );
			layer = _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, layer,
		x1, y1,
		x2, y2,
		r, temp;

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

			params = new jCanvasObject( args );
			layer = _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, layer;

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

			params = new jCanvasObject( args );
			layer = _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, layer,
		controlW,
		controlH;

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

			params = new jCanvasObject( args );
			layer = _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, layer,
		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 );
			layer = _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, $canvas, e, ctx,
		params, layer,
		angle, dx, dy;

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

			params = new jCanvasObject( args );
			layer = _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, layer;

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

			params = new jCanvasObject( args );
			layer = _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, layer;

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

			params = new jCanvasObject( args );
			layer = _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, layer;

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

			params = new jCanvasObject( args );
			layer = _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,
		x2, y2,
		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 = x2 = x3 = x4 = path.x + offsetX;
	y = y2 = 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 );
			// Store the second point
			if ( l === 1 ) {
				x2 = x4;
				y2 = y4;
			}
			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, layer;

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

			params = new jCanvasObject( args );
			layer = _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, layer,
		l, lp;

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

			params = new jCanvasObject( args );
			layer = _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, $canvas, e, ctx,
		params, layer,
		lines, line, l,
		fontSize, constantCloseness = 500,
		nchars, chars, ch, c,
		x, y;

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

			params = new jCanvasObject( args );
			layer = _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
		lines = _wrapText( ctx, params );
		_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
					$canvas.drawLayers( {
						clear: FALSE,
						resetFire: TRUE,
						index: params._next
					} );
				}
			}
		};
	}
	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 );
						// Fix onload() bug in IE9
						img.src = img.src;
					}
				}

			}
		}
	}
	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;
				// Fix onload() bug in IE9
				img.src = img.src;
			}

		}

	} 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,
		params, layer,
		px,
		imgData, data, i, len;

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

			params = new jCanvasObject( args );
			layer = _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 ), params.y - ( params.height / 2 ), params.width, params.height );
				data = imgData.data;
				len = data.length;

				// Loop through pixels with the "each" callback function
				if ( params.each ) {
					for ( i = 0; i < len; i += 4 ) {
						px = {
							r: data[ i ],
							g: data[ i + 1 ],
							b: data[ i + 2 ],
							a: data[ i + 3 ]
						};
						params.each.call( canvas, px, params );
						data[ i ] = px.r;
						data[ i + 1 ] = px.g;
						data[ i + 2 ] = px.b;
						data[ i + 3 ] = px.a;
					}
				}
				// Put pixels on canvas
				ctx.putImageData( imgData, params.x - ( params.width / 2 ), params.y - ( params.height / 2 ) );
				// 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, 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 ];
		$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 ( caches.hasOwnProperty( 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;

}));
