text: (function($) {
  	var has_VML, has_canvas, create_canvas_for, add_shape_to, clear_canvas, shape_from_area,
  		canvas_style, hex_to_decimal, css3color, is_image_loaded, options_from_area;
  
  	has_canvas = !!document.createElement('canvas').getContext;
  
  	// VML: more complex
  	has_VML = (function() {
  		var a = document.createElement('div');
  		a.innerHTML = '<v:shape id="vml_flag1" adj="1" />';
  		var b = a.firstChild;
  		b.style.behavior = "url(#default#VML)";
  		return b ? typeof b.adj == "object": true;
  	})();
  
  	if(!(has_canvas || has_VML)) {
  		$.fn.maphilight = function() { return this; };
  		return;
  	}
  
  	if(has_canvas) {
  		hex_to_decimal = function(hex) {
  			return Math.max(0, Math.min(parseInt(hex, 16), 255));
  		};
  		css3color = function(color, opacity) {
  			return 'rgba('+hex_to_decimal(color.substr(0,2))+','+hex_to_decimal(color.substr(2,2))+','+hex_to_decimal(color.substr(4,2))+','+opacity+')';
  		};
  		create_canvas_for = function(img) {
  			var c = $('<canvas style="width:'+$(img).width()+'px;height:'+$(img).height()+'px;"></canvas>').get(0);
  			c.getContext("2d").clearRect(0, 0, $(img).width(), $(img).height());
  			return c;
  		};
  		var draw_shape = function(context, shape, coords, x_shift, y_shift) {
  			x_shift = x_shift || 0;
  			y_shift = y_shift || 0;
  
  			context.beginPath();
  			if(shape == 'rect') {
  				// x, y, width, height
  				context.rect(coords[0] + x_shift, coords[1] + y_shift, coords[2] - coords[0], coords[3] - coords[1]);
  			} else if(shape == 'poly') {
  				context.moveTo(coords[0] + x_shift, coords[1] + y_shift);
  				for(i=2; i < coords.length; i+=2) {
  					context.lineTo(coords[i] + x_shift, coords[i+1] + y_shift);
  				}
  			} else if(shape == 'circ') {
  				// x, y, radius, startAngle, endAngle, anticlockwise
  				context.arc(coords[0] + x_shift, coords[1] + y_shift, coords[2], 0, Math.PI * 2, false);
  			}
  			context.closePath();
  		};
  		add_shape_to = function(canvas, shape, coords, options, name) {
  			var i, context = canvas.getContext('2d');
  
  			// Because I don't want to worry about setting things back to a base state
  
  			// Shadow has to happen first, since it's on the bottom, and it does some clip /
  			// fill operations which would interfere with what comes next.
  			if(options.shadow) {
  				context.save();
  				if(options.shadowPosition == "inside") {
  					// Cause the following stroke to only apply to the inside of the path
  					draw_shape(context, shape, coords);
  					context.clip();
  				}
  
  				// Redraw the shape shifted off the canvas massively so we can cast a shadow
  				// onto the canvas without having to worry about the stroke or fill (which
  				// cannot have 0 opacity or width, since they're what cast the shadow).
  				var x_shift = canvas.width * 100;
  				var y_shift = canvas.height * 100;
  				draw_shape(context, shape, coords, x_shift, y_shift);
  
  				context.shadowOffsetX = options.shadowX - x_shift;
  				context.shadowOffsetY = options.shadowY - y_shift;
  				context.shadowBlur = options.shadowRadius;
  				context.shadowColor = css3color(options.shadowColor, options.shadowOpacity);
  
  				// Now, work out where to cast the shadow from! It looks better if it's cast
  				// from a fill when it's an outside shadow or a stroke when it's an interior
  				// shadow. Allow the user to override this if they need to.
  				var shadowFrom = options.shadowFrom;
  				if (!shadowFrom) {
  					if (options.shadowPosition == 'outside') {
  						shadowFrom = 'fill';
  					} else {
  						shadowFrom = 'stroke';
  					}
  				}
  				if (shadowFrom == 'stroke') {
  					context.strokeStyle = "rgba(0,0,0,1)";
  					context.stroke();
  				} else if (shadowFrom == 'fill') {
  					context.fillStyle = "rgba(0,0,0,1)";
  					context.fill();
  				}
  				context.restore();
  
  				// and now we clean up
  				if(options.shadowPosition == "outside") {
  					context.save();
  					// Clear out the center
  					draw_shape(context, shape, coords);
  					context.globalCompositeOperation = "destination-out";
  					context.fillStyle = "rgba(0,0,0,1);";
  					context.fill();
  					context.restore();
  				}
  			}
  
  			context.save();
  
  			draw_shape(context, shape, coords);
  
  			// fill has to come after shadow, otherwise the shadow will be drawn over the fill,
  			// which mostly looks weird when the shadow has a high opacity
  			if(options.fill) {
  				context.fillStyle = css3color(options.fillColor, options.fillOpacity);
  				context.fill();
  			}
  			// Likewise, stroke has to come at the very end, or it'll wind up under bits of the
  			// shadow or the shadow-background if it's present.
  			if(options.stroke) {
  				context.strokeStyle = css3color(options.strokeColor, options.strokeOpacity);
  				context.lineWidth = options.strokeWidth;
  				context.stroke();
  			}
  
  			context.restore();
  
  			if(options.fade) {
  				$(canvas).css('opacity', 0).animate({opacity: 1}, 100);
  			}
  		};
  		clear_canvas = function(canvas) {
  			canvas.getContext('2d').clearRect(0, 0, canvas.width,canvas.height);
  		};
  	} else {   // ie executes this code
  		create_canvas_for = function(img) {
  			return $('<var style="zoom:1;overflow:hidden;display:block;width:'+img.width+'px;height:'+img.height+'px;"></var>').get(0);
  		};
  		add_shape_to = function(canvas, shape, coords, options, name) {
  			var fill, stroke, opacity, e;
  			for (var i in coords) { coords[i] = parseInt(coords[i], 10); }
  			fill = '<v:fill color="#'+options.fillColor+'" opacity="'+(options.fill ? options.fillOpacity : 0)+'" />';
  			stroke = (options.stroke ? 'strokeweight="'+options.strokeWidth+'" stroked="t" strokecolor="#'+options.strokeColor+'"' : 'stroked="f"');
  			opacity = '<v:stroke opacity="'+options.strokeOpacity+'"/>';
  			if(shape == 'rect') {
  				e = $('<v:rect name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+coords[0]+'px;top:'+coords[1]+'px;width:'+(coords[2] - coords[0])+'px;height:'+(coords[3] - coords[1])+'px;"></v:rect>');
  			} else if(shape == 'poly') {
  				e = $('<v:shape name="'+name+'" filled="t" '+stroke+' coordorigin="0,0" coordsize="'+canvas.width+','+canvas.height+'" path="m '+coords[0]+','+coords[1]+' l '+coords.join(',')+' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:'+canvas.width+'px;height:'+canvas.height+'px;"></v:shape>');
  			} else if(shape == 'circ') {
  				e = $('<v:oval name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+(coords[0] - coords[2])+'px;top:'+(coords[1] - coords[2])+'px;width:'+(coords[2]*2)+'px;height:'+(coords[2]*2)+'px;"></v:oval>');
  			}
  			e.get(0).innerHTML = fill+opacity;
  			$(canvas).append(e);
  		};
  		clear_canvas = function(canvas) {
  			// jquery1.8 + ie7
  			var $html = $("<div>" + canvas.innerHTML + "</div>");
  			$html.children('[name=highlighted]').remove();
  			canvas.innerHTML = $html.html();
  		};
  	}
  
  	shape_from_area = function(area) {
  		var i, coords = area.getAttribute('coords').split(',');
  		for (i=0; i < coords.length; i++) { coords[i] = parseFloat(coords[i]); }
  		return [area.getAttribute('shape').toLowerCase().substr(0,4), coords];
  	};
  
  	options_from_area = function(area, options) {
  		var $area = $(area);
  		return $.extend({}, options, $.metadata ? $area.metadata() : false, $area.data('maphilight'));
  	};
  
  	is_image_loaded = function(img) {
  		if(!img.complete) { return false; } // IE
  		if(typeof img.naturalWidth != "undefined" && img.naturalWidth === 0) { return false; } // Others
  		return true;
  	};
  
  	canvas_style = {
  		position: 'absolute',
  		left: 0,
  		top: 0,
  		padding: 0,
  		border: 0
  	};
  
  	var ie_hax_done = false;
  	$.fn.maphilight = function(opts) {
  		opts = $.extend({}, $.fn.maphilight.defaults, opts);
  
  		if(!has_canvas && !ie_hax_done) {
  			$(window).ready(function() {
  				document.namespaces.add("v", "urn:schemas-microsoft-com:vml");
  				var style = document.createStyleSheet();
  				var shapes = ['shape','rect', 'oval', 'circ', 'fill', 'stroke', 'imagedata', 'group','textbox'];
  				$.each(shapes,
  					function() {
  						style.addRule('v\\:' + this, "behavior: url(#default#VML); antialias:true");
  					}
  				);
  			});
  			ie_hax_done = true;
  		}
  
  		return this.each(function() {
  			var img, wrap, options, map, canvas, canvas_always, highlighted_shape, usemap;
  			img = $(this);
  
  			if(!is_image_loaded(this)) {
  				// If the image isn't fully loaded, this won't work right.  Try again later.
  				return window.setTimeout(function() {
  					img.maphilight(opts);
  				}, 200);
  			}
  
  			options = $.extend({}, opts, $.metadata ? img.metadata() : false, img.data('maphilight'));
  
  			// jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
  			// So use raw getAttribute instead.
  			usemap = img.get(0).getAttribute('usemap');
  
  			if (!usemap) {
  				return;
  			}
  
  			map = $('map[name="'+usemap.substr(1)+'"]');
  
  			if(!(img.is('img,input[type="image"]') && usemap && map.size() > 0)) {
  				return;
  			}
  
  			if(img.hasClass('maphilighted')) {
  				// We're redrawing an old map, probably to pick up changes to the options.
  				// Just clear out all the old stuff.
  				var wrapper = img.parent();
  				img.insertBefore(wrapper);
  				wrapper.remove();
  				$(map).unbind('.maphilight');
  			}
  
  			wrap = $('<div></div>').css({
  				display:'block',
  				backgroundImage:'url("'+this.src+'")',
  				backgroundSize:'contain',
  				position:'relative',
  				padding:0,
  				width:this.width,
  				height:this.height
  				});
  			if(options.wrapClass) {
  				if(options.wrapClass === true) {
  					wrap.addClass($(this).attr('class'));
  				} else {
  					wrap.addClass(options.wrapClass);
  				}
  			}
  			img.before(wrap).css('opacity', 0).css(canvas_style).remove();
  			if(has_VML) { img.css('filter', 'Alpha(opacity=0)'); }
  			wrap.append(img);
  
  			canvas = create_canvas_for(this);
  			$(canvas).css(canvas_style);
  			canvas.height = this.height;
  			canvas.width = this.width;
  
  			$(map).bind('alwaysOn.maphilight', function() {
  				// Check for areas with alwaysOn set. These are added to a *second* canvas,
  				// which will get around flickering during fading.
  				if(canvas_always) {
  					clear_canvas(canvas_always);
  				}
  				if(!has_canvas) {
  					$(canvas).empty();
  				}
  				$(map).find('area[coords]').each(function() {
  					var shape, area_options;
  					area_options = options_from_area(this, options);
  					if(area_options.alwaysOn) {
  						if(!canvas_always && has_canvas) {
  							canvas_always = create_canvas_for(img[0]);
  							$(canvas_always).css(canvas_style);
  							canvas_always.width = img[0].width;
  							canvas_always.height = img[0].height;
  							img.before(canvas_always);
  						}
  						area_options.fade = area_options.alwaysOnFade; // alwaysOn shouldn't fade in initially
  						shape = shape_from_area(this);
  						if (has_canvas) {
  							add_shape_to(canvas_always, shape[0], shape[1], area_options, "");
  						} else {
  							add_shape_to(canvas, shape[0], shape[1], area_options, "");
  						}
  					}
  				});
  			}).trigger('alwaysOn.maphilight')
  			.bind('mouseover.maphilight, focus.maphilight', function(e) {
  				var shape, area_options, area = e.target;
  				area_options = options_from_area(area, options);
  				if(!area_options.neverOn && !area_options.alwaysOn) {
  					shape = shape_from_area(area);
  					add_shape_to(canvas, shape[0], shape[1], area_options, "highlighted");
  					if(area_options.groupBy) {
  						var areas;
  						// two ways groupBy might work; attribute and selector
  						if(/^[a-zA-Z][\-a-zA-Z]+$/.test(area_options.groupBy)) {
  							areas = map.find('area['+area_options.groupBy+'="'+$(area).attr(area_options.groupBy)+'"]');
  						} else {
  							areas = map.find(area_options.groupBy);
  						}
  						var first = area;
  						areas.each(function() {
  							if(this != first) {
  								var subarea_options = options_from_area(this, options);
  								if(!subarea_options.neverOn && !subarea_options.alwaysOn) {
  									var shape = shape_from_area(this);
  									add_shape_to(canvas, shape[0], shape[1], subarea_options, "highlighted");
  								}
  							}
  						});
  					}
  					// workaround for IE7, IE8 not rendering the final rectangle in a group
  					if(!has_canvas) {
  						$(canvas).append('<v:rect></v:rect>');
  					}
  				}
  			}).bind('mouseout.maphilight, blur.maphilight', function(e) { clear_canvas(canvas); });
  
  			img.before(canvas); // if we put this after, the mouseover events wouldn't fire.
  
  			img.addClass('maphilighted');
  		});
  	};
  	$.fn.maphilight.defaults = {
  		fill: false,
  		fillColor: '000000',
  		fillOpacity: 0.2,
  		stroke: true,
  		strokeColor: '000000',
  		strokeOpacity: 0.5,
  		strokeWidth: 1,
  		fade: true,
  		alwaysOn: false,
  		neverOn: false,
  		groupBy: false,
  		wrapClass: true,
  		// plenty of shadow:
  		shadow: true,
  		shadowX: 0,
  		shadowY: 0,
  		shadowRadius: 100,
  		shadowColor: 'ffd700',
  		shadowOpacity: 1.0,
  		shadowPosition: 'outside',
  		shadowFrom: false
  	};
  })(jQuery);
  