<commit_before><html>
	<head>
		<title>My first Three.js app</title>
		<style>
			body { margin: 0; }
			
			
			#selectTransition {
				margin-top: 15
				px;
				margin-left :100px;
			}

			label {
			    display: inline-block;
			    cursor: pointer;
			    position: relative;
			    padding-left: 5px;
			    margin-right: 18px;
			    font-size: 18px;
			}			

			canvas { 
				background-color: #000;
				border: 2px solid red;
				width: 1000px; height: 1000px; 
				margin: 5px; 
				position: fixed; /*uses margin to offset, ignores top/left */
				/* added one line */
			}
			div.stats { float: right; }
		</style>
	</head>
	<body>
		<form id="selectTransition">
			<input type="radio" name="transition" id="ripples" value="ripples" checked>
			 	<label for="ripples">Ripples</label>
			<input type="radio" name="transition" id = "pinwheel" value="pinwheel" >
			 	<label for="pinwheel">Pinwheel</label>
			<input type="radio" name="transition" id = "flips" value="flips" >
			 	<label for="flips">Flips</label>
			<input type="radio" name="transition" id = "shutters" value="shutters" >		
			 	<label for="shutters">Shutters</label>			
			<input type="radio" name="transition" id = "sphere" value="sphere" >		
			 	<label for="sphere">Sphere</label>	
			<button type="button" id="showImage">Show Image</button>
		</form>
		
<!-- 	<button type="button" id="b1">One</button>
		<button type="button" id="b2">Two</button>
		<button type="button" id="b3">Three</button>
		<button type="button" id="b4">Pinwheel</button>
		<button type="button" id="b5">Texture</button>
		<button type="button" id="b6">Show Image</button> -->

		<div id="stats" class="stats"></div>
		<canvas id="rendercanvas"></canvas>
		<script src="/static/three.js"></script>
		<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/TweenMax.min.js"></script>
		<script src="/static/OrbitControls.js"></script>
		<script src="/static/stats.min.js"></script>
		<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
		<script>

		THREE.Cache.enabled = true;

		//set up webgl stuff
		var scene = new THREE.Scene();
		
		var camera = new THREE.PerspectiveCamera( 750, 1000/1000, 0.1, 1000 );
		camera.position.z = 35;
		camera.position.y = 0; //-13;
		camera.position.x = 0;

		//camera.rotation.x = 0; //.4; 

		var renderer = new THREE.WebGLRenderer({canvas:rendercanvas, });
		renderer.setSize( 1000, 1000 ); //fixme
		renderer.sortObjects = true;

		var orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

		//var statsdiv = document.getElementById("stats");
		//stats = new Stats();
		//statsdiv.appendChild( stats.dom );

		var mouse = new THREE.Vector2();
		var raycaster = new THREE.Raycaster();

		//globals for image layout, etc
		var rows = 32;
		var cols = 32;
		var tileEdge = 256; //each tile is 256x256
		var tilesPerSlabEdge = 4096/tileEdge; //number of tiles on slab edge (square slabs)
		var tilesPerSlab = tilesPerSlabEdge*tilesPerSlabEdge;

		var white = new THREE.Color(0xffffff);
		var black = new THREE.Color(0x000000);
		var hilightColor = new THREE.Color(1,0,0);

		//globals for card locations and stuff
		var map = []; //2D array maps [row][col] to card object (parent of two sides)
		var inv_map = new Map(); //hash maps parent card object to original row and col
		var slabTextures = new Map(); //index the textures by name
		var slabMaterials = new Map(); //index the materials for each slab by name
		//var generation = 0; //track updates to main image. increaed +=1 for each new image


		//build the geometry of a card
		//var geometry = new THREE.PlaneGeometry( 0.5, 0.5, 1, 1);
		//var geometryBack = new THREE.PlaneGeometry( 0.5, 0.5, 1, 1); //to serve as backside, inverted texture
		//geometryBack.applyMatrix( new THREE.Matrix4().makeRotationY( Math.PI ) );

		//this loader will keep track of asynch loading textures
		var textureLoadingManager = new THREE.LoadingManager();

		textureLoadingManager.onLoad = function ( ) {
			startWorld();  //stuff that has to wait for textures to be loaded
		};

		//load and parse the slabMap - FIXME - make asych
		var slabMap = null;
		$.getJSON("/slabmap", function(data) {
			slabMap = data;
			console.log("slabMap = ", slabMap);
		});

		textureURLs = [];
		
		for (var i=0; i<6; i++) {	
			textureURLs.push("/static/tiles/slab-" + i.toString() + ".jpg");		
		}


		loadImageTexturesAndMaterials(textureLoadingManager, textureURLs, startWorld);

		var startWorld = function() {

			createLotsOfCards();

			positionLotsofCards(map, rows, cols);

			//stats - image IMG_0510 - initial screen has 992 geometries, 1984 faces, 5952 vertices

			loadImageTileMap("IMG_0510", function(data){
				imageJSONmap = data;
				loadImageTilesToSide(imageJSONmap);  //fixme - use the global?
				TweenLite.ticker.addEventListener("tick", render);
			});

			//TweenLite.ticker.addEventListener("tick", render);

		}

		function loadImageTileMap(imageName, callback) {
		//load JSON map of all the tiles that make up this image
			var imageURL = "/imagemap/" + imageName;
			$.ajax({
				method: "GET",
				url: imageURL,
				dataType: "jsonp",
				jsonp: false,
				jsonpCallback: "callback"  //funky leftover from JSONP era - FIXME
			}).done(function(data) {
				console.log("loaded ImageTileMap ", data);
				callback(data);
			});			
		}

		function loadImageTexturesAndMaterials(loadingManager, listOfURLs) {
			//for each ULR, load the image into a texture, using the loading manager

			textureLoader = new THREE.TextureLoader(loadingManager);

			for (var i=0; i<listOfURLs.length; i++) {
				let imageURL = listOfURLs[i];  //creates local copy so that closure works!!
				
				textureLoader.load(imageURL, 
					function(texture) {
						slabTextures[imageURL] = texture; //fixme - not needed?
						slabMaterials[imageURL] = new THREE.MeshBasicMaterial({map:slabTextures[imageURL], side:THREE.DoubleSide});
						//console.log("loaded texture ", imageURL);
					},
					function ( xhr ) {
						console.log("xhr call = ", xhr);
						if ( xhr.lengthComputable ) {
							var percentComplete = xhr.loaded / xhr.total * 100;
							console.log( imageURL, Math.round(percentComplete, 2) + ' % downloaded' );
						}
					},
					function ( xhr ) {
						console.log( 'An error happened' );
					}
				);
			}
		}

		function createLotsOfCards() {

			for (var row=0; row<rows; row++) {
				map[row] = [];
			}

			for (var row=0; row<rows; row++) {
				for (var col=0; col<cols; col++) {


					var frontGeometry = new THREE.PlaneGeometry( 0.5, 0.5);
					var frontMaterial = new THREE.MeshBasicMaterial({color: 'black', transparent: true});

					var card = new THREE.Mesh(frontGeometry, frontMaterial);

					//var card = new THREE.Object3D();
					card.userData = {"side": "front", "row": row, "col": col};
					//frontMesh.visible = false;
					//card.add(frontMesh);

					
					map[row][col] = card;
					inv_map[card.id] = {'row': row, 'col':col};

					card.visible = false;
					scene.add(card);
				}
			}
		}

		function setPlaneGeometryUVstoSlabRowCol(geometry, row, col) {
			//set the UV offsets for this PlaneGeometry to row, col of the 16x16 slab
			//only works with PlaneGeometry! Not with BufferedGeometry
			//console.log("setting UV for:", geometry);

			var chunkOffset = 1.0 / tilesPerSlabEdge;

			var ll = new THREE.Vector2( col    * chunkOffset,  (tilesPerSlabEdge-row-1)  * chunkOffset );  //u,v = x, y, where 1,1 = upper right 
			var ur = new THREE.Vector2((col+1) * chunkOffset,  (tilesPerSlabEdge-row )   * chunkOffset );
			var lr = new THREE.Vector2((col+1) * chunkOffset,  (tilesPerSlabEdge-row-1)  * chunkOffset );
			var ul = new THREE.Vector2( col    * chunkOffset,  (tilesPerSlabEdge-row )   * chunkOffset );

			//geometry.faceVertexUvs[layer#][face#][vertex#] = UV-vec2
			//NOTE -- must use COPY, not assignment, for some reason!
			(geometry.faceVertexUvs[0][0][0]).copy(ul);  //first vertex is v0 is plane's UL corner
			(geometry.faceVertexUvs[0][0][1]).copy(ll);  //second vertex is V2 = LL corner
			(geometry.faceVertexUvs[0][0][2]).copy(ur);  //third vertex is v1 = UR	corner			

			geometry.faceVertexUvs[0][1][0].copy(ll);  //first vertex is LL
			geometry.faceVertexUvs[0][1][1].copy(lr);  //second vertex is LR
			geometry.faceVertexUvs[0][1][2].copy(ur);  //third vertex is UR

		}

		function positionLotsofCards(map, rows, cols) {
			//each square tile (card) is 0.5x0.5, positioned from CENTER of the square!
			var tileEdgeGLsize = 0.5;
			var leftMostCardCenter = (cols*tileEdgeGLsize)/2.0 - (tileEdgeGLsize/2.0);
			var topMostCardCenter = (rows*tileEdgeGLsize)/2.0 - (tileEdgeGLsize/2.0);
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					map[r][c].position.x = -leftMostCardCenter + (c * tileEdgeGLsize);
					map[r][c].position.y = topMostCardCenter - (r * tileEdgeGLsize);
					map[r][c].position.z = 0;
					//save the initial Position in the Card's userData object
					let card = map[r][c];
					card.userData['initialPosition'] = new THREE.Vector3().copy(card.position);
				}
			}
		}

		function loadImageTilesToSide(imageJSONmap) {
			//ASSUMES that imageJSONmap has been loaded!
			//only used on initial image??

			var imageName = imageJSONmap.name;
			var iRows = imageJSONmap.rows;
			var iCols = imageJSONmap.cols;
			var minRow, minCol;
			
			var gapRows = Math.floor((rows - iRows) / 2);
			firstRow = gapRows;
			
			var gapCols =  Math.floor((cols - iCols) / 2);
			firstCol = gapCols;


			var imageRow = 0, imageCol = 0;
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					//var flipC = iCols - c - 1;

					var card = map[r][c];

					if (r < firstRow || r > firstRow + iRows - 1  || c < firstCol || c > firstCol + iCols - 1) {
						//blank tile
						//side.visible = false;
						card.visible = false;

					} else {

						imageRow = r - firstRow;
						imageCol = c - firstCol;

						var tileName = imageJSONmap.map[imageRow][imageCol];

						var slabDetails = slabMap[tileName];
						var slabRow = slabDetails.row;
						var slabCol = slabDetails.col;

						setPlaneGeometryUVstoSlabRowCol(card.geometry, slabRow, slabCol);
						card.geometry.uvsNeedUpdate = true;

						card.userData["imageName"] = tileName;
						//side.userData["generation"] = generation;
						//console.log(side.userData);

						var slabName = "/static/tiles/" + slabDetails.slabName + ".jpg";
						card.material = slabMaterials[slabName];
						//side.visible = true;
						card.visible = true;
					}
				}
			}		
		}

		function isCardIncludedInThisImageMap(imageJmap, card) {
			//return true/false if this tile is included in the listed imageJSONmap
			var iRows = imageJmap.rows;
			var iCols = imageJmap.cols;
			var minRow, minCol;
			
			var firstRow = Math.floor((rows - iRows) / 2);
			var firstCol =  Math.floor((cols - iCols) / 2);
			
			tileRow = card.userData.row; //the card's absolute position
			tileCol = card.userData.col;

			if (tileRow < firstRow || tileRow > firstRow + iRows - 1 ||
				tileCol < firstCol || tileCol > firstCol + iCols - 1)    {
					return false;  //card not in the specificed imagemap
			}
			return true;
		}

		function updateOneCardToNextImageSudden(card) {
			//usually called repeatedly during tween.
			//points to the Object3D card that needs update
			//assumes global imageJSONmap is ready to go!  -- fixme

			var imageName = imageJSONmap.name;
			var iRows = imageJSONmap.rows;
			var iCols = imageJSONmap.cols;
			var minRow, minCol;
			
			var gapRows = Math.floor((rows - iRows) / 2);
			firstRow = gapRows;
			
			var gapCols =  Math.floor((cols - iCols) / 2);
			firstCol = gapCols;

			//if this card falls within the image, update the geomtry and material to new UV map
			//otherwise, set card's face to black
			
			//decode which card this is from userData of the (front) Mesh
			tileRow = card.userData.row; //the card's absolute position
			tileCol = card.userData.col;

			if (tileRow < firstRow || tileRow > firstRow + iRows - 1 ||
				tileCol < firstCol || tileCol > firstCol + iCols -1)    {
					//side.visible = false;
					card.visible = false;  //set the whole card invisible?

			} else {

				//find the image to show on this card - and set UV coords
				imageRow = tileRow - firstRow;
				imageCol = tileCol - firstCol;

				var tileName = imageJSONmap.map[imageRow][imageCol];

				//if this card is already showing the right card, skip the rest
				//console.log("compare: current to desired = ", side.userData.imageName, tileName);
				if (card.userData.imageName === tileName) {
					return;
				}


				var slabDetails = slabMap[tileName];
				if (typeof slabDetails === 'undefined') {
					console.log("failed to find slabMap named ", tileName);
					console.log("imageJSONmap = ", imageJSONmap);
					console.log("tile's userData = ", side.userData);
					console.log("image row/col = ", imageRow, imageCol);
				}
				var slabRow = slabDetails.row;
				var slabCol = slabDetails.col;

				setPlaneGeometryUVstoSlabRowCol(card.geometry, slabRow, slabCol);
				card.geometry.uvsNeedUpdate = true;

				card.userData["imageName"] = tileName;

				var slabName = "/static/tiles/" + slabDetails.slabName + ".jpg";
				card.material = slabMaterials[slabName];

				card.material.transparent = true;
				card.material.opacity = 1;

				card.visible = true;

			}	
		}

		function resetToNormalLocation(card) {
			//fixme
		}
			

		function transform( map, rows, cols, duration ) {
			//TWEEN.removeAll();

			var duration = duration / 1000;
			for (r=0; r<rows; r++) {
				for (c=0; c<cols; c++) {
					let aCard = map[r][c];
					
					if ( (r % 2 == 0) && (c % 2 == 0) ) { 
						//start flip
						TweenMax.to(aCard.rotation, duration*2, { x: Math.PI} );
						TweenMax.to(aCard.position, duration*2, { z:1.8,
							//onUpdateParams:[card], onUpdate: function () {
							//	if (this.progress() > 0.5) updateOneCardToNextImage(card); },
							//onCompleteParams:[card], onComplete: function() { 
							//	resetToNormalLocation(card); }
							});
						//que delayed return
						TweenMax.to(aCard.rotation, duration, { x:0 } ).delay(duration*2);
						TweenMax.to(aCard.position, duration, { z:0,
							onUpdateParams:[aCard], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[aCard], onComplete: function() { 
								resetToNormalLocation(aCard); }
						 }).delay(duration*2);						

					} else {

						TweenMax.to(aCard.rotation, duration*2, { y: Math.PI} ).delay(1);
						TweenMax.to(aCard.rotation, duration, { y: 0,
							onUpdateParams:[aCard], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[aCard], onComplete: function() { 
								resetToNormalLocation(aCard); }
						 }).delay(duration*1.5);
					}							
				}
			}
		}


		var flipStatusIsFlipped = false; //fixme

		function randomlyFlipTiles() {
			tiles = generateShuffledListofAllTiles(rows, cols);
			//fixme - should this be a simple toggle -- all 0 or all 180?
			var numSplits = 15;
			tileSets = splitTiles(tiles, numSplits);
			for (var i=0; i<numSplits; i++) {
				for (var t=0; t<tileSets[i].length; t++) {
					let card = tileSets[i][t];
					if (flipStatusIsFlipped) {
						TweenMax.to(card.rotation,2, { y:0, overwrite:"auto",
							onUpdateParams:[card], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[card], onComplete: function() { 
								resetToNormalLocation(card); }
						}).delay(0.2*i); 
					} else {
						TweenMax.to(card.rotation,2, { y:Math.PI*2, overwrite:"auto",
							onUpdateParams:[card], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[card], onComplete: function() { 
								resetToNormalLocation(card); }							
						}).delay(0.2*i);
					}
				}	
			}
			flipStatusIsFlipped = !flipStatusIsFlipped;
		}

		function generateShuffledListofAllTiles(numRows, numCols) {
			//returns array of elements
			list = []
			for (r=0; r<numRows; r++) {
				for (c=0; c<numCols; c++) {
					var tile = map[r][c]; //card is object of the card parent
					list.push(tile);
				}
			}
			shuffleArray(list);
			return list;
		}

		function shuffleArray(array) {
			//Durstenfeld shuffle, from stackoverflow
		    for (var i = array.length - 1; i > 0; i--) {
		        var j = Math.floor(Math.random() * (i + 1));
		        var temp = array[i];
		        array[i] = array[j];
		        array[j] = temp;
		    }
		    return array;
		}

		function splitTiles(tileArray, numSplits) {
			//split this list into two sublists
			var tileSets = []; //will return an array of arrays
			for (var i=0; i<numSplits; i++) { tileSets[i] = []; }

			var toggle = 0;
			for (var i=0; i<tileArray.length; i++) {
				(tileSets[toggle]).push(tileArray[i]);
				toggle = toggle + 1;
				if (toggle >= numSplits) { toggle = 0; }
			}
			return tileSets
		}

		function doExpandingRingsFrom(r,c) {
			var rings = []; //will hold sets of tiles, in expanding rings
			var ringNum = 0;
			var alreadyUsed = new Set();
			var startingTile = map[r][c];
			alreadyUsed.add(startingTile); //consider first point already used

			while (true) {
				rings[ringNum] = newNeighborsOf(rows, cols, alreadyUsed);
				//console.log("rings = ", rings[ringNum]);
				if ((rings[ringNum]).size == 0) break;
				var tempArr = Array.from(rings[ringNum]);
				for (var i=0; i<tempArr.length; i++) {
					alreadyUsed.add(tempArr[i]);  //need a UNION operator!
				}
				ringNum = ringNum + 1;
			}
			//tline = new TimelineLite({paused:true});
			for (var r2=0; r2<rings.length; r2++) {
				//console.log("ring = ", rings[r]);
				var tempArr = Array.from(rings[r2]);
				for (var t=0; t<tempArr.length; t++) {
					//adjust rotation based on direction of the "wave"
					var rc = inv_map[tempArr[t].id];
					let card = tempArr[t];
					//console.log(rc);
					//var zmag = 2 - (r2*.125);
					if (rc.col > c ) {
						var tl = new TimelineLite({delay:0.1*r2, overwrite: "auto", onUpdateParams:[card], onUpdate: function (c) {
							let p = this.progress();
							if (p > 0.5) updateOneCardToNextImageSudden(c); },
						onCompleteParams:[card], onComplete: function() { resetToNormalLocation(card); }});
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:1.5, ease: Power2.easeOut }));
						tl.add(TweenLite.to(tempArr[t].rotation, 2, { x:"+=3.1415", z:"+=3.1415", y:"+=3.1415", delay:"-.7" }));
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:0,  ease: Power2.easeIn, delay:"-1"}));
						tl.play();

						//TweenMax.to(tempArr[t].rotation, 2.5, { y:"+=3.1415", overwrite:"auto"}).delay(0.11*r2); 
						//TweenMax.to(tempArr[t].position, 1.1, { z:1, 
						//	ease: Power2.easeInOut, overwrite:"auto"}).delay(0.11*r2);
						//TweenMax.to(tempArr[t].position, .9, { z:0,  
						//	ease: Power2.easeInOut, overwrite:"auto"}).delay((0.11*r2)+1.1); 
					} else {
						var tl = new TimelineLite({delay:0.1*r2, overwrite: "auto", onUpdateParams:[card], onUpdate: function (c) {
							let p = this.progress();
							if (p > 0.5) updateOneCardToNextImageSudden(c);
						}, onCompleteParams:[card], onComplete: function() { resetToNormalLocation(card); }});
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:1.5, ease: Power2.easeOut }));
						tl.add(TweenLite.to(tempArr[t].rotation, 2, { x:"+=3.1415", z:"+=3.1415", y:"+=3.1415", delay:"-.7" }));
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:0,  ease: Power2.easeIn, delay:"-1"}));
						tl.play();

						//TweenMax.to(tempArr[t].rotation, 2.5, { y:"-=3.1415", overwrite:"auto"}).delay(0.18*r2); 
						//TweenMax.to(tempArr[t].position, 1, { z:1, overwrite:"auto"}).delay(0.18*r2);
						//TweenMax.to(tempArr[t].position, 1, { z:0, overwrite:"auto"}).delay((0.18*r2)+1); 
					}
				}
			}
			let card = startingTile;
			TweenMax.to(startingTile.rotation, 3, { y:"+=3.1415*5", overwrite:"all", onUpdateParams:[card], 
				onUpdate: function (c) {
					if (this.progress() > 0.3) updateOneCardToNextImageSudden(c);
			}}); 
		};					

		function newNeighborsOf(numRows, numCols, currentNeighbors) {
			//return set of new next-door neighbors of the currentNeighbors set
			//currNeighbors is a list of Card parent IDs (from map[][])

			function considerPoint(row, col) {
				//see if point at (row,col) is new. If so, add to newNeighbors
				//access outer Sets newNeighbors and currentNeighbors
				var newPoint = map[row][col];
				//console.log("considering ", newPoint);
				if ( currentNeighbors.has(newPoint) ) {
					//console.log("point already in use: ", newPoint);
					delete newPoint;
				} else {
					//console.log("found new point: ", newPoint);
					newNeighbors.add(newPoint);
				}
				return;
			}

			var newNeighbors = new Set();

			for (let point of currentNeighbors) {  //each point is Object id parent card
				var rc = inv_map[point.id];
				//console.log("considering tile = ", point, " with rc = ", rc);
				var mcol = rc['col'];
				var mrow = rc['row'];
				var left = mcol - 1;
				var right = mcol + 1;
				var top = mrow - 1;
				var bot = mrow + 1;

				if (top > -1 && left > -1)				considerPoint(top, left);   //UL
				if (top > -1 )							considerPoint(top, mcol);   //Above
				if (top > -1 && right < numCols)		considerPoint(top, right);  //UR

				if (left > -1)        					considerPoint(mrow, left);   //L
				//center point not considered
				if (right < numCols)					considerPoint(mrow, right);  //R

				if (bot < numRows && left > -1)			considerPoint(bot, left);    //LL
				if (bot < numRows )						considerPoint(bot, mcol);    //below
				if (bot < numRows && right < numCols)	considerPoint(bot, right);   //LR
			}
			return newNeighbors;
		}

		//function generateTileIdFromRowCol(row, col) {
		//	return "#row_" + row.toString() + "_" + "col_" + col.toString();
		//}

		//function getRowColFromTileName(name) {
		//	nums = name.match(/\d+/g); //array, row=0, col=1
		//	r = parseInt(nums[0]);
		//	c = parseInt(nums[1]);
		//	return [r,c];
		//}

		function findLineBetweenCards(rowStart, colStart, rowEnd, colEnd) {
			//from rosettacode.org - Bresenham's line drawing algorithm
			//returns an array of card address tuples [[r,c], [r,c]...]
			var x0 = colStart; 
			var y0 = rowStart;
			var x1 = colEnd;
			var y1 = rowEnd;
  			var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  			var dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1; 
  			var err = (dx>dy ? dx : -dy)/2;
 
 			cardsCoords = [];

  			while (true) {
    			cardsCoords.push([y0, x0]);
    			if (x0 === x1 && y0 === y1) break;
    			var e2 = err;
    			if (e2 > -dx) { err -= dy; x0 += sx; }
    			if (e2 < dy) { err += dx; y0 += sy; }
  			}
  			return cardsCoords;
		}

		function doSpiral360(row, col) {
			//assumes that next image has been loaded
			var midRow = Math.floor(rows / 2);
			var midCol = Math.floor(cols / 2);
			var r, c;
			var cardsInLine = [];
			var i = 0, j = 0;
			var allLines = [];

			function filterDups(fullList, newLine) {
				//eliminate cards already in the fullList (due to low-rez lines)
				//returns the cleaned up list
				//wish I could use sets, but can't due to "===" problem
				var filteredLine = [];
				for (var l=0; l<newLine.length; l++) {
					var row = newLine[l][0];
					var col = newLine[l][1];
					var found = false;
					for (var fl=0; fl<fullList.length; fl++) {
						if (row == fullList[fl][0] && col == fullList[fl][1]) {
							found = true;
							//console.log("dup at ", row, col);	
							break;
						}
					}
					if (!found) filteredLine.push(newLine[l]);
				}
				//update the fullList to know about new line
				for (l=0; l<filteredLine.length; l++) fullList.push(filteredLine[l]);
				return filteredLine;
			}

			//quad 1
			allLines = []; i = 0; cardsInLine = [];
			for (c=midCol; c<cols; c++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,0,c));
			}
			for (r=0; r<midRow; r++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,cols-1));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

			//quad 2
			i = 0; cardsInLine = [];
			for (r=midRow; r<rows; r++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,cols-1));
			}
			for (c=cols-1; c>=midCol; c--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,rows-1,c));
			}


			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}			

			//quad3
			i = 0; cardsInLine = [];
			for (c=midCol-1; c>=0; c--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,rows-1,c));
			}
			for (r=rows-1; r>midCol; r--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,0));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

			//quad4
			i = 0; cardsInLine = [];
			for (r=midRow; r>0; r--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,0));
			}
			for (c=0; c<midCol; c++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,0,c));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

		}

		function doSplineTween360(rStart, cStart, delay) {
			//goes all the way around and back to start
			var tileStart = map[rStart][cStart];
			//var tileEnd =   map[rEnd][cEnd];

			var midRow = Math.floor(rows / 2);
			var midCol = Math.floor(cols / 2);
			midTile = map[midRow][midCol];
			//curEdges = new THREE.EdgesHelper(midTile.children[0], 0x00ff00);
			//scene.add( curEdges);
			var midX = midTile.position.x;
			var midY = midTile.position.y;
			var delX = tileStart.position.x - midX;
			var delY = tileStart.position.y - midY;
			var rad = Math.sqrt( Math.pow(delX,2) + Math.pow(delY,2) );
			var delZ = rad/2;
			

			var pt = [];

			if        (delX > 0 && delY > 0) {
				pts = [[rad,0], [0,-rad], [-rad,0], [0,rad]];
			} else if (delX > 0 && delY < 0) {
				pts = [[0,-rad], [-rad,0], [0,rad], [rad,0]];
			} else if (delX < 0 && delY < 0) {
				pts = [[-rad,0], [0,rad], [rad,0], [0,-rad]];
			} else {
				pts = [[0,rad], [rad,0], [0,-rad], [-rad,0]];
			}

			spline = new THREE.CatmullRomCurve3([
			    new THREE.Vector3(tileStart.position.x, tileStart.position.y, 0.1), //start point
			    new THREE.Vector3(pts[0][0], pts[0][1] , delZ*0.6), // delZ*0.3), 
			    new THREE.Vector3(pts[1][0], pts[1][1] , delZ*1.5), //delZ*0.6),
			    new THREE.Vector3(pts[2][0], pts[2][1] , delZ*3.0), //delZ*1.0),
			    new THREE.Vector3(pts[3][0], pts[3][1] , delZ*1.5), //delZ*0.7),
			    new THREE.Vector3(tileStart.position.x, tileStart.position.y, 0.1)
			    ]);

			let wrapper = {'tile':tileStart, 'curve': spline, curveLoc: 0};
			TweenLite.to(wrapper, 6, { curveLoc:1, onUpdate: moveAlongSpline, 
				onUpdateParams:[wrapper], ease: Circ}).delay(delay); 

			//wrapper = {'tile':map[10][10], 'curve': spline, curveLoc: 0};
			//TweenLite.to(wrapper, 10, { curveLoc:1, onUpdate: moveAlongSpline, onUpdateParams:[wrapper]} ).delay(2); 

			function moveAlongSpline(tileWrapper) {
				//console.log("tweening with", tileWrapper, tileWrapper.curveLoc);
				var tile = tileWrapper.tile;
				var curveLoc = tileWrapper.curveLoc;
				var spline = tileWrapper.curve;
				var tangent = new THREE.Vector3();
				var axis = new THREE.Vector3();
				var up = new THREE.Vector3(0, 0, 1);

				var pt = spline.getPointAt(curveLoc);

				//tangent = spline.getTangentAt(curveLoc).normalize();
				//axis.crossVectors(up, tangent).normalize();
				//var radians = Math.acos(up.dot(tangent));

				tile.position.copy(pt);
				//tile.rotation.y = curveLoc*3.14159;
				tile.rotation.x = curveLoc*3.14159;	
				tile.rotation.z = curveLoc*3.14159;		

				//is it time to flip to the landing tile image?

				//if (this.progress() > 0.5) updateOneCardToNextImage(tile);
				let p = this.progress();
				if (p > 0.5) updateOneCardToNextImageSudden(tile);

				//tile.quaternion.setFromAxisAngle(axis, radians);

				/*
				scale = new THREE.Vector3();
				if (curveLoc < 0.6) {
					scale.x = (curveLoc / 0.6) + 1;
					scale.y = (curveLoc / 0.6) + 1;
					tile.scale = scale;
				} else {
					scale.x = (2.0 / 0.6 - 1.0) * (curveLoc - 1);
					scale.y = (2.0 / 0.6 - 1.0) * (curveLoc - 1);
					tile.scale = scale;
				}
				tile.matrixWorldNeedsUpdate = true;
				*/
			}

		}

		var autoRotateSpeed = 0;  //global fix me
		var sphereIsDisplayed = false;

		function doSphere() {

			cards = generateShuffledListofAllTiles(rows, cols);

			var vector = new THREE.Vector3();
			var object = new THREE.Object3D();
			var radius = 7;
			var i = 0;

			var visibleCards = []; //these go into sphere
			var inVisibleCards = []; //these go away

			//for (var r=0; r<rows; r++) {
			//	for (var c=0; c<cols; c++) {
			//		let card = map[r][c];
			for (i=0; i<cards.length; i++) {

				if (isCardIncludedInThisImageMap(imageJSONmap, cards[i]))
					//yes, this card goes into the sphere
					visibleCards.push(cards[i]);
				else 
					//no, card fades away
					inVisibleCards.push(cards[i]);

			}

			console.log("visible cards = ", visibleCards.length);
			console.log("INvisible cards = ", inVisibleCards.length);

			for (i = 0, l = visibleCards.length; i < l; i ++ ) {

				var phi = Math.acos( -1 + ( 2 * i ) / l );
				var theta = Math.sqrt( l * Math.PI ) * phi;
				//console.log("l, i, phi, theta", l, i, phi, theta, phi*180/Math.PI, (theta*180/Math.PI) % 360, Math.cos(theta)  );

				object.position.x = radius * Math.cos( theta ) * Math.sin( phi );
				object.position.z = radius * Math.sin( theta ) * Math.sin( phi );
				object.position.y = radius * Math.cos( phi );

				vector.copy( object.position ).multiplyScalar( 2 );

				//object.lookAt( new THREE.Vector3(0,0,0));
				object.lookAt( vector );

				let aCard = visibleCards[i];

				TweenMax.to(aCard.position, 2, { x: object.position.x, y: object.position.y, z:object.position.z,
					ease: Power2.easeInOut,
					onUpdateParams:[aCard],
					onUpdate: function (c) {
						let p = this.progress();
						if (p > 0.5) updateOneCardToNextImageSudden(c);} 
					}).delay(0.01*i);
				TweenMax.to(aCard.rotation, 2, { x: object.rotation.x, y: object.rotation.y, z:object.rotation.z,
					ease: Power2.easeInOut}).delay(0.01*i);
				TweenMax.to(aCard.scale, 2, { x:1.2, y:1.2, ease: Power2.easeInOut}).delay(0.01*i);

			}

			//and remove the no longer needed cards
			for (i=0; i<inVisibleCards.length; i++) {
				let bCard = inVisibleCards[i];
				let wrapper = {'card':bCard, 'opacity': 1};
				TweenMax.to(wrapper, 2, { opacity: 0, ease: Power2.easeInOut, overwrite:"all",
					onUpdateParams: [wrapper],
					onUpdate: function(cardWrapper) {
						cardWrapper.card.material = cardWrapper.opacity;
					},
					onCompleteParams:[bCard],
					onComplete: function(card) { 
						//bCardSide.visible = false;
						card.visible = false;
						//console.log("completed invisible card ", bCard);
					}}
				).delay(0.01*i);
			}

			autoRotateSpeed = 1.6;
			sphereIsDisplayed = true;

		}

		function unDoSphere() {
			//flatten sphere back out to flat image
			//assumes JSONmap has been fetched already

			//for each potential tile, tween to proper location, updating the tile to new image (or to black)
			autoRotateSpeed = 0; //fixme - too sudden!
			camera.position.z = 35;
			camera.position.y = 0; //-13;
			camera.position.x = 0;
			//camera.updateProjectionMatrix();  //fixme - make this a slow tween?
			orbitControls.update();
			var origPos = new THREE.Vector3();
			var i = 0;
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					let card = map[r][c];
					//get initial position 
					origPos.copy(card.userData['initialPosition']);
					//tween there, updating to new image in mid-tween
					TweenMax.to(card.position, 3, { x: origPos.x, y:origPos.y, z:0, overwrite:"auto",
							onUpdateParams:[card],
							onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); 
							}
						}
					).delay(0.005*i);
					TweenMax.to(card.rotation, 3, {x: 0, y: 0, z:0, overwrite:"auto" }).delay(0.005*i);
					TweenMax.to(card.scale, 3, {x: 1, y: 1, overwrite:"all", 
						onCompleteParams: card,
						onComplete: function(c) { 
							card.position.z = 0;  //fixme ??????
							card.rotation.z = 0;
						}
					}).delay(0.005*i);
					i++;				
				}
			}
			sphereIsDisplayed = false;

		}	

		//window listeners

		window.addEventListener( 'resize', onWindowResize, false );
		function onWindowResize() {

			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();

			renderer.setSize( window.innerWidth, window.innerHeight );

			render();

		}



		//FIXME - don't want these to be globals
		var wiMesh = null;
		var wiMaterial = null;
		var wiGeometry = null;

		document.getElementById("showImage").addEventListener("click", function(event) {
			//assumes imageJSONmap is ready to use

			if (typeof imageJSONmap === 'undefined') return;
<commit_msg>added support for Helix (partial)<commit_after><html>
	<head>
		<title>Mosaic2016</title>
		<style>
			body { margin: 0; }
			
			
			#selectTransition {
				margin-top: 15
				px;
				margin-left :100px;
			}

			label {
			    display: inline-block;
			    cursor: pointer;
			    position: relative;
			    padding-left: 5px;
			    margin-right: 18px;
			    font-size: 18px;
			}			

			canvas { 
				background-color: #000;
				border: 2px solid red;
				width: 1000px; height: 1000px; 
				margin: 5px; 
				position: fixed; /*uses margin to offset, ignores top/left */
				/* added one line */
			}
			div.stats { float: right; }
		</style>
	</head>
	<body>
		<form id="selectTransition">
			<input type="radio" name="transition" id="ripples" value="ripples" checked>
			 	<label for="ripples">Ripples</label>
			<input type="radio" name="transition" id = "pinwheel" value="pinwheel" >
			 	<label for="pinwheel">Pinwheel</label>
			<input type="radio" name="transition" id = "flips" value="flips" >
			 	<label for="flips">Flips</label>
			<input type="radio" name="transition" id = "shutters" value="shutters" >		
			 	<label for="shutters">Shutters</label>			
			<input type="radio" name="transition" id = "sphere" value="sphere" >		
			 	<label for="sphere">Sphere</label>	
			<button type="button" id="showImage">Show Image</button>
		</form>
		
<!-- 	<button type="button" id="b1">One</button>
		<button type="button" id="b2">Two</button>
		<button type="button" id="b3">Three</button>
		<button type="button" id="b4">Pinwheel</button>
		<button type="button" id="b5">Texture</button>
		<button type="button" id="b6">Show Image</button> -->

		<div id="stats" class="stats"></div>
		<canvas id="rendercanvas"></canvas>
		<script src="/static/three.js"></script>
		<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/TweenMax.min.js"></script>
		<script src="/static/OrbitControls.js"></script>
		<script src="/static/stats.min.js"></script>
		<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
		<script>

		THREE.Cache.enabled = true;

		//set up webgl stuff
		var scene = new THREE.Scene();
		
		var camera = new THREE.PerspectiveCamera( 750, 1000/1000, 0.1, 1000 );
		camera.position.z = 35;
		camera.position.y = 0; //-13;
		camera.position.x = 0;

		//camera.rotation.x = 0; //.4; 

		var renderer = new THREE.WebGLRenderer({canvas:rendercanvas, });
		renderer.setSize( 1000, 1000 ); //fixme
		renderer.sortObjects = true;

		var orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

		//var statsdiv = document.getElementById("stats");
		//stats = new Stats();
		//statsdiv.appendChild( stats.dom );

		var mouse = new THREE.Vector2();
		var raycaster = new THREE.Raycaster();

		//globals for image layout, etc
		var rows = 32;
		var cols = 32;
		var tileEdge = 256; //each tile is 256x256
		var tilesPerSlabEdge = 4096/tileEdge; //number of tiles on slab edge (square slabs)
		var tilesPerSlab = tilesPerSlabEdge*tilesPerSlabEdge;

		var white = new THREE.Color(0xffffff);
		var black = new THREE.Color(0x000000);
		var hilightColor = new THREE.Color(1,0,0);

		//globals for card locations and stuff
		var map = []; //2D array maps [row][col] to card object (parent of two sides)
		var inv_map = new Map(); //hash maps parent card object to original row and col
		var slabTextures = new Map(); //index the textures by name
		var slabMaterials = new Map(); //index the materials for each slab by name
		//var generation = 0; //track updates to main image. increaed +=1 for each new image


		//build the geometry of a card
		//var geometry = new THREE.PlaneGeometry( 0.5, 0.5, 1, 1);
		//var geometryBack = new THREE.PlaneGeometry( 0.5, 0.5, 1, 1); //to serve as backside, inverted texture
		//geometryBack.applyMatrix( new THREE.Matrix4().makeRotationY( Math.PI ) );

		//this loader will keep track of asynch loading textures
		var textureLoadingManager = new THREE.LoadingManager();

		textureLoadingManager.onLoad = function ( ) {
			startWorld();  //stuff that has to wait for textures to be loaded
		};

		//load and parse the slabMap - FIXME - make asych
		var slabMap = null;
		$.getJSON("/slabmap", function(data) {
			slabMap = data;
			console.log("slabMap = ", slabMap);
		});

		textureURLs = [];
		
		for (var i=0; i<6; i++) {	
			textureURLs.push("/static/tiles/slab-" + i.toString() + ".jpg");		
		}


		loadImageTexturesAndMaterials(textureLoadingManager, textureURLs, startWorld);

		var startWorld = function() {

			createLotsOfCards();

			positionLotsofCards(map, rows, cols);

			//stats - image IMG_0510 - initial screen has 992 geometries, 1984 faces, 5952 vertices

			loadImageTileMap("IMG_0510", function(data){
				imageJSONmap = data;
				loadImageTilesToSide(imageJSONmap);  //fixme - use the global?
				TweenLite.ticker.addEventListener("tick", render);
			});

			//TweenLite.ticker.addEventListener("tick", render);

		}

		function loadImageTileMap(imageName, callback) {
		//load JSON map of all the tiles that make up this image
			var imageURL = "/imagemap/" + imageName;
			$.ajax({
				method: "GET",
				url: imageURL,
				dataType: "jsonp",
				jsonp: false,
				jsonpCallback: "callback"  //funky leftover from JSONP era - FIXME
			}).done(function(data) {
				console.log("loaded ImageTileMap ", data);
				callback(data);
			});			
		}

		function loadImageTexturesAndMaterials(loadingManager, listOfURLs) {
			//for each ULR, load the image into a texture, using the loading manager

			textureLoader = new THREE.TextureLoader(loadingManager);

			for (var i=0; i<listOfURLs.length; i++) {
				let imageURL = listOfURLs[i];  //creates local copy so that closure works!!
				
				textureLoader.load(imageURL, 
					function(texture) {
						slabTextures[imageURL] = texture; //fixme - not needed?
						slabMaterials[imageURL] = new THREE.MeshBasicMaterial({map:slabTextures[imageURL], side:THREE.DoubleSide});
						//console.log("loaded texture ", imageURL);
					},
					function ( xhr ) {
						console.log("xhr call = ", xhr);
						if ( xhr.lengthComputable ) {
							var percentComplete = xhr.loaded / xhr.total * 100;
							console.log( imageURL, Math.round(percentComplete, 2) + ' % downloaded' );
						}
					},
					function ( xhr ) {
						console.log( 'An error happened' );
					}
				);
			}
		}

		function createLotsOfCards() {

			for (var row=0; row<rows; row++) {
				map[row] = [];
			}

			for (var row=0; row<rows; row++) {
				for (var col=0; col<cols; col++) {


					var frontGeometry = new THREE.PlaneGeometry( 0.5, 0.5);
					var frontMaterial = new THREE.MeshBasicMaterial({color: 'black', transparent: true});

					var card = new THREE.Mesh(frontGeometry, frontMaterial);

					//var card = new THREE.Object3D();
					card.userData = {"side": "front", "row": row, "col": col};
					//frontMesh.visible = false;
					//card.add(frontMesh);

					
					map[row][col] = card;
					inv_map[card.id] = {'row': row, 'col':col};

					card.visible = false;
					scene.add(card);
				}
			}
		}

		function setPlaneGeometryUVstoSlabRowCol(geometry, row, col) {
			//set the UV offsets for this PlaneGeometry to row, col of the 16x16 slab
			//only works with PlaneGeometry! Not with BufferedGeometry
			//console.log("setting UV for:", geometry);

			var chunkOffset = 1.0 / tilesPerSlabEdge;

			var ll = new THREE.Vector2( col    * chunkOffset,  (tilesPerSlabEdge-row-1)  * chunkOffset );  //u,v = x, y, where 1,1 = upper right 
			var ur = new THREE.Vector2((col+1) * chunkOffset,  (tilesPerSlabEdge-row )   * chunkOffset );
			var lr = new THREE.Vector2((col+1) * chunkOffset,  (tilesPerSlabEdge-row-1)  * chunkOffset );
			var ul = new THREE.Vector2( col    * chunkOffset,  (tilesPerSlabEdge-row )   * chunkOffset );

			//geometry.faceVertexUvs[layer#][face#][vertex#] = UV-vec2
			//NOTE -- must use COPY, not assignment, for some reason!
			(geometry.faceVertexUvs[0][0][0]).copy(ul);  //first vertex is v0 is plane's UL corner
			(geometry.faceVertexUvs[0][0][1]).copy(ll);  //second vertex is V2 = LL corner
			(geometry.faceVertexUvs[0][0][2]).copy(ur);  //third vertex is v1 = UR	corner			

			geometry.faceVertexUvs[0][1][0].copy(ll);  //first vertex is LL
			geometry.faceVertexUvs[0][1][1].copy(lr);  //second vertex is LR
			geometry.faceVertexUvs[0][1][2].copy(ur);  //third vertex is UR

		}

		function positionLotsofCards(map, rows, cols) {
			//each square tile (card) is 0.5x0.5, positioned from CENTER of the square!
			var tileEdgeGLsize = 0.5;
			var leftMostCardCenter = (cols*tileEdgeGLsize)/2.0 - (tileEdgeGLsize/2.0);
			var topMostCardCenter = (rows*tileEdgeGLsize)/2.0 - (tileEdgeGLsize/2.0);
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					map[r][c].position.x = -leftMostCardCenter + (c * tileEdgeGLsize);
					map[r][c].position.y = topMostCardCenter - (r * tileEdgeGLsize);
					map[r][c].position.z = 0;
					//save the initial Position in the Card's userData object
					let card = map[r][c];
					card.userData['initialPosition'] = new THREE.Vector3().copy(card.position);
				}
			}
		}

		function loadImageTilesToSide(imageJSONmap) {
			//ASSUMES that imageJSONmap has been loaded!
			//only used on initial image??

			var imageName = imageJSONmap.name;
			var iRows = imageJSONmap.rows;
			var iCols = imageJSONmap.cols;
			var minRow, minCol;
			
			var gapRows = Math.floor((rows - iRows) / 2);
			firstRow = gapRows;
			
			var gapCols =  Math.floor((cols - iCols) / 2);
			firstCol = gapCols;


			var imageRow = 0, imageCol = 0;
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					//var flipC = iCols - c - 1;

					var card = map[r][c];

					if (r < firstRow || r > firstRow + iRows - 1  || c < firstCol || c > firstCol + iCols - 1) {
						//blank tile
						//side.visible = false;
						card.visible = false;

					} else {

						imageRow = r - firstRow;
						imageCol = c - firstCol;

						var tileName = imageJSONmap.map[imageRow][imageCol];

						var slabDetails = slabMap[tileName];
						var slabRow = slabDetails.row;
						var slabCol = slabDetails.col;

						setPlaneGeometryUVstoSlabRowCol(card.geometry, slabRow, slabCol);
						card.geometry.uvsNeedUpdate = true;

						card.userData["imageName"] = tileName;
						//side.userData["generation"] = generation;
						//console.log(side.userData);

						var slabName = "/static/tiles/" + slabDetails.slabName + ".jpg";
						card.material = slabMaterials[slabName];
						//side.visible = true;
						card.visible = true;
					}
				}
			}		
		}

		function isCardIncludedInThisImageMap(imageJmap, card) {
			//return true/false if this tile is included in the listed imageJSONmap
			var iRows = imageJmap.rows;
			var iCols = imageJmap.cols;
			var minRow, minCol;
			
			var firstRow = Math.floor((rows - iRows) / 2);
			var firstCol =  Math.floor((cols - iCols) / 2);
			
			tileRow = card.userData.row; //the card's absolute position
			tileCol = card.userData.col;

			if (tileRow < firstRow || tileRow > firstRow + iRows - 1 ||
				tileCol < firstCol || tileCol > firstCol + iCols - 1)    {
					return false;  //card not in the specificed imagemap
			}
			return true;
		}

		function updateOneCardToNextImageSudden(card) {
			//usually called repeatedly during tween.
			//points to the Object3D card that needs update
			//assumes global imageJSONmap is ready to go!  -- fixme

			var imageName = imageJSONmap.name;
			var iRows = imageJSONmap.rows;
			var iCols = imageJSONmap.cols;
			var minRow, minCol;
			
			var gapRows = Math.floor((rows - iRows) / 2);
			firstRow = gapRows;
			
			var gapCols =  Math.floor((cols - iCols) / 2);
			firstCol = gapCols;

			//if this card falls within the image, update the geomtry and material to new UV map
			//otherwise, set card's face to black
			
			//decode which card this is from userData of the (front) Mesh
			tileRow = card.userData.row; //the card's absolute position
			tileCol = card.userData.col;

			if (tileRow < firstRow || tileRow > firstRow + iRows - 1 ||
				tileCol < firstCol || tileCol > firstCol + iCols -1)    {
					//side.visible = false;
					card.visible = false;  //set the whole card invisible?

			} else {

				//find the image to show on this card - and set UV coords
				imageRow = tileRow - firstRow;
				imageCol = tileCol - firstCol;

				var tileName = imageJSONmap.map[imageRow][imageCol];

				//if this card is already showing the right card, skip the rest
				//console.log("compare: current to desired = ", side.userData.imageName, tileName);
				if (card.userData.imageName === tileName) {
					return;
				}


				var slabDetails = slabMap[tileName];
				if (typeof slabDetails === 'undefined') {
					console.log("failed to find slabMap named ", tileName);
					console.log("imageJSONmap = ", imageJSONmap);
					console.log("tile's userData = ", side.userData);
					console.log("image row/col = ", imageRow, imageCol);
				}
				var slabRow = slabDetails.row;
				var slabCol = slabDetails.col;

				setPlaneGeometryUVstoSlabRowCol(card.geometry, slabRow, slabCol);
				card.geometry.uvsNeedUpdate = true;

				card.userData["imageName"] = tileName;

				var slabName = "/static/tiles/" + slabDetails.slabName + ".jpg";
				card.material = slabMaterials[slabName];

				card.material.transparent = true;
				card.material.opacity = 1;

				card.visible = true;

			}	
		}

		function resetToNormalLocation(card) {
			//fixme
		}
			

		function transform( map, rows, cols, duration ) {
			//TWEEN.removeAll();

			var duration = duration / 1000;
			for (r=0; r<rows; r++) {
				for (c=0; c<cols; c++) {
					let aCard = map[r][c];
					
					if ( (r % 2 == 0) && (c % 2 == 0) ) { 
						//start flip
						TweenMax.to(aCard.rotation, duration*2, { x: Math.PI} );
						TweenMax.to(aCard.position, duration*2, { z:1.8,
							//onUpdateParams:[card], onUpdate: function () {
							//	if (this.progress() > 0.5) updateOneCardToNextImage(card); },
							//onCompleteParams:[card], onComplete: function() { 
							//	resetToNormalLocation(card); }
							});
						//que delayed return
						TweenMax.to(aCard.rotation, duration, { x:0 } ).delay(duration*2);
						TweenMax.to(aCard.position, duration, { z:0,
							onUpdateParams:[aCard], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[aCard], onComplete: function() { 
								resetToNormalLocation(aCard); }
						 }).delay(duration*2);						

					} else {

						TweenMax.to(aCard.rotation, duration*2, { y: Math.PI} ).delay(1);
						TweenMax.to(aCard.rotation, duration, { y: 0,
							onUpdateParams:[aCard], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[aCard], onComplete: function() { 
								resetToNormalLocation(aCard); }
						 }).delay(duration*1.5);
					}							
				}
			}
		}


		var flipStatusIsFlipped = false; //fixme

		function randomlyFlipTiles() {
			tiles = generateShuffledListofAllTiles(rows, cols);
			//fixme - should this be a simple toggle -- all 0 or all 180?
			var numSplits = 15;
			tileSets = splitTiles(tiles, numSplits);
			for (var i=0; i<numSplits; i++) {
				for (var t=0; t<tileSets[i].length; t++) {
					let card = tileSets[i][t];
					if (flipStatusIsFlipped) {
						TweenMax.to(card.rotation,2, { y:0, overwrite:"auto",
							onUpdateParams:[card], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[card], onComplete: function() { 
								resetToNormalLocation(card); }
						}).delay(0.2*i); 
					} else {
						TweenMax.to(card.rotation,2, { y:Math.PI*2, overwrite:"auto",
							onUpdateParams:[card], onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); },
							onCompleteParams:[card], onComplete: function() { 
								resetToNormalLocation(card); }							
						}).delay(0.2*i);
					}
				}	
			}
			flipStatusIsFlipped = !flipStatusIsFlipped;
		}

		function generateShuffledListofAllTiles(numRows, numCols) {
			//returns array of elements
			list = []
			for (r=0; r<numRows; r++) {
				for (c=0; c<numCols; c++) {
					var tile = map[r][c]; //card is object of the card parent
					list.push(tile);
				}
			}
			shuffleArray(list);
			return list;
		}

		function shuffleArray(array) {
			//Durstenfeld shuffle, from stackoverflow
		    for (var i = array.length - 1; i > 0; i--) {
		        var j = Math.floor(Math.random() * (i + 1));
		        var temp = array[i];
		        array[i] = array[j];
		        array[j] = temp;
		    }
		    return array;
		}

		function splitTiles(tileArray, numSplits) {
			//split this list into two sublists
			var tileSets = []; //will return an array of arrays
			for (var i=0; i<numSplits; i++) { tileSets[i] = []; }

			var toggle = 0;
			for (var i=0; i<tileArray.length; i++) {
				(tileSets[toggle]).push(tileArray[i]);
				toggle = toggle + 1;
				if (toggle >= numSplits) { toggle = 0; }
			}
			return tileSets
		}

		function doExpandingRingsFrom(r,c) {
			var rings = []; //will hold sets of tiles, in expanding rings
			var ringNum = 0;
			var alreadyUsed = new Set();
			var startingTile = map[r][c];
			alreadyUsed.add(startingTile); //consider first point already used

			while (true) {
				rings[ringNum] = newNeighborsOf(rows, cols, alreadyUsed);
				//console.log("rings = ", rings[ringNum]);
				if ((rings[ringNum]).size == 0) break;
				var tempArr = Array.from(rings[ringNum]);
				for (var i=0; i<tempArr.length; i++) {
					alreadyUsed.add(tempArr[i]);  //need a UNION operator!
				}
				ringNum = ringNum + 1;
			}
			//tline = new TimelineLite({paused:true});
			for (var r2=0; r2<rings.length; r2++) {
				//console.log("ring = ", rings[r]);
				var tempArr = Array.from(rings[r2]);
				for (var t=0; t<tempArr.length; t++) {
					//adjust rotation based on direction of the "wave"
					var rc = inv_map[tempArr[t].id];
					let card = tempArr[t];
					//console.log(rc);
					//var zmag = 2 - (r2*.125);
					if (rc.col > c ) {
						var tl = new TimelineLite({delay:0.1*r2, overwrite: "auto", onUpdateParams:[card], onUpdate: function (c) {
							let p = this.progress();
							if (p > 0.5) updateOneCardToNextImageSudden(c); },
						onCompleteParams:[card], onComplete: function() { resetToNormalLocation(card); }});
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:1.5, ease: Power2.easeOut }));
						tl.add(TweenLite.to(tempArr[t].rotation, 2, { x:"+=3.1415", z:"+=3.1415", y:"+=3.1415", delay:"-.7" }));
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:0,  ease: Power2.easeIn, delay:"-1"}));
						tl.play();

						//TweenMax.to(tempArr[t].rotation, 2.5, { y:"+=3.1415", overwrite:"auto"}).delay(0.11*r2); 
						//TweenMax.to(tempArr[t].position, 1.1, { z:1, 
						//	ease: Power2.easeInOut, overwrite:"auto"}).delay(0.11*r2);
						//TweenMax.to(tempArr[t].position, .9, { z:0,  
						//	ease: Power2.easeInOut, overwrite:"auto"}).delay((0.11*r2)+1.1); 
					} else {
						var tl = new TimelineLite({delay:0.1*r2, overwrite: "auto", onUpdateParams:[card], onUpdate: function (c) {
							let p = this.progress();
							if (p > 0.5) updateOneCardToNextImageSudden(c);
						}, onCompleteParams:[card], onComplete: function() { resetToNormalLocation(card); }});
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:1.5, ease: Power2.easeOut }));
						tl.add(TweenLite.to(tempArr[t].rotation, 2, { x:"+=3.1415", z:"+=3.1415", y:"+=3.1415", delay:"-.7" }));
						tl.add(TweenLite.to(tempArr[t].position, 1,  { z:0,  ease: Power2.easeIn, delay:"-1"}));
						tl.play();

						//TweenMax.to(tempArr[t].rotation, 2.5, { y:"-=3.1415", overwrite:"auto"}).delay(0.18*r2); 
						//TweenMax.to(tempArr[t].position, 1, { z:1, overwrite:"auto"}).delay(0.18*r2);
						//TweenMax.to(tempArr[t].position, 1, { z:0, overwrite:"auto"}).delay((0.18*r2)+1); 
					}
				}
			}
			let card = startingTile;
			TweenMax.to(startingTile.rotation, 3, { y:"+=3.1415*5", overwrite:"all", onUpdateParams:[card], 
				onUpdate: function (c) {
					if (this.progress() > 0.3) updateOneCardToNextImageSudden(c);
			}}); 
		};					

		function newNeighborsOf(numRows, numCols, currentNeighbors) {
			//return set of new next-door neighbors of the currentNeighbors set
			//currNeighbors is a list of Card parent IDs (from map[][])

			function considerPoint(row, col) {
				//see if point at (row,col) is new. If so, add to newNeighbors
				//access outer Sets newNeighbors and currentNeighbors
				var newPoint = map[row][col];
				//console.log("considering ", newPoint);
				if ( currentNeighbors.has(newPoint) ) {
					//console.log("point already in use: ", newPoint);
					delete newPoint;
				} else {
					//console.log("found new point: ", newPoint);
					newNeighbors.add(newPoint);
				}
				return;
			}

			var newNeighbors = new Set();

			for (let point of currentNeighbors) {  //each point is Object id parent card
				var rc = inv_map[point.id];
				//console.log("considering tile = ", point, " with rc = ", rc);
				var mcol = rc['col'];
				var mrow = rc['row'];
				var left = mcol - 1;
				var right = mcol + 1;
				var top = mrow - 1;
				var bot = mrow + 1;

				if (top > -1 && left > -1)				considerPoint(top, left);   //UL
				if (top > -1 )							considerPoint(top, mcol);   //Above
				if (top > -1 && right < numCols)		considerPoint(top, right);  //UR

				if (left > -1)        					considerPoint(mrow, left);   //L
				//center point not considered
				if (right < numCols)					considerPoint(mrow, right);  //R

				if (bot < numRows && left > -1)			considerPoint(bot, left);    //LL
				if (bot < numRows )						considerPoint(bot, mcol);    //below
				if (bot < numRows && right < numCols)	considerPoint(bot, right);   //LR
			}
			return newNeighbors;
		}

		//function generateTileIdFromRowCol(row, col) {
		//	return "#row_" + row.toString() + "_" + "col_" + col.toString();
		//}

		//function getRowColFromTileName(name) {
		//	nums = name.match(/\d+/g); //array, row=0, col=1
		//	r = parseInt(nums[0]);
		//	c = parseInt(nums[1]);
		//	return [r,c];
		//}

		function findLineBetweenCards(rowStart, colStart, rowEnd, colEnd) {
			//from rosettacode.org - Bresenham's line drawing algorithm
			//returns an array of card address tuples [[r,c], [r,c]...]
			var x0 = colStart; 
			var y0 = rowStart;
			var x1 = colEnd;
			var y1 = rowEnd;
  			var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  			var dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1; 
  			var err = (dx>dy ? dx : -dy)/2;
 
 			cardsCoords = [];

  			while (true) {
    			cardsCoords.push([y0, x0]);
    			if (x0 === x1 && y0 === y1) break;
    			var e2 = err;
    			if (e2 > -dx) { err -= dy; x0 += sx; }
    			if (e2 < dy) { err += dx; y0 += sy; }
  			}
  			return cardsCoords;
		}

		function doSpiral360(row, col) {
			//assumes that next image has been loaded
			var midRow = Math.floor(rows / 2);
			var midCol = Math.floor(cols / 2);
			var r, c;
			var cardsInLine = [];
			var i = 0, j = 0;
			var allLines = [];

			function filterDups(fullList, newLine) {
				//eliminate cards already in the fullList (due to low-rez lines)
				//returns the cleaned up list
				//wish I could use sets, but can't due to "===" problem
				var filteredLine = [];
				for (var l=0; l<newLine.length; l++) {
					var row = newLine[l][0];
					var col = newLine[l][1];
					var found = false;
					for (var fl=0; fl<fullList.length; fl++) {
						if (row == fullList[fl][0] && col == fullList[fl][1]) {
							found = true;
							//console.log("dup at ", row, col);	
							break;
						}
					}
					if (!found) filteredLine.push(newLine[l]);
				}
				//update the fullList to know about new line
				for (l=0; l<filteredLine.length; l++) fullList.push(filteredLine[l]);
				return filteredLine;
			}

			//quad 1
			allLines = []; i = 0; cardsInLine = [];
			for (c=midCol; c<cols; c++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,0,c));
			}
			for (r=0; r<midRow; r++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,cols-1));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

			//quad 2
			i = 0; cardsInLine = [];
			for (r=midRow; r<rows; r++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,cols-1));
			}
			for (c=cols-1; c>=midCol; c--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,rows-1,c));
			}


			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}			

			//quad3
			i = 0; cardsInLine = [];
			for (c=midCol-1; c>=0; c--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,rows-1,c));
			}
			for (r=rows-1; r>midCol; r--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,0));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

			//quad4
			i = 0; cardsInLine = [];
			for (r=midRow; r>0; r--) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,r,0));
			}
			for (c=0; c<midCol; c++) {
				cardsInLine[i++] = filterDups(allLines, findLineBetweenCards(midRow,midCol,0,c));
			}			

			for (i=0; i<cardsInLine.length; i++) {
				for (j=0; j<cardsInLine[i].length; j++) {
					doSplineTween360(cardsInLine[i][j][0], cardsInLine[i][j][1],  (0.05*j)+(0.2*i));
				}
			}

		}

		function doSplineTween360(rStart, cStart, delay) {
			//goes all the way around and back to start
			var tileStart = map[rStart][cStart];
			//var tileEnd =   map[rEnd][cEnd];

			var midRow = Math.floor(rows / 2);
			var midCol = Math.floor(cols / 2);
			midTile = map[midRow][midCol];
			//curEdges = new THREE.EdgesHelper(midTile.children[0], 0x00ff00);
			//scene.add( curEdges);
			var midX = midTile.position.x;
			var midY = midTile.position.y;
			var delX = tileStart.position.x - midX;
			var delY = tileStart.position.y - midY;
			var rad = Math.sqrt( Math.pow(delX,2) + Math.pow(delY,2) );
			var delZ = rad/2;
			

			var pt = [];

			if        (delX > 0 && delY > 0) {
				pts = [[rad,0], [0,-rad], [-rad,0], [0,rad]];
			} else if (delX > 0 && delY < 0) {
				pts = [[0,-rad], [-rad,0], [0,rad], [rad,0]];
			} else if (delX < 0 && delY < 0) {
				pts = [[-rad,0], [0,rad], [rad,0], [0,-rad]];
			} else {
				pts = [[0,rad], [rad,0], [0,-rad], [-rad,0]];
			}

			spline = new THREE.CatmullRomCurve3([
			    new THREE.Vector3(tileStart.position.x, tileStart.position.y, 0.1), //start point
			    new THREE.Vector3(pts[0][0], pts[0][1] , delZ*0.6), // delZ*0.3), 
			    new THREE.Vector3(pts[1][0], pts[1][1] , delZ*1.5), //delZ*0.6),
			    new THREE.Vector3(pts[2][0], pts[2][1] , delZ*3.0), //delZ*1.0),
			    new THREE.Vector3(pts[3][0], pts[3][1] , delZ*1.5), //delZ*0.7),
			    new THREE.Vector3(tileStart.position.x, tileStart.position.y, 0.1)
			    ]);

			let wrapper = {'tile':tileStart, 'curve': spline, curveLoc: 0};
			TweenLite.to(wrapper, 6, { curveLoc:1, onUpdate: moveAlongSpline, 
				onUpdateParams:[wrapper], ease: Circ}).delay(delay); 

			//wrapper = {'tile':map[10][10], 'curve': spline, curveLoc: 0};
			//TweenLite.to(wrapper, 10, { curveLoc:1, onUpdate: moveAlongSpline, onUpdateParams:[wrapper]} ).delay(2); 

			function moveAlongSpline(tileWrapper) {
				//console.log("tweening with", tileWrapper, tileWrapper.curveLoc);
				var tile = tileWrapper.tile;
				var curveLoc = tileWrapper.curveLoc;
				var spline = tileWrapper.curve;
				var tangent = new THREE.Vector3();
				var axis = new THREE.Vector3();
				var up = new THREE.Vector3(0, 0, 1);

				var pt = spline.getPointAt(curveLoc);

				//tangent = spline.getTangentAt(curveLoc).normalize();
				//axis.crossVectors(up, tangent).normalize();
				//var radians = Math.acos(up.dot(tangent));

				tile.position.copy(pt);
				//tile.rotation.y = curveLoc*3.14159;
				tile.rotation.x = curveLoc*3.14159;	
				tile.rotation.z = curveLoc*3.14159;		

				//is it time to flip to the landing tile image?

				//if (this.progress() > 0.5) updateOneCardToNextImage(tile);
				let p = this.progress();
				if (p > 0.5) updateOneCardToNextImageSudden(tile);

				//tile.quaternion.setFromAxisAngle(axis, radians);

				/*
				scale = new THREE.Vector3();
				if (curveLoc < 0.6) {
					scale.x = (curveLoc / 0.6) + 1;
					scale.y = (curveLoc / 0.6) + 1;
					tile.scale = scale;
				} else {
					scale.x = (2.0 / 0.6 - 1.0) * (curveLoc - 1);
					scale.y = (2.0 / 0.6 - 1.0) * (curveLoc - 1);
					tile.scale = scale;
				}
				tile.matrixWorldNeedsUpdate = true;
				*/
			}

		}

		var autoRotateSpeed = 0;  //global fix me
		var sphereIsDisplayed = false;

		function calcSphereCoordinates(i, l, radius) {
			//i = instance, l = total cards in shape, radius = radius

			var object = new THREE.Object3D();
			var vector = new THREE.Vector3();

			var phi = Math.acos( -1 + ( 2 * i ) / l );
			var theta = Math.sqrt( l * Math.PI ) * phi;
				//console.log("l, i, phi, theta", l, i, phi, theta, phi*180/Math.PI, (theta*180/Math.PI) % 360, Math.cos(theta)  );

			object.position.x = radius * Math.cos( theta ) * Math.sin( phi );
			object.position.z = radius * Math.sin( theta ) * Math.sin( phi );
			object.position.y = radius * Math.cos( phi );

			vector.copy( object.position ).multiplyScalar( 2 );

			return {'object': object, 'vector': vector};

		}

		function calcHelixCoordinates(i, l, radius) {
			//i = instance, l = total cards in shape, radius = radius

			var object = new THREE.Object3D();
			var vector = new THREE.Vector3();

			var phi = i * 0.175 + Math.PI;

			object.position.x = radius * Math.sin( phi );
			//object.position.z = - ( i * 8 ) + 450;
			object.position.y = - ( i * 0.025 ) + 10;
			object.position.z = radius * Math.cos( phi );

			vector.x = object.position.x * 2;
			vector.y = object.position.y;
			vector.z = object.position.z * 2;
		

			return {'object': object, 'vector': vector};

		}

		function doSphere() {


			cards = generateShuffledListofAllTiles(rows, cols);

			//var vector = new THREE.Vector3();
			//var object = new THREE.Object3D();
			var radius = 7;
			var i = 0;

			var visibleCards = []; //these go into sphere
			var inVisibleCards = []; //these go away

			//for (var r=0; r<rows; r++) {
			//	for (var c=0; c<cols; c++) {
			//		let card = map[r][c];
			for (i=0; i<cards.length; i++) {

				if (isCardIncludedInThisImageMap(imageJSONmap, cards[i]))
					//yes, this card goes into the sphere
					visibleCards.push(cards[i]);
				else 
					//no, card fades away
					inVisibleCards.push(cards[i]);

			}

			console.log("visible cards = ", visibleCards.length);
			console.log("INvisible cards = ", inVisibleCards.length);

			for (i = 0, l = visibleCards.length; i < l; i ++ ) {

				/*
				var phi = Math.acos( -1 + ( 2 * i ) / l );
				var theta = Math.sqrt( l * Math.PI ) * phi;

				object.position.x = radius * Math.cos( theta ) * Math.sin( phi );
				object.position.z = radius * Math.sin( theta ) * Math.sin( phi );
				object.position.y = radius * Math.cos( phi );
				*/

				coords = calcHelixCoordinates(i, l, radius);
				var object = coords.object;
				var vector = coords.vector;

				vector.copy( object.position ).multiplyScalar( 2 );

				//object.lookAt( new THREE.Vector3(0,0,0));
				object.lookAt( vector );

				let aCard = visibleCards[i];

				TweenMax.to(aCard.position, 2, { x: object.position.x, y: object.position.y, z:object.position.z,
					ease: Power2.easeInOut,
					onUpdateParams:[aCard],
					onUpdate: function (c) {
						let p = this.progress();
						if (p > 0.5) updateOneCardToNextImageSudden(c);} 
					}).delay(0.01*i);
				TweenMax.to(aCard.rotation, 2, { x: object.rotation.x, y: object.rotation.y, z:object.rotation.z,
					ease: Power2.easeInOut}).delay(0.01*i);
				TweenMax.to(aCard.scale, 2, { x:1.2, y:1.2, ease: Power2.easeInOut}).delay(0.01*i);

			}

			//and remove the no longer needed cards
			for (i=0; i<inVisibleCards.length; i++) {
				let bCard = inVisibleCards[i];
				let wrapper = {'card':bCard, 'opacity': 1};
				TweenMax.to(wrapper, 2, { opacity: 0, ease: Power2.easeInOut, overwrite:"all",
					onUpdateParams: [wrapper],
					onUpdate: function(cardWrapper) {
						cardWrapper.card.material = cardWrapper.opacity;
					},
					onCompleteParams:[bCard],
					onComplete: function(card) { 
						//bCardSide.visible = false;
						card.visible = false;
						//console.log("completed invisible card ", bCard);
					}}
				).delay(0.01*i);
			}

			autoRotateSpeed = 1.6;
			sphereIsDisplayed = true;

		}

		function unDoSphere() {
			//flatten sphere back out to flat image
			//assumes JSONmap has been fetched already

			//for each potential tile, tween to proper location, updating the tile to new image (or to black)
			autoRotateSpeed = 0; //fixme - too sudden!
			camera.position.z = 35;
			camera.position.y = 0; //-13;
			camera.position.x = 0;
			//camera.updateProjectionMatrix();  //fixme - make this a slow tween?
			orbitControls.update();
			var origPos = new THREE.Vector3();
			var i = 0;
			for (var r=0; r<rows; r++) {
				for (var c=0; c<cols; c++) {
					let card = map[r][c];
					//get initial position 
					origPos.copy(card.userData['initialPosition']);
					//tween there, updating to new image in mid-tween
					TweenMax.to(card.position, 3, { x: origPos.x, y:origPos.y, z:0, overwrite:"auto",
							onUpdateParams:[card],
							onUpdate: function (c) {
								let p = this.progress();
								if (p > 0.5) updateOneCardToNextImageSudden(c); 
							}
						}
					).delay(0.005*i);
					TweenMax.to(card.rotation, 3, {x: 0, y: 0, z:0, overwrite:"auto" }).delay(0.005*i);
					TweenMax.to(card.scale, 3, {x: 1, y: 1, overwrite:"all", 
						onCompleteParams: card,
						onComplete: function(c) { 
							//card.position.z = 0;
							//card.rotation.z = 0;
							card.position.copy(card.userData.initialPosition);
						}
					}).delay(0.005*i);
					i++;				
				}
			}
			sphereIsDisplayed = false;

		}	

		//window listeners

		window.addEventListener( 'resize', onWindowResize, false );
		function onWindowResize() {

			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();

			renderer.setSize( window.innerWidth, window.innerHeight );

			render();

		}



		//FIXME - don't want these to be globals
		var wiMesh = null;
		var wiMaterial = null;
		var wiGeometry = null;

		document.getElementById("showImage").addEventListener("click", function(event) {
			//assumes imageJSONmap is ready to use

			if (typeof imageJSONmap === 'undefined') return;

			TweenMax.killAll(false,true,true,true);

{'fraction_non_alphanumeric': 0.16193062190535654, 'fraction_numerical': 0.02304374972774398, 'mean_word_length': 2.8782520554116453, 'pattern_counts': {'":': 6, '<': 235, '<?xml version=': 0, '>': 185, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2113359', 'n_tokens_mistral': 30828, 'n_tokens_neox': 24457, 'n_words': 5820}