/*     
	Canvas Query 1.12
	http://canvasquery.org
	(c) 2012-2014 http://rezoner.net
	Canvas Query may be freely distributed under the MIT license.
*/

(function() {

	var COCOONJS = false;
	var NODEJS = !(typeof exports === "undefined");

	if (NODEJS) {
		var Canvas = require('canvas');
		var Image = Canvas.Image;
		var fs = require("fs");
	} else {
		var Canvas = window.HTMLCanvasElement;
		var Image = window.HTMLImageElement;
		var COCOONJS = navigator.isCocoonJS;
	}

	var cq = function(selector) {
		if (arguments.length === 0) {
			var canvas = cq.createCanvas(window.innerWidth, window.innerHeight);
			window.addEventListener("resize", function() {
				// canvas.width = window.innerWidth;
				// canvas.height = window.innerHeight;
			});
		} else if (typeof selector === "string") {
			var canvas = document.querySelector(selector);
		} else if (typeof selector === "number") {
			var canvas = cq.createCanvas(arguments[0], arguments[1]);
		} else if (selector instanceof Image) {
			var canvas = cq.createCanvas(selector);
		} else if (selector instanceof cq.Layer) {
			return selector;
		} else {
			var canvas = selector;
		}

		return new cq.Layer(canvas);
	};

	cq.lineSpacing = 1.0;

	cq.cocoon = function(selector) {
		if (arguments.length === 0) {
			var canvas = cq.createCocoonCanvas(window.innerWidth, window.innerHeight);
			window.addEventListener("resize", function() {});
		} else if (typeof selector === "string") {
			var canvas = document.querySelector(selector);
		} else if (typeof selector === "number") {
			var canvas = cq.createCocoonCanvas(arguments[0], arguments[1]);
		} else if (selector instanceof Image) {
			var canvas = cq.createCocoonCanvas(selector);
		} else if (selector instanceof cq.Layer) {
			return selector;
		} else {
			var canvas = selector;
		}

		return new cq.Layer(canvas);
	}


	cq.extend = function() {
		for (var i = 1; i < arguments.length; i++) {
			for (var j in arguments[i]) {
				arguments[0][j] = arguments[i][j];
			}
		}

		return arguments[0];
	};

	cq.augment = function() {
		for (var i = 1; i < arguments.length; i++) {
			_.extend(arguments[0], arguments[i]);
			arguments[i](arguments[0]);
		}
	};

	cq.distance = function(x1, y1, x2, y2) {
		if (arguments.length > 2) {
			var dx = x1 - x2;
			var dy = y1 - y2;

			return Math.sqrt(dx * dx + dy * dy);
		} else {
			return Math.abs(x1 - y1);
		}
	};

	cq.extend(cq, {

		smoothing: true,

		blend: function(below, above, mode, mix) {

			if (typeof mix === "undefined") mix = 1;

			var below = cq(below);
			var mask = below.clone();
			var above = cq(above);

			below.save();
			below.globalAlpha(mix);
			below.globalCompositeOperation(mode);
			below.drawImage(above.canvas, 0, 0);
			below.restore();

			mask.save();
			mask.globalCompositeOperation("source-in");
			mask.drawImage(below.canvas, 0, 0);
			mask.restore();

			return mask;
		},

		matchColor: function(color, palette) {
			var rgbPalette = [];

			for (var i = 0; i < palette.length; i++) {
				rgbPalette.push(cq.color(palette[i]));
			}

			var imgData = cq.color(color);

			var difList = [];
			for (var j = 0; j < rgbPalette.length; j++) {
				var rgbVal = rgbPalette[j];
				var rDif = Math.abs(imgData[0] - rgbVal[0]),
					gDif = Math.abs(imgData[1] - rgbVal[1]),
					bDif = Math.abs(imgData[2] - rgbVal[2]);
				difList.push(rDif + gDif + bDif);
			}

			var closestMatch = 0;
			for (var j = 0; j < palette.length; j++) {
				if (difList[j] < difList[closestMatch]) {
					closestMatch = j;
				}
			}

			return palette[closestMatch];
		},

		temp: function(width, height) {
			if (!this.tempLayer) {
				this.tempLayer = cq(1, 1);
			}

			if (width instanceof Image) {
				this.tempLayer.width = width.width;
				this.tempLayer.height = width.height;
				this.tempLayer.context.drawImage(width, 0, 0);
			} else if (width instanceof Canvas) {
				this.tempLayer.width = width.width;
				this.tempLayer.height = width.height;
				this.tempLayer.context.drawImage(width, 0, 0);
			} else if (width instanceof CanvasQuery.Layer) {
				this.tempLayer.width = width.width;
				this.tempLayer.height = width.height;
				this.tempLayer.context.drawImage(width.canvas, 0, 0);
			} else {
				this.tempLayer.width = width;
				this.tempLayer.height = height;
			}

			return this.tempLayer;
		},

		wrapValue: function(value, min, max) {
			if (value < min) return max + (value % max);
			if (value >= max) return value % max;
			return value;
		},

		limitValue: function(value, min, max) {
			return value < min ? min : value > max ? max : value;
		},

		mix: function(a, b, amount) {
			return a + (b - a) * amount;
		},

		hexToRgb: function(hex) {
			if (hex.length === 7) return ['0x' + hex[1] + hex[2] | 0, '0x' + hex[3] + hex[4] | 0, '0x' + hex[5] + hex[6] | 0];
			else return ['0x' + hex[1] + hex[1] | 0, '0x' + hex[2] + hex[2] | 0, '0x' + hex[3] + hex[3] | 0];
		},

		rgbToHex: function(r, g, b) {
			return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1, 7);
		},

		/* author: http://mjijackson.com/ */

		rgbToHsl: function(r, g, b) {

			if (r instanceof Array) {
				b = r[2];
				g = r[1];
				r = r[0];
			}

			r /= 255, g /= 255, b /= 255;
			var max = Math.max(r, g, b),
				min = Math.min(r, g, b);
			var h, s, l = (max + min) / 2;

			if (max == min) {
				h = s = 0; // achromatic
			} else {
				var d = max - min;
				s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
				switch (max) {
					case r:
						h = (g - b) / d + (g < b ? 6 : 0);
						break;
					case g:
						h = (b - r) / d + 2;
						break;
					case b:
						h = (r - g) / d + 4;
						break;
				}
				h /= 6;
			}

			return [h, s, l];
		},

		/* author: http://mjijackson.com/ */

		hue2rgb: function(p, q, t) {
			if (t < 0) t += 1;
			if (t > 1) t -= 1;
			if (t < 1 / 6) return p + (q - p) * 6 * t;
			if (t < 1 / 2) return q;
			if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
			return p;
		},

		hslToRgb: function(h, s, l) {
			var r, g, b;

			if (s == 0) {
				r = g = b = l; // achromatic
			} else {

				var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
				var p = 2 * l - q;
				r = this.hue2rgb(p, q, h + 1 / 3);
				g = this.hue2rgb(p, q, h);
				b = this.hue2rgb(p, q, h - 1 / 3);
			}

			return [r * 255 | 0, g * 255 | 0, b * 255 | 0];
		},

		rgbToHsv: function(r, g, b) {
			if (r instanceof Array) {
				b = r[2];
				g = r[1];
				r = r[0];
			}

			r = r / 255, g = g / 255, b = b / 255;
			var max = Math.max(r, g, b),
				min = Math.min(r, g, b);
			var h, s, v = max;

			var d = max - min;
			s = max == 0 ? 0 : d / max;

			if (max == min) {
				h = 0; // achromatic
			} else {
				switch (max) {
					case r:
						h = (g - b) / d + (g < b ? 6 : 0);
						break;
					case g:
						h = (b - r) / d + 2;
						break;
					case b:
						h = (r - g) / d + 4;
						break;
				}
				h /= 6;
			}

			return [h, s, v];
		},

		hsvToRgb: function(h, s, v) {
			var r, g, b;

			var i = Math.floor(h * 6);
			var f = h * 6 - i;
			var p = v * (1 - s);
			var q = v * (1 - f * s);
			var t = v * (1 - (1 - f) * s);

			switch (i % 6) {
				case 0:
					r = v, g = t, b = p;
					break;
				case 1:
					r = q, g = v, b = p;
					break;
				case 2:
					r = p, g = v, b = t;
					break;
				case 3:
					r = p, g = q, b = v;
					break;
				case 4:
					r = t, g = p, b = v;
					break;
				case 5:
					r = v, g = p, b = q;
					break;
			}

			return [r * 255, g * 255, b * 255];
		},

		color: function() {
			var result = new cq.Color();
			result.parse(arguments[0], arguments[1]);
			return result;
		},

		poolArray: [],

		pool: function() {

			if (!this.poolArray.length) {
				for (var i = 0; i < 100; i++) {
					this.poolArray.push(this.createCanvas(1, 1));
				}
			}

			return this.poolArray.pop();

		},

		createCanvas: function(width, height) {
			if (NODEJS) {
				var result = new Canvas;
			} else {
				var result = document.createElement("canvas");
			}

			if (arguments[0] instanceof Image || arguments[0] instanceof Canvas) {
				var image = arguments[0];
				result.width = image.width;
				result.height = image.height;
				result.getContext("2d").drawImage(image, 0, 0);
			} else {
				result.width = width;
				result.height = height;
			}


			return result;
		},

		createCocoonCanvas: function(width, height) {
			var result = document.createElement("screencanvas");

			if (arguments[0] instanceof Image) {
				var image = arguments[0];
				result.width = image.width;
				result.height = image.height;
				result.getContext("2d").drawImage(image, 0, 0);
			} else {
				result.width = width;
				result.height = height;
			}

			return result;
		},

		createImageData: function(width, height) {
			return cq.createCanvas(width, height).getContext("2d").createImageData(width, height);
		}

	});

	cq.Layer = function(canvas) {
		this.context = canvas.getContext("2d");
		this.canvas = canvas;
		this.update();
	}

	cq.Layer.prototype = {

		update: function() {
			this.context.webkitImageSmoothingEnabled = cq.smoothing;
			this.context.mozImageSmoothingEnabled = cq.smoothing;
			this.context.msImageSmoothingEnabled = cq.smoothing;
			this.context.imageSmoothingEnabled = cq.smoothing;

			if (COCOONJS) Cocoon.Utils.setAntialias(cq.smoothing);
		},

		appendTo: function(selector) {
			if (typeof selector === "object") {
				var element = selector;
			} else {
				var element = document.querySelector(selector);
			}

			element.appendChild(this.canvas);

			return this;
		},

		a: function(a) {
			if (arguments.length) {
				this.previousAlpha = this.globalAlpha();
				return this.globalAlpha(a);
			} else
				return this.globalAlpha();
		},

		ra: function() {
			return this.a(this.previousAlpha);
		},

		drawRegion: function(image, region, x, y, scale) {
			scale = scale || 1;

			return this.drawImage(
				image, region[0], region[1], region[2], region[3],
				x | 0, y | 0, region[2] * scale | 0, region[3] * scale | 0
			);
		},

		cache: function() {
			return this.clone().canvas;

			/* FFS .... image.src is no longer synchronous when assigning dataURL */

			var image = new Image;
			image.src = this.canvas.toDataURL();
			return image;
		},

		blendOn: function(what, mode, mix) {
			cq.blend(what, this, mode, mix);

			return this;
		},

		posterize: function(pc, inc) {
			pc = pc || 32;
			inc = inc || 4;
			var imgdata = this.getImageData(0, 0, this.width, this.height);
			var data = imgdata.data;

			for (var i = 0; i < data.length; i += inc) {
				data[i] -= data[i] % pc; // set value to nearest of 8 possibilities
				data[i + 1] -= data[i + 1] % pc; // set value to nearest of 8 possibilities
				data[i + 2] -= data[i + 2] % pc; // set value to nearest of 8 possibilities
			}

			this.putImageData(imgdata, 0, 0); // put image data to canvas
		},


		bw: function(pc) {
			pc = 128;
			var imgdata = this.getImageData(0, 0, this.width, this.height);
			var data = imgdata.data;
			// 8-bit: rrr ggg bb
			for (var i = 0; i < data.length; i += 4) {
				var v = ((data[i] + data[i + 1] + data[i + 2]) / 3);

				v = (v / 128 | 0) * 128;
				//data[i] = v; // set value to nearest of 8 possibilities
				//data[i + 1] = v; // set value to nearest of 8 possibilities
				data[i + 2] = (v / 255) * data[i]; // set value to nearest of 8 possibilities

			}

			this.putImageData(imgdata, 0, 0); // put image data to canvas
		},

		blend: function(what, mode, mix) {
			if (typeof what === "string") {
				var color = what;
				what = cq(this.canvas.width, this.canvas.height);
				what.fillStyle(color).fillRect(0, 0, this.canvas.width, this.canvas.height);
			}

			var result = cq.blend(this, what, mode, mix);

			this.canvas = result.canvas;
			this.context = result.context;

			return this;
		},


		textWithBackground: function(text, x, y, background, padding) {
			var w = this.measureText(text).width;
			var h = this.fontHeight() * 0.8;
			var f = this.fillStyle();
			var padding = padding || 2;

			this.fillStyle(background).fillRect(x - w / 2 - padding * 2, y - padding, w + padding * 4, h + padding * 2)
			this.fillStyle(f).textAlign("center").textBaseline("top").fillText(text, x, y);

			return this;
		},

		fillCircle: function(x, y, r) {
			this.context.beginPath();
			this.context.arc(x, y, r, 0, Math.PI * 2);
			this.context.fill();
			return this;
		},

		strokeCircle: function(x, y, r) {
			this.context.beginPath();
			this.context.arc(x, y, r, 0, Math.PI * 2);
			this.context.stroke();
			return this;
		},

		circle: function(x, y, r) {
			this.context.beginPath();
			this.context.arc(x, y, r, 0, Math.PI * 2);
			return this;
		},

		crop: function(x, y, w, h) {

			if (arguments.length === 1) {

				var y = arguments[0][1];
				var w = arguments[0][2];
				var h = arguments[0][3];
				var x = arguments[0][0];
			}

			var canvas = cq.createCanvas(w, h);
			var context = canvas.getContext("2d");

			context.drawImage(this.canvas, x, y, w, h, 0, 0, w, h);
			this.canvas.width = w;
			this.canvas.height = h;
			this.clear();
			this.context.drawImage(canvas, 0, 0);

			return this;
		},

		set: function(properties) {
			cq.extend(this.context, properties);
		},

		resize: function(width, height) {
			var w = width,
				h = height;

			if (arguments.length === 1) {
				w = arguments[0] * this.canvas.width | 0;
				h = arguments[0] * this.canvas.height | 0;
			} else {

				if (height === false) {
					if (this.canvas.width > width) {
						h = this.canvas.height * (width / this.canvas.width) | 0;
						w = width;
					} else {
						w = this.canvas.width;
						h = this.canvas.height;
					}
				} else if (width === false) {
					if (this.canvas.width > width) {
						w = this.canvas.width * (height / this.canvas.height) | 0;
						h = height;
					} else {
						w = this.canvas.width;
						h = this.canvas.height;
					}
				}
			}

			var cqresized = cq(w, h).drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, 0, 0, w, h);
			this.canvas = cqresized.canvas;
			this.context = cqresized.context;

			return this;
		},

		imageLine: function(image, region, x, y, ex, ey, scale) {
			if (!region) region = [0, 0, image.width, image.height];

			var distance = cq.distance(x, y, ex, ey);
			var count = distance / region[3] + 0.5 | 0;
			var angle = Math.atan2(ey - y, ex - x) + Math.PI / 2;

			this.save();

			this.translate(x, y);
			this.rotate(angle);

			if (scale) this.scale(scale, 1.0);

			for (var i = 0; i <= count; i++) {
				this.drawRegion(image, region, -region[2] / 2 | 0, -region[3] * (i + 1));
			}

			this.restore();

			return this;
		},

		trim: function(color, changes) {
			var transparent;

			if (color) {
				color = cq.color(color).toArray();
				transparent = !color[3];
			} else transparent = true;

			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			var bound = [this.canvas.width, this.canvas.height, 0, 0];

			var width = this.canvas.width;
			var height = this.canvas.height;

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				if (transparent) {
					if (!sourcePixels[i + 3]) continue;
				} else if (sourcePixels[i + 0] === color[0] && sourcePixels[i + 1] === color[1] && sourcePixels[i + 2] === color[2]) continue;

				var x = (i / 4 | 0) % this.canvas.width | 0;
				var y = (i / 4 | 0) / this.canvas.width | 0;

				if (x < bound[0]) bound[0] = x;
				if (x > bound[2]) bound[2] = x;

				if (y < bound[1]) bound[1] = y;
				if (y > bound[3]) bound[3] = y;
			}


			if (bound[2] === 0 && bound[3] === 0) {} else {
				if (changes) {
					changes.left = bound[0];
					changes.top = bound[1];

					changes.bottom = height - bound[3];
					changes.right = width - bound[2] - bound[0];

					changes.width = bound[2] - bound[0];
					changes.height = bound[3] - bound[1];
				}

				this.crop(bound[0], bound[1], bound[2] - bound[0] + 1, bound[3] - bound[1] + 1);
			}

			return this;
		},

		matchPalette: function(palette) {
			var imgData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);

			var rgbPalette = [];

			for (var i = 0; i < palette.length; i++) {
				rgbPalette.push(cq.color(palette[i]));
			}


			for (var i = 0; i < imgData.data.length; i += 4) {
				var difList = [];
				if (!imgData.data[i + 3]) continue;

				for (var j = 0; j < rgbPalette.length; j++) {
					var rgbVal = rgbPalette[j];
					var rDif = Math.abs(imgData.data[i] - rgbVal[0]),
						gDif = Math.abs(imgData.data[i + 1] - rgbVal[1]),
						bDif = Math.abs(imgData.data[i + 2] - rgbVal[2]);
					difList.push(rDif + gDif + bDif);
				}

				var closestMatch = 0;

				for (var j = 0; j < palette.length; j++) {
					if (difList[j] < difList[closestMatch]) {
						closestMatch = j;
					}
				}

				var paletteRgb = cq.hexToRgb(palette[closestMatch]);
				imgData.data[i] = paletteRgb[0];
				imgData.data[i + 1] = paletteRgb[1];
				imgData.data[i + 2] = paletteRgb[2];

				/* dithering */
				//imgData.data[i + 3] = (255 * Math.random() < imgData.data[i + 3]) ? 255 : 0;

				//imgData.data[i + 3] = imgData.data[i + 3] > 128 ? 255 : 0;
				/*
				if (i % 3 === 0) {
					imgData.data[i] -= cq.limitValue(imgData.data[i] - 50, 0, 255);
					imgData.data[i + 1] -= cq.limitValue(imgData.data[i + 1] - 50, 0, 255);
					imgData.data[i + 2] -= cq.limitValue(imgData.data[i + 2] - 50, 0, 255);
				}
				*/

			}

			this.context.putImageData(imgData, 0, 0);

			return this;
		},

		getPalette: function() {
			var palette = [];
			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				if (sourcePixels[i + 3]) {
					var hex = cq.rgbToHex(sourcePixels[i + 0], sourcePixels[i + 1], sourcePixels[i + 2]);
					if (palette.indexOf(hex) === -1) palette.push(hex);
				}
			}

			return palette;
		},

		mapPalette: function() {

		},

		polygon: function(array) {

			this.beginPath();

			this.moveTo(array[0][0], array[0][1]);

			for (var i = 1; i < array.length; i++) {
				this.lineTo(array[i][0], array[i][1]);
			}

			this.closePath();

			return this;
		},

		colorToMask: function(color, inverted) {
			color = cq.color(color).toArray();
			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			var mask = [];

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				if (sourcePixels[i + 3] > 0) mask.push(inverted ? false : true);
				else mask.push(inverted ? true : false);
			}

			return mask;
		},

		grayscaleToMask: function() {

			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			var mask = [];

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				mask.push(((sourcePixels[i + 0] + sourcePixels[i + 1] + sourcePixels[i + 2]) / 3) / 255);
			}

			return mask;
		},

		applyMask: function(mask) {
			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			var mode = typeof mask[0] === "boolean" ? "bool" : "byte";

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				var value = mask[i / 4];
				sourcePixels[i + 3] = value * 255 | 0;
			}

			this.context.putImageData(sourceData, 0, 0);
			return this;
		},

		fillMask: function(mask) {

			var sourceData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var sourcePixels = sourceData.data;

			var maskType = typeof mask[0] === "boolean" ? "bool" : "byte";
			var colorMode = arguments.length === 2 ? "normal" : "gradient";

			var color = cq.color(arguments[1]);
			if (colorMode === "gradient") colorB = cq.color(arguments[2]);

			for (var i = 0, len = sourcePixels.length; i < len; i += 4) {
				var value = mask[i / 4];

				if (maskType === "byte") value /= 255;

				if (colorMode === "normal") {
					if (value) {
						sourcePixels[i + 0] = color[0] | 0;
						sourcePixels[i + 1] = color[1] | 0;
						sourcePixels[i + 2] = color[2] | 0;
						sourcePixels[i + 3] = value * 255 | 0;
					}
				} else {
					sourcePixels[i + 0] = color[0] + (colorB[0] - color[0]) * value | 0;
					sourcePixels[i + 1] = color[1] + (colorB[1] - color[1]) * value | 0;
					sourcePixels[i + 2] = color[2] + (colorB[2] - color[2]) * value | 0;
					sourcePixels[i + 3] = 255;
				}
			}

			this.context.putImageData(sourceData, 0, 0);
			return this;
		},

		clear: function(color) {
			if (color) {
				this.context.fillStyle = color;
				this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
			} else {
				this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
			}

			return this;
		},

		clone: function() {

			// var result = cq.createCanvas(this.canvas);

			var result = cq.pool();
			result.width = this.width;
			result.height = this.height;
			result.getContext("2d").drawImage(this.canvas, 0, 0);

			return cq(result);
		},

		gradientText: function(text, x, y, maxWidth, gradient) {

			var words = text.split(" ");

			var h = this.fontHeight() * 2;

			var ox = 0;
			var oy = 0;

			if (maxWidth) {
				var line = 0;
				var lines = [""];

				for (var i = 0; i < words.length; i++) {
					var word = words[i] + " ";
					var wordWidth = this.context.measureText(word).width;

					if (ox + wordWidth > maxWidth) {
						lines[++line] = "";
						ox = 0;
					}

					lines[line] += word;

					ox += wordWidth;
				}
			} else var lines = [text];

			for (var i = 0; i < lines.length; i++) {
				var oy = y + i * h * 0.6 | 0;
				var lingrad = this.context.createLinearGradient(0, oy, 0, oy + h * 0.6 | 0);

				for (var j = 0; j < gradient.length; j += 2) {
					lingrad.addColorStop(gradient[j], gradient[j + 1]);
				}

				var text = lines[i];

				this.fillStyle(lingrad).fillText(text, x, oy);
			}

			return this;
		},

		removeColor: function(color) {

			color = cq.color(color);

			var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var pixels = data.data;

			for (var x = 0; x < this.canvas.width; x++) {
				for (var y = 0; y < this.canvas.height; y++) {
					var i = (y * this.canvas.width + x) * 4;

					if (pixels[i + 0] === color[0] && pixels[i + 1] === color[1] && pixels[i + 2] === color[2]) {
						pixels[i + 3] = 0;
					}


				}
			}

			this.clear();
			this.context.putImageData(data, 0, 0);

			return this;
		},

		outline: function() {
			var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var pixels = data.data;

			var newData = this.createImageData(this.canvas.width, this.canvas.height);
			var newPixels = newData.data;

			var canvas = this.canvas;

			function check(x, y) {

				if (x < 0) return 0;
				if (x >= canvas.width) return 0;
				if (y < 0) return 0;
				if (y >= canvas.height) return 0;

				var i = (x + y * canvas.width) * 4;

				return pixels[i + 3] > 0;

			}

			for (var x = 0; x < this.canvas.width; x++) {
				for (var y = 0; y < this.canvas.height; y++) {

					var full = 0;
					var i = (y * canvas.width + x) * 4;

					if (!pixels[i + 3]) continue;

					full += check(x - 1, y);
					full += check(x + 1, y);
					full += check(x, y - 1);
					full += check(x, y + 1);

					if (full !== 4) {

						newPixels[i] = 255;
						newPixels[i + 1] = 255;
						newPixels[i + 2] = 255;
						newPixels[i + 3] = 255;
					}

				}
			}

			this.context.putImageData(newData, 0, 0);

			return this;
		},

		setHsl: function() {

			if (arguments.length === 1) {
				var args = arguments[0];
			} else {
				var args = arguments;
			}

			var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var pixels = data.data;
			var r, g, b, a, h, s, l, hsl = [],
				newPixel = [];

			for (var i = 0, len = pixels.length; i < len; i += 4) {
				hsl = cq.rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);

				h = args[0] === false ? hsl[0] : cq.limitValue(args[0], 0, 1);
				s = args[1] === false ? hsl[1] : cq.limitValue(args[1], 0, 1);
				l = args[2] === false ? hsl[2] : cq.limitValue(args[2], 0, 1);

				newPixel = cq.hslToRgb(h, s, l);

				pixels[i + 0] = newPixel[0];
				pixels[i + 1] = newPixel[1];
				pixels[i + 2] = newPixel[2];
			}

			this.context.putImageData(data, 0, 0);

			return this;
		},

		shiftHsl: function() {

			if (arguments.length === 1) {
				var args = arguments[0];
			} else {
				var args = arguments;
			}

			var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var pixels = data.data;
			var r, g, b, a, h, s, l, hsl = [],
				newPixel = [];

			for (var i = 0, len = pixels.length; i < len; i += 4) {
				hsl = cq.rgbToHsl(pixels[i + 0], pixels[i + 1], pixels[i + 2]);

				if (pixels[i + 0] !== pixels[i + 1] || pixels[i + 1] !== pixels[i + 2]) {
					h = args[0] === false ? hsl[0] : cq.wrapValue(hsl[0] + args[0], 0, 1);
					s = args[1] === false ? hsl[1] : cq.limitValue(hsl[1] + args[1], 0, 1);
				} else {
					h = hsl[0];
					s = hsl[1];
				}

				l = args[2] === false ? hsl[2] : cq.limitValue(hsl[2] + args[2], 0, 1);

				newPixel = cq.hslToRgb(h, s, l);

				pixels[i + 0] = newPixel[0];
				pixels[i + 1] = newPixel[1];
				pixels[i + 2] = newPixel[2];
			}


			this.context.putImageData(data, 0, 0);

			return this;
		},

		applyColor: function(color) {

			if (COCOONJS) return this;
			this.save();

			this.globalCompositeOperation("source-in");
			this.clear(color);

			this.restore();

			return this;
		},

		negative: function(src, dst) {

			var data = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			var pixels = data.data;
			var r, g, b, a, h, s, l, hsl = [],
				newPixel = [];

			for (var i = 0, len = pixels.length; i < len; i += 4) {
				pixels[i + 0] = 255 - pixels[i + 0];
				pixels[i + 1] = 255 - pixels[i + 1];
				pixels[i + 2] = 255 - pixels[i + 2];
			}

			this.context.putImageData(data, 0, 0);

			return this;
		},

		roundRect: function(x, y, width, height, radius) {

			this.beginPath();
			this.moveTo(x + radius, y);
			this.lineTo(x + width - radius, y);
			this.quadraticCurveTo(x + width, y, x + width, y + radius);
			this.lineTo(x + width, y + height - radius);
			this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
			this.lineTo(x + radius, y + height);
			this.quadraticCurveTo(x, y + height, x, y + height - radius);
			this.lineTo(x, y + radius);
			this.quadraticCurveTo(x, y, x + radius, y);
			this.closePath();

			return this;
		},

		markupText: function(text) {


		},

		wrappedText: function(text, x, y, maxWidth, newlineCallback) {

			var words = text.split(" ");

			var lineHeight = this.fontHeight();

			var ox = 0;
			var oy = 0;

			if (maxWidth) {
				var line = 0;
				var lines = [""];

				for (var i = 0; i < words.length; i++) {
					var word = words[i] + " ";
					var wordWidth = this.context.measureText(word).width;

					if (ox + wordWidth > maxWidth || words[i] === "\n") {
						lines[++line] = "";
						ox = 0;
					}
					if (words[i] !== "\n") {
						lines[line] += word;

						ox += wordWidth;
					}


				}
			} else {
				var lines = [text];
			}

			for (var i = 0; i < lines.length; i++) {
				var oy = y + i * lineHeight | 0;

				var text = lines[i];

				if (newlineCallback) newlineCallback.call(this, x, y + oy);

				this.fillText(text, x, oy);
			}

			return this;
		},

		fontHeights: {},

		fontHeight: function() {
			var font = this.font();

			if (!this.fontHeights[font]) {
				var temp = cq(100, 100);
				var height = 0;
				var changes = {};
				temp.font(font).fillStyle("#fff");
				temp.textBaseline("bottom").fillText("gM", 25, 100);
				temp.trim(false, changes);
				height += changes.bottom;

				var temp = cq(100, 100);
				var changes = {};
				temp.font(font).fillStyle("#fff");
				temp.textBaseline("top").fillText("gM", 25, 0);
				temp.trim(false, changes);
				height += changes.top;

				var temp = cq(100, 100);
				var changes = {};
				temp.font(font).fillStyle("#fff");
				temp.textBaseline("alphabetic").fillText("gM", 50, 50);
				temp.trim(false, changes);
				height += temp.height;

				this.fontHeights[font] = height;
			}

			return this.fontHeights[font];
		},

		textBoundaries: function(text, maxWidth) {
			var words = text.split(" ");

			var h = this.fontHeight();

			var ox = 0;
			var oy = 0;

			if (maxWidth) {
				var line = 0;
				var lines = [""];

				for (var i = 0; i < words.length; i++) {
					var word = words[i] + " ";
					var wordWidth = this.context.measureText(word).width;

					if (ox + wordWidth > maxWidth || words[i] === "\n") {
						lines[++line] = "";
						ox = 0;
					}

					if (words[i] !== "\n") {
						lines[line] += word;
						ox += wordWidth;
					}
				}
			} else {
				var lines = [text];
				maxWidth = this.measureText(text).width;
			}

			return {
				height: lines.length * h,
				width: maxWidth,
				lines: lines.length,
				lineHeight: h
			}
		},

		repeatImageRegion: function(image, sx, sy, sw, sh, dx, dy, dw, dh) {
			this.save();
			this.rect(dx, dy, dw, dh);
			this.clip();

			for (var x = 0, len = Math.ceil(dw / sw); x < len; x++) {
				for (var y = 0, leny = Math.ceil(dh / sh); y < leny; y++) {
					this.drawImage(image, sx, sy, sw, sh, dx + x * sw, dy + y * sh, sw, sh);
				}
			}

			this.restore();

			return this;
		},

		repeatImage: function(image, x, y, w, h) {
			// if (!env.details) return this;

			if (arguments.length < 9) {
				this.repeatImageRegion(image, 0, 0, image.width, image.height, x, y, w, h);
			} else {
				this.repeatImageRegion.apply(this, arguments);
			}

			return this;
		},

		borderImage: function(image, x, y, w, h, t, r, b, l, fill) {

			// if (!env.details) return this;

			if (typeof t === "object") {

				var bottomLeft = t.bottomLeft || [0, 0, 0, 0];
				var bottomRight = t.bottomRight || [0, 0, 0, 0];
				var topLeft = t.topLeft || [0, 0, 0, 0];
				var topRight = t.topRight || [0, 0, 0, 0];

				var clh = bottomLeft[3] + topLeft[3];
				var crh = bottomRight[3] + topRight[3];
				var ctw = topLeft[2] + topRight[2];
				var cbw = bottomLeft[2] + bottomRight[2];

				t.fillPadding = [0, 0, 0, 0];

				if (t.left) t.fillPadding[0] = t.left[2];
				if (t.top) t.fillPadding[1] = t.top[3];
				if (t.right) t.fillPadding[2] = t.right[2];
				if (t.bottom) t.fillPadding[3] = t.bottom[3];

				// if (!t.fillPadding) t.fillPadding = [0, 0, 0, 0];

				if (t.fill) {
					this.drawImage(image, t.fill[0], t.fill[1], t.fill[2], t.fill[3], x + t.fillPadding[0], y + t.fillPadding[1], w - t.fillPadding[2] - t.fillPadding[0], h - t.fillPadding[3] - t.fillPadding[1]);
				} else {
					// this.fillRect(x + t.fillPadding[0], y + t.fillPadding[1], w - t.fillPadding[2] - t.fillPadding[0], h - t.fillPadding[3] - t.fillPadding[1]);
				}

				if (t.left) this[t.left[4] === "stretch" ? "drawImage" : "repeatImage"](image, t.left[0], t.left[1], t.left[2], t.left[3], x, y + topLeft[3], t.left[2], h - clh);
				if (t.right) this[t.right[4] === "stretch" ? "drawImage" : "repeatImage"](image, t.right[0], t.right[1], t.right[2], t.right[3], x + w - t.right[2], y + topRight[3], t.right[2], h - crh);
				if (t.top) this[t.top[4] === "stretch" ? "drawImage" : "repeatImage"](image, t.top[0], t.top[1], t.top[2], t.top[3], x + topLeft[2], y, w - ctw, t.top[3]);
				if (t.bottom) this[t.bottom[4] === "stretch" ? "drawImage" : "repeatImage"](image, t.bottom[0], t.bottom[1], t.bottom[2], t.bottom[3], x + bottomLeft[2], y + h - t.bottom[3], w - cbw, t.bottom[3]);

				if (t.bottomLeft) this.drawImage(image, t.bottomLeft[0], t.bottomLeft[1], t.bottomLeft[2], t.bottomLeft[3], x, y + h - t.bottomLeft[3], t.bottomLeft[2], t.bottomLeft[3]);
				if (t.topLeft) this.drawImage(image, t.topLeft[0], t.topLeft[1], t.topLeft[2], t.topLeft[3], x, y, t.topLeft[2], t.topLeft[3]);
				if (t.topRight) this.drawImage(image, t.topRight[0], t.topRight[1], t.topRight[2], t.topRight[3], x + w - t.topRight[2], y, t.topRight[2], t.topRight[3]);
				if (t.bottomRight) this.drawImage(image, t.bottomRight[0], t.bottomRight[1], t.bottomRight[2], t.bottomRight[3], x + w - t.bottomRight[2], y + h - t.bottomRight[3], t.bottomRight[2], t.bottomRight[3]);


			} else {


				/* top */
				if (t > 0 && w - l - r > 0) this.drawImage(image, l, 0, image.width - l - r, t, x + l, y, w - l - r, t);

				/* bottom */
				if (b > 0 && w - l - r > 0) this.drawImage(image, l, image.height - b, image.width - l - r, b, x + l, y + h - b, w - l - r, b);
				//      console.log(x, y, w, h, t, r, b, l);
				//      console.log(image, 0, t, l, image.height - b - t, x, y + t, l, h - b - t);
				/* left */
				if (l > 0 && h - b - t > 0) this.drawImage(image, 0, t, l, image.height - b - t, x, y + t, l, h - b - t);


				/* right */
				if (r > 0 && h - b - t > 0) this.drawImage(image, image.width - r, t, r, image.height - b - t, x + w - r, y + t, r, h - b - t);

				/* top-left */
				if (l > 0 && t > 0) this.drawImage(image, 0, 0, l, t, x, y, l, t);

				/* top-right */
				if (r > 0 && t > 0) this.drawImage(image, image.width - r, 0, r, t, x + w - r, y, r, t);

				/* bottom-right */
				if (r > 0 && b > 0) this.drawImage(image, image.width - r, image.height - b, r, b, x + w - r, y + h - b, r, b);

				/* bottom-left */
				if (l > 0 && b > 0) this.drawImage(image, 0, image.height - b, l, b, x, y + h - b, l, b);

				if (fill) {
					if (typeof fill === "string") {
						this.fillStyle(fill).fillRect(x + l, y + t, w - l - r, h - t - b);
					} else {
						if (w - l - r > 0 && h - t - b > 0)
							this.drawImage(image, l, t, image.width - r - l, image.height - b - t, x + l, y + t, w - l - r, h - t - b);
					}
				}
			}
		},

		setPixel: function(color, x, y) {

			/* fillRect is slow! */

			return this.fillStyle(color).fillRect(x, y, 1, 1);

			/* this is how it should work - but it does not */

			color = cq.color(color);

			var pixel = this.createImageData(1, 1);

			pixel.data[0] = color[0];
			pixel.data[1] = color[1];
			pixel.data[2] = color[2];
			pixel.data[3] = 1.0;

			this.putImageData(pixel, x, y);

			return this;
		},

		getPixel: function(x, y) {
			var pixel = this.context.getImageData(x, y, 1, 1).data;
			return cq.color([pixel[0], pixel[1], pixel[2], pixel[3]]);
		},

		createImageData: function(width, height) {
			if (false && this.context.createImageData) {
				return this.context.createImageData.apply(this.context, arguments);
			} else {
				if (!this.emptyCanvas) {
					this.emptyCanvas = cq.createCanvas(width, height);
					this.emptyCanvasContext = this.emptyCanvas.getContext("2d");
				}

				this.emptyCanvas.width = width;
				this.emptyCanvas.height = height;
				return this.emptyCanvasContext.getImageData(0, 0, width, height);
			}
		},

		setLineDash: function(dash) {
			if (this.context.setLineDash) {
				this.context.setLineDash(dash);
				return this;
			} else return this;
		},

		measureText: function() {
			return this.context.measureText.apply(this.context, arguments);
		},

		getLineDash: function() {
			return this.context.getLineDash();
		},

		createRadialGradient: function() {
			return this.context.createRadialGradient.apply(this.context, arguments);
		},

		createLinearGradient: function() {
			return this.context.createLinearGradient.apply(this.context, arguments);
		},

		createPattern: function() {
			return this.context.createPattern.apply(this.context, arguments);
		},

		getImageData: function() {
			return this.context.getImageData.apply(this.context, arguments);
		},

		get width() {
			return this.canvas.width;
		},

		get height() {
			return this.canvas.height;
		},

		set width(w) {
			this.canvas.width = w;
			this.update();
			return this.canvas.width;
		},

		set height(h) {
			this.canvas.height = h;
			this.update();
			return this.canvas.height;
		}


	};

	/* extend Layer with drawing context methods */

	var methods = ["arc", "arcTo", "beginPath", "bezierCurveTo", "clearRect", "clip", "closePath", "createLinearGradient", "createRadialGradient", "createPattern", "drawFocusRing", "drawImage", "fill", "fillRect", "fillText", "getImageData", "isPointInPath", "lineTo", "measureText", "moveTo", "putImageData", "quadraticCurveTo", "rect", "restore", "rotate", "save", "scale", "setTransform", "stroke", "strokeRect", "strokeText", "transform", "translate", "setLineDash"];

	for (var i = 0; i < methods.length; i++) {
		var name = methods[i];

		// this.debug = true;

		if (cq.Layer.prototype[name]) continue;

		if (!this.debug) {
			// if (!cq.Layer.prototype[name]) cq.Layer.prototype[name] = Function("this.context." + name + ".apply(this.context, arguments); return this;");

			var self = this;

			(function(name) {

				cq.Layer.prototype[name] = function() {
					this.context[name].apply(this.context, arguments);
					return this;
				}

			})(name);

		} else {

			var self = this;

			(function(name) {

				cq.Layer.prototype[name] = function() {
					try {
						this.context[name].apply(this.context, arguments);
						return this;
					} catch (e) {
						var err = new Error();
						console.log(err.stack);
						throw (e + err.stack);

						console.log(e, name, arguments);
					}
				}

			})(name);

		}

	};

	/* create setters and getters */

	var properties = ["canvas", "fillStyle", "font", "globalAlpha", "globalCompositeOperation", "lineCap", "lineJoin", "lineWidth", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor", "strokeStyle", "textAlign", "textBaseline", "lineDashOffset"];

	for (var i = 0; i < properties.length; i++) {
		var name = properties[i];
		if (!cq.Layer.prototype[name]) cq.Layer.prototype[name] = Function("if(arguments.length) { this.context." + name + " = arguments[0]; return this; } else { return this.context." + name + "; }");
	};

	/* color */

	cq.Color = function(data, type) {

		if (arguments.length) this.parse(data, type);
	}

	cq.Color.prototype = {

		toString: function() {
			return this.toRgb();
		},

		parse: function(args, type) {
			if (args[0] instanceof cq.Color) {
				this[0] = args[0][0];
				this[1] = args[0][1];
				this[2] = args[0][2];
				this[3] = args[0][3];
				return;
			}

			if (typeof args === "string") {
				var match = null;

				if (args[0] === "#") {
					var rgb = cq.hexToRgb(args);
					this[0] = rgb[0];
					this[1] = rgb[1];
					this[2] = rgb[2];
					this[3] = 1.0;
				} else if (match = args.match(/rgb\((.*),(.*),(.*)\)/)) {
					this[0] = match[1] | 0;
					this[1] = match[2] | 0;
					this[2] = match[3] | 0;
					this[3] = 1.0;
				} else if (match = args.match(/rgba\((.*),(.*),(.*)\)/)) {
					this[0] = match[1] | 0;
					this[1] = match[2] | 0;
					this[2] = match[3] | 0;
					this[3] = match[4] | 0;
				} else if (match = args.match(/hsl\((.*),(.*),(.*)\)/)) {
					this.fromHsl(match[1], match[2], match[3]);
				} else if (match = args.match(/hsv\((.*),(.*),(.*)\)/)) {
					this.fromHsv(match[1], match[2], match[3]);
				}
			} else {
				switch (type) {
					case "hsl":
					case "hsla":

						this.fromHsl(args[0], args[1], args[2], args[3]);
						break;

					case "hsv":
					case "hsva":

						this.fromHsv(args[0], args[1], args[2], args[3]);
						break;

					default:
						this[0] = args[0];
						this[1] = args[1];
						this[2] = args[2];
						this[3] = typeof args[3] === "undefined" ? 1.0 : args[3];
						break;
				}
			}
		},

		a: function(a) {
			return this.alpha(a);
		},

		alpha: function(a) {
			this[3] = a;
			return this;
		},

		fromHsl: function() {
			var components = arguments[0] instanceof Array ? arguments[0] : arguments;
			var color = cq.hslToRgb(components[0], components[1], components[2]);

			this[0] = color[0];
			this[1] = color[1];
			this[2] = color[2];
			this[3] = typeof arguments[3] === "undefined" ? 1.0 : arguments[3];
		},

		fromHsv: function() {
			var components = arguments[0] instanceof Array ? arguments[0] : arguments;
			var color = cq.hsvToRgb(components[0], components[1], components[2]);

			this[0] = color[0];
			this[1] = color[1];
			this[2] = color[2];
			this[3] = typeof arguments[3] === "undefined" ? 1.0 : arguments[3];
		},

		toArray: function() {
			return [this[0], this[1], this[2], this[3]];
		},

		toRgb: function() {
			return "rgb(" + this[0] + ", " + this[1] + ", " + this[2] + ")";
		},

		toRgba: function() {
			return "rgba(" + this[0] + ", " + this[1] + ", " + this[2] + ", " + this[3] + ")";
		},

		toHex: function() {
			return cq.rgbToHex(this[0], this[1], this[2]);
		},

		toHsl: function() {
			var c = cq.rgbToHsl(this[0], this[1], this[2]);
			c[3] = this[3];
			return c;
		},

		toHsv: function() {
			var c = cq.rgbToHsv(this[0], this[1], this[2]);
			c[3] = this[3];
			return c;
		},

		gradient: function(target, steps) {
			var targetColor = cq.color(target);
		},

		shiftHsl: function() {
			var hsl = this.toHsl();

			if (this[0] !== this[1] || this[1] !== this[2]) {
				var h = arguments[0] === false ? hsl[0] : cq.wrapValue(hsl[0] + arguments[0], 0, 1);
				var s = arguments[1] === false ? hsl[1] : cq.limitValue(hsl[1] + arguments[1], 0, 1);
			} else {
				var h = hsl[0];
				var s = hsl[1];
			}

			var l = arguments[2] === false ? hsl[2] : cq.limitValue(hsl[2] + arguments[2], 0, 1);

			this.fromHsl(h, s, l);

			return this;
		},

		setHsl: function() {
			var hsl = this.toHsl();

			var h = arguments[0] === false ? hsl[0] : cq.limitValue(arguments[0], 0, 1);
			var s = arguments[1] === false ? hsl[1] : cq.limitValue(arguments[1], 0, 1);
			var l = arguments[2] === false ? hsl[2] : cq.limitValue(arguments[2], 0, 1);

			this.fromHsl(h, s, l);

			return this;
		}

	};

	if (NODEJS)
		global["cq"] = global["CanvasQuery"] = cq;
	else
		window["cq"] = window["CanvasQuery"] = cq;


	/* nodejs specific stuff */

	cq.Layer.prototype.saveAsPNG = function(path) {
		fs.writeFileSync(path, this.canvas.toBuffer());
	}

	cq.loadFromFile = function(path) {
		var buffer = fs.readFileSync(path);
		var img = new Image;
		img.src = buffer;
		return cq(img);
	}


})();