
/**
 * Copyright (C) 2011 by Justin Windle
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * --------------------
 * SETTINGS
 * --------------------
 */

var CONFIG  = {};
var PRESETS = {};
var RENDER_MODES = {
	Darkness:  'darkness',
	Segmented: 'segmented',
	Sketched:  'sketched'
};

PRESETS['Vines']          = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.2572,MAX_CONCURRENT:388,NUM_BRANCHES:4,MIN_RADIUS:0.1,MAX_RADIUS:69,MIN_WANDER_STEP:1.0184,MAX_WANDER_STEP:0.1702,MIN_GROWTH_RATE:10.6214,MAX_GROWTH_RATE:11.8251,MIN_SHRINK_RATE:0.99656,MAX_SHRINK_RATE:0.91265,MIN_DIVERGENCE:0.5101,MAX_DIVERGENCE:0.37466};
PRESETS['Fibrous']        = {RENDER_MODE:RENDER_MODES.Segmented,BRANCH_PROBABILITY:0.05,MAX_CONCURRENT:800,NUM_BRANCHES:3,MIN_RADIUS:0.1,MAX_RADIUS:50,MIN_WANDER_STEP:0.28,MAX_WANDER_STEP:0.7,MIN_GROWTH_RATE:5,MAX_GROWTH_RATE:9,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05};
PRESETS['Graffiti']       = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.05,MAX_CONCURRENT:500,NUM_BRANCHES:6,MIN_RADIUS:0.15,MAX_RADIUS:70,MIN_WANDER_STEP:0.1197,MAX_WANDER_STEP:1.8269,MIN_GROWTH_RATE:13.66,MAX_GROWTH_RATE:17.35,MIN_SHRINK_RATE:0.95,MAX_SHRINK_RATE:0.98,MIN_DIVERGENCE:1.3268,MAX_DIVERGENCE:1.3885};
PRESETS['Knarled']        = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.09,MAX_CONCURRENT:500,NUM_BRANCHES:5,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.1,MAX_WANDER_STEP:0.2,MIN_GROWTH_RATE:3.7,MAX_GROWTH_RATE:10,MIN_SHRINK_RATE:0.97,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05};
PRESETS['Beech Tree']     = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.085,MAX_CONCURRENT:500,NUM_BRANCHES:1,MIN_RADIUS:0.1,MAX_RADIUS:40,MIN_WANDER_STEP:0.1599,MAX_WANDER_STEP:0.4,MIN_GROWTH_RATE:8,MAX_GROWTH_RATE:15,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.982,MIN_DIVERGENCE:0.31,MAX_DIVERGENCE:0.87};
PRESETS['Frost']          = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.09,MAX_CONCURRENT:1000,NUM_BRANCHES:6,MIN_RADIUS:0.1,MAX_RADIUS:40,MIN_WANDER_STEP:0,MAX_WANDER_STEP:0,MIN_GROWTH_RATE:9.2,MAX_GROWTH_RATE:9.8,MIN_SHRINK_RATE:0.97,MAX_SHRINK_RATE:0.97,MIN_DIVERGENCE:0.4,MAX_DIVERGENCE:0.8};
PRESETS['Wooly']          = {RENDER_MODE:RENDER_MODES.Segmented,BRANCH_PROBABILITY:0.07,MAX_CONCURRENT:348,NUM_BRANCHES:9,MIN_RADIUS:1.5,MAX_RADIUS:99,MIN_WANDER_STEP:0.5093,MAX_WANDER_STEP:2.654,MIN_GROWTH_RATE:7.8279,MAX_GROWTH_RATE:18.2956,MIN_SHRINK_RATE:0.94489,MAX_SHRINK_RATE:0.98716,MIN_DIVERGENCE:1.4656,MAX_DIVERGENCE:2.6998};
PRESETS['Vegetable Root'] = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:0.06,MAX_CONCURRENT:437,NUM_BRANCHES:1,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.05,MAX_WANDER_STEP:0.25,MIN_GROWTH_RATE:5,MAX_GROWTH_RATE:9,MIN_SHRINK_RATE:0.98,MAX_SHRINK_RATE:0.99,MIN_DIVERGENCE:0,MAX_DIVERGENCE:0.1};
PRESETS['Hairball']       = {RENDER_MODE:RENDER_MODES.Sketched,BRANCH_PROBABILITY:0.6,MAX_CONCURRENT:800,MAX_DIVERGENCE:2.1,MAX_GROWTH_RATE:4.5,MAX_RADIUS:30,MAX_SHRINK_RATE:0.992,MAX_WANDER_STEP:0.2,MIN_DIVERGENCE:2,MIN_GROWTH_RATE:3.5,MIN_RADIUS:0.5,MIN_SHRINK_RATE:0.992,MIN_WANDER_STEP:0.1,NUM_BRANCHES:7};
PRESETS['Intenstines']    = {RENDER_MODE:RENDER_MODES.Darkness,BRANCH_PROBABILITY:1,MAX_CONCURRENT:350,NUM_BRANCHES:3,MIN_RADIUS:0.1,MAX_RADIUS:100,MIN_WANDER_STEP:0.1,MAX_WANDER_STEP:0.72,MIN_GROWTH_RATE:0.9,MAX_GROWTH_RATE:6.15,MIN_SHRINK_RATE:0.935,MAX_SHRINK_RATE:0.999,MIN_DIVERGENCE:0.01,MAX_DIVERGENCE:0.05};

function configure(settings) {
	for(var prop in settings) {
		CONFIG[prop] = settings[prop];
	}
}

configure(PRESETS['Graffiti']);

/**
 * --------------------
 * UTILS
 * --------------------
 */

var PI           = Math.PI;
var TWO_PI       = Math.PI * 2;
var HALF_PI      = Math.PI / 2;
var BRANCHES     = [];

function random(min, max) {
	return min + Math.random() * (max - min);
}

/**
 * --------------------
 * BRANCH
 * --------------------
 */

var Branch = function(x, y, theta, radius, scale, generation) {

	this.x           = x;
	this.y           = y;
	this.ox          = x;
	this.oy          = y;
	this.x1          = NaN;
	this.x2          = NaN;
	this.y1          = NaN;
	this.y2          = NaN;
	this.scale       = scale || 1.0;
	this.theta       = theta;
	this.oTheta      = theta;
	this.radius      = radius;
	this.generation  = generation || 1;
	this.growing     = true;
	this.age         = 0;

	this.wanderStep  = random(CONFIG.MIN_WANDER_STEP, CONFIG.MAX_WANDER_STEP);
	this.growthRate  = random(CONFIG.MIN_GROWTH_RATE, CONFIG.MAX_GROWTH_RATE);
	this.shrinkRate  = random(CONFIG.MIN_SHRINK_RATE, CONFIG.MAX_SHRINK_RATE);
}

Branch.prototype = {

	update: function() {
		
		if(this.growing) {
			
			this.ox = this.x;
			this.oy = this.y;
			this.oTheta = this.theta;

			this.theta += random(-this.wanderStep, this.wanderStep);

			this.x += Math.cos(this.theta) * this.growthRate * this.scale;
			this.y += Math.sin(this.theta) * this.growthRate * this.scale;

			this.scale  *= this.shrinkRate;

			// Branch
			if(BRANCHES.length < CONFIG.MAX_CONCURRENT && Math.random() < CONFIG.BRANCH_PROBABILITY) {
				
				var offset = random(CONFIG.MIN_DIVERGENCE, CONFIG.MAX_DIVERGENCE);
				var theta  = this.theta + offset * (Math.random() < 0.5 ? 1 : -1);
				var scale  = this.scale * 0.95;
				var radius = this.radius * scale;
				var branch = new Branch(this.x, this.y, theta, radius, scale);

				branch.generation = this.generation + 1;

				BRANCHES.push(branch);
			}

			// Limit
			if(this.radius * this.scale <= CONFIG.MIN_RADIUS) {
				this.growing = false;
			}

			this.age++;
		}
	},

	render: function(context) {

		if(this.growing) {

			var x1, x2, y1, y2;
			var scale = this.scale;
			var radius = this.radius * scale;

			context.save();

			switch(CONFIG.RENDER_MODE) {

				case RENDER_MODES.Segmented :

					// Draw outline
					context.beginPath();
					context.moveTo(this.ox, this.oy);
					context.lineTo(this.x, this.y);
					
					if(radius > 5.0) {
						context.shadowOffsetX = 1;
						context.shadowOffsetY = 1;
						context.shadowBlur    = scale;
						context.shadowColor   = 'rgba(0,0,0,0.05)';	
					}
					
					context.lineWidth = radius + scale;
					context.strokeStyle = 'blue';
					context.lineCap = 'round';
					context.stroke();
					context.closePath();
					
					// Draw fill
					context.beginPath();
					context.moveTo(this.ox, this.oy);
					context.lineTo(this.x, this.y);

					context.lineWidth = radius;
					context.strokeStyle = 'red';
					context.lineCap = 'round';
					context.stroke();

					context.closePath();

					break;
				
				case RENDER_MODES.Sketched :

					radius *= 0.5;
					radius += 0.5;

					x1 = this.x + Math.cos(this.theta - HALF_PI) * radius;
					x2 = this.x + Math.cos(this.theta + HALF_PI) * radius;

					y1 = this.y + Math.sin(this.theta - HALF_PI) * radius;
					y2 = this.y + Math.sin(this.theta + HALF_PI) * radius;

					context.lineWidth = 0.5 + scale;
					context.strokeStyle = 'yellow';
					context.fillStyle = 'red';
					context.lineCap = 'round';
					
					// Starting point
					if(this.generation === 1 && this.age === 1) {
						context.beginPath();
						context.arc(this.x, this.y, radius, 0, TWO_PI);
						context.stroke();
						context.fill();
					}

					// Draw sides
					if(this.age > 1) {
						context.beginPath();
						context.moveTo(this.x1, this.y1);
						context.lineTo(x1, y1);
						context.moveTo(this.x2, this.y2);
						context.lineTo(x2, y2);
						context.stroke();
					}

					// Draw ribbon
					context.beginPath();
					context.moveTo(this.x1, this.y1);
					context.lineTo(x1, y1);
					context.lineTo(x2, y2);
					context.lineTo(this.x2, this.y2);
					context.closePath();
					context.fill();

					this.x1 = x1;
					this.x2 = x2;

					this.y1 = y1;
					this.y2 = y2;

					break;

				case RENDER_MODES.Darkness :

					radius *= 0.5;

					x1 = this.x + Math.cos(this.theta - HALF_PI) * radius;
					x2 = this.x + Math.cos(this.theta + HALF_PI) * radius;

					y1 = this.y + Math.sin(this.theta - HALF_PI) * radius;
					y2 = this.y + Math.sin(this.theta + HALF_PI) * radius;

					context.lineWidth = scale;
					context.strokeStyle = 'rgba(255,255,255,0.9)';
					context.lineCap = 'round';
					context.fillStyle = '#111';

					// Starting point
					if(this.generation === 1 && this.age === 1) {
						context.beginPath();
						context.arc(this.x, this.y, radius, 0, TWO_PI);
						context.closePath();
						context.fill();
						context.stroke();
					}
					
					// Shadow
					if(scale > 0.05) {
						context.shadowOffsetX = scale;
						context.shadowOffsetY = scale;
						context.shadowBlur    = scale;
						context.shadowColor   = '#111';	
					}	

					// Draw ribbon
					context.beginPath();
					context.moveTo(this.x1, this.y1);
					context.lineTo(x1, y1);
					context.lineTo(x2, y2);
					context.lineTo(this.x2, this.y2);
					context.closePath();
					context.fill();

					// Draw sides
					if(this.age > 1 && scale > 0.1) {
						context.beginPath();
						context.moveTo(this.x1, this.y1);
						context.lineTo(x1, y1);
						context.moveTo(this.x2, this.y2);
						context.lineTo(x2, y2);
						context.stroke();
					}

					this.x1 = x1;
					this.x2 = x2;

					this.y1 = y1;
					this.y2 = y2;

					break;
			}
			
			context.restore();
		}
	},

	destroy: function() {
		
	}
};

/**
 * --------------------
 * SKETCH
 * --------------------
 */

var Recursion = new function() {

	var started      = false;
	var $canvas      = $('#canvas');
	var $branchCount = $('#output .branchCount');
	var canvas       = $canvas[0];
	var context      = canvas.getContext('2d');

	function spawn(x, y) {

		var theta, radius;

		for(var i = 0; i < CONFIG.NUM_BRANCHES; i++) {
			theta = (i / CONFIG.NUM_BRANCHES) * TWO_PI;
			radius = CONFIG.MAX_RADIUS;
			BRANCHES.push(new Branch(x, y, theta - HALF_PI, radius));
		}
	}

	function update() {

		//cancelRequestAnimFrame(update);
		requestAnimFrame(update);

		var i, n, branch;

		for(i = 0, n = BRANCHES.length; i < n; i++) {
			branch = BRANCHES[i];
			branch.update();
			branch.render(context);
		}

		// Strip dead branches
		for(i = BRANCHES.length - 1; i >= 0; i--) {
			if(!BRANCHES[i].growing) {
				BRANCHES.splice(i,1);
			}
		}

		var count = BRANCHES.length.toString();
		while(count.length < 3) { count = '0' + count; }
		$branchCount.text('Branch count: ' + count);
	}

	function onClick(e) {

		Recursion.reset();
		spawn(e.offsetX, e.offsetY);
	}

	function onResize(e) {

		canvas.width  = window.innerWidth;
		canvas.height = window.innerHeight;

		Recursion.reset();
		spawn(window.innerWidth / 2, window.innerHeight / 2);
	}

	return {

		init: function() {

			onResize();

			if(!started) {
				started = true;
				$(window).resize(onResize);
				$canvas.click(onClick);
				update();
			}
		},

		reset: function() {

			for(var i = 0, n = BRANCHES.length; i < n; i++) {
				BRANCHES[i].destroy();
			}

			BRANCHES = [];
		},

		save: function() {
			
			var image = canvas.toDataURL('image/png');

			var win = window.open('about:blank', '_blank', 'width=1000,height=700');

			var html = $('<html>');
			var head = $('<head>');
			var body = $('<body>');

			body.css({
				background: '#f2f2f2',
				padding: 0,
				margin: 0
			});

			head.append($('<title>Recursion &raquo Right Click &amp; Save the Image Below</title>'));
			body.append($('<img src="' + image + '"/>'));

			html.append(head);
			html.append(body);

			win.document.write('<!DOCTYPE html>' + html.html());
			win.document.close();
		},

		clear: function() {
			canvas.width = canvas.width;
		}
	};
}

/**
 * --------------------
 * GUI
 * --------------------
 */

function saveConfig() {
	var config = [];
	for(var i in CONFIG) { config.push(i + ':' + CONFIG[i]); }
	console.log("PRESETS['__name__'] = {" + config.join(',') + "};");
}

// Build preset map for GUI
var preset = {key:''}, keys = {};
for(var i in PRESETS) { keys[i] = i; }

function randomise() {
	CONFIG.BRANCH_PROBABILITY  = random(0.01,1.0);
	CONFIG.MAX_CONCURRENT      = random(10,1000);
	CONFIG.NUM_BRANCHES        = random(1,20);
	CONFIG.MIN_RADIUS          = random(0.1,2.0);
	CONFIG.MAX_RADIUS          = random(CONFIG.MIN_RADIUS,100);
	CONFIG.MIN_WANDER_STEP     = random(0.1,PI);
	CONFIG.MAX_WANDER_STEP     = random(CONFIG.MIN_WANDER_STEP,PI);
	CONFIG.MIN_GROWTH_RATE     = random(0.1,20);
	CONFIG.MAX_GROWTH_RATE     = random(CONFIG.MIN_GROWTH_RATE,20);
	CONFIG.MIN_SHRINK_RATE     = random(0.9,0.999);
	CONFIG.MAX_SHRINK_RATE     = random(CONFIG.MIN_SHRINK_RATE,0.999);
	CONFIG.MIN_DIVERGENCE      = random(0.0,PI);
	CONFIG.MAX_DIVERGENCE      = random(CONFIG.MIN_DIVERGENCE,PI);
	Recursion.init();
	GUI.listenAll();
}

var GUI = new DAT.GUI({width: 340});
GUI.name('Recursion Settings');

// Config
GUI.add(CONFIG, 'NUM_BRANCHES').name('Trunk Count').min(1).max(20).step(1);
GUI.add(CONFIG, 'MAX_CONCURRENT').name('Max Concurrent').min(10).max(1000).step(1);
GUI.add(CONFIG, 'BRANCH_PROBABILITY').name('Branch Probability').min(0.01).max(1.0).step(0.01);
GUI.add(CONFIG, 'MIN_RADIUS').name('Radius (Min)').min(0.1).max(100);
GUI.add(CONFIG, 'MAX_RADIUS').name('Radius (Max)').min(0.1).max(100);
GUI.add(CONFIG, 'MIN_WANDER_STEP').name('Wander (Min)').min(0.0).max(PI).step(0.01);
GUI.add(CONFIG, 'MAX_WANDER_STEP').name('Wander (Max)').min(0.0).max(PI).step(0.01);
GUI.add(CONFIG, 'MIN_GROWTH_RATE').name('Growth (Min)').min(0.1).max(20).step(0.1);
GUI.add(CONFIG, 'MAX_GROWTH_RATE').name('Growth (Max)').min(0.1).max(20).step(0.1);
GUI.add(CONFIG, 'MIN_SHRINK_RATE').name('Shrink (Min)').min(0.9).max(0.999).step(0.005);
GUI.add(CONFIG, 'MAX_SHRINK_RATE').name('Shrink (Max)').min(0.9).max(0.999).step(0.005);
GUI.add(CONFIG, 'MIN_DIVERGENCE').name('Divergeence (Min)').min(0.0).max(PI).step(0.01);
GUI.add(CONFIG, 'MAX_DIVERGENCE').name('Divergeence (Max)').min(0.0).max(PI).step(0.01);

GUI.add(preset, 'key').name('Preset Behaviors').options(keys).onChange(function(){
	configure(PRESETS[preset.key]);
	Recursion.init();
	GUI.listenAll();
});
GUI.add(CONFIG, 'RENDER_MODE').name('Render Style').options(RENDER_MODES).onChange(Recursion.init);
GUI.add(Recursion, 'save').name('Save as PNG');
GUI.add(Recursion, 'clear').name('Clear').onFire(Recursion.reset);
GUI.add(Recursion, 'init').name('Clear & Regenerate');

//GUI.add(this, 'randomise').name('Randomise');
//GUI.add(this, 'saveConfig').name('Save Config');

/**
 * --------------------
 * INIT
 * --------------------
 */

Recursion.init();

