Index: /issm/trunk-jpl/src/m/plot/applyoptions.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 22434)
@@ -61,8 +61,12 @@
 			//}}}
 			//{{{ Draw colorbar gradient
+			var position;
+			var offset = 1 / (colorbar.length - 1) / 2;
+			var scaling = 1 - 2 * offset;
 			for (var i=0; i < colorbar.length; i++) {
 				color = colorbar[colorbar.length-i-1];
 				color = [Math.round(color[0]*255), Math.round(color[1]*255), Math.round(color[2]*255)];
-				cgradient.addColorStop(i/colorbar.length*(cdivisions/(cdivisions+1.0))+(1.0/(cdivisions+1.0)),'rgba('+color.toString()+', 1.0)');
+				position = (i / (colorbar.length - 1) * scaling) + offset;
+				cgradient.addColorStop(position, 'rgba(' + color.toString() + ', 1.0)');
 			}
 			ccontext.fillStyle=cgradient;
@@ -119,22 +123,41 @@
 	tcontext = tcanvas.getContext('2d');
 	tgradient = tcontext.createLinearGradient(0, 0, 0, 256);
-
+	
 	var cmap = options.getfieldvalue('colormap','jet');
 	var colorbar = colorbars[cmap];
-	for (var i=0; i < colorbar.length; i++) {
-		color = colorbar[colorbar.length-i-1];
-		color = [Math.round(color[0]*255), Math.round(color[1]*255), Math.round(color[2]*255)];
-		tgradient.addColorStop(i/colorbar.length,'rgba('+color.toString()+', 1.0)');
+	for (var i = 0; i < colorbar.length; i++) {
+		color = colorbar[colorbar.length - i - 1];
+		color = [Math.round(color[0] * 255), Math.round(color[1] * 255), Math.round(color[2] * 255)];
+		tgradient.addColorStop(i / (colorbar.length - 1), 'rgba(' + color.toString() + ', 1.0)');
 	}
-
 	tcontext.fillStyle = tgradient;
 	tcontext.fillRect(0, 0, 256, 256);
+	
+	//Allow for special texture colors, drawing each color in equal width vertical rectangles. The last rectanglar section is reserved for the colormap.
+	if (options.exist('maskregion')) {
+		var maskObject = options.getfieldvalue('maskregion',{'enabled':false});
+		if (maskObject.enabled && !VESL.Helpers.isEmptyOrUndefined(maskObject.colors)) {
+			var x = 0;
+			var sections = Object.keys(maskObject.colors).length + 1;
+			var size = 256;
+			var width = Math.floor(1 / sections * size);
+			for (var color in maskObject.colors) {
+				tcontext.fillStyle = maskObject.colors[color];
+				tcontext.fillRect(x++ * width, 0, width, size);
+			}
+		}
+	}
+	
 	tURL = tcanvas.toDataURL();
-	canvas.unitNode.texture = initTexture(canvas.gl, tURL);
+	if (options.getfieldvalue('clf','on')=='off') {
+		canvas.nodes['unit' + (Object.keys(canvas.nodes).length - 1)].texture = initTexture(canvas.gl, tURL);
+	} else {
+		canvas.nodes.unit.texture = initTexture(canvas.gl, tURL);
+	}
 	//}}}
 	//{{{ text display
 	var overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid')+'-overlay');
 	var overlaycanvas = $('#'+overlaycanvasid)[0];
-	if (!isEmptyOrUndefined(overlaycanvas)) {
+	if (!VESL.Helpers.isEmptyOrUndefined(overlaycanvas)) {
 		//Get drawing context and save reference on main WebGL canvas
 		var ctx = overlaycanvas.getContext('2d');
@@ -216,5 +239,5 @@
 				// function declared in slr-gfm sim-front-end-controller.js
 				// if labels are behind the globe sphere then skip iteartion and do not display them
-				if (isLabelVisible(textLabel)) {
+				if (VESL.UI.isLabelVisible(textLabel)) {
 					//Transform from world space to viewport space
 					var screenPoint = vec3.transformMat4(vec3.create(), textLabel.position, canvas.camera.vpMatrix);
@@ -272,5 +295,5 @@
 			if ('sky' === renderObject && !('sky' in canvas.nodes)) {
 				var mesh = GL.Mesh.icosahedron({size: 6371000 * canvas.atmosphere.scaleHeight, subdivisions: 5});
-				var texture = initTexture(gl, canvas.rootPath + 'textures/TychoSkymapII_t4_2k.jpg');
+				var texture = initTexture(gl, canvas.assetsPath + '/textures/TychoSkymapII_t4_2k.jpg');
 				node = new Node(
 					'canvas', canvas,
@@ -287,5 +310,5 @@
 			if ('space' === renderObject && !('space' in canvas.nodes)) {
 				var mesh = GL.Mesh.sphere({size: 6371000 * 20});
-				var texture = initTexture(gl, canvas.rootPath + 'textures/TychoSkymapII_t4_2k.jpg');
+				var texture = initTexture(gl, canvas.assetsPath + '/textures/TychoSkymapII_t4_2k.jpg');
 				node = new Node(
 					'canvas', canvas,
@@ -301,5 +324,5 @@
 				);
 			}
-			if ('coastlines' === renderObject && !('coastlines' in canvas.nodes)) {
+			if ('coastlines' === renderObject) {
 				node = new Node(
 					'canvas', canvas,
@@ -352,4 +375,5 @@
 					'shaderName', 'Colored',
 					'drawMode', gl.LINES,
+					'diffuseColor', [1.0, 0.0, 0.0, 1.0],
 					'lineWidth', options.getfieldvalue('linewidth', 1),
 					'scale', [object.scale, object.scale, object.scale],
@@ -390,10 +414,12 @@
 			if ('clouds' === renderObject && !('clouds0' in canvas.nodes)) {
 				//clouds				
-				var mesh = GL.Mesh.fromURL(canvas.rootPath+'obj/cloud.obj');
+				var mesh = GL.Mesh.fromURL(canvas.assetsPath + '/obj/cloud.obj');
 				for (var i = 0; i < object.quantity; i++) {
 					//TODO: More options, less magic numbers. Add animation. Better shading.
-					var offset = [	translation[0] + (Math.random() - 0.5) * 2 * object.range, 
-									translation[1] + object.height + (Math.random() - 0.5) * 0.2 * object.range, 
-									translation[2] + (Math.random() - 0.5) * 2 * object.range];
+					var offset = [randomizeAxis(translation[0], object.range), 
+												randomizeCloudHeight(translation[1], object), 
+												randomizeAxis(translation[2], object.range)];
+					var randomSize = randomizeCloudSize(object.scale);
+					var randomColor = randomizeCloudColor();
 					node = new Node(
 						'canvas', canvas,
@@ -402,9 +428,8 @@
 						'name', 'clouds' + i,
 						'shaderName', 'ColoredDiffuse',
-						'animation', {'time': Date.now(),'target': translation,'current': translation},
-						'diffuseColor', [0.7,0.7,0.7,1.0],
-						'specularColor', [0.0,0.0,0.0,1.0],
+						'diffuseColor', [randomColor, randomColor, randomColor, 1.0],
+						'specularColor', [0.1, 0.1, 0.1, 1.0],
 						'mesh', mesh,
-						'scale', [object.scale, object.scale, object.scale],
+						'scale', [randomSize, randomSize, randomSize],
 						'translation', offset
 					);
@@ -414,2 +439,30 @@
 	} //}}}
 } //}}}
+
+function randomizeCloudHeight(canvasGroundHeight, object) {
+		// -+7000 seems a reasonable range
+		var maxHeight = object.height + 7000;
+		var minHeigth = object.height - 7000;
+		var randomHeight = (Math.random() * (maxHeight - minHeigth)) + minHeigth;
+		
+		return canvasGroundHeight + randomHeight;
+}
+
+// assumes that originAxisValue is the mid-value between min and max.
+function randomizeAxis(originAxisValue, range) {
+		return originAxisValue + (Math.random() - 0.5) * (range * 2); 
+}
+
+function randomizeCloudSize(scale) {
+	var maxResize = 1.3;
+	var minResize = 0.5;
+	var randomizationFactor = Math.random() * (maxResize - minResize) + minResize; 
+	return scale * randomizationFactor;
+}
+
+function randomizeCloudColor() {
+	var lighestColor = 1;
+	var darkestColor = 0.9;
+	var randomColor = Math.random() * (lighestColor - darkestColor) + darkestColor;
+	return randomColor;
+}
Index: /issm/trunk-jpl/src/m/plot/plot_mesh.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 22434)
@@ -31,5 +31,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskregion',{'enabled':false}));
 		scale = [1, 1, 1];
 	}
Index: /issm/trunk-jpl/src/m/plot/plot_overlay.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 22434)
@@ -41,5 +41,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskregion',{'enabled':false}));
 		scale = [1, 1, 1];
 	}
Index: /issm/trunk-jpl/src/m/plot/plot_quiver.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 22434)
@@ -7,4 +7,7 @@
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
+	//Disabling for now, since quivers are "sticky" - once turned on, they won't turn off. This is due to cachenodes, but should find better way to handle it.
+	return;
+	
 	if ('quiver' in  canvas.nodes && noCacheNodesOverride && options.getfieldvalue('cachenodes','on') === 'on') return;
 	
@@ -26,7 +29,7 @@
 
 	//Only displaying velocity fields for now
-	var v = isEmptyOrUndefined(md.results) ?  md.initialization.vel : md.results[canvas.animation.frame].Vel;
-	var vx = isEmptyOrUndefined(md.results) ? md.initialization.vx : md.results[canvas.animation.frame].Vx;
-	var vy = isEmptyOrUndefined(md.results) ? md.initialization.vy : md.results[canvas.animation.frame].Vy;
+	var v = VESL.Helpers.isEmptyOrUndefined(md.results) ?  md.initialization.vel : md.results[canvas.animation.frame].Vel;
+	var vx = VESL.Helpers.isEmptyOrUndefined(md.results) ? md.initialization.vx : md.results[canvas.animation.frame].Vx;
+	var vy = VESL.Helpers.isEmptyOrUndefined(md.results) ? md.initialization.vy : md.results[canvas.animation.frame].Vy;
 
 	//Handle heightscale
@@ -37,5 +40,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskregion',{'enabled':false}));
 		scale = [1, 1, 1];
 	}
Index: /issm/trunk-jpl/src/m/plot/plot_transient_movie.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_transient_movie.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plot_transient_movie.js	(revision 22434)
@@ -21,6 +21,6 @@
 		for (var i in steps) {
 			dataresults = processdata(md, data[i], options);
-			range[1] = Math.min(range[0], ArrayMin(dataresults[0]));
-			range[2] = Math.max(range[1], ArrayMax(dataresults[0]));
+			range[0] = Math.min(range[0], ArrayMin(dataresults[1]));
+			range[1] = Math.max(range[1], ArrayMax(dataresults[1]));
 		}
 		datatype = dataresults[1];
@@ -31,5 +31,5 @@
 	//Create unit node if it does not already exist
 	if (!('unit' in canvas.nodes)) {
-		var	dataresults = processdata(md, data[i],options);
+		var	dataresults = processdata(md, data[0],options);
 		var	data2 = dataresults[0]; 
 		var	datatype = dataresults[1];
@@ -39,5 +39,5 @@
 	}
 	
-	//display movie
+	//Setup rendering node
 	var node = canvas.nodes.unit;
 	node.options = options;
@@ -46,5 +46,15 @@
 	node.enabled = options.getfieldvalue('nodata', 'off') == 'off';
 	node.log = options.getfieldvalue('log', false);
-	canvas.unitMovieData = data;
+	
+	//process data
+	var	dataresults;
+	var processedData = [];
+	for (var i in steps) {
+		dataresults = processdata(md, data[i].slice(), options);
+		processedData[i] = dataresults[0];
+	}
+	
+	//display movie
+	canvas.unitMovieData = processedData;
 	canvas.animation.frame = 0;
 	canvas.animation.handler = setInterval(function () {
@@ -53,6 +63,6 @@
 		if (canvas.animation.play) {
 			if (canvas.animation.increment) {
-				if (frame > steps.length - 1) {
-					if (node.movieLoop) {
+				if (frame >= steps.length - 1) {
+					if (canvas.animation.loop) {
 						frame = 0;
 					}
@@ -69,16 +79,16 @@
 		//If frame has changed, update unit node and data marker display.
 		if (frame !== canvas.animation.lastFrame) {
-			node.updateBuffer('Coords', data[frame]);
-			canvas.unitData = data[frame];
+			node.updateBuffer('Coords', processedData[frame]);
+			canvas.unitData = processedData[frame];
 			if (canvas.dataMarkers.enabled) {
 				updateMarker(canvas, false);
 			}
-			if (canvas.progressBar) {
-				canvas.progressBar.val(frame).slider('refresh');
+			if (canvas.playbackSlider) {
+				canvas.playbackSlider.val(frame).slider('refresh');
 			}
-			if (canvas.timeLabel) {
-				canvas.timeLabel.html(steps[frame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr"));
+			if (canvas.playbackTextProgress) {
+				canvas.playbackTextProgress.html(steps[frame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr"));
 			}
-			if (!isEmptyOrUndefined(canvas.nodes.quiver)) {
+			if (!VESL.Helpers.isEmptyOrUndefined(canvas.nodes.quiver)) {
 				plot_quiver(md,options,canvas,false);
 			}
@@ -91,8 +101,8 @@
 	
 	//Update progress bar with new frame info.
-	if (canvas.progressBar) {
-		canvas.progressBar.val(canvas.animation.frame);
-		canvas.progressBar.attr('max', steps.length - 1);
-		canvas.progressBar.slider('refresh');
+	if (canvas.playbackSlider) {
+		canvas.playbackSlider.val(canvas.animation.frame);
+		canvas.playbackSlider.attr('max', steps.length - 1);
+		canvas.playbackSlider.slider('refresh');
 	}
 				
Index: /issm/trunk-jpl/src/m/plot/plot_unit.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 22434)
@@ -7,10 +7,18 @@
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//if ('unit' in canvas.nodes) {
-	//	if (
-	//	canvas.nodes.unit.updateBuffer('Coords', data);
-	//	return;
-	//}
-	//else {
+	var name = 'unit';
+	if ('unit' in canvas.nodes) {
+		if (options.getfieldvalue('clf','on')=='on') {
+			for (var node in canvas.nodes) {
+				if (node.startsWith('unit')) {
+					delete canvas.octrees[node];
+					delete canvas.nodes[node];
+				}
+			}
+		}
+		else {
+			name = 'unit' + Object.keys(canvas.nodes).length;
+		}
+	}
 
 	//{{{ declare variables:
@@ -37,5 +45,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskregion',{'enabled':false}));
 		scale = [1, 1, 1];
 	}
@@ -44,21 +52,24 @@
 	var edgecolor = options.getfieldvalue('edgecolor', [1.0, 1.0, 1.0 ,1.0]);
 	var maskzeros = options.getfieldvalue('maskzeros', {});
+	var render = options.getfieldvalue('render', {});
+	var cullFace = ("unit" in render) ? canvas.gl[render.unit.cullFace] : canvas.gl.BACK;
 	var node = new Node(
 		'canvas', canvas,
 		'options', options,
-		'name', 'unit',
+		'name', name,
 		'shaderName', 'TexturedDiffuse',
 		'alpha', options.getfieldvalue('alpha', 1.0),
 		'caxis', options.getfieldvalue('caxis',[ArrayMin(data), ArrayMax(data)]),
-		//'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, md.mesh.classname() === 'mesh3dsurface' ? (zlim[0] + zlim[1]) / 2 : zlim[0]],
 		'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2],
-		'lightingBias', 0.8,
+		'cullFace', cullFace,
+		'lightingBias', canvas.view.lightingBias,
 		'diffuseColor', edgecolor,
 		'specularStrength', 0.0,
-		'enabled', options.getfieldvalue('nodata','off') == 'off',
+		'enabled', options.getfieldvalue('nodata','off') == 'off' || (("unit" in render) && render.unit.enabled),
 		'log', options.getfieldvalue('log',false),
 		'maskEnabled', options.getfieldvalue('innermask','off') == 'on',
 		'maskHeight', options.getfieldvalue('innermaskheight', 150.0) / options.getfieldvalue('heightscale', 1),
 		'maskColor', options.getfieldvalue('innermaskcolor',[0.0, 0.0, 1.0, 1.0]),
+		'maskObject', options.getfieldvalue('maskregion',{'enabled':false}),
 		'maskZerosColor', defaultFor(maskzeros.color,[1.0, 1.0, 1.0, 1.0]),
 		'maskZerosEnabled', defaultFor(maskzeros.enabled,false),
@@ -66,9 +77,11 @@
 		'maskZerosZeroValue', defaultFor(maskzeros.zeroValue,0.5),
 		'rotation', [-90, 0, 0],
-		'scale', scale
+		'scale', ("unit" in render) ? [render.unit.scale, render.unit.scale, render.unit.scale] : scale
 	);
 	//}
-	canvas.unitNode = node;
-	canvas.unitData = data;
+	if (options.getfieldvalue('clf','on')=='on') {
+		canvas.unitNode = node;
+		canvas.unitData = data;
+	}
 	//}}}
 	switch(datatype){
Index: /issm/trunk-jpl/src/m/plot/plotdoc.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plotdoc.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plotdoc.js	(revision 22434)
@@ -39,5 +39,5 @@
 	console.log('       "datamarkers": object cotaining data marker parameters. See webgl.js for defaults. (ex: {"enabled":true,"format":["<div id="sim-plot"></div>"],"labels":["thickness","velocity","value"],"animated":true})');
 	console.log('       	"enabled": toggle data marker displays (default true, ex: false)');
-	console.log('       	"image": image used for marking the clicked point (ex: "/textures/data_marker.svg")');
+	console.log('       	"image": image used for marking the clicked point (ex: "/canvas/data-markers/data_marker.svg")');
 	console.log('       	"labels": when displaying a sim-plot graph, display these model fields. (ex: ["thickness","velocity","value"])');
 	console.log('       	"font": font to be used for display (ex: "24px "Comic Sans MS", cursive")');
Index: /issm/trunk-jpl/src/m/plot/plotmodel.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plotmodel.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/plotmodel.js	(revision 22434)
@@ -36,5 +36,5 @@
 		//Reinitialize all canvases
 		for (var i=0;i<numberofplots;i++){
-			document.getElementById(options.list[i].getfieldvalue('canvasid')).initialized = false;
+			if (options.list[i].getfieldvalue('clf','on')!='off') document.getElementById(options.list[i].getfieldvalue('canvasid')).initialized = false;
 		}
 		//Go through all data plottable and close window if an error occurs
Index: /issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/webgl.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/webgl.js	(revision 22434)
@@ -6,6 +6,6 @@
 	//var canvas = document.getElementById(options.getfieldvalue('canvasid'));
 	if (!canvas.initialized) {
-		if (!isEmptyOrUndefined(canvas.draw) && canvas.draw.handler !== 0)	{ window.cancelAnimationFrame(canvas.draw.handler); }
-		if (!isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
+		if (!VESL.Helpers.isEmptyOrUndefined(canvas.draw) && canvas.draw.handler !== 0)	{ window.cancelAnimationFrame(canvas.draw.handler); }
+		if (!VESL.Helpers.isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
 		initWebGL(canvas, options);
 		draw(canvas);
@@ -20,5 +20,5 @@
 	//Initialize canvas.gl on page load, reusing gl context on additional runs
 	var gl = canvas.gl;
-	if (isEmptyOrUndefined(gl)) {
+	if (VESL.Helpers.isEmptyOrUndefined(gl)) {
 		gl = GL.create({canvas: canvas});
 		gl.enable(gl.DEPTH_TEST); // Enable depth testing
@@ -27,5 +27,5 @@
 		gl.enable(gl.CULL_FACE); // Enable face culling
 		gl.cullFace(gl.FRONT);
-		gl.shaders = loadShaders(gl, options.getfieldvalue('rootpath', '../../../js/')); // Load shaders and store them in gl object
+		gl.shaders = loadShaders(gl, options.getfieldvalue('rootpath', '/canvas')); // Load shaders and store them in gl object.
 		gl.textures = {};
 		
@@ -50,156 +50,130 @@
 		
 		canvas.gl = gl;
-		canvas.rootPath = options.getfieldvalue('rootpath', '../../../js/');
+		canvas.assetsPath = options.getfieldvalue('rootpath', '/canvas');
 		canvas.id = options.getfieldvalue('canvasid', '.sim-canvas');
 		canvas.selector = $('#' + canvas.id);
 		canvas.textcanvas = null;
 		canvas.overlaycanvas = null;
+		canvas.permalinkUsed = false;
 		
 		typedArraySliceSupport();
 	}
 	
-	//Add context state variables
-	canvas.render = options.getfieldvalue('render', {});
-	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
-	canvas.overlayHandlers = {};
-	var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor', 'lightcyan'));
-	if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
-	else { throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for curent canvas ', canvas)); }
-	
-	//Property intiialization, using values from options first, then from default values.
-	var atmosphere = options.getfieldvalue('atmosphere', {});
-	canvas.atmosphere = { 												//Default Values
-		wavelength_r: defaultFor(atmosphere.wavelength_r, 			0.65), 		//0.65		Red wavelength (micrometers)
-		wavelength_g: defaultFor(atmosphere.wavelength_g, 			0.57),		//0.57		Green wavelength (micrometers)
-		wavelength_b: defaultFor(atmosphere.wavelength_b, 			0.475),		//0.475		Green wavelength (micrometers)
-		eSun: 	defaultFor(atmosphere.eSun, 						100.0),		//20.0		Sun intensity	
-		kRayleigh: defaultFor(atmosphere.kRayleigh, 				0.0025),	//0.0025	Rayleigh scattering amount
-		kMie: defaultFor(atmosphere.kMie, 							0.000), 	//0.01		Mie scattering amount
-		g: defaultFor(atmosphere.g, 								-0.99),		//-0.99		Mie phase asymmetry/direction factor
-		hdr_exposure: defaultFor(atmosphere.hdr_exposure, 			0.8),		//0.8		High Dynamic Range Exposure
-		scaleHeight: defaultFor(atmosphere.scaleHeight, 			1.25), 		//1.025		Scale of height of atmosphere to earth radius.
-		scaleDepth: defaultFor(atmosphere.scaleDepth, 				0.25), 		//0.25		Percentage altitude at which the atmosphere's average density is found
-		a: defaultFor(atmosphere.a, 								-0.00287),	//-0.00287	Scaling constant a
-		b: defaultFor(atmosphere.b, 								0.459),		//0.459		Scaling constant b
-		c: defaultFor(atmosphere.c, 								3.83),		//3.83		Scaling constant c
-		d: defaultFor(atmosphere.d, 								-6.80),		//-6.80		Scaling constant d
-		e: defaultFor(atmosphere.e, 								3.6),		//5.25		Scaling constant e. Lower when increasing atmosphere scale.
-		attenuation: defaultFor(atmosphere.attenuation, 			0.5)		//0.5		Strength of atmospheric scattering on ground shading.
-	};
-	updateAtmosphereParameters(canvas);
-	var animation = options.getfieldvalue('movies', {});
-	canvas.animation = {
-		frame: defaultFor(animation.frame, 							0),
-		play: defaultFor(animation.play, 							true),
-		increment: defaultFor(animation.increment, 					true),
-		fps: defaultFor(animation.fps, 								4),
-		interval: defaultFor(animation.interval, 					1000 / defaultFor(animation.fps, 4)),
-		loop: defaultFor(animation.loop, 							true),
-		handler: defaultFor(animation.handler, 						0)
-	}
-	var brush = options.getfieldvalue('brush', {});
-	canvas.brush = {
-		enabled: defaultFor(brush.enabled, 							false),
-		strength: defaultFor(brush.strength, 						0.075),
-		falloff: defaultFor(brush.falloff, 							0.5),
-		hit: defaultFor(brush.hit, 									{})
-	};
-	var camera = options.getfieldvalue('camera', {});
-	canvas.camera = {
-		position: defaultFor(camera.position, 						vec3.create()),
-		rotation: defaultFor(camera.rotation, 						quat.create()),
-		relativePosition: defaultFor(camera.relativePosition, 		vec3.create()),
-		direction: defaultFor(camera.direction, 					vec3.create()),
-		near: defaultFor(camera.near, 								1e3),
-		far: defaultFor(camera.far, 								1e10),
-		fov: defaultFor(camera.fov, 								45),
-		vMatrix: defaultFor(camera.vMatrix, 						mat4.create()),
-		pMatrix: defaultFor(camera.pMatrix, 						mat4.create()),
-		vpMatrix: defaultFor(camera.vpMatrix, 						mat4.create()),
-		vInverseMatrix: defaultFor(camera.vInverseMatrix, 			mat4.create()),
-		pInverseMatrix: defaultFor(camera.pInverseMatrix, 			mat4.create()),
-		vpInverseMatrix: defaultFor(camera.vpInverseMatrix, 		mat4.create()),
-		ready: defaultFor(camera.ready, 							false)
-	};
-	var dataMarkers = options.getfieldvalue('datamarkers', {});
-	canvas.dataMarkers = {
-		enabled: defaultFor(dataMarkers.enabled, 					true),
-		values: defaultFor(dataMarkers.values, 						[]),
-		image: defaultFor(dataMarkers.image, 						canvas.rootPath + 'textures/data_marker.svg'),
-		size: defaultFor(dataMarkers.size, 							[32, 32]),
-		format: defaultFor(dataMarkers.format, 						['X: %.2e<br>Y: %.2e<br>Z: %.2e<br>Value: %0.1f', 'x', 'y', 'z', 'value']),
-		animated: defaultFor(dataMarkers.animated, 					false),
-		labels: defaultFor(dataMarkers.labels, 						[]),
-		font: defaultFor(dataMarkers.font, 							''),
-		marker: defaultFor(dataMarkers.marker, 						document.getElementById('sim-data-marker-' + canvas.id)),
-		reposition: defaultFor(dataMarkers.reposition, 				true)
-	};
-	var draw = options.getfieldvalue('draw', {});
-	canvas.draw = {
-		ready: defaultFor(draw.ready, 								false),
-		handler: defaultFor(draw.handler, 							null)
-	};
-	var view = options.getfieldvalue('view', {});
-	canvas.view = {
-		position: defaultFor(view.position, 						[0.0, 0.0, 0.0]),
-		rotation: defaultFor(view.rotation, 						[0, 90]),
-		zoom: defaultFor(view.zoom, 								1.0),
-		zoomLimits: defaultFor(view.zoomLimits, 					[0.001, 100.0]),
-		lastZoom: defaultFor(view.lastZoom, 						1.0),
-		azimuthLimits: defaultFor(view.azimuthLimits, 				[0, 360]),
-		elevationLimits: defaultFor(view.elevationLimits, 			[-180, 180]),
-		panningEnabled: defaultFor(view.panningEnabled, 			false),
-		preventDefaultOnPan: defaultFor(view.preventDefaultOnPan, 	false),
-		twod: defaultFor(view.twod, 								false)
-	};
-
-	//Override with parameters from URL, if any
-	//TODO: Make permalinks more robust and less interdependent on UI
-	if (!canvas.usedparemters) {
-		var parameters = {};
-		location.search.substr(1).split('&').forEach(function(part) {
-			var item = part.split('=');
-			parameters[item[0]] = decodeURIComponent(item[1]);
-		});
-
-		if (parameters['pos']) { canvas.view.position = JSON.parse(parameters['pos']); }
-		if (parameters['rot']) { canvas.view.rotation = JSON.parse(parameters['rot']); }
-		if (parameters['zoom']) { canvas.view.zoom = JSON.parse(parameters['zoom']); }
-		if (parameters['twod']) { canvas.view.twod = JSON.parse(parameters['twod']); }
-		if (parameters['initial']) {
-			initial = JSON.parse(parameters['initial']);
-			if (!initial) {
-				if (typeof SolveGlacier === 'function') {
-					SolveGlacier();
-				}
-				if (typeof SolveSlr === 'function') {
-					SolveSlr();
-				}
-			}
+	if (options.getfieldvalue('clf','on')=='on') {
+		//Add context state variables
+		canvas.render = options.getfieldvalue('render', {});
+		canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
+		if (options.getfieldvalue('clf','on')=='on') canvas.overlayHandlers = {};
+		var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor', 'lightcyan'));
+		if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
+		else { throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for current canvas ', canvas)); }
+		
+		//Property intiialization, using values from options first, then from default values.
+		var atmosphere = options.getfieldvalue('atmosphere', {});
+		canvas.atmosphere = { 														//Default Values
+			wavelength_r: defaultFor(atmosphere.wavelength_r, 			0.65), 		//0.65		Red wavelength (micrometers)
+			wavelength_g: defaultFor(atmosphere.wavelength_g, 			0.57),		//0.57		Green wavelength (micrometers)
+			wavelength_b: defaultFor(atmosphere.wavelength_b, 			0.475),		//0.475		Green wavelength (micrometers)
+			eSun: 	defaultFor(atmosphere.eSun, 						100.0),		//20.0		Sun intensity	
+			kRayleigh: defaultFor(atmosphere.kRayleigh, 				0.0025),	//0.0025	Rayleigh scattering amount
+			kMie: defaultFor(atmosphere.kMie, 							0.000), 	//0.01		Mie scattering amount
+			g: defaultFor(atmosphere.g, 								-0.99),		//-0.99		Mie phase asymmetry/direction factor
+			hdr_exposure: defaultFor(atmosphere.hdr_exposure, 			0.8),		//0.8		High Dynamic Range Exposure
+			scaleHeight: defaultFor(atmosphere.scaleHeight, 			1.25), 		//1.025		Scale of height of atmosphere to earth radius.
+			scaleDepth: defaultFor(atmosphere.scaleDepth, 				0.25), 		//0.25		Percentage altitude at which the atmosphere's average density is found
+			a: defaultFor(atmosphere.a, 								-0.00287),	//-0.00287	Scaling constant a
+			b: defaultFor(atmosphere.b, 								0.459),		//0.459		Scaling constant b
+			c: defaultFor(atmosphere.c, 								3.83),		//3.83		Scaling constant c
+			d: defaultFor(atmosphere.d, 								-6.80),		//-6.80		Scaling constant d
+			e: defaultFor(atmosphere.e, 								3.6),		//5.25		Scaling constant e. Lower when increasing atmosphere scale.
+			attenuation: defaultFor(atmosphere.attenuation, 			0.5)		//0.5		Strength of atmospheric scattering on ground shading.
+		};
+		updateAtmosphereParameters(canvas);
+			
+		var animation = options.getfieldvalue('movies', {});
+		canvas.animation = {
+			frame: defaultFor(animation.frame, 							0),
+			play: defaultFor(animation.play, 							true),
+			increment: defaultFor(animation.increment, 					true),
+			fps: defaultFor(animation.fps, 								4),
+			interval: defaultFor(animation.interval, 					1000 / defaultFor(animation.fps, 4)),
+			loop: defaultFor(animation.loop, 							true),
+			handler: defaultFor(animation.handler, 						0)
 		}
-		canvas.usedparemters = true;
-	}
-} //}}}
-function generatePermalink() { //{{{
-	var permalink = window.location.origin + window.location.pathname + '?'
-	+ '&pos=' + encodeURIComponent(JSON.stringify(canvas.view.position))
-	+ '&rot=' + encodeURIComponent(JSON.stringify(canvas.view.rotation)) 
-	+ '&zoom=' + encodeURIComponent(JSON.stringify(canvas.view.zoom))
-	+ '&twod=' + encodeURIComponent(JSON.stringify(canvas.view.twod));
-	window.prompt('Share this simulation: ', permalink);
-} //}}}
-function loadShaders(gl, rootPath) { //{{{
+		var brush = options.getfieldvalue('brush', {});
+		canvas.brush = {
+			enabled: defaultFor(brush.enabled, 							false),
+			strength: defaultFor(brush.strength, 						0.075),
+			falloff: defaultFor(brush.falloff, 							0.5),
+			hit: defaultFor(brush.hit, 									{})
+		};
+		var camera = options.getfieldvalue('camera', {});
+		canvas.camera = {
+			position: defaultFor(camera.position, 						vec3.create()),
+			rotation: defaultFor(camera.rotation, 						quat.create()),
+			relativePosition: defaultFor(camera.relativePosition, 		vec3.create()),
+			direction: defaultFor(camera.direction, 					vec3.create()),
+			near: defaultFor(camera.near, 								1e3),
+			far: defaultFor(camera.far, 								1e10),
+			fov: defaultFor(camera.fov, 								45),
+			vMatrix: defaultFor(camera.vMatrix, 						mat4.create()),
+			pMatrix: defaultFor(camera.pMatrix, 						mat4.create()),
+			vpMatrix: defaultFor(camera.vpMatrix, 						mat4.create()),
+			vInverseMatrix: defaultFor(camera.vInverseMatrix, 			mat4.create()),
+			pInverseMatrix: defaultFor(camera.pInverseMatrix, 			mat4.create()),
+			vpInverseMatrix: defaultFor(camera.vpInverseMatrix, 		mat4.create()),
+			ready: defaultFor(camera.ready, 							false)
+		};
+		var dataMarkers = options.getfieldvalue('datamarkers', {});
+		canvas.dataMarkers = {
+			enabled: defaultFor(dataMarkers.enabled, 					true),
+			values: defaultFor(dataMarkers.values, 						[]),
+			image: defaultFor(dataMarkers.image, 						canvas.assetsPath + '/data-markers/data_marker.svg'),
+			size: defaultFor(dataMarkers.size, 							[32, 32]),
+			format: defaultFor(dataMarkers.format, 						['X: %.2em<br>Y: %.2em<br>Z: %.2em<br>Value: %0.1f']),
+			animated: defaultFor(dataMarkers.animated, 					false),
+			labels: defaultFor(dataMarkers.labels, 						['x', 'y', 'z', 'value']),
+			font: defaultFor(dataMarkers.font, 							''),
+			marker: defaultFor(dataMarkers.marker, 						document.getElementById('sim-data-marker-' + canvas.id)),
+			reposition: defaultFor(dataMarkers.reposition, 				true)
+		};
+		var draw = options.getfieldvalue('draw', {});
+		canvas.draw = {
+			ready: defaultFor(draw.ready, 								false),
+			handler: defaultFor(draw.handler, 							null)
+		};
+		var view = options.getfieldvalue('view', {});
+		canvas.view = {
+			position: defaultFor(view.position, 						[0.0, 0.0, 0.0]),
+			rotation: defaultFor(view.rotation, 						[0, 90]),
+			zoom: defaultFor(view.zoom, 								1.0),
+			zoomLimits: defaultFor(view.zoomLimits, 					[0.001, 100.0]),
+			lastZoom: defaultFor(view.lastZoom, 						1.0),
+			lightingBias: defaultFor(view.lightingBias, 				0.75),
+			azimuthLimits: defaultFor(view.azimuthLimits, 				[0, 360]),
+			elevationLimits: defaultFor(view.elevationLimits, 			[-180, 180]),
+			panningEnabled: defaultFor(view.panningEnabled, 			false),
+			preventDefaultOnPan: defaultFor(view.preventDefaultOnPan, 	false),
+			twod: defaultFor(view.twod, 								false)
+		};
+
+		// Override with parameters from URL, if any
+		VESL.UI.parsePermalinkCanvas(canvas);
+	}
+} //}}}
+function loadShaders(gl, assetsPath) { //{{{
 	var shaders = {};
-	shaders.Colored = new GL.Shader.fromURL(rootPath+'shaders/Colored.vsh', rootPath+'shaders/Colored.fsh', null, gl);
-	shaders.ColoredDiffuse = new GL.Shader.fromURL(rootPath+'shaders/ColoredDiffuse.vsh', rootPath+'shaders/ColoredDiffuse.fsh', null, gl);
-	shaders.Textured = new GL.Shader.fromURL(rootPath+'shaders/Textured.vsh', rootPath+'shaders/Textured.fsh', null, gl);
-	shaders.TexturedDiffuse = new GL.Shader.fromURL(rootPath+'shaders/TexturedDiffuse.vsh', rootPath+'shaders/TexturedDiffuse.fsh', null, gl);
-	shaders.SkyFromSpace = new GL.Shader.fromURL(rootPath+'shaders/SkyFromSpace.vert', rootPath+'shaders/SkyFromSpace.frag', null, gl);
-	shaders.GroundFromSpace = new GL.Shader.fromURL(rootPath+'shaders/GroundFromSpace.vert', rootPath+'shaders/GroundFromSpace.frag', null, gl);
+	// NOTE: Consider changing fromURL() to XMLHttpRequest with responseType = 'text' to avoid XML Parsing Error (shader files are not encoded as XML).
+	shaders.Colored = new GL.Shader.fromURL(assetsPath + '/shaders/Colored.vsh', assetsPath + '/shaders/Colored.fsh', null, gl);
+	shaders.ColoredDiffuse = new GL.Shader.fromURL(assetsPath + '/shaders/ColoredDiffuse.vsh', assetsPath + '/shaders/ColoredDiffuse.fsh', null, gl);
+	shaders.Textured = new GL.Shader.fromURL(assetsPath + '/shaders/Textured.vsh', assetsPath + '/shaders/Textured.fsh', null, gl);
+	shaders.TexturedDiffuse = new GL.Shader.fromURL(assetsPath + '/shaders/TexturedDiffuse.vsh', assetsPath + '/shaders/TexturedDiffuse.fsh', null, gl);
+	shaders.SkyFromSpace = new GL.Shader.fromURL(assetsPath + '/shaders/SkyFromSpace.vert', assetsPath + '/shaders/SkyFromSpace.frag', null, gl);
+	shaders.GroundFromSpace = new GL.Shader.fromURL(assetsPath + '/shaders/GroundFromSpace.vert', assetsPath + '/shaders/GroundFromSpace.frag', null, gl);
 	return shaders;
 } //}}}
 function initTexture(gl, imageSource) { //{{{
 	//Initialize textures, or load from memory if they already exist.
-	if (isEmptyOrUndefined(gl.textures[imageSource])) {
+	if (VESL.Helpers.isEmptyOrUndefined(gl.textures[imageSource])) {
 		gl.textures[imageSource] = GL.Texture.fromURL(imageSource, {minFilter: gl.LINEAR_MIPMAP_LINEAR, magFilter: gl.LINEAR}, null, gl);
 	}
@@ -230,13 +204,4 @@
 	return typeof name !== 'undefined' ? name : value;
 } //}}}
-function isEmptyOrUndefined(object) { //{{{
-	return object === undefined || isEmpty(object);
-} //}}}
-function isEmpty(object) { //{{{
-	for (var key in object) {
-		return false;
-	}
-	return true;
-} //}}}
 function recover(canvasid, name, value) { //{{{
 	//Traverse canvas object tree for property defined by dot delimited string, returning it, or a default value if it is not found.
@@ -245,50 +210,7 @@
 	for (var i = 0; i < properties.length; i++) {
 		object = object[properties[i]];
-		if (isEmptyOrUndefined(object)) { break; }
+		if (VESL.Helpers.isEmptyOrUndefined(object)) { break; }
     }
 	return defaultFor(object, value);
-} //}}}
-function typedArraySliceSupport() { //{{{
-	//TypedArray compatibility for Safari/IE
-	if (typeof Int8Array !== 'undefined') {
-		if (!Int8Array.prototype.fill) { Int8Array.prototype.fill = Array.prototype.fill; }
-		if (!Int8Array.prototype.slice) { Int8Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Uint8Array !== 'undefined') {
-		if (!Uint8Array.prototype.fill) { Uint8Array.prototype.fill = Array.prototype.fill; }
-		if (!Uint8Array.prototype.slice) { Uint8Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Uint8ClampedArray !== 'undefined') {
-		if (!Uint8ClampedArray.prototype.fill) { Uint8ClampedArray.prototype.fill = Array.prototype.fill; }
-		if (!Uint8ClampedArray.prototype.slice) { Uint8ClampedArray.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Int16Array !== 'undefined') {
-		if (!Int16Array.prototype.fill) { Int16Array.prototype.fill = Array.prototype.fill; }
-		if (!Int16Array.prototype.slice) { Int16Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Uint16Array !== 'undefined') {
-		if (!Uint16Array.prototype.fill) { Uint16Array.prototype.fill = Array.prototype.fill; }
-		if (!Uint16Array.prototype.slice) { Uint16Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Int32Array !== 'undefined') {
-		if (!Int32Array.prototype.fill) { Int32Array.prototype.fill = Array.prototype.fill; }
-		if (!Int32Array.prototype.slice) { Int32Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Uint32Array !== 'undefined') {
-		if (!Uint32Array.prototype.fill) { Uint32Array.prototype.fill = Array.prototype.fill; }
-		if (!Uint32Array.prototype.slice) { Uint32Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Float32Array !== 'undefined') {
-		if (!Float32Array.prototype.fill) { Float32Array.prototype.fill = Array.prototype.fill; }
-		if (!Float32Array.prototype.slice) { Float32Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof Float64Array !== 'undefined') {
-		if (!Float64Array.prototype.fill) { Float64Array.prototype.fill = Array.prototype.fill; }
-		if (!Float64Array.prototype.slice) { Float64Array.prototype.slice = Array.prototype.slice; }
-	}
-	if (typeof TypedArray !== 'undefined') {
-		if (!TypedArray.prototype.fill) { TypedArray.prototype.fill = Array.prototype.fill; }
-		if (!TypedArray.prototype.slice) { TypedArray.prototype.slice = Array.prototype.slice; }
-	}
 } //}}}
 //}}}
@@ -356,5 +278,8 @@
 	ev.preventDefault();
 	if (ev.type === 'pinchstart') { canvas.view.lastZoom = canvas.view.zoom; }
-	else { modifyZoom(ev.scale * canvas.view.lastZoom, canvas, displaylog, ev, 0); }
+	else { 
+		canvas.view.zoom = ev.scale * canvas.view.lastZoom;
+		if (displaylog) { console.log(canvas.view.zoom); }
+	}
 } //}}}
 function onZoom(ev, canvas, displaylog) { //{{{
@@ -364,5 +289,5 @@
 } //}}}
 function modifyZoom(value, canvas, displaylog, ev, duration) { //{{{
-	if (isEmptyOrUndefined(duration)) duration = 200;
+	if (VESL.Helpers.isEmptyOrUndefined(duration)) duration = 200;
 	var targetZoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
 	var currentZoom = canvas.view.zoom;
@@ -371,4 +296,5 @@
 		1.0,
 		duration,
+		'swing',
 		function(value, info) {
 			canvas.view.zoom = currentZoom * (1 - value) + targetZoom * value;
@@ -382,12 +308,4 @@
 function toggleMoviePlay(canvas) { //{{{
 	canvas.animation.play = !canvas.animation.play;
-	if (canvas.animation.play){
-		canvas.playButton.find('span').removeClass('fa-play');
-		canvas.playButton.find('span').addClass('fa-pause');
-	}
-	else{
-		canvas.playButton.find('span').removeClass('fa-pause');
-		canvas.playButton.find('span').addClass('fa-play');
-	}
 } //}}}
 function screenToWorldPoint(canvas, x, y) { //{{{
@@ -403,5 +321,5 @@
 	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], inverseMVPMatrix);
 	var far = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 1.0], inverseMVPMatrix);
-	var direction = vec3.subtract(vec3.create(), far, origin);
+	var direction = vec3.normalize(vec3.create(), vec3.subtract(vec3.create(), far, origin));
 	return {'origin':origin, 'direction':direction};
 } //}}}
@@ -420,5 +338,5 @@
 	return hit;
 } //}}}
-function raycastXY(canvas, x, y, node) { //{{{
+function raycastXY(canvas, x, y, node, faceWinding) { //{{{
 	//Performs raycast on given node using x and y screenspace coordinates.
 	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
@@ -427,9 +345,9 @@
 	return raycast(canvas, ray.origin, ray.direction, node);
 } //}}}
-function animateValue(current, target, duration, stepCallback, doneCallback) { //{{{
-	//Animates scalar value for length duration, calling callback each step.
-	//TODO: Specify lerp easing as option (cubic, linear, cosine).
+function animateValue(current, target, duration, easing, stepCallback, doneCallback) { //{{{
+	//Animates scalar value for length duration, calling callback each step. Specify smooth easing as a string ('swing', 'linear').
 	$({'value':current}).animate({'value':target}, {
 		duration: duration,
+		easing: easing,
 		step: stepCallback,
 		done: doneCallback
@@ -581,5 +499,5 @@
 	canvas.gl.viewport(0, 0, canvas.width, canvas.height);
 	
-	if (!isEmptyOrUndefined(canvas.overlaycanvas)) {
+	if (!VESL.Helpers.isEmptyOrUndefined(canvas.overlaycanvas)) {
 		rect = canvas.overlaycanvas.getBoundingClientRect();
 		canvas.overlaycanvas.width  = rect.width;
Index: /issm/trunk-jpl/src/m/plot/webgl_node.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 22433)
+++ /issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 22434)
@@ -33,7 +33,10 @@
 	this.lightingBias = options.getfieldvalue('lightingBias', 					0.0);									//Controls width of gl lines. No reliable support across windows platforms.
 	this.log = options.getfieldvalue('log', 									false);									//Controls logarithmic color axis scaling for texturing.
+	this.maskAll = options.getfieldvalue('maskAll', 							[]);									//Masking array used for marking all region elements.
+	this.maskBoundary = options.getfieldvalue('maskBoundary', 					[]);									//Masking array used for marking region boundary edges.
 	this.maskColor = options.getfieldvalue('maskColor', 						vec4.fromValues(0.0, 0.0, 1.0, 1.0));	//ISSM shader uniform controlling ocean masking color.
 	this.maskEnabled = options.getfieldvalue('maskEnabled', 					false);									//ISSM shader uniform toggling ocean masking.
 	this.maskHeight = options.getfieldvalue('maskHeight', 						150.0);									//ISSM shader uniform controlling height at which ocean masking is cut off.
+	this.maskObject = options.getfieldvalue('maskObject', 						{'enabled':false});						//Masking array options object.
 	this.maskZerosColor = options.getfieldvalue('maskZerosColor', 				[1.0, 1.0, 1.0, 1.0]);					//ISSM shader uniform controlling value masking color.
 	this.maskZerosEnabled = options.getfieldvalue('maskZerosEnabled', 			false);									//ISSM shader uniform toggling value masking.
@@ -168,7 +171,7 @@
 	var scale = options.getfieldvalue('scale', undefined);
 	
-	if (!isEmptyOrUndefined(translation)) this.translation = translation;
-	if (!isEmptyOrUndefined(rotation)) this.rotation = rotation;
-	if (!isEmptyOrUndefined(scale)) this.scale = scale;
+	if (!VESL.Helpers.isEmptyOrUndefined(translation)) this.translation = translation;
+	if (!VESL.Helpers.isEmptyOrUndefined(rotation)) this.rotation = rotation;
+	if (!VESL.Helpers.isEmptyOrUndefined(scale)) this.scale = scale;
 	this.updateModelMatrix();
 } //}}}
@@ -194,4 +197,5 @@
 	this.diffuseColor = edgeColor;
 	this.updateDiffuseColor();
+	this.computeMasks();
 	
 	this.patchVertices(faceVertexCData, faces, vertices);
@@ -208,5 +212,5 @@
 	var face;
 	
-	if (isEmptyOrUndefined(faceVertexCData)) {
+	if (VESL.Helpers.isEmptyOrUndefined(faceVertexCData)) {
 		vertexArray = new Float32Array(vertices[0].length * 3);
 		for(var i = 0, v = 0; i < vertices[0].length; i++) {	
@@ -285,5 +289,5 @@
 	var face;
 	
-	if (isEmptyOrUndefined(faceVertexCData)) { return; }
+	if (VESL.Helpers.isEmptyOrUndefined(faceVertexCData)) { return; }
 	
 	//Use logarithmic scaling if it is valid
@@ -308,5 +312,5 @@
 					}
 					else {
-						coordArray[t++] = 0.5;
+						coordArray[t++] = this.queryCoordMask(i); //Account for special texturing
 						coordArray[t++] = clamp((faceVertexCData[i] - caxis[0]) / crange, 0.0, 1.0);
 					}
@@ -325,5 +329,5 @@
 				}
 				else {
-					coordArray[t++] = 0.5;
+					coordArray[t++] = this.queryCoordMask(i); //Account for special texturing
 					coordArray[t++] = clamp((faceVertexCData[i] - caxis[0]) / crange, 0.0, 1.0);
 				}
@@ -364,6 +368,6 @@
 	}
 	
-	if (this.computeIndices === true && !isEmptyOrUndefined(faces)) {
-		if (!isEmptyOrUndefined(faces[0])) { //Check for 2D format and process if needed
+	if (this.computeIndices === true && !VESL.Helpers.isEmptyOrUndefined(faces)) {
+		if (!VESL.Helpers.isEmptyOrUndefined(faces[0])) { //Check for 2D format and process if needed
 			if (faceColor !== 'none') { //Check for triangle rendering
 				indexArray = new Uint16Array(faces.length * 3);
@@ -399,9 +403,7 @@
 	var args = Array.prototype.slice.call(arguments);
 	var options = new pairoptions(args.slice(0,args.length));
-
+	
 	var coords = options.getfieldvalue('Coords', undefined);
-	var cacheIndex = options.getfieldvalue('CacheIndex', false);
-	
-	if (!isEmptyOrUndefined(coords)) {
+	if (!VESL.Helpers.isEmptyOrUndefined(coords)) {
 		this.patchCoords(coords, this.faces, this.vertices);
 		var buffer = this.mesh.getBuffer("coords");
@@ -413,5 +415,5 @@
 	//Computes and caches octrees for a node.
 	var octree = this.canvas.octrees[this.name];
-	if (isEmptyOrUndefined(octree)) {
+	if (VESL.Helpers.isEmptyOrUndefined(octree)) {
 		octree = new GL.Octree(this.mesh);
 	}
@@ -439,5 +441,5 @@
 			var coordinateObject = vertices[i];
 			var j = 0;
-			if (isEmptyOrUndefined(indices)) {
+			if (VESL.Helpers.isEmptyOrUndefined(indices)) {
 				for (var key in coordinateObject) {
 					console.log(key);
@@ -483,29 +485,26 @@
 Node.prototype.scaleVertices = function(md, x, y, z, elements, scale, maskObject) { //{{{
 	//Scales and returns vertices x, y, and z by factor scale. Uses md.geometry.scale for heightscaling in 3d meshes.
+	var region = maskObject.region;
+	var mask = maskObject.enabled ? maskObject.heightmask[region] : [];
+	var maskScale = maskObject.enabled ? maskObject.scale : 1;
+	
+	//The maskScaleField is the height scaling array. Use one if provided by maskObject.field, otherwise use md.geometry.surface
+	var maskScaleField = !VESL.Helpers.isEmptyOrUndefined(maskObject.scaleField) ? maskObject.scaleField : md.geometry.surface;
+	//If md.geometry.surface was empty and was used as maskScaleField, assign an empty array to both.
+	if (!maskScaleField) {
+		md.geometry.surface = NewArrayFill(md.mesh.x.length,0);
+		maskScaleField = md.geometry.surface;
+	}
+	
+	//Scale in 3D if using globe model, or in 2D otherwise.
 	if (md.mesh.classname() === 'mesh3dsurface') {
-		if (!maskObject.enabled) {
-			var xyz, magnitude;
-			x = x.slice();
-			y = y.slice();
-			z = z.slice();
-			for(var i = 0; i < x.length; i++) {
-				xyz = vec3.fromValues(x[i], y[i], z[i]);
-				magnitude = 1 + md.geometry.surface[i] * scale / vec3.length(xyz);
-				vec3.scale(xyz, xyz, magnitude);
-				x[i] = xyz[0];
-				y[i] = xyz[1];
-				z[i] = xyz[2];
-			}
-		}
-		else {
-			var mask = maskObject.mask;
-			var maskScale = maskObject.scale;
-			var element;
-			for (var i = 0; i < mask.length; i++) {
-				if (mask[i] === 1) {
+		var element;
+		if (x.length === md.mesh.numberofelements) { //element plot
+			for (var i = 0; i < x.length; i++) {
+				if (maskObject.enabled) { //Scale the element if mask is not enabled, or if it is, only if it is also in the mask
 					element = elements[i];
 					for (var j = 0; j < 3; j++) {
 						xyz = vec3.fromValues(x[element[j] - 1], y[element[j] - 1], z[element[j] - 1]);
-						magnitude = 1 + md.geometry.surface[element[j] - 1] * scale * maskScale / vec3.length(xyz);
+						magnitude = 1 + maskScaleField[element[j] - 1] * mask[i] * scale * maskScale / vec3.length(xyz);
 						vec3.scale(xyz, xyz, magnitude);
 						x[element[j] - 1] = xyz[0];
@@ -517,13 +516,125 @@
 			}
 		}
+		else if (x.length === md.mesh.numberofvertices) { //node plot
+			for (var i = 0; i < x.length; i++) {
+				if (!maskObject.enabled || mask[i] === 1) { //Scale the node if mask is not enabled, or if it is, only if it is also in the mask
+					xyz = vec3.fromValues(x[i], y[i], z[i]);
+					magnitude = 1 + maskScaleField[i] * scale * maskScale / vec3.length(xyz);
+					vec3.scale(xyz, xyz, magnitude);
+					x[i] = xyz[0];
+					y[i] = xyz[1];
+					z[i] = xyz[2];
+				}
+			}
+		}
+	} else {
+		z = z.slice();
+		var zMin = ArrayMin(maskScaleField);
+		for(var i = 0; i < z.length; i++) {
+			if (!maskObject.enabled || mask[i] === 1) { //Scale the element if mask is not enabled, or if it is, only if it is also in the mask
+				z[i] = (z[i] - zMin) * scale + zMin;
+			}
+		}
+	}
+	return [x, y, z];
+} //}}}
+Node.prototype.computeMasks = function() { //{{{
+	//NOTE: Since sets are not yet widely supported, use array with binary values to represent if object is in set.
+	//NOTE: Only support element wise masks for now.
+	var maskObject = this.maskObject;
+	if (!maskObject.enabled) { return; }
+	var region = maskObject.region;
+	var mask = maskObject.mask[region];
+	var maskRegion = []; //new Set()
+	var maskAdjacency = []; //new Set()
+	var adjacentElements;
+	var adjacentElements2;
+	var adjacentNodes;
+	var adjacentNodes2;
+	var adjacencyLength; //length of connectivity array - 3 for elements, many for nodes
+	var index;
+	//For each element in the mask elements array,
+	if (mask.length === md.mesh.numberofelements) { //element plot
+		if (md.mesh.elementconnectivity) {
+			var adjacencyLength = 3;
+			for (var i = 0; i < mask.length; i++) {
+				//Determine if element is in the mask
+				if (mask[i] === 1) {
+					//Then add each element to the adjacency set and mask region set
+					adjacentElements = md.mesh.elementconnectivity[i];
+					for (var j = 0; j < adjacencyLength; j++) {
+						index = adjacentElements[j] - 1;
+						maskAdjacency[index] = true;
+						
+						//Get second level of adjacent elemnents to form solid boundary (first layer look like this: /\/\/\/\/\; second layer looks like this \/\/\/\/\/; combined = |       |
+						adjacentElements2 = md.mesh.elementconnectivity[index];
+						for (var k = 0; k < adjacencyLength; k++) {
+							index = adjacentElements2[k] - 1;
+							maskAdjacency[index] = true;
+						}
+					}
+					maskRegion[i] = true;
+				}
+			}
+		}
+	}
+	else if (mask.length === md.mesh.numberofvertices) { //node plot
+		if (md.mesh.nodeconnectivity) {
+			var adjacencyLength = md.mesh.nodeconnectivity[0].length;
+			for (var i = 0; i < mask.length; i++) {
+				//Determine if node is in the mask
+				if (mask[i] === 1) {
+					//Then add each node to the adjacency set and mask region set
+					adjacentNodes = md.mesh.nodeconnectivity[i];
+					for (var j = 0; j < adjacencyLength; j++) {
+						index = adjacentNode[j];
+						maskAdjacency[index] = true;
+						
+						//Get second level of adjacent elemnents to form solid boundary (first layer look like this: /\/\/\/\/\; second layer looks like this \/\/\/\/\/; combined = |       |
+						adjacentNodes2 = md.mesh.nodeconnectivity[index];
+						for (var k = 0; k < adjacencyLength; k++) {
+							index = adjacentNodes2[k];
+							maskAdjacency[index] = true;
+						}
+					}
+					maskRegion[i] = true;
+				}
+			}
+		}
+	}
+	
+	//Then subtract the mask elements array from the mask adjacency array.
+	var maskBoundary = [];
+	for (var i in maskAdjacency) {
+		if (!(i in maskRegion)) {
+			maskBoundary[i] = true;
+		}
+	}
+	this.maskBoundary = maskBoundary;
+	
+	//For each element not in any mask region (default to mask[0] in slr-gfm), change color to mask color.
+	var maskAll = maskObject.mask[0];
+	this.maskAll = maskAll;
+} //}}}
+Node.prototype.queryCoordMask = function(index) { //{{{
+	//Calculate UV coordinate offsets for special color texture lookup.
+	if (!this.maskObject.enabled) {
+		return 1.0 
 	}
 	else {
-		z = z.slice();
-		var zMin = ArrayMin(md.geometry.surface);
-		for(var i = 0; i < z.length; i++) {
-			z[i] = (z[i] - zMin) * scale + zMin;
-		}
-	}
-	return [x, y, z];
+		//var sections = 3; //Object.keys(maskObject.colors).length + 1
+		//var boundaryCoord = 0.5 / sections;
+		//var maskCoord = 1.5 / sections;
+		//var textureCoord = 2.5 / sections;
+		if (index in this.maskBoundary) {
+			return 0.166666667; //boundaryCoord - edge color
+		}
+		else if (this.maskAll[index] === 0) {
+			return 0.5; //maskCoord - masked color
+		}
+		else {
+			return 0.833333333; //textureCoord - default colormap
+		}
+	}
 } //}}}
 Node.prototype.mergeVertices = function(x1, y1, z1, elements1, x2, y2, z2, elements2) { //{{{
