function Matrix(canvas, options, data){
	this.canvas = canvas;
	
	if(canvas.getContext){
		this.ctx = this.canvas.getContext('2d');
	} else {
		throw new Exception('no canvas support');
	}
	
	options = this.loadDefaults(options);
	
	this.x = options.x;
	this.y = options.y;
	this.width = options.width;
	this.height = options.height;
	this.flipDuration = options.flipDuration;
	this.fps = options.fps;
	this.onclick = options.onclick;
	this.onflipstart = options.onflipstart;
	this.onflipend = options.onflipend;
	this.onmove = options.onmove;
	this.contentHeightRatio = options.contentHeightRatio;
	
	this.initial = false;
	this.imageCount = 0;
	this.elements = [];
	this.data = [];
	this.baseScale = 0.1;
	this.animation = false;
	this.opening = false;
	this.step = 0;
	this.lastFrameTime = 0;
	this.blockHeight = 100;
	this.blockWidth = this.blockHeight;
	this.maxLevel = 0;
	this.lineFactor = 4;
	this.cache = {};
	this.input = '';
	this.currentPos = 'initial';
	this.lastPos = null;
	this.targetPos = null;
	this.posStep = null;
	this._lock = false;
	this._refresh = false;
	this.shadowScale = 1.2;
	this.easing = 'exp';
	this.afterNextAnimation = null;
	this.navCanvas = null;
	this.navCtx = null;
	this.blockScales = {
		min : 1,
		max : Math.max(Math.min(20, (this.height*this.contentHeightRatio)/this.blockHeight/this.baseScale-7), 2)
	};
	
	this.padding = 5 / this.baseScale;
	this.maxStep = (this.fps * this.flipDuration)/2;
	
	this.left = this.padding;
	
	try{
		this.setData(data);
	} catch(e){
		
	}
	
	var maxStep = this.maxStep;
	if(Math.floor(maxStep)%2){
		this.maxStep = Math.ceil(maxStep);
	} else {
		this.maxStep = Math.floor(maxStep);
	}
	
	this.navCanvas = document.createElement('canvas');
	this.navCanvas.height = this.canvas.height * (1 - this.contentHeightRatio);
	this.navCanvas.width = this.canvas.width;
	
	this.canvas.height = this.canvas.height - this.navCanvas.height;
	
	this.navCtx = this.navCanvas.getContext('2d');
	
	//if(this.canvas.nextElementSibling !== null){
	//	this.canvas.nextElementSibling.insertBefore(this.navCanvas);
	//} else {
	//	this.canvas.parentNode.appendChild(this.navCanvas);
	//}
	
	this.initCanvas();
	this.registerEvents();
}

Matrix.prototype = {
	initCanvas : function(){
		this.clearAll();
		this.initNavigation();
	},
	
	clearAll : function(){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.clearRect(this.x, this.y, this.width, this.height);
		this.elements = [];
		
		ctx.restore();
	},
	
	clearNavigation : function(x1, x2){
		var ctx = this.ctx;
		var height = this.height;
		var contentHeightRatio = this.contentHeightRatio;
		var y = height * contentHeightRatio;
		
		ctx.save();
		
		ctx.fillStyle = '#666';
		//ctx.fillRect(x1, Math.ceil(y), x2-x1, height - y);
		ctx.clearRect(x1, Math.ceil(y), x2-x1, height - y);
		
		ctx.restore();
	},
	
	initMaxLevel : function(){
		var i;
		this.maxLevel = 0;
		
		var data = this.data;
		
		for(i=0, len=data.length; i<len; ++i){
			this.maxLevel = Math.max(data[i].level, this.maxLevel);
		}
	},
	
	getNavScale : function(){
		var scale = 1;
		
		var navWidth = this.getNavWidth();
		var matrixWidth = this.width-80;
		
		if(navWidth > matrixWidth){
			scale = matrixWidth/navWidth;
		}
		
		return scale;
	},
	
	initNavigation : function(){
		this.initMaxLevel();
		this.drawNavigation();
	},
	
	drawNavigation : function(){
		var ctx = this.ctx;
		this.ctx = this.navCtx;
		
		//if(!this.cache.nav){
		//	this.cacheNavigation();
		//}
		
		var lastPos = this.lastPos;
		var currentPos = this.currentPos;
		
		var minX;
		var maxX;
		
		if(currentPos === null || lastPos === null){
			minX = 0;
			maxX = this.width;
		} else {
			minX = Math.max(Math.min(currentPos, lastPos)-10, 0);
			maxX = Math.min(Math.max(currentPos, lastPos)+10, this.width);
		}
		
		//this.clearNavigation(minX, maxX);
		//this.drawNavMarker(currentPos);
		//this.drawCachedNavigation(minX, maxX);
		
		this.ctx = ctx;
	},
	
	setNextPos : function(){
		this.lastPos = this.currentPos;
		var diff = 0;
		
		if(this.easing === 'exp'){
			diff = (this.targetPos - this.currentPos)/2;
			
			if(Math.abs(diff) < 1){
				this.currentPos = this.targetPos;
				this.targetPos = 'end';
			} else {
				this.currentPos += diff;
			}
		} else if(this.easing === 'linear'){
			diff = (this.targetPos - this.currentPos);
			
			if(this.posStep === null){
				this.posStep = 500/this.fps;
				if(diff < 0){
					this.posStep = -this.posStep;
				}
			}
			
			if(Math.abs(diff) < Math.abs(this.posStep)){
				this.currentPos = this.targetPos;
				this.targetPos = 'end';
				this.posStep = null;
			} else {
				this.currentPos += this.posStep;
			}
		}
	},
	
	drawFrame : function(){
		var lastScales = null;
		var end = false;
		var e = null;
		
		var onmove = this.onmove;
		var onload = this.onload;
		var afterNextAnimation = this.afterNextAnimation;
		
		if(this.targetPos === 'end'){
			this.lastPos = this.currentPos;
			this.targetPos = null;
			end = true;
		} else if(this.targetPos !== null){
			this.setNextPos();
		} else {
			lastScales = this.lastScales;
		}
		
		if(this.lastPos !== this.currentPos || this._refresh){
			if(this.currentPos === 'initial'){
				this.initial = true;
				this.currentPos = null;
				this.drawNavigation();
			}
			
			this._refresh = false;
			
			this.drawScene(lastScales);
			//this.drawNavigation();
			
			if(this.initial){
				this.ready();
 			}
			
			if(typeof onmove === 'function'){
				e = {
					x : this.currentPos,
					y : 0,
					type : 'move',
					parent : this.canvas,
					target : this
				};
				
				onmove(e);
			}
		}
		
		if(typeof afterNextAnimation === 'function' && end){
			var e = {
				x : this.currentPos,
				y : 0,
				type : 'move',
				parent : this.canvas,
				target : this
			};
			
			this.afterNextAnimation = null;
			afterNextAnimation(e);
		}
	},
	
	cacheNavigation : function(){
		//var ctx = this.ctx;
		//var i;
		//this.evenLabel = 1;
		//
		//ctx.save();
		//
		//this.setBasePosition();
		////ctx.translate(this.blockHeight, this.height * this.contentHeightRatio/this.baseScale);
		////ctx.translate(this.blockHeight, 0);
		//
		//var scale = this.getNavScale();
		//
		//log(scale);
		//
		//ctx.translate((this.width - this.getNavWidth()*scale)/2/this.baseScale, this.blockHeight*0);
		//
		//log((this.width-this.getNavWidth()*scale)/2);
		//
		//ctx.scale(scale, scale);
		//
		//var data = this.data;
		//
		//for(i=0, len=data.length; i<len; ++i){
		//	var block = this.getNavBlock(i, data[i].level, scale);
		//	this.drawNavBlock(block, i===len-1);
		//}
		//
		//ctx.restore();
		//
		//var x = 0;
		////var y = this.height * this.contentHeightRatio;
		//var y = 0;
		//var width = this.width;
		//var height = this.height - y;
		//var height = this.height * (1 - this.contentHeightRatio);
		//
		//this.cache.nav = ctx.getImageData(x, y, width, Math.floor(height));
		
		var self = this;
		this.cache.nav = new Image();
		this.cache.navLoaded = false;
		this.imageCount++;
		this.cache.nav.addEventListener('load', function(){
			--self.imageCount;
			self.cache.navLoaded = true;
			self.ready();
		}, false);
		
		this.cache.nav.src = 'img/timeline.png';
	},
	
	drawCachedNavigation : function(xMin, xMax){
		var ctx = this.ctx;
		if(this.cache.nav){
			var x = xMin;
			//var y = this.height * this.contentHeightRatio;
			var y = 0;
			//var width = xMax-xMin;
			//
			//var nav = this.cache.nav.data;
			//var height = this.cache.nav.height;
			//var navWidth = this.cache.nav.width;
			//var navBgRaw = ctx.getImageData(x, y, width, height);
			//var navBg = navBgRaw.data;
			
			if(this.cache.navLoaded){
				ctx.drawImage(this.cache.nav, x, y);
			} else {
				var self = this;
				this.cache.nav.addEventListener('load', function(){
					ctx.drawImage(self.cache.nav, x, y);
				}, false);
			}
			//ctx.putImageData(this.cache.nav, x, y);
		}
	},
	
	getPixelComponentAverage : function(src, dst, dstTrans){
		dstTrans = dstTrans/255.;
		return (src*(1-dstTrans))+(dst*dstTrans);
	},
	
	registerEvents : function(){
		var self = this;
		
		var buttonHandler = function(event){		
			var cx, cy;
			
			var offsetLeft = self.getOffset('offsetLeft');
			var offsetTop = self.getOffset('offsetTop');
			
			cx = event.clientX - offsetLeft;
			cy = event.clientY - offsetTop;
			
			cx = cx - self.getAbsLeft() * self.baseScale;
			
			var button = self.getButton(cx, cy);
			
			self.open(button);
		};
		
		this.canvas.addEventListener('click', buttonHandler, false);	
	},
	
	getButtonByIndex : function(i){
		if(i < 0){
			i = 0;
		} else if(i >= this.elements.length){
			i = this.elements.length-1;
		}
		
		return this.elements[i];
	},
	
	getOffset : function(n){
		var offset = 0;
		
		for(var i = this.canvas; i !== null; i = i.offsetParent){
			offset += i[n];
		}
		
		return offset;
	},
	
	cursorToFocus : function(cx){
		var navWidth = this.getNavWidth();
		var navScale = this.getNavScale();
		var blockWidth = this.blockWidth * this.baseScale;
		
		if(cx === null){
			return {
				index : null,
				offset : null
			};
		}
		
		cx -= this.getNavLeft();
		
		cx /= navScale;
		
		var x = cx*(this.data.length + 2);
		
		var focus = {
			index : Math.max(Math.floor(x/navWidth)-1, 0),
			offset : ((cx%blockWidth - (blockWidth/2))/this.baseScale+5)
		};
		
		return focus;
	},
	
	setData : function(data){
		if(typeof data === 'object' && typeof data.length === 'number'){
			this.data = data;
		} else {
			throw new Exception('invalid data');
		}
	},
	
	loadDefaults : function(options){
		var x, y, width, height, flipDuration, fps, onclick, onflipstart, onflipend, onmove, contentHeightRatio;
		
		x = this.parseDimension(options.x, this.canvas.clientWidth);
		y = this.parseDimension(options.y, this.canvas.clientHeight);
		
		width = this.parseDimension(options.width, this.canvas.clientWidth);
		height = this.parseDimension(options.height, this.canvas.clientHeight);
		
		contentHeightRatio = parseFloat(options.contentHeightRatio);
		
		flipDuration = parseFloat(options.flipDuration);
		fps = parseFloat(options.fps);
		
		onclick = options.onclick;
		onflipstart = options.onflipstart;
		onflipend = options.onflipend;
		onmove = options.onmove;
		
		return {
			x : this.getDefault(x, 0),
			y : this.getDefault(y, 0),
			width : this.getDefault(width, this.canvas.clientWidth),
			height : this.getDefault(height, this.canvas.clientHeight),
			flipDuration : this.getDefault(flipDuration, 2),
			fps : this.getDefault(fps, 25),
			onclick : this.getDefault(onclick, null),
			onflipstart : this.getDefault(onflipstart, null),
			onflipend : this.getDefault(onflipend, null),
			onmove : this.getDefault(onmove, null),
			contentHeightRatio : this.getDefault(contentHeightRatio, 0.618)
		};
	},
	
	getDefault : function(val, def){
		if(val !== null && val === val){
			return val;
		} else {
			return def;
		}
	},
	
	parseDimension : function(val, base){
		if(typeof val === 'number'){
			return val;
		}
		
		if(typeof val === 'string'){
			var matches = val.match(/(-?\d+(\.\d+)?)(px|%)?/);
			
			if(!matches){
				return NaN;
			}
			
			var num = matches[1], unit = matches[3];
			
			num = parseFloat(num);
			
			switch(unit){
				case '%': return num * base/100.;
				default: return val;
			}
		}
		
		return NaN;
	},
	
	clearScene : function(){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.clearRect(this.x, this.y, this.width, this.height*this.contentHeightRatio);
		
		ctx.restore();
	},
	
	getBlockScale : function(blockIndex, posInBlock){
		var dist = 0;
		var scales = [];
		var elements = this.elements;
		
		var width = this.getBlocksAbsWidth();
		var x;
		
		if(typeof blockIndex === 'number'){
			x = this.getBlocksAbsWidth(blockIndex);
			if(elements.length > blockIndex){
				x += elements[blockIndex].absWidth*posInBlock/(this.blockWidth);
			}
		}
		
		var blockScales = this.blockScales;
		
		for(var i=0,len=elements.length; i<len; ++i){
			var ele = elements[i];
			
			var temp = {
				scale : 1,
				dist : NaN
			};
			
			if(typeof blockIndex === 'number'){
				var dist = x - ele.absX;
				
				temp.dist = dist;
				dist = Math.abs(dist);
				
				var scale = (width-dist)/width;
				
				// x<0.01 -> 0
				if(scale < 0.01){
					scale = 0;
				}
				
				scale = Math.pow(scale, 8);
				
				// [0, 1] -> [min, max]
				scale = blockScales.min + scale * (blockScales.max-blockScales.min);
				
				temp.scale = scale;
			}
			
			scales[i] = temp;
		}
		
		return scales;
	},
	
	getAbsLeft : function(){
		var baseWidth = this.width/this.baseScale;
		var width = this.getBlocksAbsWidth();
		
		var left = 0;
		
		var focused = this.getFocusedBlock();
		
		if(focused !== null && focused < this.elements.length){
			left = (baseWidth/2 - this.getBlocksAbsWidth(focused+1) - this.elements[focused].dist);
		} else {
			left = (baseWidth - width)/2;
		}
		
		return left+this.blockWidth*15/2;
	},
	
	getLeft : function(){
		return this.getAbsLeft() * this.baseScale;
	},
	
	setBasePosition : function(){
		var ctx = this.ctx, scale = this.baseScale;
		ctx.translate(this.x, this.y);
		ctx.scale(scale, scale);
	},
	
	centerPosition : function(){
		var left = this.getAbsLeft();
		
		this.ctx.translate(left, 0);
	},
	
	getFocusedBlock : function(){
		var max = 1;
		
		var index = null;
		
		for(var i=0, len=this.elements.length; i<len; ++i){
			var scale = this.elements[i].scale;
			if(scale > max){
				max = scale;
				index = i;
			}
			if(scale < max){
				break;
			}
		}
		
		return index;
	},
	
	drawScene : function(scales){
		var ctx = this.ctx;
		
		if(typeof scales === 'object' && scales !== null && scales.length > 0){
			// nop;
		} else {
			var pos = this.cursorToFocus(this.currentPos);
			scales = this.getBlockScale(pos.index, pos.offset);
		}
		
		this.lastScales = scales;
		
		this.clearScene();
		
		ctx.save();
		
		this.setBasePosition();
		
		for(var i=0, len=this.data.length; i<len; ++i){
			this.initBlock(i, scales[i]);
		}
		
		this.centerPosition();
		var elements = this.elements;
		for(var i=0, len=elements.length; i<len; ++i){
			if(this.isVisible(elements[i])){
				this.drawCachedBlock(elements[i]);
			}
		}
		
		ctx.restore();
	},
	
	isVisible : function(block){
		var dist = this.width/this.baseScale/2;
		
		if(Math.abs(block.dist)-2000 > dist){
			return false;
		} else {
			return true;
		}
	},
	
	drawCachedBlock : function(block){
		if(block.image){
			try{
				this.ctx.drawImage(block.image, block.absX, block.absY, block.absWidth*this.shadowScale, block.absHeight*this.shadowScale)
			} catch(err){
				log(err);
				this.drawBlock(block);
			}
		} else {
			log('cache miss');
			this.drawBlock(block);
		}
	},
	
	initBlock : function(index, scale){
		var data = this.data[index];
		var center = this.height * this.contentHeightRatio/this.baseScale/3;
		var indent = center - (this.blockHeight*this.blockScales.max/2) + this.padding*data.level*4;
		var typeFactor = 1;
		
		if(data.level === 0){
			typeFactor *= 1.6;
		} else if(data.level === 1 && data.data.children > 0){
			typeFactor *= 1.2;
		}
		
		if(!this.elements[index]){
			var block = this.createBlock(indent, data.text, data.color, scale, data.data, typeFactor);
			this.elements[index] = block;
		} else {
			this.elements[index] = this.updateBlock(index, scale, typeFactor);
		}
	},
	
	getNavAbsHeight : function(){
		return this.blockHeight + this.padding * this.maxLevel*2;
	},
	
	getNavHeight : function(){
		return this.getNavAbsHeight()*this.baseScale;
	},
	
	getNavAbsWidth : function(){
		//  block width * number of blocks + 2 * padding (left and right, padding == block width)
		return this.blockHeight * (this.data.length + 2);
	},
	
	getNavWidth : function(){
		return this.getNavAbsWidth()*this.baseScale;
	},
	
	getNavAbsLeft : function(){
		return this.getNavLeft()/this.baseScale;
	},
	
	getNavLeft : function(){
		return (this.width-this.getNavWidth() * this.getNavScale())/2;
	},
	
	getNavAbsTop : function(){
		var center = this.height*(1-this.contentHeightRatio)/this.baseScale/2;
		var top = center - this.getNavAbsHeight()/2;
		
		return top;
	},
	
	getNavTop : function(){
		return this.getNavAbsTop()*this.baseScale;
	},
	
	getNavBlock : function(index, level, scale){
		var data = this.data[index];
		var center = this.height*(1-this.contentHeightRatio)/this.baseScale/2;
		var indent = (center-(this.getNavAbsHeight()/2) + this.padding*data.level)/scale;
		
		var block = this.createNavBlock(index*this.blockWidth, indent, data.text, data.color, level, data.data);
		
		return block;
	},
	
	getBlocksAbsWidth : function(len){
		var x = 0;
		
		var elements = this.elements;
		
		if(typeof len !== 'number'){
			len = elements.length;
		}
		
		for(var i=0; i<len; ++i){
			try{
				x += elements[i].absWidth;
			} catch(e){
				log(e)
				log(i)
			}
		}
		
		x += this.padding * (len);
		
		x += this.left;
		
		return x;
	},
	
	getBlocksWidth : function(len){
		return this.getBlocksAbsWidth(len)*this.baseScale;
	},
	
	createBlock : function(y, text, color, scale, data, typeFactor){
		if(typeof scale !== 'object'){
			scale = {
				scale : 1,
				dist : NaN
			};
		}
		
		var x = this.getBlocksAbsWidth();
		var baseScale = this.baseScale;
		var blockScale = scale.scale;
		var height = this.blockHeight * blockScale * typeFactor;
		var width = height;
		
		var block = {
			x : x * baseScale,
			y : y * baseScale,
			absX : x,
			absY : y,
			width : width  * baseScale,
			height : height * baseScale,
			absWidth : width,
			absHeight : height,
			scale :  blockScale,
			dist :  scale.dist,
			text : text,
			color : color,
			points : [
				{ x : 0, y : 0 },
				{ x : width, y : 0 },
				{ x : width, y : height },
				{ x : 0, y : height }
			],
			data : data
		};
		
		block.image = this.getBlockImage(block);
		
		return block;
	},
	
	updateBlock : function(index, scale, typeFactor){
		if(typeof scale !== 'object'){
			scale = {
				scale : 1,
				dist : NaN
			};
		}
		
		var block = this.elements[index];
		var x = this.getBlocksAbsWidth(index);
		var baseScale = this.baseScale;
		var blockScale = scale.scale;
		var height = this.blockHeight * blockScale * typeFactor;
		var width = height;
		
		block.x = x * baseScale;
		block.absX = x;
		block.width = width * baseScale;
		block.height = height * baseScale;
		block.absWidth = width;
		block.absHeight = height;
		block.scale = blockScale;
		block.dist = scale.dist;
		block.points = [
			{ x : 0, y : 0 },
			{ x : width, y : 0 },
			{ x : width, y : height },
			{ x : 0, y : height  }
		];
		
		return block;
	},
	
	createNavBlock : function(x, y, text, color, level, data){
		var baseScale = this.baseScale;
		var height = this.blockHeight;
		var width = height;
		
		var block = {
			x : x * baseScale,
			y : y * baseScale,
			absX : x,
			absY : y,
			width : width * baseScale,
			height : height * baseScale,
			absWidth : width,
			absHeight : height,
			scale :  1,
			text : text,
			level : level,
			color : color,
			points : [
				{ x : 0, y : 0 },
				{ x : height, y : 0 },
				{ x : height, y : width },
				{ x : 0, y : width }
			],
			data : data
		};
		
		return block;
	},
	
	getBlockImage : function(block){
		//var baseScale = 3;
		//
		//if(!this.cache.imageCanvas){
		//	var canvas = document.createElement('canvas');
		//	canvas.width = (this.blockWidth*this.shadowScale)*baseScale;
		//	canvas.height = (this.blockHeight*this.shadowScale)*baseScale;
		//	this.cache.imageCanvas = canvas;
		//	this.cache.imageCtx = canvas.getContext('2d');
		//}
		//
		//var regularCtx = this.ctx;
		//this.ctx = this.cache.imageCtx;
		//
		//var absX, absY, scale;
		//absX = block.absX;
		//absY = block.absY;
		//scale = block.scale;
		//
		//block.absX = 0;
		//block.absY = 0;
		//block.scale = 1;
		//
		//this.cache.imageCanvas.width = this.cache.imageCanvas.width;
		//this.cache.imageCanvas.height = this.cache.imageCanvas.height;
		//this.ctx.scale(baseScale, baseScale);
		//this.drawBlock(block);
		//
		//block.absX = absX;
		//block.absY = absY;
		//block.scale = scale;
		//
		//this.ctx = regularCtx;
		
		var self = this;
		var img = new Image();
		//img.src = this.cache.imageCanvas.toDataURL('image/png');
		
		this.imageCount++;
		img.addEventListener('load', function(){
			--self.imageCount;
			self.ready();
		}, false);
		
		if(block.data.children === 0){
			img.src = 'img/image.php?color='+block.color.substr(1)+'&text='+encodeURIComponent(block.text)+'&open=1';
		} else {
			img.src = 'img/image.php?color='+block.color.substr(1)+'&text='+encodeURIComponent(block.text);
		}
		
		return img;
	},

	ready : function(){
		var onload = this.onload;
		
		if(typeof onload === 'function' && this.initial && this.imageCount === 0){
			this.initial = false;
			
			e = {
				x : null,
				y : null,
				type : 'load',
				parent : this.canvas,
				target : this
			};
			
			onload(e);
		}
 	},
	
	drawBlock : function (block){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.translate(block.absX, block.absY);
		
		var scale = block.scale;
		ctx.scale(scale, scale);
		
		this.drawBlockBg(block);
		this.drawBlockGradient(block);
		this.drawBlockText(block);
		
		ctx.restore();
	},
	
	drawBlockBg : function(block, shadow){
		var ctx = this.ctx;
		ctx.save();
		
		if(shadow !== false){
			ctx.shadowOffsetX = this.blockWidth/10;
			ctx.shadowOffsetY = this.blockHeight/10;
			ctx.shadowBlur = (this.blockWidth/10+this.blockHeight/10)/2;
			ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
		}
		ctx.fillStyle = block.color;
		
		var height = this.blockHeight;
		var width = this.blockWidth;
		ctx.fillRect(0, 0, width, height);
		
		ctx.restore();
	},
	
	drawBlockGradient : function (block){
		var ctx = this.ctx;
		var height = this.blockHeight;
		var width = this.blockWidth;
		ctx.save();
		
		var lingrad = ctx.createLinearGradient(0, 0, 0, height);
		lingrad.addColorStop(0.2, 'rgba(51, 51, 51, 0)');
		lingrad.addColorStop(1, 'rgba(51, 51, 51, 0.3)');
		
		ctx.fillStyle = lingrad;
		
		ctx.fillRect(0, 0, width, height);
		
		ctx.restore();	
	},
	
	drawBlockText : function (block){
		var ctx = this.ctx;
		ctx.save();
		
		var textW = this.blockHeight*0.80;
		var lineH = 13;
		
		ctx.shadowOffsetX = 1;
		ctx.shadowOffsetY = 1;
		ctx.shadowBlur    = 2;
		ctx.shadowColor   = 'rgba(0, 0, 0, 0.8)';
		ctx.fillStyle = '#fff';
		
		var text = this.wrapText(block.text.toUpperCase(), textW);
		
		ctx.textBaseline = 'bottom';
		
		//var baseline = 10+(textW-text.length*lineH)/2;
		var baseline = 10 + (textW - (text.length-1)*lineH);
		
		for(var i=0, len=text.length; i<len; ++i){
			var x = (textW - ctx.measureText(text[i]).width)/2;
			//ctx.fillText(text[i], 10+x, baseline+i*lineH, textW);
			ctx.fillText(text[i], 10, baseline+i*lineH, textW);
		}
		
		ctx.restore();
	},
	
	drawNavBlock : function (block, last){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.translate(block.absX, block.absY);
		
		var scale = block.scale;
		ctx.scale(scale, scale);
		
		this.drawBlockBg(block, false);
		this.drawBlockGradient(block);
		this.drawBlockBorder(block);
		
		if(block.level === 0){
			this.drawNavBlockText(block, last);
		}
		
		ctx.restore();
	},
	
	drawBlockBorder : function(block){
		var ctx = this.ctx;
		ctx.save();
		
		var halfWidth = 5;
		var height = this.blockHeight-2*halfWidth;
		
		ctx.lineWidth = 2*halfWidth;
		ctx.strokeStyle = '#fff';
		
		ctx.strokeRect(halfWidth, halfWidth, height, height);
		
		ctx.restore();
	},
	
	drawNavBlockText : function(block, last){
		var ctx = this.ctx;
		ctx.save();
		
		var textW = this.blockHeight*1.0;
		var lineH = 15;
		
		var text = this.wrapText(block.text.toUpperCase(), textW);
		
		ctx.textBaseline = this.evenLabel<0?'bottom':'alphabetic';
		ctx.font = '10pt Arial';
		ctx.fillStyle = '#333';
		
		ctx.translate(0, this.evenLabel*this.blockHeight*(this.maxLevel*this.lineFactor + 2*this.evenLabel));
		
		this.drawNavBlockTextLine(block);
		
		ctx.scale(1/this.baseScale, 1/this.baseScale);
		
		var x = 5;
		
		if(last){
			x = -(textW+x);
		}
		
		for(var i=0, len=text.length; i<len; ++i){
			var line = i*lineH;
			line += this.evenLabel<0 ? this.blockHeight*this.baseScale : 0;
			ctx.fillText(text[i], x, line, textW);
		}
		
		this.evenLabel *= -1;
		
		ctx.restore();	
	},
	
	drawNavBlockTextLine : function(block){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.lineWidth = 5;
		ctx.strokeStyle = '#333';
		
		var points = [
			{ x : 0, y : -1 * this.evenLabel * this.blockHeight * (this.maxLevel*this.lineFactor + 2*this.evenLabel) },
			{ x : 0, y : 0 }
		];
		
		this.strokePath(0, 0, points);
		
		ctx.restore();	
	},
	
	
	drawNavMarker : function(pos){
		var ctx = this.ctx;
		
		if(typeof pos === 'number'){
			ctx.save();
			
			this.setBasePosition();
			ctx.translate(this.blockHeight, this.height * this.contentHeightRatio/this.baseScale);
			
			pos = (pos/this.baseScale)-this.blockHeight;
			
			var points = [
				{ x : pos, y : 5 },
				{ x : pos, y : (this.height*(1-this.contentHeightRatio))/this.baseScale }
			];
			
			ctx.miterLimit = 0
			ctx.lineWidth = 10;
			ctx.strokeStyle = '#f00';
			
			this.strokePath(0, 0, points);
			
			ctx.restore();	
		}
	},
	
	wrapText : function(text, maxW){
		var ctx = this.ctx;
		var words = text.split(' ');
		
		var result = '';
		var lines = [];
		
		for(var i=0, len=words.length; i<len; ++i){
			var ws = result+' '+words[i];
			
			if(ctx.measureText(words[i]).width > maxW){
				// aktuelles Wort > max. Länge
				if(result !== ''){
					// Zwischenergebnis != leer
					lines.push(result);
					result = '';
				}			
				lines.push(words[i]);
			} else if(ctx.measureText(words[i]).width <= maxW && result === ''){
				// aktulles Wort <= max. Länge und Zwischenergebnis == leer
				result = words[i];
			} else if(ctx.measureText(ws).width <= maxW && result !== ''){
				// (aktulles Wort + Zwischenergebnis) <= max. Länge und Zwischenergebnis != leer
				result = ws;
			} else if(ctx.measureText(words[i]).width <= maxW && result !== ''){
				// aktulles Wort <= max. Länge und Zwischenergebnis != leer
				lines.push(result);
				result = words[i];
				
				//lines.push(words[i]);
			} else {
				lines.push(result);
				result = '';
			}
		}
		
		if(result !== ''){
			lines.push(result);
		}
		
		return lines;
	},
	
	getButton : function(cx, cy){
		var blocks = this.elements;
		
		if(typeof cy === "number" && typeof cx === "number"){
			var x = (cx - this.x) / this.baseScale;
			var y = (cy - this.y) / this.baseScale;
			
			for(var i=0, len=blocks.length; i<len; ++i){
				var block = blocks[i];
				if(x >= block.absX && x <= block.absX + block.absWidth
				&& y >= block.absY && y <= block.absY + block.absHeight){
					return block;
				}
			}
		} else if(typeof cx === "number" && cx >= 0 && cx < blocks.length){
			return blocks[cx];
		}
		
		return null;
	},
	
	animateBlock : function(block, reverse){
		var self = this;
		var ctx = this.ctx;
		
		if(this.animation){
			return;
		}
		
		if(typeof this.onflipstart === 'function'){
			var e = {
				type : 'flipstart',
				parent : this.canvas,
				target : this,
				reverse : reverse
			};
			
			this.onflipstart(e, block);
		}
		
		if(reverse){
			this.opening = true;
			this.step = this.maxStep - 1;
		} else {
			this.opening = false;
			this.step = 0;
		}
		
		this.reverse = reverse;
		this.animatedBlock = block;
		this.animation = true;
	},
	
	drawFlipFrame : function(){
		var ctx = this.ctx;
		var step = this.step;
		var maxStep = this.maxStep;
		var opening = this.opening;
		var block = this.animatedBlock;
		var reverse = this.reverse;
		
		ctx.save();
	
		this.setBasePosition();
		this.centerPosition();
		
		this.drawFlip(block);
		
		ctx.restore();
		
		if(reverse){
			step = (step - 1 + maxStep) % (maxStep);
			
			if(step === 0 && opening){
				opening = false;
				step = maxStep - 1;
			} else if(step === 0 && !opening){
				opening = true;
				this.stopFlip(block, reverse);
			}
		} else {
			step = (step + 1) % (maxStep);
			
			if(step === 0 && !opening){
				opening = true;
			} else if(step === 0 && opening){
				opening = false;
				this.stopFlip(block, reverse);
			}
		}
		
		this.opening = opening;
		this.step = step;
	},
	
	stopFlip : function(block, reverse){
		this.animation = false;
		
		if(typeof this.onflipend === 'function'){
			var e = {
				type : 'flipend',
				parent : this.canvas,
				target : this,
				reverse : reverse
			};
			
			this.onflipend(e, block);
		}
	},
	
	getFlipBlock : function(block){
		var rect;
		
		var stepDist = Math.PI/2 / (this.maxStep-1);
		
		if(this.opening){
			rect = this.flipOutRect(block, this.step * stepDist);
			rect.color = '#eee';
		} else {
			rect = this.flipInRect(block, this.step * stepDist);
			rect.color = block.color;
		}
		
		rect.height = block.absHeight;
		
		return rect;
	},
	
	clearFlip : function(block){
		this.ctx.clearRect(-10, -block.absHeight/3, block.absWidth + this.padding + 10, block.absHeight*5/3);
	},
	
	drawFlipBlock : function(block){
		var ctx = this.ctx;
		var rect = this.getFlipBlock(block);
		
		ctx.save();
		
		ctx.shadowOffsetX = 2;
		ctx.shadowOffsetY = 2;
		ctx.shadowBlur = 2;
		ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
		ctx.fillStyle = rect.color;
		this.fillPath(0, 0, rect.points);
		
		ctx.restore();
		
		return rect;
	},
	
	drawFlipBlockGradient : function(rect){
		var ctx = this.ctx;
		ctx.save();
		
		var lingrad = ctx.createLinearGradient(0, 0, 0, rect.height);
		lingrad.addColorStop(0.2, 'rgba(51, 51, 51, 0)');
		lingrad.addColorStop(1, 'rgba(51, 51, 51, 0.3)');
		
		ctx.fillStyle = lingrad;
		this.fillPath(0, 0, rect.points);
		
		ctx.restore();
	},
	
	drawFlip : function(block){
		var ctx = this.ctx;
		ctx.save();
		
		ctx.translate(block.absX, block.absY);
		
		this.clearFlip(block);
		var rect = this.drawFlipBlock(block);
		this.drawFlipBlockGradient(rect);
		
		ctx.restore();
	},
	
	flipOutRect : function(rect, angle){
		if(rect.points.length < 4){
			return rect;
		}
		
		var fx, fy;
		fx = Math.cos(angle);
		fy = -fx;
	
		return this.flipRect(rect, angle, fx, fy);
	},
	
	flipInRect : function(rect, angle){
		if(rect.points.length < 4){
			return rect;
		}
		
		var fx, fy;
		fx = fy = Math.sin(angle);
	
		return this.flipRect(rect, angle, fx, fy);
	},
	
	flipRect : function(rect, angle, fx, fy){
		var width = fx * rect.absWidth/2;
		var height = fy * rect.absHeight/3;
		var points = rect.points;
		var flippedPoints = [];
		
		flippedPoints[0] = {
			x : points[0].x + width,
			y : points[0].y - height
		};
		flippedPoints[1] = {
			x : points[1].x - width,
			y : points[1].y + height
		};
		flippedPoints[2] = {
			x : points[2].x - width,
			y : points[2].y - height
		};
		flippedPoints[3] = {
			x : points[3].x + width,
			y : points[3].y + height
		};
		
		var flippedRect = {
			width : flippedPoints[1].x - flippedPoints[0].x,
			height : Math.max(
				flippedPoints[3].y - flippedPoints[0].y,
				flippedPoints[1].y - flippedPoints[2].y
			),
			points : flippedPoints
		};
		
		return flippedRect;
	},
	
	fillPath : function(x, y, points){
		var ctx = this.ctx;
		ctx.save();
		
		this.drawPath(x, y, points).fill();
		
		ctx.restore();
	},
	
	strokePath : function(x, y, points){
		var ctx = this.ctx;
		ctx.save();
		
		this.drawPath(x, y, points).stroke();
		
		ctx.restore();
	},
	
	drawPath : function(x, y, points){
		var ctx = this.ctx;
		
		if(!points || !points.hasOwnProperty(length)){
			points = this.points;
		}
		
		if(points.length < 1){
			throw('path to short');
		}
		
		ctx.beginPath();
		
		ctx.translate(x, y);
		
		for(var i=0, len=points.length; i<=len; ++i){
			ctx.lineTo(points[i%len].x, points[i%len].y);
		}
		
		return ctx;
	},
	
	start : function(){
		var self = this;
		
		this.interval = setInterval(function(){
			if(self.animation){
				self.drawFlipFrame();
			} else if(!this._lock) {
				self.drawFrame();
			}
		}, 1000/this.fps);
		
		return this;
	},
	
	stop : function(){
		clearInterval(this.interval);
		return this;
	},
	
	refresh : function(){
		this._refresh = true;
	},
	
	setEasing : function(easing){
		switch(easing){
			case 'linear': this.posStep = null;
			case 'exp': this.easing = easing; break;
			default: this.easing = 'exp'; break;
		}
		
		return this;
	},
	
	goTo : function(i, easing){
		i = parseInt(i);
		
		if(isNaN(i)){
			return this;
		}
		
		this.setEasing(easing);
		var cx;
		var navWidth = this.getNavWidth();
		var navScale = this.getNavScale();
		var blockWidth = this.blockWidth * this.baseScale;
		
		if(i >= this.data.length){
			i = this.data.length;
		} else if(i < 1) {
			i = 1;
		} else {
			++i;
		}
		
		cx = i*navWidth
		cx /= this.data.length + 2;
		cx *= navScale;
		cx += blockWidth/2;
		cx += this.getNavLeft();
		
		this.targetPos = cx;
		return this;
	},
	
	goToX : function(x, easing){
		x = parseFloat(x);
		
		if(x !== x){
			return this;
		}
		
		this.setEasing(easing);
		this.targetPos = x;
		
		return this;
	},
	
	open : function(i){
		var button;
		
		if(typeof i === 'number'){
			i = parseInt(i);
			
			if(i !== i){
				return this;
			}
			
			button = this.getButtonByIndex(i);
		} else if(typeof i === 'object'){
			button = i;
		}
		
		if(typeof button === 'object' && button && button.data.children === 0){
			if(button){
				if(!this.animation){
					this.animateBlock(button);
				}
			}
			
			if(typeof this.onclick === 'function'){
				var e = {
					x : NaN,
					y : NaN,
					type : 'click',
					parent : this.canvas,
					target : this
				};
				
				this.onclick(e, button);
			}
		}
		
		return this;
	},
	
	blockAtNav : function(x, y){
		var b = this.cursorToFocus(x);
		
		return b.index;
	},
	
	lock : function(){
		this._lock = true;
		return this;
	},
	
	unlock : function(){
		this._lock = false;
		return this;
	}
}
