Raghav Arora hace 6 años
padre
commit
ba4ef91af2

+ 5 - 1
nova/static/js/dataset.js

@@ -93,7 +93,11 @@ var meta = new Vue ({
           }
         })
       }
-    }
+    },
+    openWave: function () {
+        window.open('/wave?user='+user_name+'&dataset='+dataset_name,'_blank',
+        'location=no, width=275, height=275, scrollbars=no, status=no')
+    },
   }
 })
 

+ 293 - 0
nova/static/js/wave/FlyControls.js

@@ -0,0 +1,293 @@
+/**
+ * @author James Baicoianu / http://www.baicoianu.com/
+ */
+
+THREE.FlyControls = function ( object, domElement ) {
+
+	this.object = object;
+
+	this.domElement = ( domElement !== undefined ) ? domElement : document;
+	if ( domElement ) this.domElement.setAttribute( 'tabindex', - 1 );
+
+	// API
+
+	this.movementSpeed = 1.0;
+	this.rollSpeed = 0.005;
+
+	this.dragToLook = false;
+	this.autoForward = false;
+
+	// disable default target object behavior
+
+	// internals
+
+	this.tmpQuaternion = new THREE.Quaternion();
+
+	this.mouseStatus = 0;
+
+	this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
+	this.moveVector = new THREE.Vector3( 0, 0, 0 );
+	this.rotationVector = new THREE.Vector3( 0, 0, 0 );
+
+	this.handleEvent = function ( event ) {
+
+		if ( typeof this[ event.type ] == 'function' ) {
+
+			this[ event.type ]( event );
+
+		}
+
+	};
+
+	this.keydown = function( event ) {
+
+		if ( event.altKey ) {
+
+			return;
+
+		}
+
+		//event.preventDefault();
+
+		switch ( event.keyCode ) {
+
+			case 16: /* shift */ this.movementSpeedMultiplier = .1; break;
+
+			case 87: /*W*/ this.moveState.forward = 1; break;
+			case 83: /*S*/ this.moveState.back = 1; break;
+
+			case 65: /*A*/ this.moveState.left = 1; break;
+			case 68: /*D*/ this.moveState.right = 1; break;
+
+			case 82: /*R*/ this.moveState.up = 1; break;
+			case 70: /*F*/ this.moveState.down = 1; break;
+
+			case 38: /*up*/ this.moveState.pitchUp = 1; break;
+			case 40: /*down*/ this.moveState.pitchDown = 1; break;
+
+			case 37: /*left*/ this.moveState.yawLeft = 1; break;
+			case 39: /*right*/ this.moveState.yawRight = 1; break;
+
+			case 81: /*Q*/ this.moveState.rollLeft = 1; break;
+			case 69: /*E*/ this.moveState.rollRight = 1; break;
+
+		}
+
+		this.updateMovementVector();
+		this.updateRotationVector();
+
+	};
+
+	this.keyup = function( event ) {
+
+		switch ( event.keyCode ) {
+
+			case 16: /* shift */ this.movementSpeedMultiplier = 1; break;
+
+			case 87: /*W*/ this.moveState.forward = 0; break;
+			case 83: /*S*/ this.moveState.back = 0; break;
+
+			case 65: /*A*/ this.moveState.left = 0; break;
+			case 68: /*D*/ this.moveState.right = 0; break;
+
+			case 82: /*R*/ this.moveState.up = 0; break;
+			case 70: /*F*/ this.moveState.down = 0; break;
+
+			case 38: /*up*/ this.moveState.pitchUp = 0; break;
+			case 40: /*down*/ this.moveState.pitchDown = 0; break;
+
+			case 37: /*left*/ this.moveState.yawLeft = 0; break;
+			case 39: /*right*/ this.moveState.yawRight = 0; break;
+
+			case 81: /*Q*/ this.moveState.rollLeft = 0; break;
+			case 69: /*E*/ this.moveState.rollRight = 0; break;
+
+		}
+
+		this.updateMovementVector();
+		this.updateRotationVector();
+
+	};
+
+	this.mousedown = function( event ) {
+
+		if ( this.domElement !== document ) {
+
+			this.domElement.focus();
+
+		}
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		if ( this.dragToLook ) {
+
+			this.mouseStatus ++;
+
+		} else {
+
+			switch ( event.button ) {
+
+				case 0: this.moveState.forward = 1; break;
+				case 2: this.moveState.back = 1; break;
+
+			}
+
+			this.updateMovementVector();
+
+		}
+
+	};
+
+	this.mousemove = function( event ) {
+
+		if ( ! this.dragToLook || this.mouseStatus > 0 ) {
+
+			var container = this.getContainerDimensions();
+			var halfWidth  = container.size[ 0 ] / 2;
+			var halfHeight = container.size[ 1 ] / 2;
+
+			this.moveState.yawLeft   = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth  ) / halfWidth;
+			this.moveState.pitchDown =   ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
+
+			this.updateRotationVector();
+
+		}
+
+	};
+
+	this.mouseup = function( event ) {
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		if ( this.dragToLook ) {
+
+			this.mouseStatus --;
+
+			this.moveState.yawLeft = this.moveState.pitchDown = 0;
+
+		} else {
+
+			switch ( event.button ) {
+
+				case 0: this.moveState.forward = 0; break;
+				case 2: this.moveState.back = 0; break;
+
+			}
+
+			this.updateMovementVector();
+
+		}
+
+		this.updateRotationVector();
+
+	};
+
+	this.update = function( delta ) {
+
+		var moveMult = delta * this.movementSpeed;
+		var rotMult = delta * this.rollSpeed;
+
+		this.object.translateX( this.moveVector.x * moveMult );
+		this.object.translateY( this.moveVector.y * moveMult );
+		this.object.translateZ( this.moveVector.z * moveMult );
+
+		this.tmpQuaternion.set( this.rotationVector.x * rotMult, this.rotationVector.y * rotMult, this.rotationVector.z * rotMult, 1 ).normalize();
+		this.object.quaternion.multiply( this.tmpQuaternion );
+
+		// expose the rotation vector for convenience
+		this.object.rotation.setFromQuaternion( this.object.quaternion, this.object.rotation.order );
+
+
+	};
+
+	this.updateMovementVector = function() {
+
+		var forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
+
+		this.moveVector.x = ( - this.moveState.left    + this.moveState.right );
+		this.moveVector.y = ( - this.moveState.down    + this.moveState.up );
+		this.moveVector.z = ( - forward + this.moveState.back );
+
+		//console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
+
+	};
+
+	this.updateRotationVector = function() {
+
+		this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
+		this.rotationVector.y = ( - this.moveState.yawRight  + this.moveState.yawLeft );
+		this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
+
+		//console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
+
+	};
+
+	this.getContainerDimensions = function() {
+
+		if ( this.domElement != document ) {
+
+			return {
+				size	: [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
+				offset	: [ this.domElement.offsetLeft,  this.domElement.offsetTop ]
+			};
+
+		} else {
+
+			return {
+				size	: [ window.innerWidth, window.innerHeight ],
+				offset	: [ 0, 0 ]
+			};
+
+		}
+
+	};
+
+	function bind( scope, fn ) {
+
+		return function () {
+
+			fn.apply( scope, arguments );
+
+		};
+
+	}
+
+	function contextmenu( event ) {
+
+		event.preventDefault();
+
+	}
+
+	this.dispose = function() {
+
+		this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+		this.domElement.removeEventListener( 'mousedown', _mousedown, false );
+		this.domElement.removeEventListener( 'mousemove', _mousemove, false );
+		this.domElement.removeEventListener( 'mouseup', _mouseup, false );
+
+		window.removeEventListener( 'keydown', _keydown, false );
+		window.removeEventListener( 'keyup', _keyup, false );
+
+	}
+
+	var _mousemove = bind( this, this.mousemove );
+	var _mousedown = bind( this, this.mousedown );
+	var _mouseup = bind( this, this.mouseup );
+	var _keydown = bind( this, this.keydown );
+	var _keyup = bind( this, this.keyup );
+
+	this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+
+	this.domElement.addEventListener( 'mousemove', _mousemove, false );
+	this.domElement.addEventListener( 'mousedown', _mousedown, false );
+	this.domElement.addEventListener( 'mouseup',   _mouseup, false );
+
+	window.addEventListener( 'keydown', _keydown, false );
+	window.addEventListener( 'keyup',   _keyup, false );
+
+	this.updateMovementVector();
+	this.updateRotationVector();
+
+};

+ 1034 - 0
nova/static/js/wave/OrbitControls.js

@@ -0,0 +1,1034 @@
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ * @author erich666 / http://erichaines.com
+ */
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+//    Orbit - left mouse / touch: one finger move
+//    Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
+//    Pan - right mouse, or arrow keys / touch: three finter swipe
+
+THREE.OrbitControls = function ( object, domElement ) {
+
+	this.object = object;
+
+	this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+	// Set to false to disable this control
+	this.enabled = true;
+
+	// "target" sets the location of focus, where the object orbits around
+	this.target = new THREE.Vector3();
+
+	// How far you can dolly in and out ( PerspectiveCamera only )
+	this.minDistance = 0;
+	this.maxDistance = Infinity;
+
+	// How far you can zoom in and out ( OrthographicCamera only )
+	this.minZoom = 0;
+	this.maxZoom = Infinity;
+
+	// How far you can orbit vertically, upper and lower limits.
+	// Range is 0 to Math.PI radians.
+	this.minPolarAngle = 0; // radians
+	this.maxPolarAngle = Math.PI; // radians
+
+	// How far you can orbit horizontally, upper and lower limits.
+	// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+	this.minAzimuthAngle = - Infinity; // radians
+	this.maxAzimuthAngle = Infinity; // radians
+
+	// Set to true to enable damping (inertia)
+	// If damping is enabled, you must call controls.update() in your animation loop
+	this.enableDamping = false;
+	this.dampingFactor = 0.25;
+
+	// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+	// Set to false to disable zooming
+	this.enableZoom = true;
+	this.zoomSpeed = 1.0;
+
+	// Set to false to disable rotating
+	this.enableRotate = true;
+	this.rotateSpeed = 1.0;
+
+	// Set to false to disable panning
+	this.enablePan = true;
+	this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
+
+	// Set to true to automatically rotate around the target
+	// If auto-rotate is enabled, you must call controls.update() in your animation loop
+	this.autoRotate = false;
+	this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+
+	// Set to false to disable use of the keys
+	this.enableKeys = true;
+
+	// The four arrow keys
+	this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+	// Mouse buttons
+	this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
+
+	// for reset
+	this.target0 = this.target.clone();
+	this.position0 = this.object.position.clone();
+	this.zoom0 = this.object.zoom;
+
+	//
+	// public methods
+	//
+
+	this.getPolarAngle = function () {
+
+		return spherical.phi;
+
+	};
+
+	this.getAzimuthalAngle = function () {
+
+		return spherical.theta;
+
+	};
+
+	this.reset = function () {
+
+		scope.target.copy( scope.target0 );
+		scope.object.position.copy( scope.position0 );
+		scope.object.zoom = scope.zoom0;
+
+		scope.object.updateProjectionMatrix();
+		scope.dispatchEvent( changeEvent );
+
+		scope.update();
+
+		state = STATE.NONE;
+
+	};
+
+	// this method is exposed, but perhaps it would be better if we can make it private...
+	this.update = function() {
+
+		var offset = new THREE.Vector3();
+
+		// so camera.up is the orbit axis
+		var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+		var quatInverse = quat.clone().inverse();
+
+		var lastPosition = new THREE.Vector3();
+		var lastQuaternion = new THREE.Quaternion();
+
+		return function update () {
+
+			var position = scope.object.position;
+
+			offset.copy( position ).sub( scope.target );
+
+			// rotate offset to "y-axis-is-up" space
+			offset.applyQuaternion( quat );
+
+			// angle from z-axis around y-axis
+			spherical.setFromVector3( offset );
+
+			if ( scope.autoRotate && state === STATE.NONE ) {
+
+				rotateLeft( getAutoRotationAngle() );
+
+			}
+
+			spherical.theta += sphericalDelta.theta;
+			spherical.phi += sphericalDelta.phi;
+
+			// restrict theta to be between desired limits
+			spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
+
+			// restrict phi to be between desired limits
+			spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+
+			spherical.makeSafe();
+
+
+			spherical.radius *= scale;
+
+			// restrict radius to be between desired limits
+			spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
+
+			// move target to panned location
+			scope.target.add( panOffset );
+
+			offset.setFromSpherical( spherical );
+
+			// rotate offset back to "camera-up-vector-is-up" space
+			offset.applyQuaternion( quatInverse );
+
+			position.copy( scope.target ).add( offset );
+
+			scope.object.lookAt( scope.target );
+
+			if ( scope.enableDamping === true ) {
+
+				sphericalDelta.theta *= ( 1 - scope.dampingFactor );
+				sphericalDelta.phi *= ( 1 - scope.dampingFactor );
+
+			} else {
+
+				sphericalDelta.set( 0, 0, 0 );
+
+			}
+
+			scale = 1;
+			panOffset.set( 0, 0, 0 );
+
+			// update condition is:
+			// min(camera displacement, camera rotation in radians)^2 > EPS
+			// using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+			if ( zoomChanged ||
+				lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+				8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+				scope.dispatchEvent( changeEvent );
+
+				lastPosition.copy( scope.object.position );
+				lastQuaternion.copy( scope.object.quaternion );
+				zoomChanged = false;
+
+				return true;
+
+			}
+
+			return false;
+
+		};
+
+	}();
+
+	this.dispose = function() {
+
+		scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
+		scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+		scope.domElement.removeEventListener( 'mousewheel', onMouseWheel, false );
+		scope.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
+
+		scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+		scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+		scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
+
+		document.removeEventListener( 'mousemove', onMouseMove, false );
+		document.removeEventListener( 'mouseup', onMouseUp, false );
+
+		window.removeEventListener( 'keydown', onKeyDown, false );
+
+		//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+	};
+
+	//
+	// internals
+	//
+
+	var scope = this;
+
+	var changeEvent = { type: 'change' };
+	var startEvent = { type: 'start' };
+	var endEvent = { type: 'end' };
+
+	var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
+
+	var state = STATE.NONE;
+
+	var EPS = 0.000001;
+
+	// current position in spherical coordinates
+	var spherical = new THREE.Spherical();
+	var sphericalDelta = new THREE.Spherical();
+
+	var scale = 1;
+	var panOffset = new THREE.Vector3();
+	var zoomChanged = false;
+
+	var rotateStart = new THREE.Vector2();
+	var rotateEnd = new THREE.Vector2();
+	var rotateDelta = new THREE.Vector2();
+
+	var panStart = new THREE.Vector2();
+	var panEnd = new THREE.Vector2();
+	var panDelta = new THREE.Vector2();
+
+	var dollyStart = new THREE.Vector2();
+	var dollyEnd = new THREE.Vector2();
+	var dollyDelta = new THREE.Vector2();
+
+	function getAutoRotationAngle() {
+
+		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+
+	}
+
+	function getZoomScale() {
+
+		return Math.pow( 0.95, scope.zoomSpeed );
+
+	}
+
+	function rotateLeft( angle ) {
+
+		sphericalDelta.theta -= angle;
+
+	}
+
+	function rotateUp( angle ) {
+
+		sphericalDelta.phi -= angle;
+
+	}
+
+	var panLeft = function() {
+
+		var v = new THREE.Vector3();
+
+		return function panLeft( distance, objectMatrix ) {
+
+			v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+			v.multiplyScalar( - distance );
+
+			panOffset.add( v );
+
+		};
+
+	}();
+
+	var panUp = function() {
+
+		var v = new THREE.Vector3();
+
+		return function panUp( distance, objectMatrix ) {
+
+			v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
+			v.multiplyScalar( distance );
+
+			panOffset.add( v );
+
+		};
+
+	}();
+
+	// deltaX and deltaY are in pixels; right and down are positive
+	var pan = function() {
+
+		var offset = new THREE.Vector3();
+
+		return function pan ( deltaX, deltaY ) {
+
+			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+			if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+				// perspective
+				var position = scope.object.position;
+				offset.copy( position ).sub( scope.target );
+				var targetDistance = offset.length();
+
+				// half of the fov is center to top of screen
+				targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+
+				// we actually don't use screenWidth, since perspective camera is fixed to screen height
+				panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+				panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+				// orthographic
+				panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+				panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+			} else {
+
+				// camera neither orthographic nor perspective
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+				scope.enablePan = false;
+
+			}
+
+		};
+
+	}();
+
+	function dollyIn( dollyScale ) {
+
+		if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+			scale /= dollyScale;
+
+		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+			scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+			scope.object.updateProjectionMatrix();
+			zoomChanged = true;
+
+		} else {
+
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			scope.enableZoom = false;
+
+		}
+
+	}
+
+	function dollyOut( dollyScale ) {
+
+		if ( scope.object instanceof THREE.PerspectiveCamera ) {
+
+			scale *= dollyScale;
+
+		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+
+			scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+			scope.object.updateProjectionMatrix();
+			zoomChanged = true;
+
+		} else {
+
+			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+			scope.enableZoom = false;
+
+		}
+
+	}
+
+	//
+	// event callbacks - update the object state
+	//
+
+	function handleMouseDownRotate( event ) {
+
+		//console.log( 'handleMouseDownRotate' );
+
+		rotateStart.set( event.clientX, event.clientY );
+
+	}
+
+	function handleMouseDownDolly( event ) {
+
+		//console.log( 'handleMouseDownDolly' );
+
+		dollyStart.set( event.clientX, event.clientY );
+
+	}
+
+	function handleMouseDownPan( event ) {
+
+		//console.log( 'handleMouseDownPan' );
+
+		panStart.set( event.clientX, event.clientY );
+
+	}
+
+	function handleMouseMoveRotate( event ) {
+
+		//console.log( 'handleMouseMoveRotate' );
+
+		rotateEnd.set( event.clientX, event.clientY );
+		rotateDelta.subVectors( rotateEnd, rotateStart );
+
+		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+		// rotating across whole screen goes 360 degrees around
+		rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+
+		// rotating up and down along whole screen attempts to go 360, but limited to 180
+		rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+
+		rotateStart.copy( rotateEnd );
+
+		scope.update();
+
+	}
+
+	function handleMouseMoveDolly( event ) {
+
+		//console.log( 'handleMouseMoveDolly' );
+
+		dollyEnd.set( event.clientX, event.clientY );
+
+		dollyDelta.subVectors( dollyEnd, dollyStart );
+
+		if ( dollyDelta.y > 0 ) {
+
+			dollyIn( getZoomScale() );
+
+		} else if ( dollyDelta.y < 0 ) {
+
+			dollyOut( getZoomScale() );
+
+		}
+
+		dollyStart.copy( dollyEnd );
+
+		scope.update();
+
+	}
+
+	function handleMouseMovePan( event ) {
+
+		//console.log( 'handleMouseMovePan' );
+
+		panEnd.set( event.clientX, event.clientY );
+
+		panDelta.subVectors( panEnd, panStart );
+
+		pan( panDelta.x, panDelta.y );
+
+		panStart.copy( panEnd );
+
+		scope.update();
+
+	}
+
+	function handleMouseUp( event ) {
+
+		//console.log( 'handleMouseUp' );
+
+	}
+
+	function handleMouseWheel( event ) {
+
+		//console.log( 'handleMouseWheel' );
+
+		var delta = 0;
+
+		if ( event.wheelDelta !== undefined ) {
+
+			// WebKit / Opera / Explorer 9
+
+			delta = event.wheelDelta;
+
+		} else if ( event.detail !== undefined ) {
+
+			// Firefox
+
+			delta = - event.detail;
+
+		}
+
+		if ( delta > 0 ) {
+
+			dollyOut( getZoomScale() );
+
+		} else if ( delta < 0 ) {
+
+			dollyIn( getZoomScale() );
+
+		}
+
+		scope.update();
+
+	}
+
+	function handleKeyDown( event ) {
+
+		//console.log( 'handleKeyDown' );
+
+		switch ( event.keyCode ) {
+
+			case scope.keys.UP:
+				pan( 0, scope.keyPanSpeed );
+				scope.update();
+				break;
+
+			case scope.keys.BOTTOM:
+				pan( 0, - scope.keyPanSpeed );
+				scope.update();
+				break;
+
+			case scope.keys.LEFT:
+				pan( scope.keyPanSpeed, 0 );
+				scope.update();
+				break;
+
+			case scope.keys.RIGHT:
+				pan( - scope.keyPanSpeed, 0 );
+				scope.update();
+				break;
+
+		}
+
+	}
+
+	function handleTouchStartRotate( event ) {
+
+		//console.log( 'handleTouchStartRotate' );
+
+		rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+	}
+
+	function handleTouchStartDolly( event ) {
+
+		//console.log( 'handleTouchStartDolly' );
+
+		var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+		var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+		var distance = Math.sqrt( dx * dx + dy * dy );
+
+		dollyStart.set( 0, distance );
+
+	}
+
+	function handleTouchStartPan( event ) {
+
+		//console.log( 'handleTouchStartPan' );
+
+		panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+	}
+
+	function handleTouchMoveRotate( event ) {
+
+		//console.log( 'handleTouchMoveRotate' );
+
+		rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+		rotateDelta.subVectors( rotateEnd, rotateStart );
+
+		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+		// rotating across whole screen goes 360 degrees around
+		rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+
+		// rotating up and down along whole screen attempts to go 360, but limited to 180
+		rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
+
+		rotateStart.copy( rotateEnd );
+
+		scope.update();
+
+	}
+
+	function handleTouchMoveDolly( event ) {
+
+		//console.log( 'handleTouchMoveDolly' );
+
+		var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+		var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+		var distance = Math.sqrt( dx * dx + dy * dy );
+
+		dollyEnd.set( 0, distance );
+
+		dollyDelta.subVectors( dollyEnd, dollyStart );
+
+		if ( dollyDelta.y > 0 ) {
+
+			dollyOut( getZoomScale() );
+
+		} else if ( dollyDelta.y < 0 ) {
+
+			dollyIn( getZoomScale() );
+
+		}
+
+		dollyStart.copy( dollyEnd );
+
+		scope.update();
+
+	}
+
+	function handleTouchMovePan( event ) {
+
+		//console.log( 'handleTouchMovePan' );
+
+		panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+		panDelta.subVectors( panEnd, panStart );
+
+		pan( panDelta.x, panDelta.y );
+
+		panStart.copy( panEnd );
+
+		scope.update();
+
+	}
+
+	function handleTouchEnd( event ) {
+
+		//console.log( 'handleTouchEnd' );
+
+	}
+
+	//
+	// event handlers - FSM: listen for events and reset state
+	//
+
+	function onMouseDown( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		event.preventDefault();
+
+		if ( event.button === scope.mouseButtons.ORBIT ) {
+
+			if ( scope.enableRotate === false ) return;
+
+			handleMouseDownRotate( event );
+
+			state = STATE.ROTATE;
+
+		} else if ( event.button === scope.mouseButtons.ZOOM ) {
+
+			if ( scope.enableZoom === false ) return;
+
+			handleMouseDownDolly( event );
+
+			state = STATE.DOLLY;
+
+		} else if ( event.button === scope.mouseButtons.PAN ) {
+
+			if ( scope.enablePan === false ) return;
+
+			handleMouseDownPan( event );
+
+			state = STATE.PAN;
+
+		}
+
+		if ( state !== STATE.NONE ) {
+
+			document.addEventListener( 'mousemove', onMouseMove, false );
+			document.addEventListener( 'mouseup', onMouseUp, false );
+
+			scope.dispatchEvent( startEvent );
+
+		}
+
+	}
+
+	function onMouseMove( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		event.preventDefault();
+
+		if ( state === STATE.ROTATE ) {
+
+			if ( scope.enableRotate === false ) return;
+
+			handleMouseMoveRotate( event );
+
+		} else if ( state === STATE.DOLLY ) {
+
+			if ( scope.enableZoom === false ) return;
+
+			handleMouseMoveDolly( event );
+
+		} else if ( state === STATE.PAN ) {
+
+			if ( scope.enablePan === false ) return;
+
+			handleMouseMovePan( event );
+
+		}
+
+	}
+
+	function onMouseUp( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		handleMouseUp( event );
+
+		document.removeEventListener( 'mousemove', onMouseMove, false );
+		document.removeEventListener( 'mouseup', onMouseUp, false );
+
+		scope.dispatchEvent( endEvent );
+
+		state = STATE.NONE;
+
+	}
+
+	function onMouseWheel( event ) {
+
+		if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		handleMouseWheel( event );
+
+		scope.dispatchEvent( startEvent ); // not sure why these are here...
+		scope.dispatchEvent( endEvent );
+
+	}
+
+	function onKeyDown( event ) {
+
+		if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+
+		handleKeyDown( event );
+
+	}
+
+	function onTouchStart( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		switch ( event.touches.length ) {
+
+			case 1:	// one-fingered touch: rotate
+
+				if ( scope.enableRotate === false ) return;
+
+				handleTouchStartRotate( event );
+
+				state = STATE.TOUCH_ROTATE;
+
+				break;
+
+			case 2:	// two-fingered touch: dolly
+
+				if ( scope.enableZoom === false ) return;
+
+				handleTouchStartDolly( event );
+
+				state = STATE.TOUCH_DOLLY;
+
+				break;
+
+			case 3: // three-fingered touch: pan
+
+				if ( scope.enablePan === false ) return;
+
+				handleTouchStartPan( event );
+
+				state = STATE.TOUCH_PAN;
+
+				break;
+
+			default:
+
+				state = STATE.NONE;
+
+		}
+
+		if ( state !== STATE.NONE ) {
+
+			scope.dispatchEvent( startEvent );
+
+		}
+
+	}
+
+	function onTouchMove( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		switch ( event.touches.length ) {
+
+			case 1: // one-fingered touch: rotate
+
+				if ( scope.enableRotate === false ) return;
+				if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
+
+				handleTouchMoveRotate( event );
+
+				break;
+
+			case 2: // two-fingered touch: dolly
+
+				if ( scope.enableZoom === false ) return;
+				if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
+
+				handleTouchMoveDolly( event );
+
+				break;
+
+			case 3: // three-fingered touch: pan
+
+				if ( scope.enablePan === false ) return;
+				if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
+
+				handleTouchMovePan( event );
+
+				break;
+
+			default:
+
+				state = STATE.NONE;
+
+		}
+
+	}
+
+	function onTouchEnd( event ) {
+
+		if ( scope.enabled === false ) return;
+
+		handleTouchEnd( event );
+
+		scope.dispatchEvent( endEvent );
+
+		state = STATE.NONE;
+
+	}
+
+	function onContextMenu( event ) {
+
+		event.preventDefault();
+
+	}
+
+	//
+
+	scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
+
+	scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
+	scope.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
+	scope.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox
+
+	scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
+	scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
+	scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
+
+	window.addEventListener( 'keydown', onKeyDown, false );
+
+	// force an update at start
+
+	this.update();
+
+};
+
+THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
+
+Object.defineProperties( THREE.OrbitControls.prototype, {
+
+	center: {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
+			return this.target;
+
+		}
+
+	},
+
+	// backward compatibility
+
+	noZoom: {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+			return ! this.enableZoom;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+			this.enableZoom = ! value;
+
+		}
+
+	},
+
+	noRotate: {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+			return ! this.enableRotate;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
+			this.enableRotate = ! value;
+
+		}
+
+	},
+
+	noPan: {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+			return ! this.enablePan;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
+			this.enablePan = ! value;
+
+		}
+
+	},
+
+	noKeys: {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+			return ! this.enableKeys;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
+			this.enableKeys = ! value;
+
+		}
+
+	},
+
+	staticMoving : {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+			return ! this.enableDamping;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
+			this.enableDamping = ! value;
+
+		}
+
+	},
+
+	dynamicDampingFactor : {
+
+		get: function () {
+
+			console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+			return this.dampingFactor;
+
+		},
+
+		set: function ( value ) {
+
+			console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
+			this.dampingFactor = value;
+
+		}
+
+	}
+
+} );

+ 634 - 0
nova/static/js/wave/TrackballControls.js

@@ -0,0 +1,634 @@
+/**
+ * @author Eberhard Graether / http://egraether.com/
+ * @author Mark Lundin 	/ http://mark-lundin.com
+ * @author Simone Manini / http://daron1337.github.io
+ * @author Luca Antiga 	/ http://lantiga.github.io
+ */
+
+THREE.TrackballControls = function ( object, domElement ) {
+
+	var _this = this;
+	var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
+
+	this.object = object;
+	this.domElement = ( domElement !== undefined ) ? domElement : document;
+
+	// API
+
+	this.enabled = true;
+
+	this.screen = { left: 0, top: 0, width: 0, height: 0 };
+
+	this.rotateSpeed = 1.0;
+	this.zoomSpeed = 1.2;
+	this.panSpeed = 0.3;
+
+	this.noRotate = false;
+	this.noZoom = false;
+	this.noPan = false;
+
+	this.staticMoving = false;
+	this.dynamicDampingFactor = 0.2;
+
+	this.minDistance = 0;
+	this.maxDistance = Infinity;
+
+	this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
+
+	// internals
+
+	this.target = new THREE.Vector3();
+
+	var EPS = 0.000001;
+
+	var lastPosition = new THREE.Vector3();
+
+	var _state = STATE.NONE,
+	_prevState = STATE.NONE,
+
+	_eye = new THREE.Vector3(),
+
+	_movePrev = new THREE.Vector2(),
+	_moveCurr = new THREE.Vector2(),
+
+	_lastAxis = new THREE.Vector3(),
+	_lastAngle = 0,
+
+	_zoomStart = new THREE.Vector2(),
+	_zoomEnd = new THREE.Vector2(),
+
+	_touchZoomDistanceStart = 0,
+	_touchZoomDistanceEnd = 0,
+
+	_panStart = new THREE.Vector2(),
+	_panEnd = new THREE.Vector2();
+
+	// for reset
+
+	this.target0 = this.target.clone();
+	this.position0 = this.object.position.clone();
+	this.up0 = this.object.up.clone();
+
+	// events
+
+	var changeEvent = { type: 'change' };
+	var startEvent = { type: 'start' };
+	var endEvent = { type: 'end' };
+
+
+	// methods
+
+	this.handleResize = function () {
+
+		if ( this.domElement === document ) {
+
+			this.screen.left = 0;
+			this.screen.top = 0;
+			this.screen.width = window.innerWidth;
+			this.screen.height = window.innerHeight;
+
+		} else {
+
+			var box = this.domElement.getBoundingClientRect();
+			// adjustments come from similar code in the jquery offset() function
+			var d = this.domElement.ownerDocument.documentElement;
+			this.screen.left = box.left + window.pageXOffset - d.clientLeft;
+			this.screen.top = box.top + window.pageYOffset - d.clientTop;
+			this.screen.width = box.width;
+			this.screen.height = box.height;
+
+		}
+
+	};
+
+	this.handleEvent = function ( event ) {
+
+		if ( typeof this[ event.type ] == 'function' ) {
+
+			this[ event.type ]( event );
+
+		}
+
+	};
+
+	var getMouseOnScreen = ( function () {
+
+		var vector = new THREE.Vector2();
+
+		return function getMouseOnScreen( pageX, pageY ) {
+
+			vector.set(
+				( pageX - _this.screen.left ) / _this.screen.width,
+				( pageY - _this.screen.top ) / _this.screen.height
+			);
+
+			return vector;
+
+		};
+
+	}() );
+
+	var getMouseOnCircle = ( function () {
+
+		var vector = new THREE.Vector2();
+
+		return function getMouseOnCircle( pageX, pageY ) {
+
+			vector.set(
+				( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ),
+				( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional
+			);
+
+			return vector;
+
+		};
+
+	}() );
+
+	this.rotateCamera = ( function() {
+
+		var axis = new THREE.Vector3(),
+			quaternion = new THREE.Quaternion(),
+			eyeDirection = new THREE.Vector3(),
+			objectUpDirection = new THREE.Vector3(),
+			objectSidewaysDirection = new THREE.Vector3(),
+			moveDirection = new THREE.Vector3(),
+			angle;
+
+		return function rotateCamera() {
+
+			moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
+			angle = moveDirection.length();
+
+			if ( angle ) {
+
+				_eye.copy( _this.object.position ).sub( _this.target );
+
+				eyeDirection.copy( _eye ).normalize();
+				objectUpDirection.copy( _this.object.up ).normalize();
+				objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
+
+				objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
+				objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
+
+				moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
+
+				axis.crossVectors( moveDirection, _eye ).normalize();
+
+				angle *= _this.rotateSpeed;
+				quaternion.setFromAxisAngle( axis, angle );
+
+				_eye.applyQuaternion( quaternion );
+				_this.object.up.applyQuaternion( quaternion );
+
+				_lastAxis.copy( axis );
+				_lastAngle = angle;
+
+			} else if ( ! _this.staticMoving && _lastAngle ) {
+
+				_lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor );
+				_eye.copy( _this.object.position ).sub( _this.target );
+				quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
+				_eye.applyQuaternion( quaternion );
+				_this.object.up.applyQuaternion( quaternion );
+
+			}
+
+			_movePrev.copy( _moveCurr );
+
+		};
+
+	}() );
+
+
+	this.zoomCamera = function () {
+
+		var factor;
+
+		if ( _state === STATE.TOUCH_ZOOM_PAN ) {
+
+			factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
+			_touchZoomDistanceStart = _touchZoomDistanceEnd;
+			_eye.multiplyScalar( factor );
+
+		} else {
+
+			factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
+
+			if ( factor !== 1.0 && factor > 0.0 ) {
+
+				_eye.multiplyScalar( factor );
+
+				if ( _this.staticMoving ) {
+
+					_zoomStart.copy( _zoomEnd );
+
+				} else {
+
+					_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
+
+				}
+
+			}
+
+		}
+
+	};
+
+	this.panCamera = ( function() {
+
+		var mouseChange = new THREE.Vector2(),
+			objectUp = new THREE.Vector3(),
+			pan = new THREE.Vector3();
+
+		return function panCamera() {
+
+			mouseChange.copy( _panEnd ).sub( _panStart );
+
+			if ( mouseChange.lengthSq() ) {
+
+				mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );
+
+				pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
+				pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
+
+				_this.object.position.add( pan );
+				_this.target.add( pan );
+
+				if ( _this.staticMoving ) {
+
+					_panStart.copy( _panEnd );
+
+				} else {
+
+					_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
+
+				}
+
+			}
+
+		};
+
+	}() );
+
+	this.checkDistances = function () {
+
+		if ( ! _this.noZoom || ! _this.noPan ) {
+
+			if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {
+
+				_this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );
+				_zoomStart.copy( _zoomEnd );
+
+			}
+
+			if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {
+
+				_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );
+				_zoomStart.copy( _zoomEnd );
+
+			}
+
+		}
+
+	};
+
+	this.update = function () {
+
+		_eye.subVectors( _this.object.position, _this.target );
+
+		if ( ! _this.noRotate ) {
+
+			_this.rotateCamera();
+
+		}
+
+		if ( ! _this.noZoom ) {
+
+			_this.zoomCamera();
+
+		}
+
+		if ( ! _this.noPan ) {
+
+			_this.panCamera();
+
+		}
+
+		_this.object.position.addVectors( _this.target, _eye );
+
+		_this.checkDistances();
+
+		_this.object.lookAt( _this.target );
+
+		if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {
+
+			_this.dispatchEvent( changeEvent );
+
+			lastPosition.copy( _this.object.position );
+
+		}
+
+	};
+
+	this.reset = function () {
+
+		_state = STATE.NONE;
+		_prevState = STATE.NONE;
+
+		_this.target.copy( _this.target0 );
+		_this.object.position.copy( _this.position0 );
+		_this.object.up.copy( _this.up0 );
+
+		_eye.subVectors( _this.object.position, _this.target );
+
+		_this.object.lookAt( _this.target );
+
+		_this.dispatchEvent( changeEvent );
+
+		lastPosition.copy( _this.object.position );
+
+	};
+
+	// listeners
+
+	function keydown( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		window.removeEventListener( 'keydown', keydown );
+
+		_prevState = _state;
+
+		if ( _state !== STATE.NONE ) {
+
+			return;
+
+		} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
+
+			_state = STATE.ROTATE;
+
+		} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
+
+			_state = STATE.ZOOM;
+
+		} else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
+
+			_state = STATE.PAN;
+
+		}
+
+	}
+
+	function keyup( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		_state = _prevState;
+
+		window.addEventListener( 'keydown', keydown, false );
+
+	}
+
+	function mousedown( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		if ( _state === STATE.NONE ) {
+
+			_state = event.button;
+
+		}
+
+		if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+			_movePrev.copy( _moveCurr );
+
+		} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+			_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+			_zoomEnd.copy( _zoomStart );
+
+		} else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+			_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+			_panEnd.copy( _panStart );
+
+		}
+
+		document.addEventListener( 'mousemove', mousemove, false );
+		document.addEventListener( 'mouseup', mouseup, false );
+
+		_this.dispatchEvent( startEvent );
+
+	}
+
+	function mousemove( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		if ( _state === STATE.ROTATE && ! _this.noRotate ) {
+
+			_movePrev.copy( _moveCurr );
+			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+
+		} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
+
+			_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+		} else if ( _state === STATE.PAN && ! _this.noPan ) {
+
+			_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+		}
+
+	}
+
+	function mouseup( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		_state = STATE.NONE;
+
+		document.removeEventListener( 'mousemove', mousemove );
+		document.removeEventListener( 'mouseup', mouseup );
+		_this.dispatchEvent( endEvent );
+
+	}
+
+	function mousewheel( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		var delta = 0;
+
+		if ( event.wheelDelta ) {
+
+			// WebKit / Opera / Explorer 9
+
+			delta = event.wheelDelta / 40;
+
+		} else if ( event.detail ) {
+
+			// Firefox
+
+			delta = - event.detail / 3;
+
+		}
+
+		_zoomStart.y += delta * 0.01;
+		_this.dispatchEvent( startEvent );
+		_this.dispatchEvent( endEvent );
+
+	}
+
+	function touchstart( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		switch ( event.touches.length ) {
+
+			case 1:
+				_state = STATE.TOUCH_ROTATE;
+				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+				_movePrev.copy( _moveCurr );
+				break;
+
+			case 2:
+				_state = STATE.TOUCH_ZOOM_PAN;
+				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+				_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
+
+				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+				_panStart.copy( getMouseOnScreen( x, y ) );
+				_panEnd.copy( _panStart );
+				break;
+
+			default:
+				_state = STATE.NONE;
+
+		}
+		_this.dispatchEvent( startEvent );
+
+
+	}
+
+	function touchmove( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		event.preventDefault();
+		event.stopPropagation();
+
+		switch ( event.touches.length ) {
+
+			case 1:
+				_movePrev.copy( _moveCurr );
+				_moveCurr.copy( getMouseOnCircle(  event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+				break;
+
+			case 2:
+				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
+
+				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+				_panEnd.copy( getMouseOnScreen( x, y ) );
+				break;
+
+			default:
+				_state = STATE.NONE;
+
+		}
+
+	}
+
+	function touchend( event ) {
+
+		if ( _this.enabled === false ) return;
+
+		switch ( event.touches.length ) {
+
+			case 1:
+				_movePrev.copy( _moveCurr );
+				_moveCurr.copy( getMouseOnCircle(  event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
+				break;
+
+			case 2:
+				_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
+
+				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
+				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
+				_panEnd.copy( getMouseOnScreen( x, y ) );
+				_panStart.copy( _panEnd );
+				break;
+
+		}
+
+		_state = STATE.NONE;
+		_this.dispatchEvent( endEvent );
+
+	}
+
+	function contextmenu( event ) {
+
+		event.preventDefault();
+
+	}
+
+	this.dispose = function() {
+
+		this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
+		this.domElement.removeEventListener( 'mousedown', mousedown, false );
+		this.domElement.removeEventListener( 'mousewheel', mousewheel, false );
+		this.domElement.removeEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox
+
+		this.domElement.removeEventListener( 'touchstart', touchstart, false );
+		this.domElement.removeEventListener( 'touchend', touchend, false );
+		this.domElement.removeEventListener( 'touchmove', touchmove, false );
+
+		document.removeEventListener( 'mousemove', mousemove, false );
+		document.removeEventListener( 'mouseup', mouseup, false );
+
+		window.removeEventListener( 'keydown', keydown, false );
+		window.removeEventListener( 'keyup', keyup, false );
+
+	}
+
+	this.domElement.addEventListener( 'contextmenu', contextmenu, false );
+	this.domElement.addEventListener( 'mousedown', mousedown, false );
+	this.domElement.addEventListener( 'mousewheel', mousewheel, false );
+	this.domElement.addEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox
+
+	this.domElement.addEventListener( 'touchstart', touchstart, false );
+	this.domElement.addEventListener( 'touchend', touchend, false );
+	this.domElement.addEventListener( 'touchmove', touchmove, false );
+
+	window.addEventListener( 'keydown', keydown, false );
+	window.addEventListener( 'keyup', keyup, false );
+
+	this.handleResize();
+
+	// force an update at start
+	this.update();
+
+};
+
+THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;

+ 652 - 0
nova/static/js/wave/ejs.js

@@ -0,0 +1,652 @@
+ejs = (function(){
+
+// CommonJS require()
+
+function require(p){
+    if ('fs' == p) return {};
+    if ('path' == p) return {};
+    var path = require.resolve(p)
+      , mod = require.modules[path];
+    if (!mod) throw new Error('failed to require "' + p + '"');
+    if (!mod.exports) {
+      mod.exports = {};
+      mod.call(mod.exports, mod, mod.exports, require.relative(path));
+    }
+    return mod.exports;
+  }
+
+require.modules = {};
+
+require.resolve = function (path){
+    var orig = path
+      , reg = path + '.js'
+      , index = path + '/index.js';
+    return require.modules[reg] && reg
+      || require.modules[index] && index
+      || orig;
+  };
+
+require.register = function (path, fn){
+    require.modules[path] = fn;
+  };
+
+require.relative = function (parent) {
+    return function(p){
+      if ('.' != p.substr(0, 1)) return require(p);
+      
+      var path = parent.split('/')
+        , segs = p.split('/');
+      path.pop();
+      
+      for (var i = 0; i < segs.length; i++) {
+        var seg = segs[i];
+        if ('..' == seg) path.pop();
+        else if ('.' != seg) path.push(seg);
+      }
+
+      return require(path.join('/'));
+    };
+  };
+
+
+require.register("ejs.js", function(module, exports, require){
+
+/*!
+ * EJS
+ * Copyright(c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var utils = require('./utils')
+  , path = require('path')
+  , dirname = path.dirname
+  , extname = path.extname
+  , join = path.join
+  , fs = require('fs')
+  , read = fs.readFileSync;
+
+/**
+ * Filters.
+ *
+ * @type Object
+ */
+
+var filters = exports.filters = require('./filters');
+
+/**
+ * Intermediate js cache.
+ *
+ * @type Object
+ */
+
+var cache = {};
+
+/**
+ * Clear intermediate js cache.
+ *
+ * @api public
+ */
+
+exports.clearCache = function(){
+  cache = {};
+};
+
+/**
+ * Translate filtered code into function calls.
+ *
+ * @param {String} js
+ * @return {String}
+ * @api private
+ */
+
+function filtered(js) {
+  return js.substr(1).split('|').reduce(function(js, filter){
+    var parts = filter.split(':')
+      , name = parts.shift()
+      , args = parts.join(':') || '';
+    if (args) args = ', ' + args;
+    return 'filters.' + name + '(' + js + args + ')';
+  });
+};
+
+/**
+ * Re-throw the given `err` in context to the
+ * `str` of ejs, `filename`, and `lineno`.
+ *
+ * @param {Error} err
+ * @param {String} str
+ * @param {String} filename
+ * @param {String} lineno
+ * @api private
+ */
+
+function rethrow(err, str, filename, lineno){
+  var lines = str.split('\n')
+    , start = Math.max(lineno - 3, 0)
+    , end = Math.min(lines.length, lineno + 3);
+
+  // Error context
+  var context = lines.slice(start, end).map(function(line, i){
+    var curr = i + start + 1;
+    return (curr == lineno ? ' >> ' : '    ')
+      + curr
+      + '| '
+      + line;
+  }).join('\n');
+
+  // Alter exception message
+  err.path = filename;
+  err.message = (filename || 'ejs') + ':'
+    + lineno + '\n'
+    + context + '\n\n'
+    + err.message;
+
+  throw err;
+}
+
+/**
+ * Parse the given `str` of ejs, returning the function body.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api public
+ */
+
+var parse = exports.parse = function(str, options){
+  var options = options || {}
+    , open = options.open || exports.open || '<%'
+    , close = options.close || exports.close || '%>'
+    , filename = options.filename
+    , compileDebug = options.compileDebug !== false
+    , buf = "";
+
+  buf += 'var buf = [];';
+  if (false !== options._with) buf += '\nwith (locals || {}) { (function(){ ';
+  buf += '\n buf.push(\'';
+
+  var lineno = 1;
+
+  var consumeEOL = false;
+  for (var i = 0, len = str.length; i < len; ++i) {
+    var stri = str[i];
+    if (str.slice(i, open.length + i) == open) {
+      i += open.length
+
+      var prefix, postfix, line = (compileDebug ? '__stack.lineno=' : '') + lineno;
+      switch (str[i]) {
+        case '=':
+          prefix = "', escape((" + line + ', ';
+          postfix = ")), '";
+          ++i;
+          break;
+        case '-':
+          prefix = "', (" + line + ', ';
+          postfix = "), '";
+          ++i;
+          break;
+        default:
+          prefix = "');" + line + ';';
+          postfix = "; buf.push('";
+      }
+
+      var end = str.indexOf(close, i);
+
+      if (end < 0){
+        throw new Error('Could not find matching close tag "' + close + '".');
+      }
+
+      var js = str.substring(i, end)
+        , start = i
+        , include = null
+        , n = 0;
+
+      if ('-' == js[js.length-1]){
+        js = js.substring(0, js.length - 2);
+        consumeEOL = true;
+      }
+
+      if (0 == js.trim().indexOf('include')) {
+        var name = js.trim().slice(7).trim();
+        if (!filename) throw new Error('filename option is required for includes');
+        var path = resolveInclude(name, filename);
+        include = read(path, 'utf8');
+        include = exports.parse(include, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug });
+        buf += "' + (function(){" + include + "})() + '";
+        js = '';
+      }
+
+      while (~(n = js.indexOf("\n", n))) n++, lineno++;
+      if (js.substr(0, 1) == ':') js = filtered(js);
+      if (js) {
+        if (js.lastIndexOf('//') > js.lastIndexOf('\n')) js += '\n';
+        buf += prefix;
+        buf += js;
+        buf += postfix;
+      }
+      i += end - start + close.length - 1;
+
+    } else if (stri == "\\") {
+      buf += "\\\\";
+    } else if (stri == "'") {
+      buf += "\\'";
+    } else if (stri == "\r") {
+      // ignore
+    } else if (stri == "\n") {
+      if (consumeEOL) {
+        consumeEOL = false;
+      } else {
+        buf += "\\n";
+        lineno++;
+      }
+    } else {
+      buf += stri;
+    }
+  }
+
+  if (false !== options._with) buf += "'); })();\n} \nreturn buf.join('');";
+  else buf += "');\nreturn buf.join('');";
+  return buf;
+};
+
+/**
+ * Compile the given `str` of ejs into a `Function`.
+ *
+ * @param {String} str
+ * @param {Object} options
+ * @return {Function}
+ * @api public
+ */
+
+var compile = exports.compile = function(str, options){
+  options = options || {};
+  var escape = options.escape || utils.escape;
+
+  var input = JSON.stringify(str)
+    , compileDebug = options.compileDebug !== false
+    , client = options.client
+    , filename = options.filename
+        ? JSON.stringify(options.filename)
+        : 'undefined';
+
+  if (compileDebug) {
+    // Adds the fancy stack trace meta info
+    str = [
+      'var __stack = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };',
+      rethrow.toString(),
+      'try {',
+      exports.parse(str, options),
+      '} catch (err) {',
+      '  rethrow(err, __stack.input, __stack.filename, __stack.lineno);',
+      '}'
+    ].join("\n");
+  } else {
+    str = exports.parse(str, options);
+  }
+
+  if (options.debug) console.log(str);
+  if (client) str = 'escape = escape || ' + escape.toString() + ';\n' + str;
+
+  try {
+    var fn = new Function('locals, filters, escape, rethrow', str);
+  } catch (err) {
+    if ('SyntaxError' == err.name) {
+      err.message += options.filename
+        ? ' in ' + filename
+        : ' while compiling ejs';
+    }
+    throw err;
+  }
+
+  if (client) return fn;
+
+  return function(locals){
+    return fn.call(this, locals, filters, escape, rethrow);
+  }
+};
+
+/**
+ * Render the given `str` of ejs.
+ *
+ * Options:
+ *
+ *   - `locals`          Local variables object
+ *   - `cache`           Compiled functions are cached, requires `filename`
+ *   - `filename`        Used by `cache` to key caches
+ *   - `scope`           Function execution context
+ *   - `debug`           Output generated function body
+ *   - `open`            Open tag, defaulting to "<%"
+ *   - `close`           Closing tag, defaulting to "%>"
+ *
+ * @param {String} str
+ * @param {Object} options
+ * @return {String}
+ * @api public
+ */
+
+exports.render = function(str, options){
+  var fn
+    , options = options || {};
+
+  if (options.cache) {
+    if (options.filename) {
+      fn = cache[options.filename] || (cache[options.filename] = compile(str, options));
+    } else {
+      throw new Error('"cache" option requires "filename".');
+    }
+  } else {
+    fn = compile(str, options);
+  }
+
+  options.__proto__ = options.locals;
+  return fn.call(options.scope, options);
+};
+
+/**
+ * Render an EJS file at the given `path` and callback `fn(err, str)`.
+ *
+ * @param {String} path
+ * @param {Object|Function} options or callback
+ * @param {Function} fn
+ * @api public
+ */
+
+exports.renderFile = function(path, options, fn){
+  var key = path + ':string';
+
+  if ('function' == typeof options) {
+    fn = options, options = {};
+  }
+
+  options.filename = path;
+
+  var str;
+  try {
+    str = options.cache
+      ? cache[key] || (cache[key] = read(path, 'utf8'))
+      : read(path, 'utf8');
+  } catch (err) {
+    fn(err);
+    return;
+  }
+  fn(null, exports.render(str, options));
+};
+
+/**
+ * Resolve include `name` relative to `filename`.
+ *
+ * @param {String} name
+ * @param {String} filename
+ * @return {String}
+ * @api private
+ */
+
+function resolveInclude(name, filename) {
+  var path = join(dirname(filename), name);
+  var ext = extname(name);
+  if (!ext) path += '.ejs';
+  return path;
+}
+
+// express support
+
+exports.__express = exports.renderFile;
+
+/**
+ * Expose to require().
+ */
+
+if (require.extensions) {
+  require.extensions['.ejs'] = function (module, filename) {
+    filename = filename || module.filename;
+    var options = { filename: filename, client: true }
+      , template = fs.readFileSync(filename).toString()
+      , fn = compile(template, options);
+    module._compile('module.exports = ' + fn.toString() + ';', filename);
+  };
+} else if (require.registerExtension) {
+  require.registerExtension('.ejs', function(src) {
+    return compile(src, {});
+  });
+}
+
+}); // module: ejs.js
+
+require.register("filters.js", function(module, exports, require){
+/*!
+ * EJS - Filters
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * First element of the target `obj`.
+ */
+
+exports.first = function(obj) {
+  return obj[0];
+};
+
+/**
+ * Last element of the target `obj`.
+ */
+
+exports.last = function(obj) {
+  return obj[obj.length - 1];
+};
+
+/**
+ * Capitalize the first letter of the target `str`.
+ */
+
+exports.capitalize = function(str){
+  str = String(str);
+  return str[0].toUpperCase() + str.substr(1, str.length);
+};
+
+/**
+ * Downcase the target `str`.
+ */
+
+exports.downcase = function(str){
+  return String(str).toLowerCase();
+};
+
+/**
+ * Uppercase the target `str`.
+ */
+
+exports.upcase = function(str){
+  return String(str).toUpperCase();
+};
+
+/**
+ * Sort the target `obj`.
+ */
+
+exports.sort = function(obj){
+  return Object.create(obj).sort();
+};
+
+/**
+ * Sort the target `obj` by the given `prop` ascending.
+ */
+
+exports.sort_by = function(obj, prop){
+  return Object.create(obj).sort(function(a, b){
+    a = a[prop], b = b[prop];
+    if (a > b) return 1;
+    if (a < b) return -1;
+    return 0;
+  });
+};
+
+/**
+ * Size or length of the target `obj`.
+ */
+
+exports.size = exports.length = function(obj) {
+  return obj.length;
+};
+
+/**
+ * Add `a` and `b`.
+ */
+
+exports.plus = function(a, b){
+  return Number(a) + Number(b);
+};
+
+/**
+ * Subtract `b` from `a`.
+ */
+
+exports.minus = function(a, b){
+  return Number(a) - Number(b);
+};
+
+/**
+ * Multiply `a` by `b`.
+ */
+
+exports.times = function(a, b){
+  return Number(a) * Number(b);
+};
+
+/**
+ * Divide `a` by `b`.
+ */
+
+exports.divided_by = function(a, b){
+  return Number(a) / Number(b);
+};
+
+/**
+ * Join `obj` with the given `str`.
+ */
+
+exports.join = function(obj, str){
+  return obj.join(str || ', ');
+};
+
+/**
+ * Truncate `str` to `len`.
+ */
+
+exports.truncate = function(str, len, append){
+  str = String(str);
+  if (str.length > len) {
+    str = str.slice(0, len);
+    if (append) str += append;
+  }
+  return str;
+};
+
+/**
+ * Truncate `str` to `n` words.
+ */
+
+exports.truncate_words = function(str, n){
+  var str = String(str)
+    , words = str.split(/ +/);
+  return words.slice(0, n).join(' ');
+};
+
+/**
+ * Replace `pattern` with `substitution` in `str`.
+ */
+
+exports.replace = function(str, pattern, substitution){
+  return String(str).replace(pattern, substitution || '');
+};
+
+/**
+ * Prepend `val` to `obj`.
+ */
+
+exports.prepend = function(obj, val){
+  return Array.isArray(obj)
+    ? [val].concat(obj)
+    : val + obj;
+};
+
+/**
+ * Append `val` to `obj`.
+ */
+
+exports.append = function(obj, val){
+  return Array.isArray(obj)
+    ? obj.concat(val)
+    : obj + val;
+};
+
+/**
+ * Map the given `prop`.
+ */
+
+exports.map = function(arr, prop){
+  return arr.map(function(obj){
+    return obj[prop];
+  });
+};
+
+/**
+ * Reverse the given `obj`.
+ */
+
+exports.reverse = function(obj){
+  return Array.isArray(obj)
+    ? obj.reverse()
+    : String(obj).split('').reverse().join('');
+};
+
+/**
+ * Get `prop` of the given `obj`.
+ */
+
+exports.get = function(obj, prop){
+  return obj[prop];
+};
+
+/**
+ * Packs the given `obj` into json string
+ */
+exports.json = function(obj){
+  return JSON.stringify(obj);
+};
+
+}); // module: filters.js
+
+require.register("utils.js", function(module, exports, require){
+
+/*!
+ * EJS
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Escape the given string of `html`.
+ *
+ * @param {String} html
+ * @return {String}
+ * @api private
+ */
+
+exports.escape = function(html){
+  return String(html)
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/'/g, '&#39;')
+    .replace(/"/g, '&quot;');
+};
+ 
+
+}); // module: utils.js
+
+ return require("ejs");
+})();

+ 1844 - 0
nova/static/js/wave/jscolor.js

@@ -0,0 +1,1844 @@
+/**
+ * jscolor - JavaScript Color Picker
+ *
+ * @link    http://jscolor.com
+ * @license For open source use: GPLv3
+ *          For commercial use: JSColor Commercial License
+ * @author  Jan Odvarko
+ * @version 2.0.4
+ *
+ * See usage examples at http://jscolor.com/examples/
+ */
+
+
+"use strict";
+
+
+if (!window.jscolor) { window.jscolor = (function () {
+
+
+var jsc = {
+
+
+	register : function () {
+		jsc.attachDOMReadyEvent(jsc.init);
+		jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown);
+		jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart);
+		jsc.attachEvent(window, 'resize', jsc.onWindowResize);
+	},
+
+
+	init : function () {
+		if (jsc.jscolor.lookupClass) {
+			jsc.jscolor.installByClassName(jsc.jscolor.lookupClass);
+		}
+	},
+
+
+	tryInstallOnElements : function (elms, className) {
+		var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i');
+
+		for (var i = 0; i < elms.length; i += 1) {
+			if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') {
+				if (jsc.isColorAttrSupported) {
+					// skip inputs of type 'color' if supported by the browser
+					continue;
+				}
+			}
+			var m;
+			if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) {
+				var targetElm = elms[i];
+				var optsStr = null;
+
+				var dataOptions = jsc.getDataAttr(targetElm, 'jscolor');
+				if (dataOptions !== null) {
+					optsStr = dataOptions;
+				} else if (m[4]) {
+					optsStr = m[4];
+				}
+
+				var opts = {};
+				if (optsStr) {
+					try {
+						opts = (new Function ('return (' + optsStr + ')'))();
+					} catch(eParseError) {
+						jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr);
+					}
+				}
+				targetElm.jscolor = new jsc.jscolor(targetElm, opts);
+			}
+		}
+	},
+
+
+	isColorAttrSupported : (function () {
+		var elm = document.createElement('input');
+		if (elm.setAttribute) {
+			elm.setAttribute('type', 'color');
+			if (elm.type.toLowerCase() == 'color') {
+				return true;
+			}
+		}
+		return false;
+	})(),
+
+
+	isCanvasSupported : (function () {
+		var elm = document.createElement('canvas');
+		return !!(elm.getContext && elm.getContext('2d'));
+	})(),
+
+
+	fetchElement : function (mixed) {
+		return typeof mixed === 'string' ? document.getElementById(mixed) : mixed;
+	},
+
+
+	isElementType : function (elm, type) {
+		return elm.nodeName.toLowerCase() === type.toLowerCase();
+	},
+
+
+	getDataAttr : function (el, name) {
+		var attrName = 'data-' + name;
+		var attrValue = el.getAttribute(attrName);
+		if (attrValue !== null) {
+			return attrValue;
+		}
+		return null;
+	},
+
+
+	attachEvent : function (el, evnt, func) {
+		if (el.addEventListener) {
+			el.addEventListener(evnt, func, false);
+		} else if (el.attachEvent) {
+			el.attachEvent('on' + evnt, func);
+		}
+	},
+
+
+	detachEvent : function (el, evnt, func) {
+		if (el.removeEventListener) {
+			el.removeEventListener(evnt, func, false);
+		} else if (el.detachEvent) {
+			el.detachEvent('on' + evnt, func);
+		}
+	},
+
+
+	_attachedGroupEvents : {},
+
+
+	attachGroupEvent : function (groupName, el, evnt, func) {
+		if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
+			jsc._attachedGroupEvents[groupName] = [];
+		}
+		jsc._attachedGroupEvents[groupName].push([el, evnt, func]);
+		jsc.attachEvent(el, evnt, func);
+	},
+
+
+	detachGroupEvents : function (groupName) {
+		if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
+			for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) {
+				var evt = jsc._attachedGroupEvents[groupName][i];
+				jsc.detachEvent(evt[0], evt[1], evt[2]);
+			}
+			delete jsc._attachedGroupEvents[groupName];
+		}
+	},
+
+
+	attachDOMReadyEvent : function (func) {
+		var fired = false;
+		var fireOnce = function () {
+			if (!fired) {
+				fired = true;
+				func();
+			}
+		};
+
+		if (document.readyState === 'complete') {
+			setTimeout(fireOnce, 1); // async
+			return;
+		}
+
+		if (document.addEventListener) {
+			document.addEventListener('DOMContentLoaded', fireOnce, false);
+
+			// Fallback
+			window.addEventListener('load', fireOnce, false);
+
+		} else if (document.attachEvent) {
+			// IE
+			document.attachEvent('onreadystatechange', function () {
+				if (document.readyState === 'complete') {
+					document.detachEvent('onreadystatechange', arguments.callee);
+					fireOnce();
+				}
+			})
+
+			// Fallback
+			window.attachEvent('onload', fireOnce);
+
+			// IE7/8
+			if (document.documentElement.doScroll && window == window.top) {
+				var tryScroll = function () {
+					if (!document.body) { return; }
+					try {
+						document.documentElement.doScroll('left');
+						fireOnce();
+					} catch (e) {
+						setTimeout(tryScroll, 1);
+					}
+				};
+				tryScroll();
+			}
+		}
+	},
+
+
+	warn : function (msg) {
+		if (window.console && window.console.warn) {
+			window.console.warn(msg);
+		}
+	},
+
+
+	preventDefault : function (e) {
+		if (e.preventDefault) { e.preventDefault(); }
+		e.returnValue = false;
+	},
+
+
+	captureTarget : function (target) {
+		// IE
+		if (target.setCapture) {
+			jsc._capturedTarget = target;
+			jsc._capturedTarget.setCapture();
+		}
+	},
+
+
+	releaseTarget : function () {
+		// IE
+		if (jsc._capturedTarget) {
+			jsc._capturedTarget.releaseCapture();
+			jsc._capturedTarget = null;
+		}
+	},
+
+
+	fireEvent : function (el, evnt) {
+		if (!el) {
+			return;
+		}
+		if (document.createEvent) {
+			var ev = document.createEvent('HTMLEvents');
+			ev.initEvent(evnt, true, true);
+			el.dispatchEvent(ev);
+		} else if (document.createEventObject) {
+			var ev = document.createEventObject();
+			el.fireEvent('on' + evnt, ev);
+		} else if (el['on' + evnt]) { // alternatively use the traditional event model
+			el['on' + evnt]();
+		}
+	},
+
+
+	classNameToList : function (className) {
+		return className.replace(/^\s+|\s+$/g, '').split(/\s+/);
+	},
+
+
+	// The className parameter (str) can only contain a single class name
+	hasClass : function (elm, className) {
+		if (!className) {
+			return false;
+		}
+		return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' ');
+	},
+
+
+	// The className parameter (str) can contain multiple class names separated by whitespace
+	setClass : function (elm, className) {
+		var classList = jsc.classNameToList(className);
+		for (var i = 0; i < classList.length; i += 1) {
+			if (!jsc.hasClass(elm, classList[i])) {
+				elm.className += (elm.className ? ' ' : '') + classList[i];
+			}
+		}
+	},
+
+
+	// The className parameter (str) can contain multiple class names separated by whitespace
+	unsetClass : function (elm, className) {
+		var classList = jsc.classNameToList(className);
+		for (var i = 0; i < classList.length; i += 1) {
+			var repl = new RegExp(
+				'^\\s*' + classList[i] + '\\s*|' +
+				'\\s*' + classList[i] + '\\s*$|' +
+				'\\s+' + classList[i] + '(\\s+)',
+				'g'
+			);
+			elm.className = elm.className.replace(repl, '$1');
+		}
+	},
+
+
+	getStyle : function (elm) {
+		return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle;
+	},
+
+
+	setStyle : (function () {
+		var helper = document.createElement('div');
+		var getSupportedProp = function (names) {
+			for (var i = 0; i < names.length; i += 1) {
+				if (names[i] in helper.style) {
+					return names[i];
+				}
+			}
+		};
+		var props = {
+			borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']),
+			boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow'])
+		};
+		return function (elm, prop, value) {
+			switch (prop.toLowerCase()) {
+			case 'opacity':
+				var alphaOpacity = Math.round(parseFloat(value) * 100);
+				elm.style.opacity = value;
+				elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')';
+				break;
+			default:
+				elm.style[props[prop]] = value;
+				break;
+			}
+		};
+	})(),
+
+
+	setBorderRadius : function (elm, value) {
+		jsc.setStyle(elm, 'borderRadius', value || '0');
+	},
+
+
+	setBoxShadow : function (elm, value) {
+		jsc.setStyle(elm, 'boxShadow', value || 'none');
+	},
+
+
+	getElementPos : function (e, relativeToViewport) {
+		var x=0, y=0;
+		var rect = e.getBoundingClientRect();
+		x = rect.left;
+		y = rect.top;
+		if (!relativeToViewport) {
+			var viewPos = jsc.getViewPos();
+			x += viewPos[0];
+			y += viewPos[1];
+		}
+		return [x, y];
+	},
+
+
+	getElementSize : function (e) {
+		return [e.offsetWidth, e.offsetHeight];
+	},
+
+
+	// get pointer's X/Y coordinates relative to viewport
+	getAbsPointerPos : function (e) {
+		if (!e) { e = window.event; }
+		var x = 0, y = 0;
+		if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
+			// touch devices
+			x = e.changedTouches[0].clientX;
+			y = e.changedTouches[0].clientY;
+		} else if (typeof e.clientX === 'number') {
+			x = e.clientX;
+			y = e.clientY;
+		}
+		return { x: x, y: y };
+	},
+
+
+	// get pointer's X/Y coordinates relative to target element
+	getRelPointerPos : function (e) {
+		if (!e) { e = window.event; }
+		var target = e.target || e.srcElement;
+		var targetRect = target.getBoundingClientRect();
+
+		var x = 0, y = 0;
+
+		var clientX = 0, clientY = 0;
+		if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
+			// touch devices
+			clientX = e.changedTouches[0].clientX;
+			clientY = e.changedTouches[0].clientY;
+		} else if (typeof e.clientX === 'number') {
+			clientX = e.clientX;
+			clientY = e.clientY;
+		}
+
+		x = clientX - targetRect.left;
+		y = clientY - targetRect.top;
+		return { x: x, y: y };
+	},
+
+
+	getViewPos : function () {
+		var doc = document.documentElement;
+		return [
+			(window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
+			(window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
+		];
+	},
+
+
+	getViewSize : function () {
+		var doc = document.documentElement;
+		return [
+			(window.innerWidth || doc.clientWidth),
+			(window.innerHeight || doc.clientHeight),
+		];
+	},
+
+
+	redrawPosition : function () {
+
+		if (jsc.picker && jsc.picker.owner) {
+			var thisObj = jsc.picker.owner;
+
+			var tp, vp;
+
+			if (thisObj.fixed) {
+				// Fixed elements are positioned relative to viewport,
+				// therefore we can ignore the scroll offset
+				tp = jsc.getElementPos(thisObj.targetElement, true); // target pos
+				vp = [0, 0]; // view pos
+			} else {
+				tp = jsc.getElementPos(thisObj.targetElement); // target pos
+				vp = jsc.getViewPos(); // view pos
+			}
+
+			var ts = jsc.getElementSize(thisObj.targetElement); // target size
+			var vs = jsc.getViewSize(); // view size
+			var ps = jsc.getPickerOuterDims(thisObj); // picker size
+			var a, b, c;
+			switch (thisObj.position.toLowerCase()) {
+				case 'left': a=1; b=0; c=-1; break;
+				case 'right':a=1; b=0; c=1; break;
+				case 'top':  a=0; b=1; c=-1; break;
+				default:     a=0; b=1; c=1; break;
+			}
+			var l = (ts[b]+ps[b])/2;
+
+			// compute picker position
+			if (!thisObj.smartPosition) {
+				var pp = [
+					tp[a],
+					tp[b]+ts[b]-l+l*c
+				];
+			} else {
+				var pp = [
+					-vp[a]+tp[a]+ps[a] > vs[a] ?
+						(-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) :
+						tp[a],
+					-vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ?
+						(-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) :
+						(tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c)
+				];
+			}
+
+			var x = pp[a];
+			var y = pp[b];
+			var positionValue = thisObj.fixed ? 'fixed' : 'absolute';
+			var contractShadow =
+				(pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) &&
+				(pp[1] + ps[1] < tp[1] + ts[1]);
+
+			jsc._drawPosition(thisObj, x, y, positionValue, contractShadow);
+		}
+	},
+
+
+	_drawPosition : function (thisObj, x, y, positionValue, contractShadow) {
+		var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px
+
+		jsc.picker.wrap.style.position = positionValue;
+		jsc.picker.wrap.style.left = x + 'px';
+		jsc.picker.wrap.style.top = y + 'px';
+
+		jsc.setBoxShadow(
+			jsc.picker.boxS,
+			thisObj.shadow ?
+				new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) :
+				null);
+	},
+
+
+	getPickerDims : function (thisObj) {
+		var displaySlider = !!jsc.getSliderComponent(thisObj);
+		var dims = [
+			2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width +
+				(displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0),
+			2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height +
+				(thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0)
+		];
+		return dims;
+	},
+
+
+	getPickerOuterDims : function (thisObj) {
+		var dims = jsc.getPickerDims(thisObj);
+		return [
+			dims[0] + 2 * thisObj.borderWidth,
+			dims[1] + 2 * thisObj.borderWidth
+		];
+	},
+
+
+	getPadToSliderPadding : function (thisObj) {
+		return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness));
+	},
+
+
+	getPadYComponent : function (thisObj) {
+		switch (thisObj.mode.charAt(1).toLowerCase()) {
+			case 'v': return 'v'; break;
+		}
+		return 's';
+	},
+
+
+	getSliderComponent : function (thisObj) {
+		if (thisObj.mode.length > 2) {
+			switch (thisObj.mode.charAt(2).toLowerCase()) {
+				case 's': return 's'; break;
+				case 'v': return 'v'; break;
+			}
+		}
+		return null;
+	},
+
+
+	onDocumentMouseDown : function (e) {
+		if (!e) { e = window.event; }
+		var target = e.target || e.srcElement;
+
+		if (target._jscLinkedInstance) {
+			if (target._jscLinkedInstance.showOnClick) {
+				target._jscLinkedInstance.show();
+			}
+		} else if (target._jscControlName) {
+			jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse');
+		} else {
+			// Mouse is outside the picker controls -> hide the color picker!
+			if (jsc.picker && jsc.picker.owner) {
+				jsc.picker.owner.hide();
+			}
+		}
+	},
+
+
+	onDocumentTouchStart : function (e) {
+		if (!e) { e = window.event; }
+		var target = e.target || e.srcElement;
+
+		if (target._jscLinkedInstance) {
+			if (target._jscLinkedInstance.showOnClick) {
+				target._jscLinkedInstance.show();
+			}
+		} else if (target._jscControlName) {
+			jsc.onControlPointerStart(e, target, target._jscControlName, 'touch');
+		} else {
+			if (jsc.picker && jsc.picker.owner) {
+				jsc.picker.owner.hide();
+			}
+		}
+	},
+
+
+	onWindowResize : function (e) {
+		jsc.redrawPosition();
+	},
+
+
+	onParentScroll : function (e) {
+		// hide the picker when one of the parent elements is scrolled
+		if (jsc.picker && jsc.picker.owner) {
+			jsc.picker.owner.hide();
+		}
+	},
+
+
+	_pointerMoveEvent : {
+		mouse: 'mousemove',
+		touch: 'touchmove'
+	},
+	_pointerEndEvent : {
+		mouse: 'mouseup',
+		touch: 'touchend'
+	},
+
+
+	_pointerOrigin : null,
+	_capturedTarget : null,
+
+
+	onControlPointerStart : function (e, target, controlName, pointerType) {
+		var thisObj = target._jscInstance;
+
+		jsc.preventDefault(e);
+		jsc.captureTarget(target);
+
+		var registerDragEvents = function (doc, offset) {
+			jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType],
+				jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset));
+			jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType],
+				jsc.onDocumentPointerEnd(e, target, controlName, pointerType));
+		};
+
+		registerDragEvents(document, [0, 0]);
+
+		if (window.parent && window.frameElement) {
+			var rect = window.frameElement.getBoundingClientRect();
+			var ofs = [-rect.left, -rect.top];
+			registerDragEvents(window.parent.window.document, ofs);
+		}
+
+		var abs = jsc.getAbsPointerPos(e);
+		var rel = jsc.getRelPointerPos(e);
+		jsc._pointerOrigin = {
+			x: abs.x - rel.x,
+			y: abs.y - rel.y
+		};
+
+		switch (controlName) {
+		case 'pad':
+			// if the slider is at the bottom, move it up
+			switch (jsc.getSliderComponent(thisObj)) {
+			case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break;
+			case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break;
+			}
+			jsc.setPad(thisObj, e, 0, 0);
+			break;
+
+		case 'sld':
+			jsc.setSld(thisObj, e, 0);
+			break;
+		}
+
+		jsc.dispatchFineChange(thisObj);
+	},
+
+
+	onDocumentPointerMove : function (e, target, controlName, pointerType, offset) {
+		return function (e) {
+			var thisObj = target._jscInstance;
+			switch (controlName) {
+			case 'pad':
+				if (!e) { e = window.event; }
+				jsc.setPad(thisObj, e, offset[0], offset[1]);
+				jsc.dispatchFineChange(thisObj);
+				break;
+
+			case 'sld':
+				if (!e) { e = window.event; }
+				jsc.setSld(thisObj, e, offset[1]);
+				jsc.dispatchFineChange(thisObj);
+				break;
+			}
+		}
+	},
+
+
+	onDocumentPointerEnd : function (e, target, controlName, pointerType) {
+		return function (e) {
+			var thisObj = target._jscInstance;
+			jsc.detachGroupEvents('drag');
+			jsc.releaseTarget();
+			// Always dispatch changes after detaching outstanding mouse handlers,
+			// in case some user interaction will occur in user's onchange callback
+			// that would intrude with current mouse events
+			jsc.dispatchChange(thisObj);
+		};
+	},
+
+
+	dispatchChange : function (thisObj) {
+		if (thisObj.valueElement) {
+			if (jsc.isElementType(thisObj.valueElement, 'input')) {
+				jsc.fireEvent(thisObj.valueElement, 'change');
+			}
+		}
+	},
+
+
+	dispatchFineChange : function (thisObj) {
+		if (thisObj.onFineChange) {
+			var callback;
+			if (typeof thisObj.onFineChange === 'string') {
+				callback = new Function (thisObj.onFineChange);
+			} else {
+				callback = thisObj.onFineChange;
+			}
+			callback.call(thisObj);
+		}
+	},
+
+
+	setPad : function (thisObj, e, ofsX, ofsY) {
+		var pointerAbs = jsc.getAbsPointerPos(e);
+		var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth;
+		var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;
+
+		var xVal = x * (360 / (thisObj.width - 1));
+		var yVal = 100 - (y * (100 / (thisObj.height - 1)));
+
+		switch (jsc.getPadYComponent(thisObj)) {
+		case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break;
+		case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break;
+		}
+	},
+
+
+	setSld : function (thisObj, e, ofsY) {
+		var pointerAbs = jsc.getAbsPointerPos(e);
+		var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;
+
+		var yVal = 100 - (y * (100 / (thisObj.height - 1)));
+
+		switch (jsc.getSliderComponent(thisObj)) {
+		case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break;
+		case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break;
+		}
+	},
+
+
+	_vmlNS : 'jsc_vml_',
+	_vmlCSS : 'jsc_vml_css_',
+	_vmlReady : false,
+
+
+	initVML : function () {
+		if (!jsc._vmlReady) {
+			// init VML namespace
+			var doc = document;
+			if (!doc.namespaces[jsc._vmlNS]) {
+				doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml');
+			}
+			if (!doc.styleSheets[jsc._vmlCSS]) {
+				var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image'];
+				var ss = doc.createStyleSheet();
+				ss.owningElement.id = jsc._vmlCSS;
+				for (var i = 0; i < tags.length; i += 1) {
+					ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);');
+				}
+			}
+			jsc._vmlReady = true;
+		}
+	},
+
+
+	createPalette : function () {
+
+		var paletteObj = {
+			elm: null,
+			draw: null
+		};
+
+		if (jsc.isCanvasSupported) {
+			// Canvas implementation for modern browsers
+
+			var canvas = document.createElement('canvas');
+			var ctx = canvas.getContext('2d');
+
+			var drawFunc = function (width, height, type) {
+				canvas.width = width;
+				canvas.height = height;
+
+				ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+				var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0);
+				hGrad.addColorStop(0 / 6, '#F00');
+				hGrad.addColorStop(1 / 6, '#FF0');
+				hGrad.addColorStop(2 / 6, '#0F0');
+				hGrad.addColorStop(3 / 6, '#0FF');
+				hGrad.addColorStop(4 / 6, '#00F');
+				hGrad.addColorStop(5 / 6, '#F0F');
+				hGrad.addColorStop(6 / 6, '#F00');
+
+				ctx.fillStyle = hGrad;
+				ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+				var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
+				switch (type.toLowerCase()) {
+				case 's':
+					vGrad.addColorStop(0, 'rgba(255,255,255,0)');
+					vGrad.addColorStop(1, 'rgba(255,255,255,1)');
+					break;
+				case 'v':
+					vGrad.addColorStop(0, 'rgba(0,0,0,0)');
+					vGrad.addColorStop(1, 'rgba(0,0,0,1)');
+					break;
+				}
+				ctx.fillStyle = vGrad;
+				ctx.fillRect(0, 0, canvas.width, canvas.height);
+			};
+
+			paletteObj.elm = canvas;
+			paletteObj.draw = drawFunc;
+
+		} else {
+			// VML fallback for IE 7 and 8
+
+			jsc.initVML();
+
+			var vmlContainer = document.createElement('div');
+			vmlContainer.style.position = 'relative';
+			vmlContainer.style.overflow = 'hidden';
+
+			var hGrad = document.createElement(jsc._vmlNS + ':fill');
+			hGrad.type = 'gradient';
+			hGrad.method = 'linear';
+			hGrad.angle = '90';
+			hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0'
+
+			var hRect = document.createElement(jsc._vmlNS + ':rect');
+			hRect.style.position = 'absolute';
+			hRect.style.left = -1 + 'px';
+			hRect.style.top = -1 + 'px';
+			hRect.stroked = false;
+			hRect.appendChild(hGrad);
+			vmlContainer.appendChild(hRect);
+
+			var vGrad = document.createElement(jsc._vmlNS + ':fill');
+			vGrad.type = 'gradient';
+			vGrad.method = 'linear';
+			vGrad.angle = '180';
+			vGrad.opacity = '0';
+
+			var vRect = document.createElement(jsc._vmlNS + ':rect');
+			vRect.style.position = 'absolute';
+			vRect.style.left = -1 + 'px';
+			vRect.style.top = -1 + 'px';
+			vRect.stroked = false;
+			vRect.appendChild(vGrad);
+			vmlContainer.appendChild(vRect);
+
+			var drawFunc = function (width, height, type) {
+				vmlContainer.style.width = width + 'px';
+				vmlContainer.style.height = height + 'px';
+
+				hRect.style.width =
+				vRect.style.width =
+					(width + 1) + 'px';
+				hRect.style.height =
+				vRect.style.height =
+					(height + 1) + 'px';
+
+				// Colors must be specified during every redraw, otherwise IE won't display
+				// a full gradient during a subsequential redraw
+				hGrad.color = '#F00';
+				hGrad.color2 = '#F00';
+
+				switch (type.toLowerCase()) {
+				case 's':
+					vGrad.color = vGrad.color2 = '#FFF';
+					break;
+				case 'v':
+					vGrad.color = vGrad.color2 = '#000';
+					break;
+				}
+			};
+			
+			paletteObj.elm = vmlContainer;
+			paletteObj.draw = drawFunc;
+		}
+
+		return paletteObj;
+	},
+
+
+	createSliderGradient : function () {
+
+		var sliderObj = {
+			elm: null,
+			draw: null
+		};
+
+		if (jsc.isCanvasSupported) {
+			// Canvas implementation for modern browsers
+
+			var canvas = document.createElement('canvas');
+			var ctx = canvas.getContext('2d');
+
+			var drawFunc = function (width, height, color1, color2) {
+				canvas.width = width;
+				canvas.height = height;
+
+				ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+				var grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
+				grad.addColorStop(0, color1);
+				grad.addColorStop(1, color2);
+
+				ctx.fillStyle = grad;
+				ctx.fillRect(0, 0, canvas.width, canvas.height);
+			};
+
+			sliderObj.elm = canvas;
+			sliderObj.draw = drawFunc;
+
+		} else {
+			// VML fallback for IE 7 and 8
+
+			jsc.initVML();
+
+			var vmlContainer = document.createElement('div');
+			vmlContainer.style.position = 'relative';
+			vmlContainer.style.overflow = 'hidden';
+
+			var grad = document.createElement(jsc._vmlNS + ':fill');
+			grad.type = 'gradient';
+			grad.method = 'linear';
+			grad.angle = '180';
+
+			var rect = document.createElement(jsc._vmlNS + ':rect');
+			rect.style.position = 'absolute';
+			rect.style.left = -1 + 'px';
+			rect.style.top = -1 + 'px';
+			rect.stroked = false;
+			rect.appendChild(grad);
+			vmlContainer.appendChild(rect);
+
+			var drawFunc = function (width, height, color1, color2) {
+				vmlContainer.style.width = width + 'px';
+				vmlContainer.style.height = height + 'px';
+
+				rect.style.width = (width + 1) + 'px';
+				rect.style.height = (height + 1) + 'px';
+
+				grad.color = color1;
+				grad.color2 = color2;
+			};
+			
+			sliderObj.elm = vmlContainer;
+			sliderObj.draw = drawFunc;
+		}
+
+		return sliderObj;
+	},
+
+
+	leaveValue : 1<<0,
+	leaveStyle : 1<<1,
+	leavePad : 1<<2,
+	leaveSld : 1<<3,
+
+
+	BoxShadow : (function () {
+		var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) {
+			this.hShadow = hShadow;
+			this.vShadow = vShadow;
+			this.blur = blur;
+			this.spread = spread;
+			this.color = color;
+			this.inset = !!inset;
+		};
+
+		BoxShadow.prototype.toString = function () {
+			var vals = [
+				Math.round(this.hShadow) + 'px',
+				Math.round(this.vShadow) + 'px',
+				Math.round(this.blur) + 'px',
+				Math.round(this.spread) + 'px',
+				this.color
+			];
+			if (this.inset) {
+				vals.push('inset');
+			}
+			return vals.join(' ');
+		};
+
+		return BoxShadow;
+	})(),
+
+
+	//
+	// Usage:
+	// var myColor = new jscolor(<targetElement> [, <options>])
+	//
+
+	jscolor : function (targetElement, options) {
+
+		// General options
+		//
+		this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB()
+		this.valueElement = targetElement; // element that will be used to display and input the color code
+		this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor
+		this.required = true; // whether the associated text <input> can be left empty
+		this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace)
+		this.hash = false; // whether to prefix the HEX color code with # symbol
+		this.uppercase = true; // whether to uppercase the color code
+		this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code)
+		this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it
+		this.minS = 0; // min allowed saturation (0 - 100)
+		this.maxS = 100; // max allowed saturation (0 - 100)
+		this.minV = 0; // min allowed value (brightness) (0 - 100)
+		this.maxV = 100; // max allowed value (brightness) (0 - 100)
+
+		// Accessing the picked color
+		//
+		this.hsv = [0, 0, 100]; // read-only  [0-360, 0-100, 0-100]
+		this.rgb = [255, 255, 255]; // read-only  [0-255, 0-255, 0-255]
+
+		// Color Picker options
+		//
+		this.width = 181; // width of color palette (in px)
+		this.height = 101; // height of color palette (in px)
+		this.showOnClick = true; // whether to display the color picker when user clicks on its target element
+		this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls
+		this.position = 'bottom'; // left | right | top | bottom - position relative to the target element
+		this.smartPosition = true; // automatically change picker position when there is not enough space for it
+		this.sliderSize = 16; // px
+		this.crossSize = 8; // px
+		this.closable = false; // whether to display the Close button
+		this.closeText = 'Close';
+		this.buttonColor = '#000000'; // CSS color
+		this.buttonHeight = 18; // px
+		this.padding = 12; // px
+		this.backgroundColor = '#FFFFFF'; // CSS color
+		this.borderWidth = 1; // px
+		this.borderColor = '#BBBBBB'; // CSS color
+		this.borderRadius = 8; // px
+		this.insetWidth = 1; // px
+		this.insetColor = '#BBBBBB'; // CSS color
+		this.shadow = true; // whether to display shadow
+		this.shadowBlur = 15; // px
+		this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color
+		this.pointerColor = '#4C4C4C'; // px
+		this.pointerBorderColor = '#FFFFFF'; // px
+        this.pointerBorderWidth = 1; // px
+        this.pointerThickness = 2; // px
+		this.zIndex = 1000;
+		this.container = null; // where to append the color picker (BODY element by default)
+
+
+		for (var opt in options) {
+			if (options.hasOwnProperty(opt)) {
+				this[opt] = options[opt];
+			}
+		}
+
+
+		this.hide = function () {
+			if (isPickerOwner()) {
+				detachPicker();
+			}
+		};
+
+
+		this.show = function () {
+			drawPicker();
+		};
+
+
+		this.redraw = function () {
+			if (isPickerOwner()) {
+				drawPicker();
+			}
+		};
+
+
+		this.importColor = function () {
+			if (!this.valueElement) {
+				this.exportColor();
+			} else {
+				if (jsc.isElementType(this.valueElement, 'input')) {
+					if (!this.refine) {
+						if (!this.fromString(this.valueElement.value, jsc.leaveValue)) {
+							if (this.styleElement) {
+								this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
+								this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
+								this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
+							}
+							this.exportColor(jsc.leaveValue | jsc.leaveStyle);
+						}
+					} else if (!this.required && /^\s*$/.test(this.valueElement.value)) {
+						this.valueElement.value = '';
+						if (this.styleElement) {
+							this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
+							this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
+							this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
+						}
+						this.exportColor(jsc.leaveValue | jsc.leaveStyle);
+
+					} else if (this.fromString(this.valueElement.value)) {
+						// managed to import color successfully from the value -> OK, don't do anything
+					} else {
+						this.exportColor();
+					}
+				} else {
+					// not an input element -> doesn't have any value
+					this.exportColor();
+				}
+			}
+		};
+
+
+		this.exportColor = function (flags) {
+			if (!(flags & jsc.leaveValue) && this.valueElement) {
+				var value = this.toString();
+				if (this.uppercase) { value = value.toUpperCase(); }
+				if (this.hash) { value = '#' + value; }
+
+				if (jsc.isElementType(this.valueElement, 'input')) {
+					this.valueElement.value = value;
+				} else {
+					this.valueElement.innerHTML = value;
+				}
+			}
+			if (!(flags & jsc.leaveStyle)) {
+				if (this.styleElement) {
+					this.styleElement.style.backgroundImage = 'none';
+					this.styleElement.style.backgroundColor = '#' + this.toString();
+					this.styleElement.style.color = this.isLight() ? '#000' : '#FFF';
+				}
+			}
+			if (!(flags & jsc.leavePad) && isPickerOwner()) {
+				redrawPad();
+			}
+			if (!(flags & jsc.leaveSld) && isPickerOwner()) {
+				redrawSld();
+			}
+		};
+
+
+		// h: 0-360
+		// s: 0-100
+		// v: 0-100
+		//
+		this.fromHSV = function (h, s, v, flags) { // null = don't change
+			if (h !== null) {
+				if (isNaN(h)) { return false; }
+				h = Math.max(0, Math.min(360, h));
+			}
+			if (s !== null) {
+				if (isNaN(s)) { return false; }
+				s = Math.max(0, Math.min(100, this.maxS, s), this.minS);
+			}
+			if (v !== null) {
+				if (isNaN(v)) { return false; }
+				v = Math.max(0, Math.min(100, this.maxV, v), this.minV);
+			}
+
+			this.rgb = HSV_RGB(
+				h===null ? this.hsv[0] : (this.hsv[0]=h),
+				s===null ? this.hsv[1] : (this.hsv[1]=s),
+				v===null ? this.hsv[2] : (this.hsv[2]=v)
+			);
+
+			this.exportColor(flags);
+		};
+
+
+		// r: 0-255
+		// g: 0-255
+		// b: 0-255
+		//
+		this.fromRGB = function (r, g, b, flags) { // null = don't change
+			if (r !== null) {
+				if (isNaN(r)) { return false; }
+				r = Math.max(0, Math.min(255, r));
+			}
+			if (g !== null) {
+				if (isNaN(g)) { return false; }
+				g = Math.max(0, Math.min(255, g));
+			}
+			if (b !== null) {
+				if (isNaN(b)) { return false; }
+				b = Math.max(0, Math.min(255, b));
+			}
+
+			var hsv = RGB_HSV(
+				r===null ? this.rgb[0] : r,
+				g===null ? this.rgb[1] : g,
+				b===null ? this.rgb[2] : b
+			);
+			if (hsv[0] !== null) {
+				this.hsv[0] = Math.max(0, Math.min(360, hsv[0]));
+			}
+			if (hsv[2] !== 0) {
+				this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1]));
+			}
+			this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2]));
+
+			// update RGB according to final HSV, as some values might be trimmed
+			var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]);
+			this.rgb[0] = rgb[0];
+			this.rgb[1] = rgb[1];
+			this.rgb[2] = rgb[2];
+
+			this.exportColor(flags);
+		};
+
+
+		this.fromString = function (str, flags) {
+			var m;
+			if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) {
+				// HEX notation
+				//
+
+				if (m[1].length === 6) {
+					// 6-char notation
+					this.fromRGB(
+						parseInt(m[1].substr(0,2),16),
+						parseInt(m[1].substr(2,2),16),
+						parseInt(m[1].substr(4,2),16),
+						flags
+					);
+				} else {
+					// 3-char notation
+					this.fromRGB(
+						parseInt(m[1].charAt(0) + m[1].charAt(0),16),
+						parseInt(m[1].charAt(1) + m[1].charAt(1),16),
+						parseInt(m[1].charAt(2) + m[1].charAt(2),16),
+						flags
+					);
+				}
+				return true;
+
+			} else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) {
+				var params = m[1].split(',');
+				var re = /^\s*(\d*)(\.\d+)?\s*$/;
+				var mR, mG, mB;
+				if (
+					params.length >= 3 &&
+					(mR = params[0].match(re)) &&
+					(mG = params[1].match(re)) &&
+					(mB = params[2].match(re))
+				) {
+					var r = parseFloat((mR[1] || '0') + (mR[2] || ''));
+					var g = parseFloat((mG[1] || '0') + (mG[2] || ''));
+					var b = parseFloat((mB[1] || '0') + (mB[2] || ''));
+					this.fromRGB(r, g, b, flags);
+					return true;
+				}
+			}
+			return false;
+		};
+
+
+		this.toString = function () {
+			return (
+				(0x100 | Math.round(this.rgb[0])).toString(16).substr(1) +
+				(0x100 | Math.round(this.rgb[1])).toString(16).substr(1) +
+				(0x100 | Math.round(this.rgb[2])).toString(16).substr(1)
+			);
+		};
+
+
+		this.toHEXString = function () {
+			return '#' + this.toString().toUpperCase();
+		};
+
+
+		this.toRGBString = function () {
+			return ('rgb(' +
+				Math.round(this.rgb[0]) + ',' +
+				Math.round(this.rgb[1]) + ',' +
+				Math.round(this.rgb[2]) + ')'
+			);
+		};
+
+
+		this.isLight = function () {
+			return (
+				0.213 * this.rgb[0] +
+				0.715 * this.rgb[1] +
+				0.072 * this.rgb[2] >
+				255 / 2
+			);
+		};
+
+
+		this._processParentElementsInDOM = function () {
+			if (this._linkedElementsProcessed) { return; }
+			this._linkedElementsProcessed = true;
+
+			var elm = this.targetElement;
+			do {
+				// If the target element or one of its parent nodes has fixed position,
+				// then use fixed positioning instead
+				//
+				// Note: In Firefox, getComputedStyle returns null in a hidden iframe,
+				// that's why we need to check if the returned style object is non-empty
+				var currStyle = jsc.getStyle(elm);
+				if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
+					this.fixed = true;
+				}
+
+				if (elm !== this.targetElement) {
+					// Ensure to attach onParentScroll only once to each parent element
+					// (multiple targetElements can share the same parent nodes)
+					//
+					// Note: It's not just offsetParents that can be scrollable,
+					// that's why we loop through all parent nodes
+					if (!elm._jscEventsAttached) {
+						jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
+						elm._jscEventsAttached = true;
+					}
+				}
+			} while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body'));
+		};
+
+
+		// r: 0-255
+		// g: 0-255
+		// b: 0-255
+		//
+		// returns: [ 0-360, 0-100, 0-100 ]
+		//
+		function RGB_HSV (r, g, b) {
+			r /= 255;
+			g /= 255;
+			b /= 255;
+			var n = Math.min(Math.min(r,g),b);
+			var v = Math.max(Math.max(r,g),b);
+			var m = v - n;
+			if (m === 0) { return [ null, 0, 100 * v ]; }
+			var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m);
+			return [
+				60 * (h===6?0:h),
+				100 * (m/v),
+				100 * v
+			];
+		}
+
+
+		// h: 0-360
+		// s: 0-100
+		// v: 0-100
+		//
+		// returns: [ 0-255, 0-255, 0-255 ]
+		//
+		function HSV_RGB (h, s, v) {
+			var u = 255 * (v / 100);
+
+			if (h === null) {
+				return [ u, u, u ];
+			}
+
+			h /= 60;
+			s /= 100;
+
+			var i = Math.floor(h);
+			var f = i%2 ? h-i : 1-(h-i);
+			var m = u * (1 - s);
+			var n = u * (1 - s * f);
+			switch (i) {
+				case 6:
+				case 0: return [u,n,m];
+				case 1: return [n,u,m];
+				case 2: return [m,u,n];
+				case 3: return [m,n,u];
+				case 4: return [n,m,u];
+				case 5: return [u,m,n];
+			}
+		}
+
+
+		function detachPicker () {
+			jsc.unsetClass(THIS.targetElement, THIS.activeClass);
+			jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap);
+			delete jsc.picker.owner;
+		}
+
+
+		function drawPicker () {
+
+			// At this point, when drawing the picker, we know what the parent elements are
+			// and we can do all related DOM operations, such as registering events on them
+			// or checking their positioning
+			THIS._processParentElementsInDOM();
+
+			if (!jsc.picker) {
+				jsc.picker = {
+					owner: null,
+					wrap : document.createElement('div'),
+					box : document.createElement('div'),
+					boxS : document.createElement('div'), // shadow area
+					boxB : document.createElement('div'), // border
+					pad : document.createElement('div'),
+					padB : document.createElement('div'), // border
+					padM : document.createElement('div'), // mouse/touch area
+					padPal : jsc.createPalette(),
+					cross : document.createElement('div'),
+					crossBY : document.createElement('div'), // border Y
+					crossBX : document.createElement('div'), // border X
+					crossLY : document.createElement('div'), // line Y
+					crossLX : document.createElement('div'), // line X
+					sld : document.createElement('div'),
+					sldB : document.createElement('div'), // border
+					sldM : document.createElement('div'), // mouse/touch area
+					sldGrad : jsc.createSliderGradient(),
+					sldPtrS : document.createElement('div'), // slider pointer spacer
+					sldPtrIB : document.createElement('div'), // slider pointer inner border
+					sldPtrMB : document.createElement('div'), // slider pointer middle border
+					sldPtrOB : document.createElement('div'), // slider pointer outer border
+					btn : document.createElement('div'),
+					btnT : document.createElement('span') // text
+				};
+
+				jsc.picker.pad.appendChild(jsc.picker.padPal.elm);
+				jsc.picker.padB.appendChild(jsc.picker.pad);
+				jsc.picker.cross.appendChild(jsc.picker.crossBY);
+				jsc.picker.cross.appendChild(jsc.picker.crossBX);
+				jsc.picker.cross.appendChild(jsc.picker.crossLY);
+				jsc.picker.cross.appendChild(jsc.picker.crossLX);
+				jsc.picker.padB.appendChild(jsc.picker.cross);
+				jsc.picker.box.appendChild(jsc.picker.padB);
+				jsc.picker.box.appendChild(jsc.picker.padM);
+
+				jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm);
+				jsc.picker.sldB.appendChild(jsc.picker.sld);
+				jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB);
+				jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB);
+				jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB);
+				jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS);
+				jsc.picker.box.appendChild(jsc.picker.sldB);
+				jsc.picker.box.appendChild(jsc.picker.sldM);
+
+				jsc.picker.btn.appendChild(jsc.picker.btnT);
+				jsc.picker.box.appendChild(jsc.picker.btn);
+
+				jsc.picker.boxB.appendChild(jsc.picker.box);
+				jsc.picker.wrap.appendChild(jsc.picker.boxS);
+				jsc.picker.wrap.appendChild(jsc.picker.boxB);
+			}
+
+			var p = jsc.picker;
+
+			var displaySlider = !!jsc.getSliderComponent(THIS);
+			var dims = jsc.getPickerDims(THIS);
+			var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
+			var padToSliderPadding = jsc.getPadToSliderPadding(THIS);
+			var borderRadius = Math.min(
+				THIS.borderRadius,
+				Math.round(THIS.padding * Math.PI)); // px
+			var padCursor = 'crosshair';
+
+			// wrap
+			p.wrap.style.clear = 'both';
+			p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px';
+			p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px';
+			p.wrap.style.zIndex = THIS.zIndex;
+
+			// picker
+			p.box.style.width = dims[0] + 'px';
+			p.box.style.height = dims[1] + 'px';
+
+			p.boxS.style.position = 'absolute';
+			p.boxS.style.left = '0';
+			p.boxS.style.top = '0';
+			p.boxS.style.width = '100%';
+			p.boxS.style.height = '100%';
+			jsc.setBorderRadius(p.boxS, borderRadius + 'px');
+
+			// picker border
+			p.boxB.style.position = 'relative';
+			p.boxB.style.border = THIS.borderWidth + 'px solid';
+			p.boxB.style.borderColor = THIS.borderColor;
+			p.boxB.style.background = THIS.backgroundColor;
+			jsc.setBorderRadius(p.boxB, borderRadius + 'px');
+
+			// IE hack:
+			// If the element is transparent, IE will trigger the event on the elements under it,
+			// e.g. on Canvas or on elements with border
+			p.padM.style.background =
+			p.sldM.style.background =
+				'#FFF';
+			jsc.setStyle(p.padM, 'opacity', '0');
+			jsc.setStyle(p.sldM, 'opacity', '0');
+
+			// pad
+			p.pad.style.position = 'relative';
+			p.pad.style.width = THIS.width + 'px';
+			p.pad.style.height = THIS.height + 'px';
+
+			// pad palettes (HSV and HVS)
+			p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS));
+
+			// pad border
+			p.padB.style.position = 'absolute';
+			p.padB.style.left = THIS.padding + 'px';
+			p.padB.style.top = THIS.padding + 'px';
+			p.padB.style.border = THIS.insetWidth + 'px solid';
+			p.padB.style.borderColor = THIS.insetColor;
+
+			// pad mouse area
+			p.padM._jscInstance = THIS;
+			p.padM._jscControlName = 'pad';
+			p.padM.style.position = 'absolute';
+			p.padM.style.left = '0';
+			p.padM.style.top = '0';
+			p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px';
+			p.padM.style.height = dims[1] + 'px';
+			p.padM.style.cursor = padCursor;
+
+			// pad cross
+			p.cross.style.position = 'absolute';
+			p.cross.style.left =
+			p.cross.style.top =
+				'0';
+			p.cross.style.width =
+			p.cross.style.height =
+				crossOuterSize + 'px';
+
+			// pad cross border Y and X
+			p.crossBY.style.position =
+			p.crossBX.style.position =
+				'absolute';
+			p.crossBY.style.background =
+			p.crossBX.style.background =
+				THIS.pointerBorderColor;
+			p.crossBY.style.width =
+			p.crossBX.style.height =
+				(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
+			p.crossBY.style.height =
+			p.crossBX.style.width =
+				crossOuterSize + 'px';
+			p.crossBY.style.left =
+			p.crossBX.style.top =
+				(Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px';
+			p.crossBY.style.top =
+			p.crossBX.style.left =
+				'0';
+
+			// pad cross line Y and X
+			p.crossLY.style.position =
+			p.crossLX.style.position =
+				'absolute';
+			p.crossLY.style.background =
+			p.crossLX.style.background =
+				THIS.pointerColor;
+			p.crossLY.style.height =
+			p.crossLX.style.width =
+				(crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px';
+			p.crossLY.style.width =
+			p.crossLX.style.height =
+				THIS.pointerThickness + 'px';
+			p.crossLY.style.left =
+			p.crossLX.style.top =
+				(Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px';
+			p.crossLY.style.top =
+			p.crossLX.style.left =
+				THIS.pointerBorderWidth + 'px';
+
+			// slider
+			p.sld.style.overflow = 'hidden';
+			p.sld.style.width = THIS.sliderSize + 'px';
+			p.sld.style.height = THIS.height + 'px';
+
+			// slider gradient
+			p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000');
+
+			// slider border
+			p.sldB.style.display = displaySlider ? 'block' : 'none';
+			p.sldB.style.position = 'absolute';
+			p.sldB.style.right = THIS.padding + 'px';
+			p.sldB.style.top = THIS.padding + 'px';
+			p.sldB.style.border = THIS.insetWidth + 'px solid';
+			p.sldB.style.borderColor = THIS.insetColor;
+
+			// slider mouse area
+			p.sldM._jscInstance = THIS;
+			p.sldM._jscControlName = 'sld';
+			p.sldM.style.display = displaySlider ? 'block' : 'none';
+			p.sldM.style.position = 'absolute';
+			p.sldM.style.right = '0';
+			p.sldM.style.top = '0';
+			p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px';
+			p.sldM.style.height = dims[1] + 'px';
+			p.sldM.style.cursor = 'default';
+
+			// slider pointer inner and outer border
+			p.sldPtrIB.style.border =
+			p.sldPtrOB.style.border =
+				THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor;
+
+			// slider pointer outer border
+			p.sldPtrOB.style.position = 'absolute';
+			p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
+			p.sldPtrOB.style.top = '0';
+
+			// slider pointer middle border
+			p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor;
+
+			// slider pointer spacer
+			p.sldPtrS.style.width = THIS.sliderSize + 'px';
+			p.sldPtrS.style.height = sliderPtrSpace + 'px';
+
+			// the Close button
+			function setBtnBorder () {
+				var insetColors = THIS.insetColor.split(/\s+/);
+				var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1];
+				p.btn.style.borderColor = outsetColor;
+			}
+			p.btn.style.display = THIS.closable ? 'block' : 'none';
+			p.btn.style.position = 'absolute';
+			p.btn.style.left = THIS.padding + 'px';
+			p.btn.style.bottom = THIS.padding + 'px';
+			p.btn.style.padding = '0 15px';
+			p.btn.style.height = THIS.buttonHeight + 'px';
+			p.btn.style.border = THIS.insetWidth + 'px solid';
+			setBtnBorder();
+			p.btn.style.color = THIS.buttonColor;
+			p.btn.style.font = '12px sans-serif';
+			p.btn.style.textAlign = 'center';
+			try {
+				p.btn.style.cursor = 'pointer';
+			} catch(eOldIE) {
+				p.btn.style.cursor = 'hand';
+			}
+			p.btn.onmousedown = function () {
+				THIS.hide();
+			};
+			p.btnT.style.lineHeight = THIS.buttonHeight + 'px';
+			p.btnT.innerHTML = '';
+			p.btnT.appendChild(document.createTextNode(THIS.closeText));
+
+			// place pointers
+			redrawPad();
+			redrawSld();
+
+			// If we are changing the owner without first closing the picker,
+			// make sure to first deal with the old owner
+			if (jsc.picker.owner && jsc.picker.owner !== THIS) {
+				jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass);
+			}
+
+			// Set the new picker owner
+			jsc.picker.owner = THIS;
+
+			// The redrawPosition() method needs picker.owner to be set, that's why we call it here,
+			// after setting the owner
+			if (jsc.isElementType(container, 'body')) {
+				jsc.redrawPosition();
+			} else {
+				jsc._drawPosition(THIS, 0, 0, 'relative', false);
+			}
+
+			if (p.wrap.parentNode != container) {
+				container.appendChild(p.wrap);
+			}
+
+			jsc.setClass(THIS.targetElement, THIS.activeClass);
+		}
+
+
+		function redrawPad () {
+			// redraw the pad pointer
+			switch (jsc.getPadYComponent(THIS)) {
+			case 's': var yComponent = 1; break;
+			case 'v': var yComponent = 2; break;
+			}
+			var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1));
+			var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
+			var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
+			var ofs = -Math.floor(crossOuterSize / 2);
+			jsc.picker.cross.style.left = (x + ofs) + 'px';
+			jsc.picker.cross.style.top = (y + ofs) + 'px';
+
+			// redraw the slider
+			switch (jsc.getSliderComponent(THIS)) {
+			case 's':
+				var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]);
+				var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]);
+				var color1 = 'rgb(' +
+					Math.round(rgb1[0]) + ',' +
+					Math.round(rgb1[1]) + ',' +
+					Math.round(rgb1[2]) + ')';
+				var color2 = 'rgb(' +
+					Math.round(rgb2[0]) + ',' +
+					Math.round(rgb2[1]) + ',' +
+					Math.round(rgb2[2]) + ')';
+				jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
+				break;
+			case 'v':
+				var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100);
+				var color1 = 'rgb(' +
+					Math.round(rgb[0]) + ',' +
+					Math.round(rgb[1]) + ',' +
+					Math.round(rgb[2]) + ')';
+				var color2 = '#000';
+				jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
+				break;
+			}
+		}
+
+
+		function redrawSld () {
+			var sldComponent = jsc.getSliderComponent(THIS);
+			if (sldComponent) {
+				// redraw the slider pointer
+				switch (sldComponent) {
+				case 's': var yComponent = 1; break;
+				case 'v': var yComponent = 2; break;
+				}
+				var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
+				jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px';
+			}
+		}
+
+
+		function isPickerOwner () {
+			return jsc.picker && jsc.picker.owner === THIS;
+		}
+
+
+		function blurValue () {
+			THIS.importColor();
+		}
+
+
+		// Find the target element
+		if (typeof targetElement === 'string') {
+			var id = targetElement;
+			var elm = document.getElementById(id);
+			if (elm) {
+				this.targetElement = elm;
+			} else {
+				jsc.warn('Could not find target element with ID \'' + id + '\'');
+			}
+		} else if (targetElement) {
+			this.targetElement = targetElement;
+		} else {
+			jsc.warn('Invalid target element: \'' + targetElement + '\'');
+		}
+
+		if (this.targetElement._jscLinkedInstance) {
+			jsc.warn('Cannot link jscolor twice to the same element. Skipping.');
+			return;
+		}
+		this.targetElement._jscLinkedInstance = this;
+
+		// Find the value element
+		this.valueElement = jsc.fetchElement(this.valueElement);
+		// Find the style element
+		this.styleElement = jsc.fetchElement(this.styleElement);
+
+		var THIS = this;
+		var container =
+			this.container ?
+			jsc.fetchElement(this.container) :
+			document.getElementsByTagName('body')[0];
+		var sliderPtrSpace = 3; // px
+
+		// For BUTTON elements it's important to stop them from sending the form when clicked
+		// (e.g. in Safari)
+		if (jsc.isElementType(this.targetElement, 'button')) {
+			if (this.targetElement.onclick) {
+				var origCallback = this.targetElement.onclick;
+				this.targetElement.onclick = function (evt) {
+					origCallback.call(this, evt);
+					return false;
+				};
+			} else {
+				this.targetElement.onclick = function () { return false; };
+			}
+		}
+
+		/*
+		var elm = this.targetElement;
+		do {
+			// If the target element or one of its offsetParents has fixed position,
+			// then use fixed positioning instead
+			//
+			// Note: In Firefox, getComputedStyle returns null in a hidden iframe,
+			// that's why we need to check if the returned style object is non-empty
+			var currStyle = jsc.getStyle(elm);
+			if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
+				this.fixed = true;
+			}
+
+			if (elm !== this.targetElement) {
+				// attach onParentScroll so that we can recompute the picker position
+				// when one of the offsetParents is scrolled
+				if (!elm._jscEventsAttached) {
+					jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
+					elm._jscEventsAttached = true;
+				}
+			}
+		} while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body'));
+		*/
+
+		// valueElement
+		if (this.valueElement) {
+			if (jsc.isElementType(this.valueElement, 'input')) {
+				var updateField = function () {
+					THIS.fromString(THIS.valueElement.value, jsc.leaveValue);
+					jsc.dispatchFineChange(THIS);
+				};
+				jsc.attachEvent(this.valueElement, 'keyup', updateField);
+				jsc.attachEvent(this.valueElement, 'input', updateField);
+				jsc.attachEvent(this.valueElement, 'blur', blurValue);
+				this.valueElement.setAttribute('autocomplete', 'off');
+			}
+		}
+
+		// styleElement
+		if (this.styleElement) {
+			this.styleElement._jscOrigStyle = {
+				backgroundImage : this.styleElement.style.backgroundImage,
+				backgroundColor : this.styleElement.style.backgroundColor,
+				color : this.styleElement.style.color
+			};
+		}
+
+		if (this.value) {
+			// Try to set the color from the .value option and if unsuccessful,
+			// export the current color
+			this.fromString(this.value) || this.exportColor();
+		} else {
+			this.importColor();
+		}
+	}
+
+};
+
+
+//================================
+// Public properties and methods
+//================================
+
+
+// By default, search for all elements with class="jscolor" and install a color picker on them.
+//
+// You can change what class name will be looked for by setting the property jscolor.lookupClass
+// anywhere in your HTML document. To completely disable the automatic lookup, set it to null.
+//
+jsc.jscolor.lookupClass = 'jscolor';
+
+
+jsc.jscolor.installByClassName = function (className) {
+	var inputElms = document.getElementsByTagName('input');
+	var buttonElms = document.getElementsByTagName('button');
+
+	jsc.tryInstallOnElements(inputElms, className);
+	jsc.tryInstallOnElements(buttonElms, className);
+};
+
+
+jsc.register();
+
+
+return jsc.jscolor;
+
+
+})(); }

+ 361 - 0
nova/static/js/wave/scripts.js

@@ -0,0 +1,361 @@
+$("#message").draggable();
+$("#orientation").draggable();
+$("#settings").draggable();
+
+
+function setMessage(bheader, bcontent) {
+    //console.log("Creating message");
+    var timeNow = new Date();
+    var hours   = timeNow.getHours();
+    var minutes = timeNow.getMinutes();
+    var seconds = timeNow.getSeconds();
+    //var timeString = "" + ((minutes > 12) ? - 12 : hours);
+    var timeString  = ((minutes < 10) ? "0" : "") + minutes;
+    timeString  += ((seconds < 10) ? ":0" : ":") + seconds;
+
+    var html = "<div><span class='bar-hl'>";
+    //html += timeString;
+    html += bheader;
+    html += "</span><span class='bar-info'>";
+    html += bcontent;
+    html += "</span></div>";
+
+    console.log(html);
+    $(".bar-message > .bar-wrapper").append(html);
+    $('.bar-message > .bar-wrapper').scrollTop($('.bar-message > .bar-wrapper')[0].scrollHeight);
+}
+
+$(".bar-dropdown").click(function() {
+    console.log($(this).parent().parent().attr('id'));
+    var _id = $(this).parent().parent().attr('id');
+    if (parseInt($(".bar-"+ _id).css("height")) > 0) {
+        $(".bar-dropdown", "#"+_id).css("transform", "rotate(180deg)");
+        $(".bar-"+ _id + "> .bar-wrapper").hide();
+        $(".bar-"+ _id).css("height", 0);
+        $("#"+_id).css("height", 36);
+    } else {
+        $(".bar-dropdown", "#"+_id).css("transform", "rotate(0deg)");
+        if (_id == "settings") {
+            $(".bar-"+ _id).css("height", 400);
+            $("#"+_id).css("height", 436);
+        } else {
+            $(".bar-"+ _id).css("height", 200);
+            $("#"+_id).css("height", 236);
+        }
+        $(".bar-"+ _id + "> .bar-wrapper").show();
+    }
+});
+
+// master hide all elements on screen
+$("#toggleGUI").click(function() {
+    $(".window-item").toggle();
+});
+
+$("#showTabs").hover(
+    function() {
+        $("#window-tab").show();
+    }, function() {
+        $("#window-tab").hide();
+    }
+);
+
+$("#window-tab").hover(
+    function() {
+        $("#window-tab").show();
+    }, function() {
+        $("#window-tab").hide();
+    }
+);
+
+$("#offMessage").click(function() {
+    if ($("#message").is(":visible")) {
+        $("#message").removeClass("window-item");
+        $("#message").hide();
+    } else {
+        $("#message").addClass("window-item");
+        $("#message").show();
+    }
+});
+
+$("#offOrientation").click(function() {
+    if ($("#orientation").is(":visible")) {
+        $("#orientation").removeClass("window-item");
+        $("#orientation").hide();
+    } else {
+        $("#orientation").addClass("window-item");
+        $("#orientation").show();
+    }
+});
+
+$("#offSettings").click(function() {
+    if ($("#settings").is(":visible")) {
+        $("#settings").removeClass("window-item");
+        $("#settings").hide();
+    } else {
+        $("#settings").addClass("window-item");
+        $("#settings").show();
+    }
+});
+
+
+function setMessage(bheader, bcontent) {
+    console.log("Creating message");
+    var timeNow = new Date();
+    var hours   = timeNow.getHours();
+    var minutes = timeNow.getMinutes();
+    var seconds = timeNow.getSeconds();
+    //var timeString = "" + ((minutes > 12) ? - 12 : hours);
+    var timeString  = ((minutes < 10) ? "0" : "") + minutes;
+    timeString  += ((seconds < 10) ? ":0" : ":") + seconds;
+
+    var html = "<div><span class='bar-hl'>";
+    //html += timeString;
+    html += bheader;
+    html += "</span><span class='bar-info'>";
+    html += bcontent;
+    html += "</span></div>";
+
+    console.log(html);
+    $(".bar-message > .bar-wrapper").append(html);
+    $('.bar-message > .bar-wrapper').scrollTop($('.bar-message > .bar-wrapper')[0].scrollHeight);
+}
+
+$('#settings-box :checkbox').click(function() {
+    var $this = $(this);
+    // $this will contain a reference to the checkbox
+    if ($this.is(':checked')) {
+        if (this.name == "wireframe") {
+            rcl2.addWireframe();
+            setMessage("tomo", "Add cube wireframe.");
+        }
+        // the checkbox was checked
+    } else {
+        if (this.name == "wireframe") {
+            rcl2.removeWireframe();
+            setMessage("tomo", "Remove cube wireframe.");
+        }
+        // the checkbox was unchecked
+    }
+});
+
+$('#viewIso :checkbox').click(function() {
+    var $this = $(this);
+    if ($this.is(':checked')) {
+        rcl2.showISO();
+        rcl2.hideLight();
+        $('#viewVolren :checkbox').prop('checked', false);
+    } else {
+        rcl2.showVolren();
+        $('#viewVolren :checkbox').prop('checked', true);
+    }
+
+});
+
+$('#viewVolren :checkbox').click(function() {
+    var $this = $(this);
+    if ($this.is(':checked')) {
+        console.log("Checked");
+        rcl2.showVolren();
+        $('#viewIso :checkbox').prop('checked', false);
+    } else {
+        console.log("Not Checked");
+        rcl2.showISO();
+        rcl2.hideLight();
+        $('#viewIso :checkbox').prop('checked', true);
+    }
+});
+
+$('#rotate :checkbox').click(function() {
+    var $this = $(this);
+    if ($this.is(':checked')) {
+        rcl2.startRotate();
+    } else {
+        console.log("Not Checked");
+        rcl2.stopRotate();
+    }
+});
+
+
+$( "#bg-color" ).change(function() {
+    console.log(this.value);
+    rcl2.setBackgroundColor("#"+this.value);
+    setMessage("tomo", "Set background color: #" + this.value);
+});
+
+$("#textLowerGray").change(function() {
+    rcl2.setGrayMinValue(($("#textLowerGray").val()/255.0));
+    $( "#slider-range" ).slider('values',0, ($("#textLowerGray").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#minSos").change(function() {
+    rcl2.setSosThresholdBot(($("#minSos").val()/255.0));
+    $( "#slider-range" ).slider('values',0, ($("#minSos").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#maxSos").change(function() {
+    rcl2.setSosThresholdTop(($("#maxSos").val()/255.0));
+    $( "#slider-range" ).slider('values',0, ($("#maxSos").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#minAtten").change(function() {
+    rcl2.setAttenThresholdBot(($("#minAtten").val()/255.0));
+    $( "#slider-range" ).slider('values',0, ($("#minAtten").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#maxAtten").change(function() {
+    rcl2.setAttenThresholdTop(($("#maxAtten").val()/255.0));
+    $( "#slider-range" ).slider('values',0, ($("#maxAtten").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#textUpperGray").change(function() {
+    rcl2.setGrayMaxValue(($("#textUpperGray").val()/255.0));
+    $( "#slider-range" ).slider('values', 1, ($("#textUpperGray").val()/255.0 * 100)  );
+    $( "#slider-range" ).slider("refresh");
+});
+
+$("#textLowerX").change(function() {
+    rcl2.setGeometryMinX(($("#textLowerX").val()/255.0));
+    $( "#slider-range-x" ).slider('values',0, ($("#textLowerX").val()/255.0 * 100)  );
+    $( "#slider-range-x" ).slider("refresh");
+});
+
+$("#textUpperX").change(function() {
+    rcl2.setGeometryMaxX(($("#textUpperX").val()/255.0));
+    $( "#slider-range-x" ).slider('values', 1, ($("#textUpperX").val()/255.0 * 100)  );
+    $( "#slider-range-x" ).slider("refresh");
+});
+
+$("#textLowerY").change(function() {
+    rcl2.setGeometryMinY(($("#textLowerY").val()/255.0));
+    $( "#slider-range-y" ).slider('values',0, ($("#textLowerY").val()/255.0 * 100)  );
+    $( "#slider-range-y" ).slider("refresh");
+});
+
+$("#textUpperY").change(function() {
+    rcl2.setGeometryMaxY(($("#textUpperY").val()/255.0));
+    $( "#slider-range-y" ).slider('values', 1, ($("#textUpperY").val()/255.0 * 100)  );
+    $( "#slider-range-y" ).slider("refresh");
+});
+
+$("#textLowerZ").change(function() {
+    rcl2.setGeometryMinZ(($("#textLowerZ").val()/255.0));
+    $( "#slider-range-z" ).slider('values',0, ($("#textLowerZ").val()/255.0 * 100)  );
+    $( "#slider-range-z" ).slider("refresh");
+});
+
+$("#textUpperZ").change(function() {
+    rcl2.setGeometryMaxZ(($("#textUpperZ").val()/255.0));
+    $( "#slider-range-z" ).slider('values', 1, ($("#textUpperZ").val()/255.0 * 100)  );
+    $( "#slider-range-z" ).slider("refresh");
+});
+
+
+
+
+$( 'input[type=radio][name=changeImageRadio]' ).change(function() {
+  $('#wireframe')[0].checked = true;
+    if(this.value!=3){
+      rcl2.setShaderName('secondPassGraySeparate');
+      rcl2.setIndexOfImage(this.value);
+    }
+    else{
+      rcl2.setShaderName('secondPassNormalFusion');
+    }
+});
+
+
+
+$( document ).ready(function() {
+    setMessage("GUI", "Initialized.");
+    setMessage("GUI", "After 5 seconds of idling, raycaster and light rotation will stop.");
+    setMessage("GUI", "Click on scene to resume.");
+    // Handler for .ready() called.
+    $( "#slider-range" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 0, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setGrayMinValue(ui.values[0]/100.0);
+            rcl2.setGrayMaxValue(ui.values[1]/100.0);
+            $("#textLowerGray").val( parseInt(ui.values[0]/100*255) );
+            $("#textUpperGray").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+
+    $( "#slider-sos" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 78, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setSosThresholdBot(ui.values[0]/100.0)
+            rcl2.setSosThresholdTop(ui.values[1]/100.0)
+            $("#minSos").val( parseInt(ui.values[0]/100*255) );
+            $("#maxSos").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+
+    $( "#slider-atten" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 78, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setAttenThresholdBot(ui.values[0]/100.0)
+            rcl2.setAttenThresholdTop(ui.values[1]/100.0)
+            $("#minAtten").val( parseInt(ui.values[0]/100*255) );
+            $("#maxAtten").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+
+    $( "#slider-range-x" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 0, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setGeometryMinX(ui.values[0]/100.0)
+            rcl2.setGeometryMaxX(ui.values[1]/100.0)
+            $("#textLowerX").val( parseInt(ui.values[0]/100*255) );
+            $("#textUpperX").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+
+    $( "#slider-range-y" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 0, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setGeometryMinY(ui.values[0]/100.0)
+            rcl2.setGeometryMaxY(ui.values[1]/100.0)
+            $("#textLowerY").val( parseInt(ui.values[0]/100*255) );
+            $("#textUpperY").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+
+    $( "#slider-range-z" ).slider({
+        range: true,
+        min: 0,
+        max: 100,
+        values: [ 0, 100 ],
+        slide: function( event, ui ) {
+            console.log(ui.values);
+            rcl2.setGeometryMinZ(ui.values[0]/100.0)
+            rcl2.setGeometryMaxZ(ui.values[1]/100.0)
+            $("#textLowerZ").val( parseInt(ui.values[0]/100*255) );
+            $("#textUpperZ").val( parseInt(ui.values[1]/100*255) );
+        }
+    });
+});

+ 184 - 0
nova/static/js/wave/stats.min.js

@@ -0,0 +1,184 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Stats = function () {
+
+	var now = ( self.performance && self.performance.now ) ? self.performance.now.bind( performance ) : Date.now;
+
+	var startTime = now(), prevTime = startTime;
+	var frames = 0, mode = 0;
+
+	function createElement( tag, id, css ) {
+
+		var element = document.createElement( tag );
+		element.id = id;
+		element.style.cssText = css;
+		return element;
+
+	}
+
+	function createPanel( id, fg, bg ) {
+
+		var div = createElement( 'div', id, 'padding:0 0 3px 3px;text-align:left;background:' + bg );
+
+		var text = createElement( 'div', id + 'Text', 'font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px;color:' + fg );
+		text.innerHTML = id.toUpperCase();
+		div.appendChild( text );
+
+		var graph = createElement( 'div', id + 'Graph', 'width:74px;height:30px;background:' + fg );
+		div.appendChild( graph );
+
+		for ( var i = 0; i < 74; i ++ ) {
+
+			graph.appendChild( createElement( 'span', '', 'width:1px;height:30px;float:left;opacity:0.9;background:' + bg ) );
+
+		}
+
+		return div;
+
+	}
+
+	function setMode( value ) {
+
+		var children = container.children;
+
+		for ( var i = 0; i < children.length; i ++ ) {
+
+			children[ i ].style.display = i === value ? 'block' : 'none';
+
+		}
+
+		mode = value;
+
+	}
+
+	function updateGraph( dom, value ) {
+
+		var child = dom.appendChild( dom.firstChild );
+		child.style.height = Math.min( 30, 30 - value * 30 ) + 'px';
+
+	}
+
+	//
+
+	var container = createElement( 'div', 'stats', 'width:80px;opacity:0.9;cursor:pointer' );
+	container.addEventListener( 'mousedown', function ( event ) {
+
+		event.preventDefault();
+		setMode( ++ mode % container.children.length );
+
+	}, false );
+
+	// FPS
+
+	var fps = 0, fpsMin = Infinity, fpsMax = 0;
+
+	var fpsDiv = createPanel( 'fps', '#0ff', '#002' );
+	var fpsText = fpsDiv.children[ 0 ];
+	var fpsGraph = fpsDiv.children[ 1 ];
+
+	container.appendChild( fpsDiv );
+
+	// MS
+
+	var ms = 0, msMin = Infinity, msMax = 0;
+
+	var msDiv = createPanel( 'ms', '#0f0', '#020' );
+	var msText = msDiv.children[ 0 ];
+	var msGraph = msDiv.children[ 1 ];
+
+	container.appendChild( msDiv );
+
+	// MEM
+
+	if ( self.performance && self.performance.memory ) {
+
+		var mem = 0, memMin = Infinity, memMax = 0;
+
+		var memDiv = createPanel( 'mb', '#f08', '#201' );
+		var memText = memDiv.children[ 0 ];
+		var memGraph = memDiv.children[ 1 ];
+
+		container.appendChild( memDiv );
+
+	}
+
+	//
+
+	setMode( mode );
+
+	return {
+
+		REVISION: 14,
+
+		domElement: container,
+
+		setMode: setMode,
+
+		begin: function () {
+
+			startTime = now();
+
+		},
+
+		end: function () {
+
+			var time = now();
+
+			ms = time - startTime;
+			msMin = Math.min( msMin, ms );
+			msMax = Math.max( msMax, ms );
+
+			msText.textContent = ( ms | 0 ) + ' MS (' + ( msMin | 0 ) + '-' + ( msMax | 0 ) + ')';
+			updateGraph( msGraph, ms / 200 );
+
+			frames ++;
+
+			if ( time > prevTime + 1000 ) {
+
+				fps = Math.round( ( frames * 1000 ) / ( time - prevTime ) );
+				fpsMin = Math.min( fpsMin, fps );
+				fpsMax = Math.max( fpsMax, fps );
+
+				fpsText.textContent = fps + ' FPS (' + fpsMin + '-' + fpsMax + ')';
+				updateGraph( fpsGraph, fps / 100 );
+
+				prevTime = time;
+				frames = 0;
+
+				if ( mem !== undefined ) {
+
+					var heapSize = performance.memory.usedJSHeapSize;
+					var heapSizeLimit = performance.memory.jsHeapSizeLimit;
+
+					mem = Math.round( heapSize * 0.000000954 );
+					memMin = Math.min( memMin, mem );
+					memMax = Math.max( memMax, mem );
+
+					memText.textContent = mem + ' MB (' + memMin + '-' + memMax + ')';
+					updateGraph( memGraph, heapSize / heapSizeLimit );
+
+				}
+
+			}
+
+			return time;
+
+		},
+
+		update: function () {
+
+			startTime = this.end();
+
+		}
+
+	};
+
+};
+
+if ( typeof module === 'object' ) {
+
+	module.exports = Stats;
+
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 528 - 0
nova/static/js/wave/three.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 510 - 0
nova/static/js/wave/volumeRaycaster.js


+ 1 - 0
nova/templates/dataset/header.html

@@ -50,6 +50,7 @@
 – <a href="{{ url_for("show_collection", collection_name=dataset.collection.name) }}">{{ dataset.collection.name }}</a> / {{ dataset.name }}
         <i class="clickable pull-right" :class="{ 'fa fa-bookmark': bookmarked, 'fa fa-bookmark-o': !bookmarked }" @click="bookmark"></i>
         <i class="clickable pull-right fa fa-clone" @click="beginDatasetDerive"></i>
+        <i class="clickable pull-right fa fa-eye" @click="openWave"></i>
       </h2>
     </div>
   </div>

+ 67 - 0
nova/templates/dataset/wave.html

@@ -0,0 +1,67 @@
+<!doctype html>
+
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Wave</title>
+</head>
+
+<body style="background-color: #000;">
+    <div id="container" style="width:256px; height: 256px; background-color: #aaa;"></div>
+    <script
+        src="https://code.jquery.com/jquery-3.3.1.min.js"
+        crossorigin="anonymous"></script>
+    <script src="{{ url_for('static', filename='js/wave/three.min.js') }}"></script>
+    <script src="{{ url_for('static', filename='js/wave/TrackballControls.js') }}"></script>
+    <script src="{{ url_for('static', filename='js/wave/ejs.js') }}"></script>
+    <script src="{{ url_for('static', filename='js/wave/volumeRaycaster.js') }}"></script>
+    <script src="{{ url_for('static', filename='js/wave/stats.min.js') }}"></script>
+    <script>
+        function beginWave(slicemaps) {
+            config = {
+                "dom_container": "container",
+                "slicemaps_paths": slicemaps,
+                "steps" : 144,
+                "shader_name": "secondPassSoebel",
+                "slices_range": [0, 255],
+                "row_col": [16, 16],
+                "renderer_size": [256, 256],
+                "renderer_canvas_size": ['*','*'],
+            };
+
+            rcl2 = new VRC.VolumeRaycaster(config);
+        }
+        function sendContinuousGetToLocationUntilDone(loc) {
+            $.ajax(loc, {
+                method:"GET",
+                crossDomain:true
+            }).done(function(data, textStatus, jqXHR) {
+                if (data.status == 'running') {
+                    setTimeout(function() {
+                        sendContinuousGetToLocationUntilDone(loc);
+                    }, 2000);
+                } else {
+                    $location = jqXHR.getResponseHeader('location');
+                    slicemaps = [$location];
+                    beginWave(slicemaps)
+                }
+            });
+        }
+        $( document ).ready(function() {
+            $params = JSON.stringify({
+                "token": "{{token|safe}}",
+                "user": "{{owner}}",
+                "dataset": "{{dataset}}"
+            });
+            $.ajax("http://localhost:5001/maps", {
+                data:$params, 
+                contentType:"application/json", 
+                method:"POST", crossDomain:true, 
+            }).done(function(data, textStatus, jqXHR){
+                $location = jqXHR.getResponseHeader('location');
+                sendContinuousGetToLocationUntilDone($location)
+            }).fail(function() { alert( "An error occured" ); });
+         });
+    </script>
+</body>
+</html>

+ 9 - 0
nova/views.py

@@ -553,3 +553,12 @@ def create_group():
         logic.create_group(current_user, form.name.data, description=form.description.data, users=user_list)
         return redirect(url_for('index'))
     return render_template('group/create.html', form=form)
+
+
+@app.route('/wave')
+@login_required(admin=False)
+def wave_it():
+    token = current_user.token
+    owner = request.args['user']
+    dataset_name = request.args['dataset']
+    return render_template('dataset/wave.html', owner=owner, dataset=dataset_name, token=token) 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio