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

*/

function playground(args) {
	return new Playground(args);
};

/* utitlities */

playground.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];
};

playground.throttle = function(fn, threshold) {
	threshold || (threshold = 250);
	var last,
		deferTimer;
	return function() {
		var context = this;

		var now = +new Date,
			args = arguments;
		if (last && now < last + threshold) {
			// hold on to it
			clearTimeout(deferTimer);
			deferTimer = setTimeout(function() {
				last = now;
				fn.apply(context, args);
			}, threshold);
		} else {
			last = now;
			fn.apply(context, args);
		}
	};
};

/* constructor */

function Playground(args) {

	/* defaults */

	playground.extend(this, {
		smoothing: 1,
		scale: 1,
		preventKeyboardDefault: true,
		preventContextMenu: true
	}, args);


	if (!this.width || !this.height) this.fitToContainer = true;

	if (!this.container) this.container = document.body;
	if (this.container !== document.body) this.customContainer = true;
	if (typeof this.container === "string") this.container = document.querySelector(this.container);

	/* state */

	this.state = {};

	/* layer */

	if (!args.layer) {
		cq.smoothing = this.smoothing;

		if (navigator.isCocoonJS) {
			this.layer = cq.cocoon(1, 1);
			this.layer.appendTo(this.container);
			this.screen = this.layer;
		} else {
			this.layer = cq(1, 1);
			if (this.scaleToFit) {
				this.screen = cq(1, 1);
				this.screen.appendTo(this.container);
			} else {
				this.layer.appendTo(this.container);
				this.screen = this.layer;
			}
		}

	}

	var canvas = this.screen.canvas;

	/* events */

	this.eventsHandler = this.eventsHandler.bind(this);

	/* mouse */

	this.mouse = new playground.Mouse(canvas);
	this.mouse.on("event", this.eventsHandler);

	this.mouse.preventContextMenu = this.preventContextMenu;

	/* touch */

	this.touch = new playground.Touch(canvas);
	this.touch.on("event", this.eventsHandler);

	/* keyboard */

	this.keyboard = new playground.Keyboard();

	this.keyboard.preventDefault = this.preventKeyboardDefault;
	this.keyboard.on("event", this.eventsHandler);

	/* gamepads */

	this.gamepads = new playground.Gamepads();
	this.gamepads.on("event", this.eventsHandler);

	/* window resize */

	window.addEventListener("resize", this.resizeHandler.bind(this));

	setTimeout(this.resizeHandler.bind(this), 1);

	/* video recorder */

	this.videoRecorder = new playground.VideoRecorder(this);

	/* game loop */

	var self = this;

	var lastTick = Date.now();

	function step() {

		requestAnimationFrame(step);

		var delta = Date.now() - lastTick;
		lastTick = Date.now();

		if (delta > 1000) return;

		var dt = delta / 1000;

		if (self.loader.count <= 0) {

			if (self.step) self.step(dt);
			if (self.state.step) self.state.step(dt);

			if (self.render) self.render(dt);
			if (self.state.render) self.state.render(dt);

			if (self.postrender) self.postrender(dt);
			if (self.state.postrender) self.state.postrender(dt);

		} else {
			self.renderLoader(dt);
		}

		if (self.scaleToFit) {
			self.screen.save();
			self.screen.translate(self.offsetX, self.offsetY);
			self.screen.scale(self.scale, self.scale);
			// self.layer.drawImage(self.scanlines.canvas, 0, 0);
			self.screen.drawImage(self.layer.canvas, 0, 0);
			self.screen.restore();
		}

		self.gamepads.step(dt);
		self.videoRecorder.step(dt);

	};

	requestAnimationFrame(step);

	/* assets */

	/* default audio format */

	var canPlayMp3 = (new Audio).canPlayType("audio/mp3");
	var canPlayOgg = (new Audio).canPlayType('audio/ogg; codecs="vorbis"');

	if (canPlayMp3) this.audioFormat = "mp3";
	else this.audioFormat = "ogg";

	this.loader = new playground.Loader();

	this.images = {};

	var audioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext;

	if (audioContext) {
		this.sound = new Playground.Sound(this);
	} else {
		this.sound = new Playground.SoundFallback(this);
	}

	this.loadFoo(0.5);

	if (this.create) setTimeout(this.create.bind(this));

	this.loader.on("ready", function() {
		if (self.ready) self.ready();

		self.ready = function() {};
	});



};

Playground.prototype = {

	setState: function(state) {
		state.app = this;

		if (this.state && this.state.leave) this.state.leave();

		this.state = state;

		if (this.state && this.state.enter) this.state.enter();
	},

	eventsHandler: function(event, data) {

		if (this[event]) this[event](data);
		if (this.state.proxy) this.state.proxy(event, data);
		if (this.state[event]) this.state[event](data);

	},

	resizeHandler: function() {

		if (this.customContainer) {
			var containerWidth = this.container.offsetWidth;
			var containerHeight = this.container.offsetHeight;
		} else {
			var containerWidth = window.innerWidth;
			var containerHeight = window.innerHeight;
		}

		if (this.fitToContainer) {
			this.width = this.containerWidth;
			this.height = this.containerHeight;
		}

		if (!this.scaleToFit) {

			if (this.fitToContainer) {
				this.width = containerWidth;
				this.height = containerHeight;
			}

			this.offsetX = 0;
			this.offsetY = 0;

		} else {

			this.screen.width = containerWidth;
			this.screen.height = containerHeight;

			this.scale = Math.min(containerWidth / this.width, containerHeight / this.height);

			if (this.roundScale) this.scale = Math.max(1, Math.floor(this.scale));

			this.offsetX = containerWidth / 2 - this.scale * (this.width / 2) | 0;
			this.offsetY = containerHeight / 2 - this.scale * (this.height / 2) | 0;

			this.mouse.scale = this.scale;
			this.mouse.offsetX = this.offsetX;
			this.mouse.offsetY = this.offsetY;

			this.touch.scale = this.scale;
			this.touch.offsetX = this.offsetX;
			this.touch.offsetY = this.offsetY;
		}

		this.layer.width = this.width;
		this.layer.height = this.height;

		this.center = {
			x: this.width / 2 | 0,
			y: this.height / 2 | 0
		};

		this.screen.clear("#000");

		this.eventsHandler("resize");

		this.mouse.update();
		this.touch.update();
	},

	renderLoader: function() {

		var height = this.height / 10 | 0;
		var x = 32;
		var width = this.width - x * 2;
		var y = this.height / 2 - height / 2 | 0;

		this.layer.clear("#000");
		this.layer.strokeStyle("#fff").lineWidth(2).strokeRect(x, y, width, height);
		this.layer.fillStyle("#fff").fillRect(x, y, width * this.loader.progress | 0, height);

	},

	record: function(args) {
		this.videoRecorder.toggle(args);
	},

	/* imaginary timeout to delay loading */

	loadFoo: function(timeout) {
		if (!this.foofooLoader) this.foofooLoader = 0;

		var loader = this.loader;

		this.loader.add("foo " + timeout);

		setTimeout(function() {
			loader.ready("foo " + timeout);
		}, (this.foofooLoader += timeout * 1000));

	},

	/* images */

	loadImages: function() {

		for (var i = 0; i < arguments.length; i++) {

			var arg = arguments[i];

			/* polymorphism at its finest */

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

				for (var key in arg) this.addImages(arg[key]);

			} else {

				/* if argument is not an object/array let's try to load it */

				var filename = arg;

				var loader = this.loader;

				var fileinfo = filename.match(/(.*)\..*/);
				var key = fileinfo ? fileinfo[1] : filename;

				/* filename defaults to png */

				if (!fileinfo) filename += ".png";

				var path = "images/" + filename;

				this.loader.add(path);

				var image = this.images[key] = new Image;

				image.addEventListener("load", function() {
					loader.ready(path);
				});

				image.addEventListener("error", function() {
					loader.error(path);
				});

				image.src = path;
			}
		}
	},

	/* sounds */

	loadSounds: function() {

		for (var i = 0; i < arguments.length; i++) {

			var arg = arguments[i];

			/* polymorphism at its finest */

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

				for (var key in arg) this.loadSounds(arg[key]);

			} else {
				this.sound.load(arg);
			}
		}

	},

	loadFont: function(name) {
		var styleNode = document.createElement("style");
		styleNode.type = "text/css";

		var formats = {
			"woff": "woff",
			"ttf": "truetype"
		};

		var sources = "";

		for (var ext in formats) {
			var type = formats[ext];
			sources += " url(\"fonts/" + name + "." + ext + "\") format('" + type + "');"
		}

		styleNode.textContent = "@font-face { font-family: '" + name + "'; src: " + sources + " }";

		document.head.appendChild(styleNode);

		var layer = cq(32, 32);

		layer.font("10px huj");
		layer.fillText(16, 16, 16).trim();

		var width = layer.width;
		var height = layer.height;

		this.loader.add("font " + name);

		var self = this;

		function check() {
			var layer = cq(32, 32);
			layer.font("10px " + name).fillText(16, 16, 16);
			layer.trim();

			if (layer.width !== width || layer.height !== height) {
				self.loader.ready("font " + name);
			} else {
				setTimeout(check, 250);
			}
		};

		check();
	},

	playSound: function(key, loop) {

		if (!this.audioChannels) {
			this.audioChannels = [];

			for (var i = 0; i < 16; i++) this.audioChannels.push(new Audio);

			this.audioChannelIndex = 0;
		}

		return this.sound.play(key, loop);

	},

	stopSound: function(sound) {
		this.sound.stop(sound);
	}


};

playground.Events = function() {

	this.listeners = {};

};

playground.Events.prototype = {

	on: function(event, callback) {
		if (!this.listeners[event]) this.listeners[event] = [];

		this.listeners[event].push(callback);

		return callback;
	},

	once: function(event, callback) {
		callback.once = true;

		if (!this.listeners[event]) this.listeners[event] = [];

		this.listeners[event].push(callback);

		return callback;
	},

	off: function(event, callback) {
		for (var i = 0, len = this.listeners[event].length; i < len; i++) {
			if (this.listeners[event][i]._remove) {
				this.listeners[event].splice(i--, 1);
				len--;
			}
		}
	},

	trigger: function(event, data) {

		/* if you prefer events pipe */

		if (this.listeners["event"]) {
			for (var i = 0, len = this.listeners["event"].length; i < len; i++) {
				this.listeners["event"][i](event, data);
			}
		}

		/* or subscribed to single event */

		if (this.listeners[event]) {
			for (var i = 0, len = this.listeners[event].length; i < len; i++) {
				var listener = this.listeners[event][i];
				listener(data);

				if (listener.once) {
					this.listeners[event].splice(i--, 1);
				}
			}
		}
	}
};

/* Mouse */

playground.Mouse = function(element) {

	var self = this;

	playground.Events.call(this);

	this.element = element;

	this.buttons = {};

	this.mousemoveEvent = {};
	this.mousedownEvent = {};
	this.mouseupEvent = {};
	this.mousewheelEvent = {};

	this.x = 0;
	this.y = 0;

	this.offsetX = 0;
	this.offsetY = 0;
	this.scale = 1;

	element.addEventListener("mousemove", this.mousemove.bind(this));
	element.addEventListener("mousedown", this.mousedown.bind(this));
	element.addEventListener("mouseup", this.mouseup.bind(this));

	this.enableMousewheel();

	this.element.addEventListener("contextmenu", function(e) {
		if (self.preventContextMenu) e.preventDefault();
	});

	element.requestPointerLock = element.requestPointerLock ||
		element.mozRequestPointerLock ||
		element.webkitRequestPointerLock;

	document.exitPointerLock = document.exitPointerLock ||
		document.mozExitPointerLock ||
		document.webkitExitPointerLock;
};

playground.Mouse.prototype = {

	lock: function() {
		this.locked = true;
		this.element.requestPointerLock();
	},

	release: function() {
		this.locked = false;
		document.exitPointerLock();
	},

	getElementOffset: function(element) {

		var offsetX = 0;
		var offsetY = 0;

		do {
			offsetX += element.offsetLeft;
			offsetY += element.offsetTop;
		}

		while ((element = element.offsetParent));

		return {
			x: offsetX,
			y: offsetY
		};

	},

	update: function() {
		this.elementOffset = this.getElementOffset(this.element);
	},

	mousemove: playground.throttle(function(e) {
		this.x = this.mousemoveEvent.x = (e.pageX - this.elementOffset.x - this.offsetX) / this.scale | 0;
		this.y = this.mousemoveEvent.y = (e.pageY - this.elementOffset.y - this.offsetY) / this.scale | 0;

		this.mousemoveEvent.original = e;

		if (this.locked) {
			this.mousemoveEvent.movementX = e.movementX ||
				e.mozMovementX ||
				e.webkitMovementX ||
				0;

			this.mousemoveEvent.movementY = e.movementY ||
				e.mozMovementY ||
				e.webkitMovementY ||
				0;
		}


		this.trigger("mousemove", this.mousemoveEvent);
	}, 16),

	mousedown: function(e) {

		var buttonName = ["left", "middle", "right"][e.button];

		this.mousedownEvent.x = this.mousemoveEvent.x;
		this.mousedownEvent.y = this.mousemoveEvent.y;
		this.mousedownEvent.button = buttonName;
		this.mousedownEvent.original = e;

		this[buttonName] = true;

		this.trigger("mousedown", this.mousedownEvent);
	},

	mouseup: function(e) {

		var buttonName = ["left", "middle", "right"][e.button];

		this.mouseupEvent.x = this.mousemoveEvent.x;
		this.mouseupEvent.y = this.mousemoveEvent.y;
		this.mouseupEvent.button = buttonName;
		this.mouseupEvent.original = e;

		this[buttonName] = false;

		this.trigger("mouseup", this.mouseupEvent);
	},

	mousewheel: function(e) {
		this.mousewheelEvent.x = this.mousemoveEvent.x;
		this.mousewheelEvent.y = this.mousemoveEvent.y;
		this.mousewheelEvent.button = ["none", "left", "middle", "right"][e.button];
		this.mousewheelEvent.original = e;

		this[e.button] = false;

		this.trigger("mousewheel", this.mousewheelEvent);
	},


	enableMousewheel: function() {

		var eventNames = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
		var callback = this.mousewheel.bind(this);
		var self = this;

		for (var i = eventNames.length; i;) {

			self.element.addEventListener(eventNames[--i], playground.throttle(function(event) {

				var orgEvent = event || window.event,
					args = [].slice.call(arguments, 1),
					delta = 0,
					deltaX = 0,
					deltaY = 0,
					absDelta = 0,
					absDeltaXY = 0,
					fn;
				event.type = "mousewheel";

				// Old school scrollwheel delta
				if (orgEvent.wheelDelta) {
					delta = orgEvent.wheelDelta;
				}

				if (orgEvent.detail) {
					delta = orgEvent.detail * -1;
				}

				// New school wheel delta (wheel event)
				if (orgEvent.deltaY) {
					deltaY = orgEvent.deltaY * -1;
					delta = deltaY;
				}

				// Webkit
				if (orgEvent.wheelDeltaY !== undefined) {
					deltaY = orgEvent.wheelDeltaY;
				}

				var result = delta ? delta : deltaY;

				self.mousewheelEvent.x = self.mousemoveEvent.x;
				self.mousewheelEvent.y = self.mousemoveEvent.y;
				self.mousewheelEvent.delta = result / Math.abs(result);
				self.mousewheelEvent.original = orgEvent;

				callback(self.mousewheelEvent);

				event.preventDefault();

			}, 40), false);
		}

	}

};

playground.extend(playground.Mouse.prototype, playground.Events.prototype);

/* Touch */

playground.Touch = function(element) {

	playground.Events.call(this);

	this.element = element;

	this.buttons = {};

	this.touchmoveEvent = {};
	this.touchstartEvent = {};
	this.touchendEvent = {};

	this.touches = {};

	this.x = 0;
	this.y = 0;

	this.offsetX = 0;
	this.offsetY = 0;
	this.scale = 1;

	element.addEventListener("touchmove", this.touchmove.bind(this));
	element.addEventListener("touchstart", this.touchstart.bind(this));
	element.addEventListener("touchend", this.touchend.bind(this));
};

playground.Touch.prototype = {

	getElementOffset: function(element) {

		var offsetX = 0;
		var offsetY = 0;

		do {
			offsetX += element.offsetLeft;
			offsetY += element.offsetTop;
		}

		while ((element = element.offsetParent));

		return {
			x: offsetX,
			y: offsetY
		};

	},

	update: function() {
		this.elementOffset = this.getElementOffset(this.element);
	},

	touchmove: function(e) {
		for (var i = 0; i < e.changedTouches.length; i++) {

			var touch = e.changedTouches[i];

			this.x = this.touchmoveEvent.x = (touch.pageX - this.elementOffset.x - this.offsetX) / this.scale | 0;
			this.y = this.touchmoveEvent.y = (touch.pageY - this.elementOffset.y - this.offsetY) / this.scale | 0;

			this.touchmoveEvent.original = touch;
			this.touchmoveEvent.id = touch.identifier;

			this.touches[touch.identifier].x = this.touchmoveEvent.x;
			this.touches[touch.identifier].y = this.touchmoveEvent.y;

			this.trigger("touchmove", this.touchmoveEvent);

			e.preventDefault();
		}
	},

	touchstart: function(e) {

		for (var i = 0; i < e.changedTouches.length; i++) {

			var touch = e.changedTouches[i];

			this.x = this.touchstartEvent.x = (touch.pageX - this.elementOffset.x - this.offsetX) / this.scale | 0;
			this.y = this.touchstartEvent.y = (touch.pageY - this.elementOffset.y - this.offsetY) / this.scale | 0;

			this.touchstartEvent.original = e.touch;
			this.touchstartEvent.id = touch.identifier;

			this.touches[touch.identifier] = {
				x: this.touchstartEvent.x,
				y: this.touchstartEvent.y
			};

			this.pressed = true;

			this.trigger("touchstart", this.touchstartEvent);
		}

	},

	touchend: function(e) {
		for (var i = 0; i < e.changedTouches.length; i++) {

			var touch = e.changedTouches[i];

			this.touchendEvent.x = this.touchmoveEvent.x;
			this.touchendEvent.y = this.touchmoveEvent.y;

			this.touchendEvent.original = touch;
			this.touchendEvent.id = touch.identifier;

			delete this.touches[touch.identifier];

			this.pressed = false;

			this.trigger("touchend", this.touchendEvent);

		}

	}

};

playground.extend(playground.Touch.prototype, playground.Events.prototype);

/* Keyboard */

playground.Keyboard = function() {

	playground.Events.call(this);

	this.keys = {};

	document.addEventListener("keydown", this.keydown.bind(this));
	document.addEventListener("keyup", this.keyup.bind(this));
	document.addEventListener("keypress", this.keypress.bind(this));

	this.keydownEvent = {};
	this.keyupEvent = {};

};

playground.Keyboard.prototype = {

	keycodes: {
		37: "left",
		38: "up",
		39: "right",
		40: "down",
		45: "insert",
		46: "delete",
		8: "backspace",
		9: "tab",
		13: "enter",
		16: "shift",
		17: "ctrl",
		18: "alt",
		19: "pause",
		20: "capslock",
		27: "escape",
		32: "space",
		33: "pageup",
		34: "pagedown",
		35: "end",
		36: "home",
		112: "f1",
		113: "f2",
		114: "f3",
		115: "f4",
		116: "f5",
		117: "f6",
		118: "f7",
		119: "f8",
		120: "f9",
		121: "f10",
		122: "f11",
		123: "f12",
		144: "numlock",
		145: "scrolllock",
		186: "semicolon",
		187: "equal",
		188: "comma",
		189: "dash",
		190: "period",
		191: "slash",
		192: "graveaccent",
		219: "openbracket",
		220: "backslash",
		221: "closebraket",
		222: "singlequote"
	},

	keypress: function(e) {

	},

	keydown: function(e) {
		if (e.which >= 48 && e.which <= 90) var keyName = String.fromCharCode(e.which).toLowerCase();
		else var keyName = this.keycodes[e.which];

		if (this.keys[keyName]) return;

		this.keydownEvent.key = keyName;
		this.keydownEvent.original = e;

		this.keys[keyName] = true;

		this.trigger("keydown", this.keydownEvent);

		if (this.preventDefault && document.activeElement === document.body) {
			e.returnValue = false;
			e.keyCode = 0;
			e.preventDefault();
			e.stopPropagation();
		}
	},

	keyup: function(e) {

		if (e.which >= 48 && e.which <= 90) var keyName = String.fromCharCode(e.which).toLowerCase();
		else var keyName = this.keycodes[e.which];

		this.keyupEvent.key = keyName;
		this.keyupEvent.original = e;

		this.keys[keyName] = false;

		this.trigger("keyup", this.keyupEvent);
	}

};

playground.extend(playground.Keyboard.prototype, playground.Events.prototype);

/* Gamepad */

playground.Gamepads = function() {

	playground.Events.call(this);

	this.getGamepads = navigator.getGamepads || navigator.webkitGetGamepads;

	this.gamepadmoveEvent = {};
	this.gamepaddownEvent = {};
	this.gamepadupEvent = {};

	this.gamepads = {};

};

playground.Gamepads.prototype = {

	buttons: {
		0: "1",
		1: "2",
		2: "3",
		3: "4",
		4: "l1",
		5: "r1",
		6: "l2",
		7: "r2",
		8: "select",
		9: "start",
		12: "up",
		13: "down",
		14: "left",
		15: "right"
	},

	zeroState: function() {
		var buttons = [];

		for (var i = 0; i <= 15; i++) {
			buttons.push({
				pressed: false,
				value: 0
			});
		}

		return {
			axes: [],
			buttons: buttons
		};
	},

	createGamepad: function() {
		var result = {
			buttons: {},
			sticks: [{
				x: 0,
				y: 0
			}, {
				x: 0,
				y: 0
			}]
		};


		for (var i = 0; i < 16; i++) {
			var key = this.buttons[i];
			result.buttons[key] = false;
		}

		return result;
	},

	step: function() {
		if (!navigator.getGamepads) return;

		var gamepads = navigator.getGamepads();

		for (var i = 0; i < gamepads.length; i++) {
			var current = gamepads[i];

			if (!current) continue;

			if (!this[i]) this[i] = this.createGamepad();

			/* have to concat the current.buttons because the are read-only */

			var buttons = [].concat(current.buttons);

			/* hack for missing  dpads */

			for (var h = 12; h <= 15; h++) {
				if (!buttons[h]) buttons[h] = {
					pressed: false,
					value: 0
				};
			}

			var previous = this[i];

			/* axes (sticks) to buttons */

			if (current.axes) {

				if (current.axes[0] < 0) buttons[14].pressed = true;
				if (current.axes[0] > 0) buttons[15].pressed = true;
				if (current.axes[1] < 0) buttons[12].pressed = true;
				if (current.axes[1] > 0) buttons[13].pressed = true;

				previous.sticks[0].x = current.axes[0].value;
				previous.sticks[0].y = current.axes[1].value;
				previous.sticks[1].x = current.axes[2].value;
				previous.sticks[1].y = current.axes[3].value;

			}

			/* check buttons changes */

			for (var j = 0; j < buttons.length; j++) {

				var key = this.buttons[j];

				/* gamepad down */

				if (buttons[j].pressed && !previous.buttons[key]) {
					previous.buttons[key] = true;
					this.gamepaddownEvent.button = this.buttons[j];
					this.gamepaddownEvent.gamepad = i;
					this.trigger("gamepaddown", this.gamepaddownEvent);
				}

				/* gamepad up */
				else if (!buttons[j].pressed && previous.buttons[key]) {
					previous.buttons[key] = false;
					this.gamepadupEvent.button = this.buttons[j];
					this.gamepadupEvent.gamepad = i;
					this.trigger("gamepadup", this.gamepadupEvent);
				}

			}

		}

	}
};

playground.extend(playground.Gamepads.prototype, playground.Events.prototype);


/* Loader */

playground.Loader = function() {

	playground.Events.call(this);

	this.reset();

};

playground.Loader.prototype = {

	/* loader */

	add: function(id) {
		this.queue++;
		this.count++;
		this.trigger("add", id);
	},

	error: function(id) {
		console.log("unable to load " + id);
		this.trigger("error", id);
	},

	ready: function(id) {
		this.queue--;

		this.progress = 1 - this.queue / this.count;

		this.trigger("load", id);

		if (this.queue <= 0) {
			this.trigger("ready");
			this.reset();
		}
	},

	reset: function() {
		this.progress = 0;
		this.queue = 0;
		this.count = 0;
	}
};

playground.extend(playground.Loader.prototype, playground.Events.prototype);

CanvasQuery.Layer.prototype.playground = function(args) {
	args.layer = this;
	return playground(args);
};

/* Video recorder */

/* whammy - https://github.com/antimatter15/whammy */

window.Whammy = function() {
	function h(a, b) {
		for (var c = r(a), c = [{
				id: 440786851,
				data: [{
					data: 1,
					id: 17030
				}, {
					data: 1,
					id: 17143
				}, {
					data: 4,
					id: 17138
				}, {
					data: 8,
					id: 17139
				}, {
					data: "webm",
					id: 17026
				}, {
					data: 2,
					id: 17031
				}, {
					data: 2,
					id: 17029
				}]
			}, {
				id: 408125543,
				data: [{
					id: 357149030,
					data: [{
						data: 1E6,
						id: 2807729
					}, {
						data: "whammy",
						id: 19840
					}, {
						data: "whammy",
						id: 22337
					}, {
						data: s(c.duration),
						id: 17545
					}]
				}, {
					id: 374648427,
					data: [{
						id: 174,
						data: [{
							data: 1,
							id: 215
						}, {
							data: 1,
							id: 25541
						}, {
							data: 0,
							id: 156
						}, {
							data: "und",
							id: 2274716
						}, {
							data: "V_VP8",
							id: 134
						}, {
							data: "VP8",
							id: 2459272
						}, {
							data: 1,
							id: 131
						}, {
							id: 224,
							data: [{
								data: c.width,
								id: 176
							}, {
								data: c.height,
								id: 186
							}]
						}]
					}]
				}]
			}], e = 0, d = 0; e < a.length;) {
			var g = [],
				f = 0;
			do g.push(a[e]), f += a[e].duration, e++; while (e < a.length && 3E4 > f);
			var h = 0,
				g = {
					id: 524531317,
					data: [{
						data: d,
						id: 231
					}].concat(g.map(function(a) {
						var b = t({
							discardable: 0,
							frame: a.data.slice(4),
							invisible: 0,
							keyframe: 1,
							lacing: 0,
							trackNum: 1,
							timecode: Math.round(h)
						});
						h += a.duration;
						return {
							data: b,
							id: 163
						}
					}))
				};
			c[1].data.push(g);
			d += f
		}
		return m(c, b)
	}

	function r(a) {
		for (var b = a[0].width, c = a[0].height, e = a[0].duration,
				d = 1; d < a.length; d++) {
			if (a[d].width != b) throw "Frame " + (d + 1) + " has a different width";
			if (a[d].height != c) throw "Frame " + (d + 1) + " has a different height";
			if (0 > a[d].duration || 32767 < a[d].duration) throw "Frame " + (d + 1) + " has a weird duration (must be between 0 and 32767)";
			e += a[d].duration
		}
		return {
			duration: e,
			width: b,
			height: c
		}
	}

	function u(a) {
		for (var b = []; 0 < a;) b.push(a & 255), a >>= 8;
		return new Uint8Array(b.reverse())
	}

	function n(a) {
		var b = [];
		a = (a.length % 8 ? Array(9 - a.length % 8).join("0") : "") + a;
		for (var c = 0; c < a.length; c += 8) b.push(parseInt(a.substr(c,
			8), 2));
		return new Uint8Array(b)
	}

	function m(a, b) {
		for (var c = [], e = 0; e < a.length; e++) {
			var d = a[e].data;
			"object" == typeof d && (d = m(d, b));
			"number" == typeof d && (d = n(d.toString(2)));
			if ("string" == typeof d) {
				for (var g = new Uint8Array(d.length), f = 0; f < d.length; f++) g[f] = d.charCodeAt(f);
				d = g
			}
			f = d.size || d.byteLength || d.length;
			g = Math.ceil(Math.ceil(Math.log(f) / Math.log(2)) / 8);
			f = f.toString(2);
			f = Array(7 * g + 8 - f.length).join("0") + f;
			g = Array(g).join("0") + "1" + f;
			c.push(u(a[e].id));
			c.push(n(g));
			c.push(d)
		}
		return b ? (c = p(c), new Uint8Array(c)) :
			new Blob(c, {
				type: "video/webm"
			})
	}

	function p(a, b) {
		null == b && (b = []);
		for (var c = 0; c < a.length; c++) "object" == typeof a[c] ? p(a[c], b) : b.push(a[c]);
		return b
	}

	function t(a) {
		var b = 0;
		a.keyframe && (b |= 128);
		a.invisible && (b |= 8);
		a.lacing && (b |= a.lacing << 1);
		a.discardable && (b |= 1);
		if (127 < a.trackNum) throw "TrackNumber > 127 not supported";
		return [a.trackNum | 128, a.timecode >> 8, a.timecode & 255, b].map(function(a) {
			return String.fromCharCode(a)
		}).join("") + a.frame
	}

	function q(a) {
		for (var b = a.RIFF[0].WEBP[0], c = b.indexOf("\u009d\u0001*"),
				e = 0, d = []; 4 > e; e++) d[e] = b.charCodeAt(c + 3 + e);
		e = d[1] << 8 | d[0];
		c = e & 16383;
		e = d[3] << 8 | d[2];
		return {
			width: c,
			height: e & 16383,
			data: b,
			riff: a
		}
	}

	function k(a) {
		for (var b = 0, c = {}; b < a.length;) {
			var e = a.substr(b, 4),
				d = parseInt(a.substr(b + 4, 4).split("").map(function(a) {
					a = a.charCodeAt(0).toString(2);
					return Array(8 - a.length + 1).join("0") + a
				}).join(""), 2),
				g = a.substr(b + 4 + 4, d),
				b = b + (8 + d);
			c[e] = c[e] || [];
			"RIFF" == e || "LIST" == e ? c[e].push(k(g)) : c[e].push(g)
		}
		return c
	}

	function s(a) {
		return [].slice.call(new Uint8Array((new Float64Array([a])).buffer),
			0).map(function(a) {
			return String.fromCharCode(a)
		}).reverse().join("")
	}

	function l(a, b) {
		this.frames = [];
		this.duration = 1E3 / a;
		this.quality = b || .8
	}
	l.prototype.add = function(a, b) {
		if ("undefined" != typeof b && this.duration) throw "you can't pass a duration if the fps is set";
		if ("undefined" == typeof b && !this.duration) throw "if you don't have the fps set, you ned to have durations here.";
		"canvas" in a && (a = a.canvas);
		if ("toDataURL" in a) a = a.toDataURL("image/webp", this.quality);
		else if ("string" != typeof a) throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string";
		if (!/^data:image\/webp;base64,/ig.test(a)) throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
		this.frames.push({
			image: a,
			duration: b || this.duration
		})
	};
	l.prototype.compile = function(a) {
		return new h(this.frames.map(function(a) {
			var c = q(k(atob(a.image.slice(23))));
			c.duration = a.duration;
			return c
		}), a)
	};
	return {
		Video: l,
		fromImageArray: function(a, b, c) {
			return h(a.map(function(a) {
				a = q(k(atob(a.slice(23))));
				a.duration = 1E3 / b;
				return a
			}), c)
		},
		toWebM: h
	}
}();

playground.VideoRecorder = function(app, args) {

	this.app = app;

};

playground.VideoRecorder.prototype = {

	setup: function(args) {

		this.region = false;

		playground.extend(this, {
			followMouse: false,
			framerate: 20,
			scale: 1
		}, args);

		if (!this.region) {
			this.region = [0, 0, this.app.layer.width, this.app.layer.height];
		}

		this.playbackRate = this.framerate / 60;

		this.layer = cq(this.region[2] * this.scale | 0, this.region[3] * this.scale | 0);
	},

	start: function(args) {
		this.setup(args);
		this.encoder = new Whammy.Video(this.framerate);
		this.captureTimeout = 0;
		this.recording = true;
	},

	step: function(delta) {

		if (this.encoder) {

			this.captureTimeout -= delta * 1000;

			if (this.captureTimeout <= 0) {
				this.captureTimeout = 1000 / this.framerate + this.captureTimeout;

				this.layer.drawImage(this.app.layer.canvas, this.region[0], this.region[1], this.region[2], this.region[3], 0, 0, this.layer.width, this.layer.height);
				this.encoder.add(this.layer.canvas);
			}

			this.app.screen.save().lineWidth(8).strokeStyle("#c00").strokeRect(0, 0, this.app.screen.width, this.app.screen.height).restore();
		}

	},

	stop: function() {
		if (!this.encoder) return;
		var output = this.encoder.compile();
		var url = (window.webkitURL || window.URL).createObjectURL(output);
		window.open(url);
		this.recording = false;

		delete this.encoder;
	},

	toggle: function(args) {
		if (this.encoder) this.stop();
		else this.start(args);
	}

};

Playground.SoundInterface = {

};

Playground.Sound = function(parent) {

	this.parent = parent;

	var canPlayMp3 = (new Audio).canPlayType("audio/mp3");
	var canPlayOgg = (new Audio).canPlayType('audio/ogg; codecs="vorbis"');

	if (canPlayMp3) this.audioFormat = "mp3";
	else this.audioFormat = "ogg";

	var audioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext;

	this.context = new audioContext;

	this.gainNode = this.context.createGain()
	this.gainNode.connect(this.context.destination);

	this.compressor = this.context.createDynamicsCompressor();
	this.compressor.connect(this.gainNode);

	this.output = this.gainNode;

	this.gainNode.gain.value = 1.0;

	this.buffers = [];
	this.pool = [];
	this.volume = 1.0;

	this.setMasterPosition(0, 0, 0);

};

Playground.Sound.prototype = {

	setMaster: function(volume) {

		this.volume = volume;

		this.gainNode.gain.value = volume;

	},

	load: function(file) {

		var url = "sounds/" + file + "." + this.audioFormat;
		var sampler = this;

		var request = new XMLHttpRequest();

		request.open("GET", url, true);
		request.responseType = "arraybuffer";

		var id = this.parent.loader.add();

		request.onload = function() {

			sampler.context.decodeAudioData(this.response, function(decodedBuffer) {
				sampler.buffers[file] = decodedBuffer;
				sampler.parent.loader.ready(id);
			});

		}

		request.send();

	},

	cleanArray: function(array, property) {
		for (var i = 0, len = array.length; i < len; i++) {
			if (array[i] === null || (property && array[i][property])) {
				array.splice(i--, 1);
				len--;
			}
		}
	},

	setMasterPosition: function(x, y, z) {

		this.masterPosition = {
			x: x,
			y: y,
			z: z
		};

		this.context.listener.setPosition(x, y, z)
			// this.context.listener.setOrientation(0, 0, -1, 0, 1, 0);
			// this.context.listener.dopplerFactor = 1;
			// this.context.listener.speedOfSound = 343.3;
	},

	getSoundBuffer: function() {
		if (!this.pool.length) {
			for (var i = 0; i < 100; i++) {

				var buffer, gain, panner;

				var nodes = [
					buffer = this.context.createBufferSource(),
					gain = this.context.createGain(),
					panner = this.context.createPanner()
				];

				panner.distanceModel = "linear";

				// 1 - rolloffFactor * (distance - refDistance) / (maxDistance - refDistance)
				// refDistance / (refDistance + rolloffFactor * (distance - refDistance))
				panner.refDistance = 1;
				panner.maxDistance = 600;
				panner.rolloffFactor = 1.0;


				// panner.setOrientation(-1, -1, 0);

				this.pool.push(nodes);

				nodes[0].connect(nodes[1]);
				// nodes[1].connect(nodes[2]);
				nodes[1].connect(this.output);
			}
		}

		return this.pool.pop();
	},

	play: function(name, loop) {

		var nodes = this.getSoundBuffer();

		bufferSource = nodes[0];
		bufferSource.gainNode = nodes[1];
		bufferSource.pannerNode = nodes[2];
		bufferSource.buffer = this.buffers[name];
		bufferSource.loop = loop || false;
		bufferSource.key = name;

		if (this.loop) {
			//  bufferSource.loopStart = this.loopStart;
			// bufferSource.loopEnd = this.loopEnd;
		}

		bufferSource.gainNode.gain.value = 1.0;

		bufferSource.start(0);

		bufferSource.volumeLimit = 1;

		this.setPosition(bufferSource, this.masterPosition.x, this.masterPosition.y, this.masterPosition.z);


		return bufferSource;
	},

	stop: function(what) {

		if (!what) return;

		what.stop(0);

	},

	setPlaybackRate: function(sound, rate) {

		if (!sound) return;

		return sound.playbackRate.value = rate;
	},

	setPosition: function(sound, x, y, z) {

		if (!sound) return;

		sound.pannerNode.setPosition(x, y || 0, z || 0);
	},

	setVelocity: function(sound, x, y, z) {

		if (!sound) return;

		sound.pannerNode.setPosition(x, y || 0, z || 0);

	},

	setVolume: function(sound, volume) {

		if (!sound) return;

		return sound.gainNode.gain.value = Math.max(0, volume);
	}

};

playground.extend(Playground.Sound.prototype, Playground.SoundInterface);

Playground.SoundFallback = function(parent) {

	this.parent = parent;

	var canPlayMp3 = (new Audio).canPlayType("audio/mp3");
	var canPlayOgg = (new Audio).canPlayType('audio/ogg; codecs="vorbis"');

	if (canPlayMp3) this.audioFormat = "mp3";
	else this.audioFormat = "ogg";

	this.samples = {};

};

Playground.SoundFallback.prototype = {

	setMaster: function(volume) {

		this.volume = volume;

	},

	setPosition: function(x, y, z) {
		return;
	},

	load: function(file) {

		var filename = arg;

		var loader = this.parent.loader;

		var key = filename;

		filename += "." + this.audioFormat;

		var path = "sounds/" + filename;

		this.loader.add(path);

		var audio = this.samples[key] = new Audio;

		audio.addEventListener("canplay", function() {
			loader.ready(path);
		});

		audio.addEventListener("error", function() {
			loader.error(path);
		});

		audio.src = path;


	},

	play: function(key, loop) {

		var sound = this.sounds[key];

		sound.currentTime = 0;
		sound.loop = loop;
		sound.play();

		return sound;

	},

	stop: function(what) {

		if (!what) return;

		what.pause();

	},

	setPlaybackRate: function(sound, rate) {

		return;
	},

	setVolume: function(sound, volume) {

		sound.volume = volume * this.volume;

	}

};

playground.extend(Playground.SoundFallback.prototype, Playground.SoundInterface);