diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..5171c540
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
\ No newline at end of file
diff --git a/Final Report.pdf b/Final Report.pdf
new file mode 100644
index 00000000..680b7a90
Binary files /dev/null and b/Final Report.pdf differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..8dc1ee76
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Aman Sachan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 2adc1c9e..17eaf2b9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,101 @@
-# FinalProject
\ No newline at end of file
+# Interesting Level Generator
+
+[](https://vimeo.com/227360081)
+
+## Overview
+
+The 'Interesting Level Generator' is a procedural multi-layer dungeon generator that generates levels based on a dynamically generated voronoi like graph after it has been heavily modified by various filters. Realistic fog, crumbling pathways, and terrain are added over the basic level layout to give it a unique mysterious foreign world aesthetic.
+
+
+
+### Original Design Goals
+
+- Procedural 3D game level ( multiple stacks of 2D maps ).
+- Implicit surfaces to create terrain on the resulting map floors
+- Custom fog shader
+
+The specifications below will be implemented if I have enough time in the final week:
+- Controllable Player character
+- Collision handling so player can’t walk through geometry.
+
+## Techniques Used
+### 2D Maps Level Generation:
+#### Room Generation:
+
+
+
+
+ - Using sampling and rejection to layout the slabs in a given space.
+ - Then use a fake voronoi generation technique to create a graph. The fake voronoi technique consists of first starting with 3 connected nodes, and then for every new node you want to add to the graph you find the 2 closest neighbours from the existing graph and form edges between them.
+ - We can improve the above technique a bit more by sorting the positions of the slabs along one axis. This makes the connections look more like a voronoi pattern.
+ - This graph is highly connected so we randomly remove connections.
+ - The graph can end up with intersecting edges in a few edge cases, so we carry out 2D line intersection tests and remove any intersections if they exist.
+
+#### Walkways between slabs:
+
+
+
+ - Because of the graph we created above we have edge and node data, in other words we know which points to connect. So between any 2 points we could lay out corridors(planes) to connect them, but this is boring.
+ - Instead we can use instancing to place many many tiny cubes along the line segment and then randomly remove cubes to make it look like a crumbling walkway.
+ - We also need to give the walkway some width so we take the cross product of the direction of the line segment and the y axis (up) to get a horizontal ortho-normal direction for width. Add instanced cubes not just on the line segment but for a certain width for each line segment.
+
+### 3D Level Generation:
+ We can create multiple 2D maps at different heights.
+
+#### Interlevel Connecting Paths:
+
+
+
+- This is a similar problem to the one we solved in “Walkway between slabs” section; But now it’s in 3D.
+- For every layer we pick a random node/slab as the starting point of our path between layers.
+- For the end point of that line segment we search through the nodes in the layer above for rooms that are beyond a certain distance from the randomly picked starting node (so paths don’t go straight up and there is more complexity in connections), and form a list of these “toNodes”.
+- Pick a random “toNode”, and using the random starting node we have a 3D line segment.
+- Create a similar instancing cubes setup for these paths as we did with the walkways.
+- Remove random lines, and also carry out 3D intersection tests and remove any intersecting paths, if they exist.
+
+#### Path shifting for Paths:
+
+
+
+- To make the paths connecting walkways seem more organic and prevent janky looking paths that start at the center of each cell and end at the center of the other one ( this is a problem as a player can never go to a higher layer, they will be stuck underneath the “toNode” ).
+- We need to shift paths to the edges of the cells they are connecting. Simply offset by the width and length in the correct direction.
+- To add organic paths we should shift by both the width and length.
+
+#### Fog:
+
+
+
+
+- Created in the shader, with global control of density, color, and a on/off switch.
+- A good approximation of fog fall-off is: e-(fogDensity2 x distance2)
+- Fog also appears to have different densities at the periphery of our vision, so we need to account for rimColor.
+- Resource: http://in2gpu.com/2014/07/22/create-fog-shader/
+
+#### Terrain:
+
+
+
+- Terrain was created in the shader.
+- Create an elevation map and a moisture map using smoothed 2D noise with different seed values (or different noise functions).
+- Use the elevation map to deform vertices in the shader.
+- Create a moisture map ( similar to the elevation map ).
+- Use the float values from the elevation and moisture as uv values to determine colors from gradients.
+
+#### Grid based Acceleration:
+- Takes too much memory, and so was never used for anything.
+
+## Future Work
+- kD tree acceleration Structure for Collision Handling
+- Character with basic character controls
+- Collision handling for the character
+- Trampolines to fill up empty and negative space
+
+## Resources
+- Terrain: http://www.redblobgames.com/maps/terrain-from-noise/
+- Fog: http://in2gpu.com/2014/07/22/create-fog-shader/
+
+## Moar Images!!
+
+
+
+
\ No newline at end of file
diff --git a/Results/Final Report.pdf b/Results/Final Report.pdf
new file mode 100644
index 00000000..680b7a90
Binary files /dev/null and b/Results/Final Report.pdf differ
diff --git a/Results/FinalProjectDesignDoc.pdf b/Results/FinalProjectDesignDoc.pdf
new file mode 100644
index 00000000..70b8505e
Binary files /dev/null and b/Results/FinalProjectDesignDoc.pdf differ
diff --git a/Results/Images/CrossRoadsEmergentBehaviour.png b/Results/Images/CrossRoadsEmergentBehaviour.png
new file mode 100644
index 00000000..34c8297a
Binary files /dev/null and b/Results/Images/CrossRoadsEmergentBehaviour.png differ
diff --git a/Results/Images/CrumbleStatusMax.png b/Results/Images/CrumbleStatusMax.png
new file mode 100644
index 00000000..cbb70b1a
Binary files /dev/null and b/Results/Images/CrumbleStatusMax.png differ
diff --git a/Results/Images/CrumbleStatusMid.png b/Results/Images/CrumbleStatusMid.png
new file mode 100644
index 00000000..7e09eedc
Binary files /dev/null and b/Results/Images/CrumbleStatusMid.png differ
diff --git a/Results/Images/CrumbleStatusMin.png b/Results/Images/CrumbleStatusMin.png
new file mode 100644
index 00000000..d7488084
Binary files /dev/null and b/Results/Images/CrumbleStatusMin.png differ
diff --git a/Results/Images/FatWalkway.png b/Results/Images/FatWalkway.png
new file mode 100644
index 00000000..ec498ff1
Binary files /dev/null and b/Results/Images/FatWalkway.png differ
diff --git a/Results/Images/Fog1.png b/Results/Images/Fog1.png
new file mode 100644
index 00000000..9e52ce39
Binary files /dev/null and b/Results/Images/Fog1.png differ
diff --git a/Results/Images/Fog2.png b/Results/Images/Fog2.png
new file mode 100644
index 00000000..46daa287
Binary files /dev/null and b/Results/Images/Fog2.png differ
diff --git a/Results/Images/LayerConnectivity.png b/Results/Images/LayerConnectivity.png
new file mode 100644
index 00000000..c3561979
Binary files /dev/null and b/Results/Images/LayerConnectivity.png differ
diff --git a/Results/Images/LayerConnectivityMore.png b/Results/Images/LayerConnectivityMore.png
new file mode 100644
index 00000000..1fe635ce
Binary files /dev/null and b/Results/Images/LayerConnectivityMore.png differ
diff --git a/Results/Images/LimitedConnection2DMap.png b/Results/Images/LimitedConnection2DMap.png
new file mode 100644
index 00000000..ebec5303
Binary files /dev/null and b/Results/Images/LimitedConnection2DMap.png differ
diff --git a/Results/Images/LimitedConnection2DMapHalf.png b/Results/Images/LimitedConnection2DMapHalf.png
new file mode 100644
index 00000000..6544976a
Binary files /dev/null and b/Results/Images/LimitedConnection2DMapHalf.png differ
diff --git a/Results/Images/MultipleLayers.png b/Results/Images/MultipleLayers.png
new file mode 100644
index 00000000..18b23c71
Binary files /dev/null and b/Results/Images/MultipleLayers.png differ
diff --git a/Results/Images/Terrain1.png b/Results/Images/Terrain1.png
new file mode 100644
index 00000000..5312dae1
Binary files /dev/null and b/Results/Images/Terrain1.png differ
diff --git a/Results/Images/VariableRoomSize.png b/Results/Images/VariableRoomSize.png
new file mode 100644
index 00000000..c7c5a8ca
Binary files /dev/null and b/Results/Images/VariableRoomSize.png differ
diff --git a/Results/Images/Voronoi1.png b/Results/Images/Voronoi1.png
new file mode 100644
index 00000000..a8d3934b
Binary files /dev/null and b/Results/Images/Voronoi1.png differ
diff --git a/Results/Images/Voronoi2.png b/Results/Images/Voronoi2.png
new file mode 100644
index 00000000..370471c5
Binary files /dev/null and b/Results/Images/Voronoi2.png differ
diff --git a/Results/Images/thinWalkway.png b/Results/Images/thinWalkway.png
new file mode 100644
index 00000000..8b9d892d
Binary files /dev/null and b/Results/Images/thinWalkway.png differ
diff --git a/Results/InterestingLevelGeneratorEdited.mp4 b/Results/InterestingLevelGeneratorEdited.mp4
new file mode 100644
index 00000000..15385a36
Binary files /dev/null and b/Results/InterestingLevelGeneratorEdited.mp4 differ
diff --git a/Results/Videos/InterestingLevelGenerator.mp4 b/Results/Videos/InterestingLevelGenerator.mp4
new file mode 100644
index 00000000..9445ef36
Binary files /dev/null and b/Results/Videos/InterestingLevelGenerator.mp4 differ
diff --git a/bundle.js b/bundle.js
new file mode 100644
index 00000000..6ad56300
--- /dev/null
+++ b/bundle.js
@@ -0,0 +1,50398 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var _framework = __webpack_require__(1);
+
+ var _framework2 = _interopRequireDefault(_framework);
+
+ var _datGui = __webpack_require__(3);
+
+ var _datGui2 = _interopRequireDefault(_datGui);
+
+ var _player = __webpack_require__(8);
+
+ var _player2 = _interopRequireDefault(_player);
+
+ var _cell = __webpack_require__(9);
+
+ var _cell2 = _interopRequireDefault(_cell);
+
+ var _voronoiPoint = __webpack_require__(10);
+
+ var _voronoiPoint2 = _interopRequireDefault(_voronoiPoint);
+
+ var _layer = __webpack_require__(11);
+
+ var _layer2 = _interopRequireDefault(_layer);
+
+ var _walkwaylayer = __webpack_require__(12);
+
+ var _walkwaylayer2 = _interopRequireDefault(_walkwaylayer);
+
+ var _gridcell = __webpack_require__(13);
+
+ var _gridcell2 = _interopRequireDefault(_gridcell);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+ //simplex noise function: https://www.npmjs.com/package/simplex-noise
+ //terrain generation: http://www.redblobgames.com/maps/terrain-from-noise/
+
+ var THREE = __webpack_require__(6); // older modules are imported like this. You shouldn't have to worry about this much
+
+ var OBJLoader = __webpack_require__(14);
+ var textureLoader = new THREE.TextureLoader();
+
+ OBJLoader(THREE);
+
+ //------------------------------------------------------------------------------
+
+ var RAND = __webpack_require__(15).create(Math.random());
+ var Pi = 3.14;
+ var interlayerwalkwaypos = new THREE.Vector3(0, 0, 0);
+
+ //------------------------------------------------------------------------------
+
+ var generalParameters = {
+ Collisions: false,
+ Fog: true,
+ FogDensity: 0.01,
+ fog_Col: new THREE.Color(0xd5ddea),
+ voxelsize: 0.25,
+ maxInstanceCount: 200000
+
+ // var fogParameters = {
+ // color: new THREE.Color(0x000000)
+ // }
+
+ };var map2D = {
+ numberOfCells: 10,
+ connectivity: 0.68,
+ roomSizeMin: 2.0, //controls min of width and length of rooms
+ roomSizeMax: 3.0, //controls max of width and length of rooms
+ walkWayWidth: 2.5,
+ crumbleStatus: 0.83
+ };
+
+ var level3D = {
+ numberOfLayers: 2,
+ connectivity: 0.61
+
+ //material for slab below mountains
+ };var slabMat = new THREE.ShaderMaterial({
+ uniforms: {
+ albedo: {
+ type: "v3",
+ value: new THREE.Vector3(35.0 / 255.0, 70.0 / 255.0, 175.0 / 255.0)
+ },
+ ambientLight: {
+ type: "v3",
+ value: new THREE.Vector3(0.2, 0.2, 0.2)
+ },
+ lightVec: {
+ type: "v3",
+ value: new THREE.Vector3(1, 2, 3)
+ },
+ camPos: {
+ type: "v3",
+ value: new THREE.Vector3(10, 10, 10)
+ },
+ fogSwitch: {
+ type: "f",
+ value: 0
+ },
+ fogColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.5, 0.5, 0.5)
+ },
+ fogDensity: {
+ type: "f",
+ value: 0.1
+ },
+ rimColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.1, 0.1, 0.1)
+ }
+ },
+ vertexShader: __webpack_require__(17),
+ fragmentShader: __webpack_require__(18),
+ side: THREE.DoubleSide
+ });
+
+ // material for instanced objects
+ var pathMat = new THREE.RawShaderMaterial({
+ uniforms: {
+ image1: { // Check the Three.JS documentation for the different allowed types and values
+ type: "t",
+ value: THREE.ImageUtils.loadTexture('./images/tex_nor_maps/path/TilingStone1.jpg')
+ },
+ ambientLight: {
+ type: "v3",
+ value: new THREE.Vector3(0.1, 0.1, 0.1)
+ },
+ lightVec: {
+ type: "v3",
+ value: new THREE.Vector3(1, 1, 1)
+ },
+ camPos: {
+ type: "v3",
+ value: new THREE.Vector3(10, 10, 10)
+ },
+ fogSwitch: {
+ type: "f",
+ value: 0
+ },
+ fogColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.5, 0.5, 0.5)
+ },
+ fogDensity: {
+ type: "f",
+ value: 0.1
+ },
+ rimColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.1, 0.1, 0.1)
+ }
+ },
+ vertexShader: __webpack_require__(19),
+ fragmentShader: __webpack_require__(20),
+ side: THREE.DoubleSide
+ });
+
+ //------------------------------------------------------------------------------
+
+ var directionalLight;
+ var levelLayers = []; //list of 2D Layers
+ var grid = new Uint8Array(17600000); //could be more efficient if it was a array of bits
+ var connectingWalkways = []; //list of walkwaylayers connecting
+ //grid indexing scheme: index = currIndy*gridsize*gridsize + currIndx*gridsize + currIndz;
+ var gridsize = new THREE.Vector3(200, 440, 200); //max dimensions of scene
+
+ //------------------------------------------------------------------------------
+
+ var TextActions = function TextActions(scene) {
+ this.NewLevel = function () {
+ onreset(scene);
+ };
+ };
+
+ function changeGUI(gui, camera, scene, renderer) {
+ var tweaks = gui.addFolder('Tweaks');
+
+ var level3DFolder = tweaks.addFolder('3D Level parameters');
+ level3DFolder.add(level3D, 'numberOfLayers', 1, 5).step(1).onChange(function (newVal) {});
+ level3DFolder.add(level3D, 'connectivity', 0.1, 1.0).onChange(function (newVal) {});
+
+ var map2DFolder = tweaks.addFolder('2D Layer parameters');
+ map2DFolder.add(map2D, 'numberOfCells', 10, 30).step(1).onChange(function (newVal) {});
+ map2DFolder.add(map2D, 'roomSizeMin', 1.0, 4.9).onChange(function (newVal) {});
+ map2DFolder.add(map2D, 'roomSizeMax', 1.1, 5.0).onChange(function (newVal) {});
+ map2DFolder.add(map2D, 'connectivity', 0.1, 0.9).onChange(function (newVal) {});
+ map2DFolder.add(map2D, 'walkWayWidth', 2.0, 6.0).onChange(function (newVal) {});
+ map2DFolder.add(map2D, 'crumbleStatus', 0.35, 0.9).onChange(function (newVal) {
+ map2D.crumbleStatus = map2D.crumbleStatus;
+ });
+
+ var fog = tweaks.addFolder('Fog');
+ fog.add(generalParameters, 'Fog').onChange(function (newVal) {
+
+ if (newVal) {
+ renderer.setClearColor(generalParameters.fog_Col);
+ } else {
+ renderer.setClearColor(0x000000);
+ }
+
+ pathMat.uniforms.fogSwitch.value = newVal;
+ slabMat.uniforms.fogSwitch.value = newVal;
+ for (var i = 0; i < levelLayers.length; i++) {
+ if (levelLayers[i].instancedWalkwayMaterial.uniforms.fogSwitch) {
+ levelLayers[i].instancedWalkwayMaterial.uniforms.fogSwitch.value = newVal;
+ }
+ }
+
+ //terrain
+ for (var i = 0; i < levelLayers.length; i++) {
+ for (var j = 0; j < levelLayers[i].cellList.length; j++) {
+ var cell = levelLayers[i].cellList[j];
+ if (cell.mountainMaterial.uniforms.fogSwitch) {
+ cell.mountainMaterial.uniforms.fogSwitch.value = newVal;
+ }
+ }
+ }
+ });
+
+ fog.add(generalParameters, 'FogDensity', 0.0000001, 0.15).onChange(function (newVal) {
+ pathMat.uniforms.fogDensity.value = newVal;
+ slabMat.uniforms.fogDensity.value = newVal;
+ for (var i = 0; i < levelLayers.length; i++) {
+ if (levelLayers[i].instancedWalkwayMaterial.uniforms.fogDensity) {
+ levelLayers[i].instancedWalkwayMaterial.uniforms.fogDensity.value = newVal;
+ }
+ }
+
+ //terrain
+ for (var i = 0; i < levelLayers.length; i++) {
+ for (var j = 0; j < levelLayers[i].cellList.length; j++) {
+ var cell = levelLayers[i].cellList[j];
+ if (cell.mountainMaterial.uniforms.fogDensity) {
+ cell.mountainMaterial.uniforms.fogDensity.value = newVal;
+ }
+ }
+ }
+ });
+
+ // fog.add(fogParameters, 'color', ).onChange(function(newVal) {
+ // generalParameters.fog_Col = color;
+ // });
+
+ // gui.add(generalParameters, 'Collisions').onChange(function(newVal) {});
+
+ var text = new TextActions(scene);
+ gui.add(text, 'NewLevel');
+
+ tweaks.closed = false;
+ level3DFolder.closed = false;
+ map2DFolder.closed = false;
+ fog.closed = false;
+ }
+
+ function setupLightsandSkybox(scene, camera, renderer) {
+ // Set light
+ directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.color.setHSL(0.1, 1, 0.95);
+ directionalLight.position.set(0, 1, 0);
+ directionalLight.position.multiplyScalar(10);
+ scene.add(directionalLight);
+
+ if (generalParameters.Fog) {
+ renderer.setClearColor(generalParameters.fog_Col);
+ } else {
+ renderer.setClearColor(0x000000);
+ }
+
+ // set camera position
+ camera.position.set(33, 7, 50);
+ camera.lookAt(new THREE.Vector3(50, 5, 50));
+ }
+
+ function onreset(scene) {
+ cleanscene(scene);
+ initgrid();
+ create3DMap(scene);
+ createTerrain(scene);
+ setMaterialValues();
+ }
+
+ function cleanscene(scene) {
+ //remove all objects from the scene
+ for (var i = scene.children.length - 1; i >= 0; i--) {
+ var obj = scene.children[i];
+ scene.remove(obj);
+ }
+
+ //remove instanced objects separately, cause threejs's scene doesn't contain it as a child
+ for (var i = 0; i < levelLayers.length; i++) {
+ scene.remove(levelLayers[i].instancedWalkway);
+ }
+ }
+
+ //------------------------------------------------------------------------------
+
+ function initwalkwayGeo(scene, walkwayGeo, walkwayMat) {
+ //define and set attributes of the instanced walkway
+ // geometry
+ var instances = generalParameters.maxInstanceCount;
+ // per mesh data
+ var vertices = new THREE.BufferAttribute(new Float32Array([
+ // Front
+ -1, 1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1,
+ // Back
+ 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1,
+ // Left
+ -1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, 1,
+ // Right
+ 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1,
+ // Top
+ -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, -1,
+ // Bottom
+ 1, -1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1]), 3);
+ walkwayGeo.addAttribute('position', vertices);
+
+ var normals = new THREE.BufferAttribute(new Float32Array([
+ // Front
+ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
+ // Back
+ 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
+ // Left
+ -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0,
+ // Right
+ 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
+ // Top
+ 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
+ // Bottom
+ 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0]), 3);
+ walkwayGeo.addAttribute('normal', normals);
+
+ var uvs = new THREE.BufferAttribute(new Float32Array([
+ //x y z
+ // Front
+ 0, 0, 1, 0, 0, 1, 1, 1,
+ // Back
+ 1, 0, 0, 0, 1, 1, 0, 1,
+ // Left
+ 1, 1, 1, 0, 0, 1, 0, 0,
+ // Right
+ 1, 0, 1, 1, 0, 0, 0, 1,
+ // Top
+ 0, 0, 1, 0, 0, 1, 1, 1,
+ // Bottom
+ 1, 0, 0, 0, 1, 1, 0, 1]), 2);
+ walkwayGeo.addAttribute('uv', uvs);
+ var indices = new Uint16Array([0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7, 8, 9, 10, 10, 9, 11, 12, 13, 14, 14, 13, 15, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23]);
+ walkwayGeo.setIndex(new THREE.BufferAttribute(indices, 1));
+
+ //giving it random positions; -- change later with actual positions
+ // per instance data
+ var offsets = new THREE.InstancedBufferAttribute(new Float32Array(instances * 3), 3, 1);
+ var vector = new THREE.Vector4();
+ for (var i = 0, ul = offsets.count; i < ul; i++) {
+ var x = Math.random() * 100 - 50;
+ var y = Math.random() * 100 - 50;
+ var z = Math.random() * 100 - 50;
+ vector.set(x, y, z, 0);
+ // move out at least 5 units from center in current direction
+ offsets.setXYZ(i, x + vector.x * 5, y + vector.y * 5, z + vector.z * 5);
+ }
+ walkwayGeo.addAttribute('offset', offsets); // per mesh translation
+
+ //scale cubes that form walkway
+ walkwayGeo.scale(generalParameters.voxelsize, generalParameters.voxelsize, generalParameters.voxelsize);
+
+ //creating bounding sphere
+ var boundingSphereCenter = new THREE.Vector3(0, 0, 0);
+ var boundingSphereRadius = 300;
+ walkwayGeo.boundingSphere = new THREE.Sphere(boundingSphereCenter, boundingSphereRadius);
+
+ //create mesh
+ return new THREE.Mesh(walkwayGeo, walkwayMat);
+ }
+
+ function setWalkWayVoxels(walkwayMesh, walkway) {
+ var offsets = walkwayMesh.geometry.getAttribute("offset");
+ walkwayMesh.geometry.maxInstancedCount = walkway.length;
+
+ for (var i = 0; i < walkway.length; i++) {
+ var x = walkway[i].x;
+ var y = walkway[i].y;
+ var z = walkway[i].z;
+ offsets.setXYZ(i, x, y, z);
+ }
+ }
+
+ //------------------------------------------------------------------------------
+
+ function cellCreateHelper(cellList, centx, centz, w, l, flag, spacing) {
+ var size = cellList.length;
+ if (size != 0) {
+ var currRadius = Math.sqrt(w * w + l * l) * 0.5;
+ var counter = 0;
+
+ for (var j = 0; j < size; j++) {
+ var cent = new THREE.Vector2(centx, centz);
+ var cent2 = new THREE.Vector2(cellList[j].center.x, cellList[j].center.z);
+ var r = cellList[j].radius;
+ var currdist = cent.distanceTo(cent2);
+
+ if (currdist > currRadius + r + spacing) {
+ counter++;
+ }
+ }
+
+ if (counter == cellList.length) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function spawn2DCells(scene, cellList, floorHeight) {
+ var count = 0;
+ var count1 = 0;
+ var roomscale = 2.8;
+ var spacing = 3 * roomscale;
+
+ while (count < map2D.numberOfCells) {
+ count1++;
+ if (count1 > 100) {
+ break;
+ }
+
+ var flag_create = true;
+
+ var centx = RAND.random() * 150;
+ var centz = RAND.random() * 150;
+
+ var w = (map2D.roomSizeMin + RAND.random() * (map2D.roomSizeMax - map2D.roomSizeMin)) * roomscale;
+ var l = (map2D.roomSizeMin + RAND.random() * (map2D.roomSizeMax - map2D.roomSizeMin)) * roomscale;
+
+ //loop through other cells to see if there is an overlap --> sample and rejection technique
+ flag_create = cellCreateHelper(cellList, centx, centz, w, l, flag_create, spacing);
+
+ if (flag_create) {
+ count++;
+ var box_geo = new THREE.BoxGeometry(w * 2.0, 1, l * 2.0);
+ var slab = new THREE.Mesh(box_geo, slabMat);
+
+ var cent = new THREE.Vector3(centx, floorHeight, centz);
+
+ var cell = new _cell2.default("undetermined", cent, w, l, slab);
+ // cell.emptyslots(map2D.numslots);
+ cellList.push(cell);
+ cellList[cellList.length - 1].drawCell(scene);
+ }
+ }
+
+ //To display the number of cells created and drawn per layer, uncomment below
+ // console.log("number of cells: " + map2D.numberOfCells);
+ // console.log("number of cells drawn: " + cellList.length);
+ }
+
+ //------------------------------------------------------------------------------
+
+ function removeIntersectingLines(linePoints) {
+ for (var i = 0; i < linePoints.length; i += 2) {
+ var p1 = linePoints[i];
+ var p2 = linePoints[i + 1];
+ var m1 = (p2.z - p1.z) / (p2.x - p1.x);
+
+ var _loop = function _loop() {
+ q1 = linePoints[j];
+ q2 = linePoints[j + 1];
+ m2 = (q2.z - q1.z) / (q2.x - q1.x);
+
+
+ if (i == j) {
+ return 'continue';
+ }
+
+ //actually solving
+ X = (q1.z - p1.z + (m1 * p1.x - m2 * q1.x)) / (m1 - m2);
+ Z = m2 * (X - q1.x) + q1.z;
+
+
+ function between(test, a, b) {
+ return a + 0.01 < test && test < b - 0.01 || b + 0.01 < test && test < a - 0.01;
+ }
+
+ function betweenPoints(test, a, b) {
+ return between(test.x, a.x, b.x) && between(test.z, a.z, b.z);
+ }
+
+ pt = {
+ x: X,
+ z: Z
+ };
+
+
+ if (betweenPoints(pt, p1, p2) && betweenPoints(pt, q1, q2)) {
+ //lines intersect
+ //delete one of them
+ linePoints.splice(j, 2);
+ j -= 2;
+ }
+ };
+
+ for (var j = 0; j < linePoints.length; j += 2) {
+ var q1;
+ var q2;
+ var m2;
+ var X;
+ var Z;
+ var pt;
+
+ var _ret = _loop();
+
+ if (_ret === 'continue') continue;
+ }
+ }
+ }
+
+ function removeRandomLines(voronoi) {
+ for (var i = 0; i < voronoi.length; i++) {
+ var cell = voronoi[i];
+ for (var j = 1; j < voronoi[i].edgeEndPoints.length; j++) {
+ if (RAND.random() > map2D.connectivity) {
+ voronoi[i].edgeEndPoints.splice(j, 1);
+ }
+ }
+ }
+ }
+
+ function createWalkWays(pathPoints, walkway, height) {
+ //draw planes instead of line segments that represent walk ways
+ for (var i = 0; i < pathPoints.length; i = i + 2) {
+ var p1 = pathPoints[i];
+ var p2 = pathPoints[i + 1];
+
+ var w = map2D.walkWayWidth;
+
+ var curve = new THREE.SplineCurve([new THREE.Vector2(p1.x, p1.z), new THREE.Vector2(p2.x, p2.z)]);
+
+ var len = p1.distanceTo(p2);
+ var stepsize = generalParameters.voxelsize;
+ var numcurvepoints = len / stepsize;
+ var path = new THREE.Path(curve.getPoints(numcurvepoints + 1));
+ var curvegeo = path.createPointsGeometry(numcurvepoints + 1);
+
+ //create voxelized walkways; will look cooler than solid planes
+ for (var j = 0; j < numcurvepoints; j++) {
+ var curvepos = new THREE.Vector3(curvegeo.vertices[j].x, height, curvegeo.vertices[j].y);
+ var up = new THREE.Vector3(0.0, 1.0, 0.0);
+ var forward = new THREE.Vector3(curvegeo.vertices[j + 1].x - curvegeo.vertices[j].x, 0.0, curvegeo.vertices[j + 1].y - curvegeo.vertices[j].y).normalize();
+ var left = new THREE.Vector3(up.x, up.y, up.z).normalize();
+ left.cross(forward).normalize();
+
+ for (var k = -w * 0.5; k <= w * 0.5; k = k + stepsize * 1.1) {
+ if (RAND.random() > map2D.crumbleStatus) {
+ var perpPos = new THREE.Vector3(curvepos.x, curvepos.y, curvepos.z);
+ var temp = new THREE.Vector3(left.x, left.y, left.z);
+ perpPos.x += temp.x * k;
+ perpPos.z += temp.z * k;
+ walkway.push(perpPos);
+
+ //fill grid
+ // console.log("grid cell filled");
+ fillGridCell(perpPos);
+ }
+ }
+ }
+ }
+ }
+
+ function compareCells(cellA, cellB) {
+ //returns 1 if cellA.center.x > cellB.center.x (if x is equal compare y then z)
+ //returns 0 if cellA.center.x = cellB.center.x
+ //returns -1 if cellA.center.x < cellB.center.x
+ var origin = new THREE.Vector3(0.0, 0.0, 0.0);
+
+ var dist1 = cellA.center.distanceTo(origin);
+ var dist2 = cellB.center.distanceTo(origin);
+
+ if (dist1 > dist2) {
+ return 1;
+ } else if (dist1 < dist2) {
+ return -1;
+ } else if (dist1 == dist2) {
+ return 0;
+ }
+ }
+
+ function createGraph(scene, cellList, voronoi, walkway, height) {
+ //sort cellList by the centers of the cells, sort centers by x (if equal use z)
+ cellList.sort(compareCells);
+
+ //Fake Cheap Traingular Voronoi
+ //create a triangle from the first three points in the cellList. This is the beginning of the fake voronoi
+ var v1 = new _voronoiPoint2.default(cellList[0].center, cellList[1].center, cellList[2].center);
+ var v2 = new _voronoiPoint2.default(cellList[1].center, cellList[2].center, cellList[0].center);
+ var v3 = new _voronoiPoint2.default(cellList[2].center, cellList[0].center, cellList[1].center);
+
+ voronoi.push(v1);
+ voronoi.push(v2);
+ voronoi.push(v3);
+
+ //for each point after that you attach it to the existing triangle by finding the 2 closest points in the fake voronoi
+ for (var i = 3; i < cellList.length; i++) {
+ var p = cellList[i].center;
+ //find the closest vertices in voronoi;
+ var min1index = 0;
+ var min2index = 1;
+ var epoint1 = voronoi[0].point;
+ var epoint2 = voronoi[1].point;
+
+ for (var j = 2; j < voronoi.length; j++) {
+ var currdist = p.distanceTo(voronoi[j].point);
+
+ var min1dist = p.distanceTo(epoint1);
+ var min2dist = p.distanceTo(epoint2);
+
+ if (currdist < min1dist && currdist < min2dist) {
+ min2index = min1index;
+ epoint2 = voronoi[min1index].point;
+ min1index = j;
+ epoint1 = voronoi[j].point;
+ }
+
+ if (!(currdist < min1dist) && currdist < min2dist) {
+ min2index = j;
+ epoint2 = voronoi[j].point;
+ }
+ }
+
+ //form voronoi triangle thing with the three points and push it into voronoi
+ var v = new _voronoiPoint2.default(cellList[i].center, epoint1, epoint2);
+ voronoi.push(v);
+ }
+ //you should have a voronoi which is a list of vertices(points) and an edgelist
+ //(a list of points that together with the vertex of the voronoi element form an edge)
+
+ //draw the edges to visualize it
+ removeRandomLines(voronoi); //so not everything is connected
+
+ var verts = [];
+ for (var i = 0; i < voronoi.length; i++) {
+ for (var j = 0; j < voronoi[i].edgeEndPoints.length; j++) {
+ verts.push(voronoi[i].point);
+ verts.push(voronoi[i].edgeEndPoints[j]);
+ }
+ }
+
+ removeIntersectingLines(verts);
+ createWalkWays(verts, walkway, height);
+
+ // console.log("number of walkways: " + verts.length*0.5);
+ }
+
+ //------------------------------------------------------------------------------
+ //Was using the grid and its related functions for intersections and collisions but it takes too much memory, try a kd tree
+ function initgrid() {
+ grid = new Uint8Array(17600000);
+ }
+
+ function fillGridCell(pos) {
+ //pass in position of voxel; if used to store
+ var indx = Math.floor(pos.x * 2); //Uint8 level
+ var indy = Math.floor(pos.y * 2); //Uint8 level
+ var indz = Math.floor(pos.z * 2); //Uint8 level
+
+ var index = indy * gridsize.x * gridsize.z + indx * gridsize.z + indz;
+ var minpos = new THREE.Vector3(indx * 200, indy * 440, indz * 200);
+ var diff = new THREE.Vector3(pos.x - minpos.x, pos.y - minpos.y, pos.z - minpos.z);
+
+ var indbitx = Math.floor(diff.x / 2); //8 bit level
+ var indbity = Math.floor(diff.y / 2); //8 bit level
+ var indbitz = Math.floor(diff.z / 2); //8 bit level
+
+ grid[index] = 0;
+
+ if (indbity == 1) {
+ if (indbitx == 1) {
+ if (indbitz == 1) {
+ grid[index] = grid[index] + 128; //top, right, front
+ } else {
+ grid[index] = grid[index] + 64; //top, right, back
+ }
+ } else {
+ if (indbitz == 1) {
+ grid[index] = grid[index] + 32; //top, left, front
+ } else {
+ grid[index] = grid[index] + 16; //top, left, back
+ }
+ }
+ } else {
+ if (indbitx == 1) {
+ if (indbitz == 1) {
+ grid[index] = grid[index] + 8; //back, right, front
+ } else {
+ grid[index] = grid[index] + 4; //back, right, back
+ }
+ } else {
+ if (indbitz == 1) {
+ grid[index] = grid[index] + 2; //back, left, front
+ } else {
+ grid[index] = grid[index] + 1; //back, left, back
+ }
+ }
+ }
+ }
+
+ function queryGridCell(pos) {
+ //pass in position of voxel; if used to store
+ var indx = Math.floor(pos.x * 2); //Uint8 level
+ var indy = Math.floor(pos.y * 2); //Uint8 level
+ var indz = Math.floor(pos.z * 2); //Uint8 level
+
+ var index = indy * gridsize.x * gridsize.z + indx * gridsize.z + indz;
+ var retVal = grid[index];
+
+ return retVal;
+ }
+
+ //------------------------------------------------------------------------------
+
+ function pathShifting(c1, c2, currCell, toCell) {
+ var p1 = new THREE.Vector3(c1.x, c1.y, c1.z);
+ var w1 = currCell.cellWidth;
+ var l1 = currCell.cellLength;
+ var p2 = new THREE.Vector3(c2.x, c2.y, c2.z);
+ var w2 = toCell.cellWidth;
+ var l2 = toCell.cellLength;
+
+ var offset = map2D.walkWayWidth * 0.3;
+
+ if (c2.x > c1.x) {
+ //ToCell is to the right of the currCell
+ p2.x = p2.x - w2 + offset;
+ p1.x = p1.x + w1 - offset;
+ } else {
+ //ToCell is to the left of the currCell
+ p2.x = p2.x + w2 - offset;
+ p1.x = p1.x - w1 + offset;
+ }
+
+ if (c2.z < c1.z) {
+ //ToCell is infront(if measured along z axis) of the currCell
+ p2.z = p2.z + l2 - offset;
+ p1.z = p1.z - l1 + offset;
+ } else {
+ //ToCell is behind(if measured along z axis) the currCell
+ p2.z = p2.z - l2 + offset;
+ p1.z = p1.z + l1 - offset;
+ }
+
+ c1.x = p1.x;
+ c1.z = p1.z;
+ c2.x = p2.x;
+ c2.z = p2.z;
+ }
+
+ function removeRandomPaths(verts) {
+ for (var i = 0; i < verts.length; i = i + 2) {
+ if (RAND.random() > level3D.connectivity) {
+ verts.splice(i, 2);
+ }
+ }
+ }
+
+ function f_equals(a, b, epsilon) {
+ if (a > b - epsilon && a < b + epsilon) {
+ return true;
+ }
+ return false;
+ }
+
+ function removeIntersectingPaths(linePoints) {
+ //reseource: https://math.stackexchange.com/questions/28503/how-to-find-intersection-of-two-lines-in-3d
+ for (var i = 0; i < linePoints.length; i = i + 2) {
+ //convert lines to parametric form
+ var A = new THREE.Vector3(linePoints[i].x, linePoints[i].y, linePoints[i].z);
+ var B = new THREE.Vector3(linePoints[i + 1].x, linePoints[i + 1].y, linePoints[i + 1].z);
+
+ for (var j = 0; j < linePoints.length; j += 2) {
+ if (i == j) {
+ continue;
+ }
+
+ var C = new THREE.Vector3(linePoints[j].x, linePoints[j].y, linePoints[j].z);
+ var D = new THREE.Vector3(linePoints[j + 1].x, linePoints[j + 1].y, linePoints[j + 1].z);
+
+ var s_numerator = (A.y - C.y) * (B.x - A.x) + (C.x - A.x) * (B.y - A.y);
+ var s_denominator = (B.x - A.x) * (D.y - C.y) - (D.x - C.x) * (B.y - A.y);
+
+ if (f_equals(s_denominator, 0.0, 0.001)) {
+ //lines don't intersect
+ continue;
+ }
+
+ var s = s_numerator / s_denominator;
+
+ var t_numerator = C.x - A.x + s * (D.x - C.x);
+ var t_denominator = B.x - A.x;
+
+ if (f_equals(t_denominator, 0.0, 0.001)) {
+ //lines don't intersect
+ continue;
+ }
+
+ var t = t_numerator / t_denominator;
+
+ var eq1 = A.z + t * (B.z - A.z);
+ var eq2 = C.z + s * (D.z - C.z);
+
+ if (f_equals(eq1, eq2, 0.00001)) {
+ //lines do intersect, so remove one of the lines
+ linePoints.splice(j, 2);
+ j -= 2;
+ // console.log("removed lines");
+ }
+ }
+ }
+ }
+
+ function repositionPath(p1, p2) {
+ //Doesn't work -- too much memory or computation -- chrome dies
+ //try using a kd tree or a BVH
+ var origin = new THREE.Vector3(p1.x, p1.y, p1.z);
+ var dir = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z); // backwards ray
+ dir = dir.normalize();
+ var dist = p1.distanceTo(p2);
+
+ for (var i = dist * 0.85; i <= dist; i = i++) {
+ var pos = new THREE.Vector3(origin.x + i * dir.x, origin.y + i * dir.y, origin.z + i * dir.z);
+
+ if (queryGridCell(pos) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function createInterConnectingWalkWays(pathPoints, walkway) {
+ //draw planes instead of line segments that represent walk ways
+ for (var i = 0; i < pathPoints.length; i = i + 2) {
+ var p1 = pathPoints[i];
+ var p2 = pathPoints[i + 1];
+
+ var w = map2D.walkWayWidth;
+
+ var len = p1.distanceTo(p2);
+ var stepsize = generalParameters.voxelsize;
+ var numcurvepoints = len / stepsize;
+
+ //Three js is a stupid graphics library --> does not have a get points method for 3d points
+ //hence the stupidity and hackiness below
+ var curve1 = new THREE.SplineCurve([new THREE.Vector2(p1.x, p1.y), new THREE.Vector2(p2.x, p2.y)]);
+
+ var curve2 = new THREE.SplineCurve([new THREE.Vector2(p1.y, p1.z), new THREE.Vector2(p2.y, p2.z)]);
+
+ var path1 = new THREE.Path(curve1.getPoints(numcurvepoints + 1));
+ var curvegeo1 = path1.createPointsGeometry(numcurvepoints + 1);
+
+ var path2 = new THREE.Path(curve2.getPoints(numcurvepoints + 1));
+ var curvegeo2 = path2.createPointsGeometry(numcurvepoints + 1);
+
+ //create voxelized walkways; will look cooler than solid planes
+ for (var j = 0; j < numcurvepoints; j++) {
+ var curvepos = new THREE.Vector3(curvegeo1.vertices[j].x, curvegeo1.vertices[j].y, curvegeo2.vertices[j].y);
+ var up = new THREE.Vector3(0.0, 1.0, 0.0);
+ var forward = new THREE.Vector3(curvegeo1.vertices[j + 1].x - curvegeo1.vertices[j].x, curvegeo2.vertices[j + 1].x - curvegeo2.vertices[j].x, curvegeo2.vertices[j + 1].y - curvegeo2.vertices[j].y).normalize();
+ var left = new THREE.Vector3(up.x, up.y, up.z).normalize();
+ left.cross(forward).normalize();
+
+ for (var k = -w * 0.5; k <= w * 0.5; k = k + 1.1 * stepsize) {
+ if (RAND.random() > map2D.crumbleStatus) {
+ var perpPos = new THREE.Vector3(curvepos.x, curvepos.y, curvepos.z);
+ var temp = new THREE.Vector3(left.x, left.y, left.z);
+ perpPos.x += temp.x * k;
+ perpPos.y += temp.y * k;
+ perpPos.z += temp.z * k;
+ walkway.push(perpPos);
+
+ //fill grid
+ // console.log("grid cell filled");
+ fillGridCell(perpPos);
+ }
+ }
+ }
+ }
+ }
+
+ function interLayerWalkways(walkway) {
+ var index = 0;
+ var verts = [];
+ //for every n randomly chosen slabs connect them to some other slab in the layer above it
+ for (var i = 0; i < level3D.numberOfLayers - 1; i++) {
+ //for every layer
+ //pick an x number of slabs
+ var minWalkways = 2 + map2D.numberOfCells * 0.1;
+ var maxWalkways = 10;
+ var n = minWalkways + RAND.random() * (minWalkways - 1.8); //level3D.numberOfLayers*map2D.numberOfCells;
+
+ if (n > maxWalkways) {
+ n = maxWalkways;
+ }
+
+ for (var j = 0; j < n; j++) {
+ var ind1 = Math.floor(RAND.random() * levelLayers[i].cellList.length);
+ var currCell = levelLayers[i].cellList[ind1];
+
+ var connectableCells = [];
+ //search in the layers above the cell in some radius
+ //cells to the right and towards the camera; -- add thing for other way too
+ for (var k = i + 1; k < Math.min(i + 2, level3D.numberOfLayers); k++) {
+ for (var m = 0; m < levelLayers[k].cellList.length; m++) {
+ var toCell = levelLayers[k].cellList[m];
+
+ if (currCell == toCell) {
+ continue;
+ }
+
+ var dist = currCell.center.distanceTo(toCell.center);
+ var spacing = 12.0;
+ var radius = 23.0 + spacing; //height between layers is 20 and so this has to be greater than 20^2 and then some
+ if (dist > radius) {
+ //create a list of those cells
+ connectableCells.push(toCell);
+ }
+ }
+ }
+
+ //pick a random cell from that list and connect the original cell to the chosen cell
+ var ind2 = Math.floor(RAND.random() * connectableCells.length);
+ var toCell = connectableCells[ind2];
+
+ var p1 = new THREE.Vector3(currCell.center.x, currCell.center.y, currCell.center.z);
+ var p2 = new THREE.Vector3(toCell.center.x, toCell.center.y, toCell.center.z);
+
+ //figure out which direction they walkway goes in and change p1 and p2 by width or length
+ pathShifting(p1, p2, currCell, toCell);
+
+ //below code was to remove an edge case where paths intersect with walkways
+ // var conflictingPath = repositionPath(p1, p2);
+ // if( conflictingPath )
+ // {
+ // //the path between 2D layers is attaching to a point on a cell
+ // //that has already been taken by a 2D layer walkway; pick another
+ // // j--;
+ // continue;
+ // }
+
+ verts.push(p1);
+ verts.push(p2);
+ }
+ }
+
+ removeRandomPaths(verts);
+ removeIntersectingPaths(verts);
+ createInterConnectingWalkWays(verts, walkway);
+
+ if (verts.length > 0) {
+ var vec = new THREE.Vector3(0, 0, 0).subVectors(verts[1], verts[0]).normalize().multiplyScalar(10);
+ interlayerwalkwaypos.x = verts[0].x - vec.x;
+ interlayerwalkwaypos.y = verts[0].y - vec.y;
+ interlayerwalkwaypos.z = verts[0].z - vec.z;
+ }
+ }
+
+ function create3DMap(scene) {
+ levelLayers.length = 0;
+ var floorOffset = 20;
+ var h = 0;
+ for (var i = 0; i < level3D.numberOfLayers; i++) {
+ //new layer
+ var layer = new _layer2.default();
+
+ //new set of platforms for layers
+ spawn2DCells(scene, layer.cellList, h);
+ //new set of walkways for layers
+ createGraph(scene, layer.cellList, layer.voronoi, layer.walkway, h);
+
+ //new instanced geometry for all of the walkways
+ var geo = new THREE.InstancedBufferGeometry();
+
+ var mat = new THREE.RawShaderMaterial({
+ uniforms: {
+ ambientLight: {
+ type: "v3",
+ value: new THREE.Vector3(0.2, 0.2, 0.2)
+ },
+ lightVec: {
+ type: "v3",
+ value: new THREE.Vector3(1, 1, 1)
+ },
+ camPos: {
+ type: "v3",
+ value: new THREE.Vector3(10, 10, 10)
+ },
+ fogSwitch: {
+ type: "f",
+ value: 0
+ },
+ fogColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.5, 0.5, 0.5)
+ },
+ fogDensity: {
+ type: "f",
+ value: 0.1
+ },
+ rimColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.5, 0.5, 0.5)
+ },
+ albedo: {
+ type: "v3",
+ value: new THREE.Vector3(RAND.random(), RAND.random(), RAND.random())
+ }
+ },
+ vertexShader: __webpack_require__(21),
+ fragmentShader: __webpack_require__(22),
+ side: THREE.DoubleSide,
+ transparent: false
+ });
+
+ layer.instancedWalkwayMaterial = mat;
+ layer.instancedWalkway = initwalkwayGeo(scene, geo, layer.instancedWalkwayMaterial);
+ setWalkWayVoxels(layer.instancedWalkway, layer.walkway);
+
+ //add walkway to scene
+ scene.add(layer.instancedWalkway);
+
+ //push layer to list of layers
+ levelLayers.push(layer);
+ h = h + floorOffset;
+ }
+
+ //now connect layers
+ //new geometry and material for between layer connections
+ var geo = new THREE.InstancedBufferGeometry();
+ var mat = pathMat;
+ var walkwayLayer = new _walkwaylayer2.default();
+
+ interLayerWalkways(walkwayLayer.walkway);
+
+ walkwayLayer.instancedWalkway = initwalkwayGeo(scene, geo, mat);
+ setWalkWayVoxels(walkwayLayer.instancedWalkway, walkwayLayer.walkway);
+
+ //add walkway to scene
+ scene.add(walkwayLayer.instancedWalkway);
+ }
+
+ //------------------------------------------------------------------------------
+
+ function createTerrain(scene) {
+ for (var i = 0; i < levelLayers.length; i++) {
+ var level = levelLayers[i];
+ for (var j = 0; j < level.cellList.length; j++) {
+ var cell = level.cellList[j];
+ var center = cell.center;
+ var w = cell.cellWidth;
+ var l = cell.cellLength;
+ var h = 10;
+ var r = cell.radius;
+
+ var mat = new THREE.ShaderMaterial({
+ uniforms: {
+ ambientLight: {
+ type: "v3",
+ value: new THREE.Vector3(0.2, 0.2, 0.2)
+ },
+ lightVec: {
+ type: "v3",
+ value: new THREE.Vector3(1, 1, 1)
+ },
+ camPos: {
+ type: "v3",
+ value: new THREE.Vector3(10, 10, 10)
+ },
+ fogSwitch: {
+ type: "f",
+ value: 0
+ },
+ fogColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.5, 0.5, 0.5)
+ },
+ fogDensity: {
+ type: "f",
+ value: 0.1
+ },
+ rimColor: {
+ type: "v3",
+ value: new THREE.Vector3(0.1, 0.1, 0.1)
+ },
+ slabCenter: {
+ type: "v3",
+ value: new THREE.Vector3(center.x, center.y, center.z)
+ },
+ width: {
+ type: "f",
+ value: w
+ },
+ length: {
+ type: "f",
+ value: l
+ },
+ slabRadius: {
+ type: "f",
+ value: r * 2.0
+ }
+ },
+ vertexShader: __webpack_require__(23),
+ fragmentShader: __webpack_require__(24),
+ side: THREE.DoubleSide
+ });
+
+ var plane_geo = new THREE.PlaneGeometry(w * 2.0, l * 2.0, 100, 100);
+ plane_geo.rotateX(0.5 * Pi);
+
+ cell.mountainMaterial = mat;
+ cell.mountain = new THREE.Mesh(plane_geo, mat);
+ cell.mountain.position.set(center.x, center.y + 0.5, center.z);
+ scene.add(cell.mountain);
+ }
+ }
+ }
+
+ //------------------------------------------------------------------------------
+
+ function setMaterialValues() {
+ //interlayer voxels
+ if (pathMat) {
+ pathMat.uniforms.lightVec.value.set(directionalLight.position.x, directionalLight.position.y, directionalLight.position.z);
+
+ pathMat.uniforms.fogColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+ pathMat.uniforms.rimColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+
+ pathMat.uniforms.fogDensity.value = generalParameters.FogDensity;
+ pathMat.uniforms.fogSwitch.value = generalParameters.Fog;
+ }
+
+ //slabs
+ if (slabMat) {
+ slabMat.uniforms.lightVec.value.set(directionalLight.position.x, directionalLight.position.y, directionalLight.position.z);
+
+ slabMat.uniforms.fogColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+ slabMat.uniforms.rimColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+
+ slabMat.uniforms.fogDensity.value = generalParameters.FogDensity;
+ slabMat.uniforms.fogSwitch.value = generalParameters.Fog;
+ }
+
+ //2D layer voxels
+ for (var i = 0; i < levelLayers.length; i++) {
+ if (levelLayers[i].instancedWalkwayMaterial) {
+ levelLayers[i].instancedWalkwayMaterial.uniforms.lightVec.value.set(directionalLight.position.x, directionalLight.position.y, directionalLight.position.z);
+
+ levelLayers[i].instancedWalkwayMaterial.uniforms.fogColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+ levelLayers[i].instancedWalkwayMaterial.uniforms.rimColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+
+ levelLayers[i].instancedWalkwayMaterial.uniforms.fogDensity.value = generalParameters.FogDensity;
+ levelLayers[i].instancedWalkwayMaterial.uniforms.fogSwitch.value = generalParameters.Fog;
+ }
+ }
+
+ //terrain
+ for (var i = 0; i < levelLayers.length; i++) {
+ for (var j = 0; j < levelLayers[i].cellList.length; j++) {
+ var cell = levelLayers[i].cellList[j];
+ if (cell.mountainMaterial) {
+ cell.mountainMaterial.uniforms.lightVec.value.set(directionalLight.position.x, directionalLight.position.y, directionalLight.position.z);
+
+ cell.mountainMaterial.uniforms.fogColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+ cell.mountainMaterial.uniforms.rimColor.value.set(generalParameters.fog_Col.r, generalParameters.fog_Col.g, generalParameters.fog_Col.b);
+
+ cell.mountainMaterial.uniforms.fogDensity.value = generalParameters.FogDensity;
+ cell.mountainMaterial.uniforms.fogSwitch.value = generalParameters.Fog;
+ }
+ }
+ }
+ }
+
+ //------------------------------------------------------------------------------
+
+ // called after the scene loads
+ function onLoad(framework) {
+ var scene = framework.scene;
+ var camera = framework.camera;
+ var renderer = framework.renderer;
+ var gui = framework.gui;
+ var stats = framework.stats;
+
+ setupLightsandSkybox(scene, camera, renderer);
+ changeGUI(gui, camera, scene, renderer);
+
+ initgrid();
+ create3DMap(scene);
+ createTerrain(scene);
+ setMaterialValues();
+
+ // cinematic setting of camera position
+ //camera position near the base of an interlayer walkway
+ var y = 20 * (levelLayers.length - 2) + 7;
+ // var slabcenter = levelLayers[levelLayers.length-2].cellList[0].center;
+ camera.position.set(interlayerwalkwaypos.x, interlayerwalkwaypos.y + 7, interlayerwalkwaypos.z);
+
+ //look at centeroid of all slabs
+ var centroid = new THREE.Vector3(0, 0, 0);
+ var count = 0;
+ for (var i = 0; i < levelLayers.length; i++) {
+ var level = levelLayers[i];
+ for (var j = 0; j < level.cellList.length; j++) {
+ centroid.add(level.cellList[j].center);
+ count++;
+ }
+ }
+ centroid.divideScalar(count);
+
+ var lookat = new THREE.Vector3(centroid.x, centroid.y, centroid.z);
+ camera.lookAt(lookat);
+ framework.controls.target.set(lookat.x, lookat.y, lookat.z);
+ }
+
+ // called on frame updates
+ function onUpdate(framework) {
+ //interlayer voxels
+ if (pathMat.uniforms.camPos) {
+ pathMat.uniforms.camPos.value.set(framework.camera.position.x, framework.camera.position.y, framework.camera.position.z);
+ }
+
+ //slabs
+ if (slabMat.uniforms.camPos) {
+ slabMat.uniforms.camPos.value.set(framework.camera.position.x, framework.camera.position.y, framework.camera.position.z);
+ }
+
+ //2D layer voxels
+ for (var i = 0; i < levelLayers.length; i++) {
+ if (levelLayers[i].instancedWalkwayMaterial.uniforms.camPos) {
+ levelLayers[i].instancedWalkwayMaterial.uniforms.camPos.value.set(framework.camera.position.x, framework.camera.position.y, framework.camera.position.z);
+ }
+ }
+
+ //terrain
+ for (var i = 0; i < levelLayers.length; i++) {
+ for (var j = 0; j < levelLayers[i].cellList.length; j++) {
+ var cell = levelLayers[i].cellList[j];
+ if (cell.mountainMaterial.uniforms.camPos) {
+ cell.mountainMaterial.uniforms.camPos.value.set(framework.camera.position.x, framework.camera.position.y, framework.camera.position.z);
+ }
+ }
+ }
+ }
+
+ // when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate
+ _framework2.default.init(onLoad, onUpdate);
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+
+ var _statsJs = __webpack_require__(2);
+
+ var _statsJs2 = _interopRequireDefault(_statsJs);
+
+ var _datGui = __webpack_require__(3);
+
+ var _datGui2 = _interopRequireDefault(_datGui);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+ var THREE = __webpack_require__(6);
+ var OrbitControls = __webpack_require__(7)(THREE);
+
+
+ // when the scene is done initializing, the function passed as `callback` will be executed
+ // then, every frame, the function passed as `update` will be executed
+ function init(callback, update) {
+ var stats = new _statsJs2.default();
+ stats.setMode(1);
+ stats.domElement.style.position = 'absolute';
+ stats.domElement.style.left = '0px';
+ stats.domElement.style.top = '0px';
+ document.body.appendChild(stats.domElement);
+
+ var gui = new _datGui2.default.GUI();
+
+ var framework = {
+ gui: gui,
+ stats: stats
+ };
+
+ // run this function after the window loads
+ window.addEventListener('load', function () {
+
+ var scene = new THREE.Scene();
+ var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+ var renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setPixelRatio(window.devicePixelRatio);
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setClearColor(0x020202, 0);
+
+ var controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.enableZoom = true;
+ controls.target.set(50, 0, 50);
+ controls.rotateSpeed = 0.3;
+ controls.zoomSpeed = 1.0;
+ controls.panSpeed = 2.0;
+
+ document.body.appendChild(renderer.domElement);
+
+ // resize the canvas when the window changes
+ window.addEventListener('resize', function () {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ }, false);
+
+ // assign THREE.js objects to the object we will return
+ framework.scene = scene;
+ framework.camera = camera;
+ framework.renderer = renderer;
+ framework.controls = controls;
+
+ // begin the animation loop
+ (function tick() {
+ stats.begin();
+ update(framework); // perform any requested updates
+ renderer.render(scene, camera); // render the scene
+ stats.end();
+ requestAnimationFrame(tick); // register to call this again when the browser renders a new frame
+ })();
+
+ // we will pass the scene, gui, renderer, camera, etc... to the callback function
+ return callback(framework);
+ });
+ }
+
+ exports.default = {
+ init: init
+ };
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+ // stats.js - http://github.com/mrdoob/stats.js
+ var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";
+ i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div");
+ k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display=
+ "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:12,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height=
+ a+"px",m=b,r=0);return b},update:function(){l=this.end()}}};"object"===typeof module&&(module.exports=Stats);
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ module.exports = __webpack_require__(4)
+ module.exports.color = __webpack_require__(5)
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+ /**
+ * dat-gui JavaScript Controller Library
+ * http://code.google.com/p/dat-gui
+ *
+ * Copyright 2011 Data Arts Team, Google Creative Lab
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+ /** @namespace */
+ var dat = module.exports = dat || {};
+
+ /** @namespace */
+ dat.gui = dat.gui || {};
+
+ /** @namespace */
+ dat.utils = dat.utils || {};
+
+ /** @namespace */
+ dat.controllers = dat.controllers || {};
+
+ /** @namespace */
+ dat.dom = dat.dom || {};
+
+ /** @namespace */
+ dat.color = dat.color || {};
+
+ dat.utils.css = (function () {
+ return {
+ load: function (url, doc) {
+ doc = doc || document;
+ var link = doc.createElement('link');
+ link.type = 'text/css';
+ link.rel = 'stylesheet';
+ link.href = url;
+ doc.getElementsByTagName('head')[0].appendChild(link);
+ },
+ inject: function(css, doc) {
+ doc = doc || document;
+ var injected = document.createElement('style');
+ injected.type = 'text/css';
+ injected.innerHTML = css;
+ doc.getElementsByTagName('head')[0].appendChild(injected);
+ }
+ }
+ })();
+
+
+ dat.utils.common = (function () {
+
+ var ARR_EACH = Array.prototype.forEach;
+ var ARR_SLICE = Array.prototype.slice;
+
+ /**
+ * Band-aid methods for things that should be a lot easier in JavaScript.
+ * Implementation and structure inspired by underscore.js
+ * http://documentcloud.github.com/underscore/
+ */
+
+ return {
+
+ BREAK: {},
+
+ extend: function(target) {
+
+ this.each(ARR_SLICE.call(arguments, 1), function(obj) {
+
+ for (var key in obj)
+ if (!this.isUndefined(obj[key]))
+ target[key] = obj[key];
+
+ }, this);
+
+ return target;
+
+ },
+
+ defaults: function(target) {
+
+ this.each(ARR_SLICE.call(arguments, 1), function(obj) {
+
+ for (var key in obj)
+ if (this.isUndefined(target[key]))
+ target[key] = obj[key];
+
+ }, this);
+
+ return target;
+
+ },
+
+ compose: function() {
+ var toCall = ARR_SLICE.call(arguments);
+ return function() {
+ var args = ARR_SLICE.call(arguments);
+ for (var i = toCall.length -1; i >= 0; i--) {
+ args = [toCall[i].apply(this, args)];
+ }
+ return args[0];
+ }
+ },
+
+ each: function(obj, itr, scope) {
+
+
+ if (ARR_EACH && obj.forEach === ARR_EACH) {
+
+ obj.forEach(itr, scope);
+
+ } else if (obj.length === obj.length + 0) { // Is number but not NaN
+
+ for (var key = 0, l = obj.length; key < l; key++)
+ if (key in obj && itr.call(scope, obj[key], key) === this.BREAK)
+ return;
+
+ } else {
+
+ for (var key in obj)
+ if (itr.call(scope, obj[key], key) === this.BREAK)
+ return;
+
+ }
+
+ },
+
+ defer: function(fnc) {
+ setTimeout(fnc, 0);
+ },
+
+ toArray: function(obj) {
+ if (obj.toArray) return obj.toArray();
+ return ARR_SLICE.call(obj);
+ },
+
+ isUndefined: function(obj) {
+ return obj === undefined;
+ },
+
+ isNull: function(obj) {
+ return obj === null;
+ },
+
+ isNaN: function(obj) {
+ return obj !== obj;
+ },
+
+ isArray: Array.isArray || function(obj) {
+ return obj.constructor === Array;
+ },
+
+ isObject: function(obj) {
+ return obj === Object(obj);
+ },
+
+ isNumber: function(obj) {
+ return obj === obj+0;
+ },
+
+ isString: function(obj) {
+ return obj === obj+'';
+ },
+
+ isBoolean: function(obj) {
+ return obj === false || obj === true;
+ },
+
+ isFunction: function(obj) {
+ return Object.prototype.toString.call(obj) === '[object Function]';
+ }
+
+ };
+
+ })();
+
+
+ dat.controllers.Controller = (function (common) {
+
+ /**
+ * @class An "abstract" class that represents a given property of an object.
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ *
+ * @member dat.controllers
+ */
+ var Controller = function(object, property) {
+
+ this.initialValue = object[property];
+
+ /**
+ * Those who extend this class will put their DOM elements in here.
+ * @type {DOMElement}
+ */
+ this.domElement = document.createElement('div');
+
+ /**
+ * The object to manipulate
+ * @type {Object}
+ */
+ this.object = object;
+
+ /**
+ * The name of the property to manipulate
+ * @type {String}
+ */
+ this.property = property;
+
+ /**
+ * The function to be called on change.
+ * @type {Function}
+ * @ignore
+ */
+ this.__onChange = undefined;
+
+ /**
+ * The function to be called on finishing change.
+ * @type {Function}
+ * @ignore
+ */
+ this.__onFinishChange = undefined;
+
+ };
+
+ common.extend(
+
+ Controller.prototype,
+
+ /** @lends dat.controllers.Controller.prototype */
+ {
+
+ /**
+ * Specify that a function fire every time someone changes the value with
+ * this Controller.
+ *
+ * @param {Function} fnc This function will be called whenever the value
+ * is modified via this Controller.
+ * @returns {dat.controllers.Controller} this
+ */
+ onChange: function(fnc) {
+ this.__onChange = fnc;
+ return this;
+ },
+
+ /**
+ * Specify that a function fire every time someone "finishes" changing
+ * the value wih this Controller. Useful for values that change
+ * incrementally like numbers or strings.
+ *
+ * @param {Function} fnc This function will be called whenever
+ * someone "finishes" changing the value via this Controller.
+ * @returns {dat.controllers.Controller} this
+ */
+ onFinishChange: function(fnc) {
+ this.__onFinishChange = fnc;
+ return this;
+ },
+
+ /**
+ * Change the value of object[property]
+ *
+ * @param {Object} newValue The new value of object[property]
+ */
+ setValue: function(newValue) {
+ this.object[this.property] = newValue;
+ if (this.__onChange) {
+ this.__onChange.call(this, newValue);
+ }
+ this.updateDisplay();
+ return this;
+ },
+
+ /**
+ * Gets the value of object[property]
+ *
+ * @returns {Object} The current value of object[property]
+ */
+ getValue: function() {
+ return this.object[this.property];
+ },
+
+ /**
+ * Refreshes the visual display of a Controller in order to keep sync
+ * with the object's current value.
+ * @returns {dat.controllers.Controller} this
+ */
+ updateDisplay: function() {
+ return this;
+ },
+
+ /**
+ * @returns {Boolean} true if the value has deviated from initialValue
+ */
+ isModified: function() {
+ return this.initialValue !== this.getValue()
+ }
+
+ }
+
+ );
+
+ return Controller;
+
+
+ })(dat.utils.common);
+
+
+ dat.dom.dom = (function (common) {
+
+ var EVENT_MAP = {
+ 'HTMLEvents': ['change'],
+ 'MouseEvents': ['click','mousemove','mousedown','mouseup', 'mouseover'],
+ 'KeyboardEvents': ['keydown']
+ };
+
+ var EVENT_MAP_INV = {};
+ common.each(EVENT_MAP, function(v, k) {
+ common.each(v, function(e) {
+ EVENT_MAP_INV[e] = k;
+ });
+ });
+
+ var CSS_VALUE_PIXELS = /(\d+(\.\d+)?)px/;
+
+ function cssValueToPixels(val) {
+
+ if (val === '0' || common.isUndefined(val)) return 0;
+
+ var match = val.match(CSS_VALUE_PIXELS);
+
+ if (!common.isNull(match)) {
+ return parseFloat(match[1]);
+ }
+
+ // TODO ...ems? %?
+
+ return 0;
+
+ }
+
+ /**
+ * @namespace
+ * @member dat.dom
+ */
+ var dom = {
+
+ /**
+ *
+ * @param elem
+ * @param selectable
+ */
+ makeSelectable: function(elem, selectable) {
+
+ if (elem === undefined || elem.style === undefined) return;
+
+ elem.onselectstart = selectable ? function() {
+ return false;
+ } : function() {
+ };
+
+ elem.style.MozUserSelect = selectable ? 'auto' : 'none';
+ elem.style.KhtmlUserSelect = selectable ? 'auto' : 'none';
+ elem.unselectable = selectable ? 'on' : 'off';
+
+ },
+
+ /**
+ *
+ * @param elem
+ * @param horizontal
+ * @param vertical
+ */
+ makeFullscreen: function(elem, horizontal, vertical) {
+
+ if (common.isUndefined(horizontal)) horizontal = true;
+ if (common.isUndefined(vertical)) vertical = true;
+
+ elem.style.position = 'absolute';
+
+ if (horizontal) {
+ elem.style.left = 0;
+ elem.style.right = 0;
+ }
+ if (vertical) {
+ elem.style.top = 0;
+ elem.style.bottom = 0;
+ }
+
+ },
+
+ /**
+ *
+ * @param elem
+ * @param eventType
+ * @param params
+ */
+ fakeEvent: function(elem, eventType, params, aux) {
+ params = params || {};
+ var className = EVENT_MAP_INV[eventType];
+ if (!className) {
+ throw new Error('Event type ' + eventType + ' not supported.');
+ }
+ var evt = document.createEvent(className);
+ switch (className) {
+ case 'MouseEvents':
+ var clientX = params.x || params.clientX || 0;
+ var clientY = params.y || params.clientY || 0;
+ evt.initMouseEvent(eventType, params.bubbles || false,
+ params.cancelable || true, window, params.clickCount || 1,
+ 0, //screen X
+ 0, //screen Y
+ clientX, //client X
+ clientY, //client Y
+ false, false, false, false, 0, null);
+ break;
+ case 'KeyboardEvents':
+ var init = evt.initKeyboardEvent || evt.initKeyEvent; // webkit || moz
+ common.defaults(params, {
+ cancelable: true,
+ ctrlKey: false,
+ altKey: false,
+ shiftKey: false,
+ metaKey: false,
+ keyCode: undefined,
+ charCode: undefined
+ });
+ init(eventType, params.bubbles || false,
+ params.cancelable, window,
+ params.ctrlKey, params.altKey,
+ params.shiftKey, params.metaKey,
+ params.keyCode, params.charCode);
+ break;
+ default:
+ evt.initEvent(eventType, params.bubbles || false,
+ params.cancelable || true);
+ break;
+ }
+ common.defaults(evt, aux);
+ elem.dispatchEvent(evt);
+ },
+
+ /**
+ *
+ * @param elem
+ * @param event
+ * @param func
+ * @param bool
+ */
+ bind: function(elem, event, func, bool) {
+ bool = bool || false;
+ if (elem.addEventListener)
+ elem.addEventListener(event, func, bool);
+ else if (elem.attachEvent)
+ elem.attachEvent('on' + event, func);
+ return dom;
+ },
+
+ /**
+ *
+ * @param elem
+ * @param event
+ * @param func
+ * @param bool
+ */
+ unbind: function(elem, event, func, bool) {
+ bool = bool || false;
+ if (elem.removeEventListener)
+ elem.removeEventListener(event, func, bool);
+ else if (elem.detachEvent)
+ elem.detachEvent('on' + event, func);
+ return dom;
+ },
+
+ /**
+ *
+ * @param elem
+ * @param className
+ */
+ addClass: function(elem, className) {
+ if (elem.className === undefined) {
+ elem.className = className;
+ } else if (elem.className !== className) {
+ var classes = elem.className.split(/ +/);
+ if (classes.indexOf(className) == -1) {
+ classes.push(className);
+ elem.className = classes.join(' ').replace(/^\s+/, '').replace(/\s+$/, '');
+ }
+ }
+ return dom;
+ },
+
+ /**
+ *
+ * @param elem
+ * @param className
+ */
+ removeClass: function(elem, className) {
+ if (className) {
+ if (elem.className === undefined) {
+ // elem.className = className;
+ } else if (elem.className === className) {
+ elem.removeAttribute('class');
+ } else {
+ var classes = elem.className.split(/ +/);
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1);
+ elem.className = classes.join(' ');
+ }
+ }
+ } else {
+ elem.className = undefined;
+ }
+ return dom;
+ },
+
+ hasClass: function(elem, className) {
+ return new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)').test(elem.className) || false;
+ },
+
+ /**
+ *
+ * @param elem
+ */
+ getWidth: function(elem) {
+
+ var style = getComputedStyle(elem);
+
+ return cssValueToPixels(style['border-left-width']) +
+ cssValueToPixels(style['border-right-width']) +
+ cssValueToPixels(style['padding-left']) +
+ cssValueToPixels(style['padding-right']) +
+ cssValueToPixels(style['width']);
+ },
+
+ /**
+ *
+ * @param elem
+ */
+ getHeight: function(elem) {
+
+ var style = getComputedStyle(elem);
+
+ return cssValueToPixels(style['border-top-width']) +
+ cssValueToPixels(style['border-bottom-width']) +
+ cssValueToPixels(style['padding-top']) +
+ cssValueToPixels(style['padding-bottom']) +
+ cssValueToPixels(style['height']);
+ },
+
+ /**
+ *
+ * @param elem
+ */
+ getOffset: function(elem) {
+ var offset = {left: 0, top:0};
+ if (elem.offsetParent) {
+ do {
+ offset.left += elem.offsetLeft;
+ offset.top += elem.offsetTop;
+ } while (elem = elem.offsetParent);
+ }
+ return offset;
+ },
+
+ // http://stackoverflow.com/posts/2684561/revisions
+ /**
+ *
+ * @param elem
+ */
+ isActive: function(elem) {
+ return elem === document.activeElement && ( elem.type || elem.href );
+ }
+
+ };
+
+ return dom;
+
+ })(dat.utils.common);
+
+
+ dat.controllers.OptionController = (function (Controller, dom, common) {
+
+ /**
+ * @class Provides a select input to alter the property of an object, using a
+ * list of accepted values.
+ *
+ * @extends dat.controllers.Controller
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ * @param {Object|string[]} options A map of labels to acceptable values, or
+ * a list of acceptable string values.
+ *
+ * @member dat.controllers
+ */
+ var OptionController = function(object, property, options) {
+
+ OptionController.superclass.call(this, object, property);
+
+ var _this = this;
+
+ /**
+ * The drop down menu
+ * @ignore
+ */
+ this.__select = document.createElement('select');
+
+ if (common.isArray(options)) {
+ var map = {};
+ common.each(options, function(element) {
+ map[element] = element;
+ });
+ options = map;
+ }
+
+ common.each(options, function(value, key) {
+
+ var opt = document.createElement('option');
+ opt.innerHTML = key;
+ opt.setAttribute('value', value);
+ _this.__select.appendChild(opt);
+
+ });
+
+ // Acknowledge original value
+ this.updateDisplay();
+
+ dom.bind(this.__select, 'change', function() {
+ var desiredValue = this.options[this.selectedIndex].value;
+ _this.setValue(desiredValue);
+ });
+
+ this.domElement.appendChild(this.__select);
+
+ };
+
+ OptionController.superclass = Controller;
+
+ common.extend(
+
+ OptionController.prototype,
+ Controller.prototype,
+
+ {
+
+ setValue: function(v) {
+ var toReturn = OptionController.superclass.prototype.setValue.call(this, v);
+ if (this.__onFinishChange) {
+ this.__onFinishChange.call(this, this.getValue());
+ }
+ return toReturn;
+ },
+
+ updateDisplay: function() {
+ this.__select.value = this.getValue();
+ return OptionController.superclass.prototype.updateDisplay.call(this);
+ }
+
+ }
+
+ );
+
+ return OptionController;
+
+ })(dat.controllers.Controller,
+ dat.dom.dom,
+ dat.utils.common);
+
+
+ dat.controllers.NumberController = (function (Controller, common) {
+
+ /**
+ * @class Represents a given property of an object that is a number.
+ *
+ * @extends dat.controllers.Controller
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ * @param {Object} [params] Optional parameters
+ * @param {Number} [params.min] Minimum allowed value
+ * @param {Number} [params.max] Maximum allowed value
+ * @param {Number} [params.step] Increment by which to change value
+ *
+ * @member dat.controllers
+ */
+ var NumberController = function(object, property, params) {
+
+ NumberController.superclass.call(this, object, property);
+
+ params = params || {};
+
+ this.__min = params.min;
+ this.__max = params.max;
+ this.__step = params.step;
+
+ if (common.isUndefined(this.__step)) {
+
+ if (this.initialValue == 0) {
+ this.__impliedStep = 1; // What are we, psychics?
+ } else {
+ // Hey Doug, check this out.
+ this.__impliedStep = Math.pow(10, Math.floor(Math.log(this.initialValue)/Math.LN10))/10;
+ }
+
+ } else {
+
+ this.__impliedStep = this.__step;
+
+ }
+
+ this.__precision = numDecimals(this.__impliedStep);
+
+
+ };
+
+ NumberController.superclass = Controller;
+
+ common.extend(
+
+ NumberController.prototype,
+ Controller.prototype,
+
+ /** @lends dat.controllers.NumberController.prototype */
+ {
+
+ setValue: function(v) {
+
+ if (this.__min !== undefined && v < this.__min) {
+ v = this.__min;
+ } else if (this.__max !== undefined && v > this.__max) {
+ v = this.__max;
+ }
+
+ if (this.__step !== undefined && v % this.__step != 0) {
+ v = Math.round(v / this.__step) * this.__step;
+ }
+
+ return NumberController.superclass.prototype.setValue.call(this, v);
+
+ },
+
+ /**
+ * Specify a minimum value for object[property].
+ *
+ * @param {Number} minValue The minimum value for
+ * object[property]
+ * @returns {dat.controllers.NumberController} this
+ */
+ min: function(v) {
+ this.__min = v;
+ return this;
+ },
+
+ /**
+ * Specify a maximum value for object[property].
+ *
+ * @param {Number} maxValue The maximum value for
+ * object[property]
+ * @returns {dat.controllers.NumberController} this
+ */
+ max: function(v) {
+ this.__max = v;
+ return this;
+ },
+
+ /**
+ * Specify a step value that dat.controllers.NumberController
+ * increments by.
+ *
+ * @param {Number} stepValue The step value for
+ * dat.controllers.NumberController
+ * @default if minimum and maximum specified increment is 1% of the
+ * difference otherwise stepValue is 1
+ * @returns {dat.controllers.NumberController} this
+ */
+ step: function(v) {
+ this.__step = v;
+ return this;
+ }
+
+ }
+
+ );
+
+ function numDecimals(x) {
+ x = x.toString();
+ if (x.indexOf('.') > -1) {
+ return x.length - x.indexOf('.') - 1;
+ } else {
+ return 0;
+ }
+ }
+
+ return NumberController;
+
+ })(dat.controllers.Controller,
+ dat.utils.common);
+
+
+ dat.controllers.NumberControllerBox = (function (NumberController, dom, common) {
+
+ /**
+ * @class Represents a given property of an object that is a number and
+ * provides an input element with which to manipulate it.
+ *
+ * @extends dat.controllers.Controller
+ * @extends dat.controllers.NumberController
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ * @param {Object} [params] Optional parameters
+ * @param {Number} [params.min] Minimum allowed value
+ * @param {Number} [params.max] Maximum allowed value
+ * @param {Number} [params.step] Increment by which to change value
+ *
+ * @member dat.controllers
+ */
+ var NumberControllerBox = function(object, property, params) {
+
+ this.__truncationSuspended = false;
+
+ NumberControllerBox.superclass.call(this, object, property, params);
+
+ var _this = this;
+
+ /**
+ * {Number} Previous mouse y position
+ * @ignore
+ */
+ var prev_y;
+
+ this.__input = document.createElement('input');
+ this.__input.setAttribute('type', 'text');
+
+ // Makes it so manually specified values are not truncated.
+
+ dom.bind(this.__input, 'change', onChange);
+ dom.bind(this.__input, 'blur', onBlur);
+ dom.bind(this.__input, 'mousedown', onMouseDown);
+ dom.bind(this.__input, 'keydown', function(e) {
+
+ // When pressing entire, you can be as precise as you want.
+ if (e.keyCode === 13) {
+ _this.__truncationSuspended = true;
+ this.blur();
+ _this.__truncationSuspended = false;
+ }
+
+ });
+
+ function onChange() {
+ var attempted = parseFloat(_this.__input.value);
+ if (!common.isNaN(attempted)) _this.setValue(attempted);
+ }
+
+ function onBlur() {
+ onChange();
+ if (_this.__onFinishChange) {
+ _this.__onFinishChange.call(_this, _this.getValue());
+ }
+ }
+
+ function onMouseDown(e) {
+ dom.bind(window, 'mousemove', onMouseDrag);
+ dom.bind(window, 'mouseup', onMouseUp);
+ prev_y = e.clientY;
+ }
+
+ function onMouseDrag(e) {
+
+ var diff = prev_y - e.clientY;
+ _this.setValue(_this.getValue() + diff * _this.__impliedStep);
+
+ prev_y = e.clientY;
+
+ }
+
+ function onMouseUp() {
+ dom.unbind(window, 'mousemove', onMouseDrag);
+ dom.unbind(window, 'mouseup', onMouseUp);
+ }
+
+ this.updateDisplay();
+
+ this.domElement.appendChild(this.__input);
+
+ };
+
+ NumberControllerBox.superclass = NumberController;
+
+ common.extend(
+
+ NumberControllerBox.prototype,
+ NumberController.prototype,
+
+ {
+
+ updateDisplay: function() {
+
+ this.__input.value = this.__truncationSuspended ? this.getValue() : roundToDecimal(this.getValue(), this.__precision);
+ return NumberControllerBox.superclass.prototype.updateDisplay.call(this);
+ }
+
+ }
+
+ );
+
+ function roundToDecimal(value, decimals) {
+ var tenTo = Math.pow(10, decimals);
+ return Math.round(value * tenTo) / tenTo;
+ }
+
+ return NumberControllerBox;
+
+ })(dat.controllers.NumberController,
+ dat.dom.dom,
+ dat.utils.common);
+
+
+ dat.controllers.NumberControllerSlider = (function (NumberController, dom, css, common, styleSheet) {
+
+ /**
+ * @class Represents a given property of an object that is a number, contains
+ * a minimum and maximum, and provides a slider element with which to
+ * manipulate it. It should be noted that the slider element is made up of
+ * <div> tags, not the html5
+ * <slider> element.
+ *
+ * @extends dat.controllers.Controller
+ * @extends dat.controllers.NumberController
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ * @param {Number} minValue Minimum allowed value
+ * @param {Number} maxValue Maximum allowed value
+ * @param {Number} stepValue Increment by which to change value
+ *
+ * @member dat.controllers
+ */
+ var NumberControllerSlider = function(object, property, min, max, step) {
+
+ NumberControllerSlider.superclass.call(this, object, property, { min: min, max: max, step: step });
+
+ var _this = this;
+
+ this.__background = document.createElement('div');
+ this.__foreground = document.createElement('div');
+
+
+
+ dom.bind(this.__background, 'mousedown', onMouseDown);
+
+ dom.addClass(this.__background, 'slider');
+ dom.addClass(this.__foreground, 'slider-fg');
+
+ function onMouseDown(e) {
+
+ dom.bind(window, 'mousemove', onMouseDrag);
+ dom.bind(window, 'mouseup', onMouseUp);
+
+ onMouseDrag(e);
+ }
+
+ function onMouseDrag(e) {
+
+ e.preventDefault();
+
+ var offset = dom.getOffset(_this.__background);
+ var width = dom.getWidth(_this.__background);
+
+ _this.setValue(
+ map(e.clientX, offset.left, offset.left + width, _this.__min, _this.__max)
+ );
+
+ return false;
+
+ }
+
+ function onMouseUp() {
+ dom.unbind(window, 'mousemove', onMouseDrag);
+ dom.unbind(window, 'mouseup', onMouseUp);
+ if (_this.__onFinishChange) {
+ _this.__onFinishChange.call(_this, _this.getValue());
+ }
+ }
+
+ this.updateDisplay();
+
+ this.__background.appendChild(this.__foreground);
+ this.domElement.appendChild(this.__background);
+
+ };
+
+ NumberControllerSlider.superclass = NumberController;
+
+ /**
+ * Injects default stylesheet for slider elements.
+ */
+ NumberControllerSlider.useDefaultStyles = function() {
+ css.inject(styleSheet);
+ };
+
+ common.extend(
+
+ NumberControllerSlider.prototype,
+ NumberController.prototype,
+
+ {
+
+ updateDisplay: function() {
+ var pct = (this.getValue() - this.__min)/(this.__max - this.__min);
+ this.__foreground.style.width = pct*100+'%';
+ return NumberControllerSlider.superclass.prototype.updateDisplay.call(this);
+ }
+
+ }
+
+
+
+ );
+
+ function map(v, i1, i2, o1, o2) {
+ return o1 + (o2 - o1) * ((v - i1) / (i2 - i1));
+ }
+
+ return NumberControllerSlider;
+
+ })(dat.controllers.NumberController,
+ dat.dom.dom,
+ dat.utils.css,
+ dat.utils.common,
+ ".slider {\n box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);\n height: 1em;\n border-radius: 1em;\n background-color: #eee;\n padding: 0 0.5em;\n overflow: hidden;\n}\n\n.slider-fg {\n padding: 1px 0 2px 0;\n background-color: #aaa;\n height: 1em;\n margin-left: -0.5em;\n padding-right: 0.5em;\n border-radius: 1em 0 0 1em;\n}\n\n.slider-fg:after {\n display: inline-block;\n border-radius: 1em;\n background-color: #fff;\n border: 1px solid #aaa;\n content: '';\n float: right;\n margin-right: -1em;\n margin-top: -1px;\n height: 0.9em;\n width: 0.9em;\n}");
+
+
+ dat.controllers.FunctionController = (function (Controller, dom, common) {
+
+ /**
+ * @class Provides a GUI interface to fire a specified method, a property of an object.
+ *
+ * @extends dat.controllers.Controller
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ *
+ * @member dat.controllers
+ */
+ var FunctionController = function(object, property, text) {
+
+ FunctionController.superclass.call(this, object, property);
+
+ var _this = this;
+
+ this.__button = document.createElement('div');
+ this.__button.innerHTML = text === undefined ? 'Fire' : text;
+ dom.bind(this.__button, 'click', function(e) {
+ e.preventDefault();
+ _this.fire();
+ return false;
+ });
+
+ dom.addClass(this.__button, 'button');
+
+ this.domElement.appendChild(this.__button);
+
+
+ };
+
+ FunctionController.superclass = Controller;
+
+ common.extend(
+
+ FunctionController.prototype,
+ Controller.prototype,
+ {
+
+ fire: function() {
+ if (this.__onChange) {
+ this.__onChange.call(this);
+ }
+ if (this.__onFinishChange) {
+ this.__onFinishChange.call(this, this.getValue());
+ }
+ this.getValue().call(this.object);
+ }
+ }
+
+ );
+
+ return FunctionController;
+
+ })(dat.controllers.Controller,
+ dat.dom.dom,
+ dat.utils.common);
+
+
+ dat.controllers.BooleanController = (function (Controller, dom, common) {
+
+ /**
+ * @class Provides a checkbox input to alter the boolean property of an object.
+ * @extends dat.controllers.Controller
+ *
+ * @param {Object} object The object to be manipulated
+ * @param {string} property The name of the property to be manipulated
+ *
+ * @member dat.controllers
+ */
+ var BooleanController = function(object, property) {
+
+ BooleanController.superclass.call(this, object, property);
+
+ var _this = this;
+ this.__prev = this.getValue();
+
+ this.__checkbox = document.createElement('input');
+ this.__checkbox.setAttribute('type', 'checkbox');
+
+
+ dom.bind(this.__checkbox, 'change', onChange, false);
+
+ this.domElement.appendChild(this.__checkbox);
+
+ // Match original value
+ this.updateDisplay();
+
+ function onChange() {
+ _this.setValue(!_this.__prev);
+ }
+
+ };
+
+ BooleanController.superclass = Controller;
+
+ common.extend(
+
+ BooleanController.prototype,
+ Controller.prototype,
+
+ {
+
+ setValue: function(v) {
+ var toReturn = BooleanController.superclass.prototype.setValue.call(this, v);
+ if (this.__onFinishChange) {
+ this.__onFinishChange.call(this, this.getValue());
+ }
+ this.__prev = this.getValue();
+ return toReturn;
+ },
+
+ updateDisplay: function() {
+
+ if (this.getValue() === true) {
+ this.__checkbox.setAttribute('checked', 'checked');
+ this.__checkbox.checked = true;
+ } else {
+ this.__checkbox.checked = false;
+ }
+
+ return BooleanController.superclass.prototype.updateDisplay.call(this);
+
+ }
+
+
+ }
+
+ );
+
+ return BooleanController;
+
+ })(dat.controllers.Controller,
+ dat.dom.dom,
+ dat.utils.common);
+
+
+ dat.color.toString = (function (common) {
+
+ return function(color) {
+
+ if (color.a == 1 || common.isUndefined(color.a)) {
+
+ var s = color.hex.toString(16);
+ while (s.length < 6) {
+ s = '0' + s;
+ }
+
+ return '#' + s;
+
+ } else {
+
+ return 'rgba(' + Math.round(color.r) + ',' + Math.round(color.g) + ',' + Math.round(color.b) + ',' + color.a + ')';
+
+ }
+
+ }
+
+ })(dat.utils.common);
+
+
+ dat.color.interpret = (function (toString, common) {
+
+ var result, toReturn;
+
+ var interpret = function() {
+
+ toReturn = false;
+
+ var original = arguments.length > 1 ? common.toArray(arguments) : arguments[0];
+
+ common.each(INTERPRETATIONS, function(family) {
+
+ if (family.litmus(original)) {
+
+ common.each(family.conversions, function(conversion, conversionName) {
+
+ result = conversion.read(original);
+
+ if (toReturn === false && result !== false) {
+ toReturn = result;
+ result.conversionName = conversionName;
+ result.conversion = conversion;
+ return common.BREAK;
+
+ }
+
+ });
+
+ return common.BREAK;
+
+ }
+
+ });
+
+ return toReturn;
+
+ };
+
+ var INTERPRETATIONS = [
+
+ // Strings
+ {
+
+ litmus: common.isString,
+
+ conversions: {
+
+ THREE_CHAR_HEX: {
+
+ read: function(original) {
+
+ var test = original.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i);
+ if (test === null) return false;
+
+ return {
+ space: 'HEX',
+ hex: parseInt(
+ '0x' +
+ test[1].toString() + test[1].toString() +
+ test[2].toString() + test[2].toString() +
+ test[3].toString() + test[3].toString())
+ };
+
+ },
+
+ write: toString
+
+ },
+
+ SIX_CHAR_HEX: {
+
+ read: function(original) {
+
+ var test = original.match(/^#([A-F0-9]{6})$/i);
+ if (test === null) return false;
+
+ return {
+ space: 'HEX',
+ hex: parseInt('0x' + test[1].toString())
+ };
+
+ },
+
+ write: toString
+
+ },
+
+ CSS_RGB: {
+
+ read: function(original) {
+
+ var test = original.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/);
+ if (test === null) return false;
+
+ return {
+ space: 'RGB',
+ r: parseFloat(test[1]),
+ g: parseFloat(test[2]),
+ b: parseFloat(test[3])
+ };
+
+ },
+
+ write: toString
+
+ },
+
+ CSS_RGBA: {
+
+ read: function(original) {
+
+ var test = original.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/);
+ if (test === null) return false;
+
+ return {
+ space: 'RGB',
+ r: parseFloat(test[1]),
+ g: parseFloat(test[2]),
+ b: parseFloat(test[3]),
+ a: parseFloat(test[4])
+ };
+
+ },
+
+ write: toString
+
+ }
+
+ }
+
+ },
+
+ // Numbers
+ {
+
+ litmus: common.isNumber,
+
+ conversions: {
+
+ HEX: {
+ read: function(original) {
+ return {
+ space: 'HEX',
+ hex: original,
+ conversionName: 'HEX'
+ }
+ },
+
+ write: function(color) {
+ return color.hex;
+ }
+ }
+
+ }
+
+ },
+
+ // Arrays
+ {
+
+ litmus: common.isArray,
+
+ conversions: {
+
+ RGB_ARRAY: {
+ read: function(original) {
+ if (original.length != 3) return false;
+ return {
+ space: 'RGB',
+ r: original[0],
+ g: original[1],
+ b: original[2]
+ };
+ },
+
+ write: function(color) {
+ return [color.r, color.g, color.b];
+ }
+
+ },
+
+ RGBA_ARRAY: {
+ read: function(original) {
+ if (original.length != 4) return false;
+ return {
+ space: 'RGB',
+ r: original[0],
+ g: original[1],
+ b: original[2],
+ a: original[3]
+ };
+ },
+
+ write: function(color) {
+ return [color.r, color.g, color.b, color.a];
+ }
+
+ }
+
+ }
+
+ },
+
+ // Objects
+ {
+
+ litmus: common.isObject,
+
+ conversions: {
+
+ RGBA_OBJ: {
+ read: function(original) {
+ if (common.isNumber(original.r) &&
+ common.isNumber(original.g) &&
+ common.isNumber(original.b) &&
+ common.isNumber(original.a)) {
+ return {
+ space: 'RGB',
+ r: original.r,
+ g: original.g,
+ b: original.b,
+ a: original.a
+ }
+ }
+ return false;
+ },
+
+ write: function(color) {
+ return {
+ r: color.r,
+ g: color.g,
+ b: color.b,
+ a: color.a
+ }
+ }
+ },
+
+ RGB_OBJ: {
+ read: function(original) {
+ if (common.isNumber(original.r) &&
+ common.isNumber(original.g) &&
+ common.isNumber(original.b)) {
+ return {
+ space: 'RGB',
+ r: original.r,
+ g: original.g,
+ b: original.b
+ }
+ }
+ return false;
+ },
+
+ write: function(color) {
+ return {
+ r: color.r,
+ g: color.g,
+ b: color.b
+ }
+ }
+ },
+
+ HSVA_OBJ: {
+ read: function(original) {
+ if (common.isNumber(original.h) &&
+ common.isNumber(original.s) &&
+ common.isNumber(original.v) &&
+ common.isNumber(original.a)) {
+ return {
+ space: 'HSV',
+ h: original.h,
+ s: original.s,
+ v: original.v,
+ a: original.a
+ }
+ }
+ return false;
+ },
+
+ write: function(color) {
+ return {
+ h: color.h,
+ s: color.s,
+ v: color.v,
+ a: color.a
+ }
+ }
+ },
+
+ HSV_OBJ: {
+ read: function(original) {
+ if (common.isNumber(original.h) &&
+ common.isNumber(original.s) &&
+ common.isNumber(original.v)) {
+ return {
+ space: 'HSV',
+ h: original.h,
+ s: original.s,
+ v: original.v
+ }
+ }
+ return false;
+ },
+
+ write: function(color) {
+ return {
+ h: color.h,
+ s: color.s,
+ v: color.v
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+
+ ];
+
+ return interpret;
+
+
+ })(dat.color.toString,
+ dat.utils.common);
+
+
+ dat.GUI = dat.gui.GUI = (function (css, saveDialogueContents, styleSheet, controllerFactory, Controller, BooleanController, FunctionController, NumberControllerBox, NumberControllerSlider, OptionController, ColorController, requestAnimationFrame, CenteredDiv, dom, common) {
+
+ css.inject(styleSheet);
+
+ /** Outer-most className for GUI's */
+ var CSS_NAMESPACE = 'dg';
+
+ var HIDE_KEY_CODE = 72;
+
+ /** The only value shared between the JS and SCSS. Use caution. */
+ var CLOSE_BUTTON_HEIGHT = 20;
+
+ var DEFAULT_DEFAULT_PRESET_NAME = 'Default';
+
+ var SUPPORTS_LOCAL_STORAGE = (function() {
+ try {
+ return 'localStorage' in window && window['localStorage'] !== null;
+ } catch (e) {
+ return false;
+ }
+ })();
+
+ var SAVE_DIALOGUE;
+
+ /** Have we yet to create an autoPlace GUI? */
+ var auto_place_virgin = true;
+
+ /** Fixed position div that auto place GUI's go inside */
+ var auto_place_container;
+
+ /** Are we hiding the GUI's ? */
+ var hide = false;
+
+ /** GUI's which should be hidden */
+ var hideable_guis = [];
+
+ /**
+ * A lightweight controller library for JavaScript. It allows you to easily
+ * manipulate variables and fire functions on the fly.
+ * @class
+ *
+ * @member dat.gui
+ *
+ * @param {Object} [params]
+ * @param {String} [params.name] The name of this GUI.
+ * @param {Object} [params.load] JSON object representing the saved state of
+ * this GUI.
+ * @param {Boolean} [params.auto=true]
+ * @param {dat.gui.GUI} [params.parent] The GUI I'm nested in.
+ * @param {Boolean} [params.closed] If true, starts closed
+ */
+ var GUI = function(params) {
+
+ var _this = this;
+
+ /**
+ * Outermost DOM Element
+ * @type DOMElement
+ */
+ this.domElement = document.createElement('div');
+ this.__ul = document.createElement('ul');
+ this.domElement.appendChild(this.__ul);
+
+ dom.addClass(this.domElement, CSS_NAMESPACE);
+
+ /**
+ * Nested GUI's by name
+ * @ignore
+ */
+ this.__folders = {};
+
+ this.__controllers = [];
+
+ /**
+ * List of objects I'm remembering for save, only used in top level GUI
+ * @ignore
+ */
+ this.__rememberedObjects = [];
+
+ /**
+ * Maps the index of remembered objects to a map of controllers, only used
+ * in top level GUI.
+ *
+ * @private
+ * @ignore
+ *
+ * @example
+ * [
+ * {
+ * propertyName: Controller,
+ * anotherPropertyName: Controller
+ * },
+ * {
+ * propertyName: Controller
+ * }
+ * ]
+ */
+ this.__rememberedObjectIndecesToControllers = [];
+
+ this.__listening = [];
+
+ params = params || {};
+
+ // Default parameters
+ params = common.defaults(params, {
+ autoPlace: true,
+ width: GUI.DEFAULT_WIDTH
+ });
+
+ params = common.defaults(params, {
+ resizable: params.autoPlace,
+ hideable: params.autoPlace
+ });
+
+
+ if (!common.isUndefined(params.load)) {
+
+ // Explicit preset
+ if (params.preset) params.load.preset = params.preset;
+
+ } else {
+
+ params.load = { preset: DEFAULT_DEFAULT_PRESET_NAME };
+
+ }
+
+ if (common.isUndefined(params.parent) && params.hideable) {
+ hideable_guis.push(this);
+ }
+
+ // Only root level GUI's are resizable.
+ params.resizable = common.isUndefined(params.parent) && params.resizable;
+
+
+ if (params.autoPlace && common.isUndefined(params.scrollable)) {
+ params.scrollable = true;
+ }
+ // params.scrollable = common.isUndefined(params.parent) && params.scrollable === true;
+
+ // Not part of params because I don't want people passing this in via
+ // constructor. Should be a 'remembered' value.
+ var use_local_storage =
+ SUPPORTS_LOCAL_STORAGE &&
+ localStorage.getItem(getLocalStorageHash(this, 'isLocal')) === 'true';
+
+ Object.defineProperties(this,
+
+ /** @lends dat.gui.GUI.prototype */
+ {
+
+ /**
+ * The parent GUI
+ * @type dat.gui.GUI
+ */
+ parent: {
+ get: function() {
+ return params.parent;
+ }
+ },
+
+ scrollable: {
+ get: function() {
+ return params.scrollable;
+ }
+ },
+
+ /**
+ * Handles GUI's element placement for you
+ * @type Boolean
+ */
+ autoPlace: {
+ get: function() {
+ return params.autoPlace;
+ }
+ },
+
+ /**
+ * The identifier for a set of saved values
+ * @type String
+ */
+ preset: {
+
+ get: function() {
+ if (_this.parent) {
+ return _this.getRoot().preset;
+ } else {
+ return params.load.preset;
+ }
+ },
+
+ set: function(v) {
+ if (_this.parent) {
+ _this.getRoot().preset = v;
+ } else {
+ params.load.preset = v;
+ }
+ setPresetSelectIndex(this);
+ _this.revert();
+ }
+
+ },
+
+ /**
+ * The width of GUI element
+ * @type Number
+ */
+ width: {
+ get: function() {
+ return params.width;
+ },
+ set: function(v) {
+ params.width = v;
+ setWidth(_this, v);
+ }
+ },
+
+ /**
+ * The name of GUI. Used for folders. i.e
+ * a folder's name
+ * @type String
+ */
+ name: {
+ get: function() {
+ return params.name;
+ },
+ set: function(v) {
+ // TODO Check for collisions among sibling folders
+ params.name = v;
+ if (title_row_name) {
+ title_row_name.innerHTML = params.name;
+ }
+ }
+ },
+
+ /**
+ * Whether the GUI is collapsed or not
+ * @type Boolean
+ */
+ closed: {
+ get: function() {
+ return params.closed;
+ },
+ set: function(v) {
+ params.closed = v;
+ if (params.closed) {
+ dom.addClass(_this.__ul, GUI.CLASS_CLOSED);
+ } else {
+ dom.removeClass(_this.__ul, GUI.CLASS_CLOSED);
+ }
+ // For browsers that aren't going to respect the CSS transition,
+ // Lets just check our height against the window height right off
+ // the bat.
+ this.onResize();
+
+ if (_this.__closeButton) {
+ _this.__closeButton.innerHTML = v ? GUI.TEXT_OPEN : GUI.TEXT_CLOSED;
+ }
+ }
+ },
+
+ /**
+ * Contains all presets
+ * @type Object
+ */
+ load: {
+ get: function() {
+ return params.load;
+ }
+ },
+
+ /**
+ * Determines whether or not to use localStorage as the means for
+ * remembering
+ * @type Boolean
+ */
+ useLocalStorage: {
+
+ get: function() {
+ return use_local_storage;
+ },
+ set: function(bool) {
+ if (SUPPORTS_LOCAL_STORAGE) {
+ use_local_storage = bool;
+ if (bool) {
+ dom.bind(window, 'unload', saveToLocalStorage);
+ } else {
+ dom.unbind(window, 'unload', saveToLocalStorage);
+ }
+ localStorage.setItem(getLocalStorageHash(_this, 'isLocal'), bool);
+ }
+ }
+
+ }
+
+ });
+
+ // Are we a root level GUI?
+ if (common.isUndefined(params.parent)) {
+
+ params.closed = false;
+
+ dom.addClass(this.domElement, GUI.CLASS_MAIN);
+ dom.makeSelectable(this.domElement, false);
+
+ // Are we supposed to be loading locally?
+ if (SUPPORTS_LOCAL_STORAGE) {
+
+ if (use_local_storage) {
+
+ _this.useLocalStorage = true;
+
+ var saved_gui = localStorage.getItem(getLocalStorageHash(this, 'gui'));
+
+ if (saved_gui) {
+ params.load = JSON.parse(saved_gui);
+ }
+
+ }
+
+ }
+
+ this.__closeButton = document.createElement('div');
+ this.__closeButton.innerHTML = GUI.TEXT_CLOSED;
+ dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BUTTON);
+ this.domElement.appendChild(this.__closeButton);
+
+ dom.bind(this.__closeButton, 'click', function() {
+
+ _this.closed = !_this.closed;
+
+
+ });
+
+
+ // Oh, you're a nested GUI!
+ } else {
+
+ if (params.closed === undefined) {
+ params.closed = true;
+ }
+
+ var title_row_name = document.createTextNode(params.name);
+ dom.addClass(title_row_name, 'controller-name');
+
+ var title_row = addRow(_this, title_row_name);
+
+ var on_click_title = function(e) {
+ e.preventDefault();
+ _this.closed = !_this.closed;
+ return false;
+ };
+
+ dom.addClass(this.__ul, GUI.CLASS_CLOSED);
+
+ dom.addClass(title_row, 'title');
+ dom.bind(title_row, 'click', on_click_title);
+
+ if (!params.closed) {
+ this.closed = false;
+ }
+
+ }
+
+ if (params.autoPlace) {
+
+ if (common.isUndefined(params.parent)) {
+
+ if (auto_place_virgin) {
+ auto_place_container = document.createElement('div');
+ dom.addClass(auto_place_container, CSS_NAMESPACE);
+ dom.addClass(auto_place_container, GUI.CLASS_AUTO_PLACE_CONTAINER);
+ document.body.appendChild(auto_place_container);
+ auto_place_virgin = false;
+ }
+
+ // Put it in the dom for you.
+ auto_place_container.appendChild(this.domElement);
+
+ // Apply the auto styles
+ dom.addClass(this.domElement, GUI.CLASS_AUTO_PLACE);
+
+ }
+
+
+ // Make it not elastic.
+ if (!this.parent) setWidth(_this, params.width);
+
+ }
+
+ dom.bind(window, 'resize', function() { _this.onResize() });
+ dom.bind(this.__ul, 'webkitTransitionEnd', function() { _this.onResize(); });
+ dom.bind(this.__ul, 'transitionend', function() { _this.onResize() });
+ dom.bind(this.__ul, 'oTransitionEnd', function() { _this.onResize() });
+ this.onResize();
+
+
+ if (params.resizable) {
+ addResizeHandle(this);
+ }
+
+ function saveToLocalStorage() {
+ localStorage.setItem(getLocalStorageHash(_this, 'gui'), JSON.stringify(_this.getSaveObject()));
+ }
+
+ var root = _this.getRoot();
+ function resetWidth() {
+ var root = _this.getRoot();
+ root.width += 1;
+ common.defer(function() {
+ root.width -= 1;
+ });
+ }
+
+ if (!params.parent) {
+ resetWidth();
+ }
+
+ };
+
+ GUI.toggleHide = function() {
+
+ hide = !hide;
+ common.each(hideable_guis, function(gui) {
+ gui.domElement.style.zIndex = hide ? -999 : 999;
+ gui.domElement.style.opacity = hide ? 0 : 1;
+ });
+ };
+
+ GUI.CLASS_AUTO_PLACE = 'a';
+ GUI.CLASS_AUTO_PLACE_CONTAINER = 'ac';
+ GUI.CLASS_MAIN = 'main';
+ GUI.CLASS_CONTROLLER_ROW = 'cr';
+ GUI.CLASS_TOO_TALL = 'taller-than-window';
+ GUI.CLASS_CLOSED = 'closed';
+ GUI.CLASS_CLOSE_BUTTON = 'close-button';
+ GUI.CLASS_DRAG = 'drag';
+
+ GUI.DEFAULT_WIDTH = 245;
+ GUI.TEXT_CLOSED = 'Close Controls';
+ GUI.TEXT_OPEN = 'Open Controls';
+
+ dom.bind(window, 'keydown', function(e) {
+
+ if (document.activeElement.type !== 'text' &&
+ (e.which === HIDE_KEY_CODE || e.keyCode == HIDE_KEY_CODE)) {
+ GUI.toggleHide();
+ }
+
+ }, false);
+
+ common.extend(
+
+ GUI.prototype,
+
+ /** @lends dat.gui.GUI */
+ {
+
+ /**
+ * @param object
+ * @param property
+ * @returns {dat.controllers.Controller} The new controller that was added.
+ * @instance
+ */
+ add: function(object, property) {
+
+ return add(
+ this,
+ object,
+ property,
+ {
+ factoryArgs: Array.prototype.slice.call(arguments, 2)
+ }
+ );
+
+ },
+
+ /**
+ * @param object
+ * @param property
+ * @returns {dat.controllers.ColorController} The new controller that was added.
+ * @instance
+ */
+ addColor: function(object, property) {
+
+ return add(
+ this,
+ object,
+ property,
+ {
+ color: true
+ }
+ );
+
+ },
+
+ /**
+ * @param controller
+ * @instance
+ */
+ remove: function(controller) {
+
+ // TODO listening?
+ this.__ul.removeChild(controller.__li);
+ this.__controllers.slice(this.__controllers.indexOf(controller), 1);
+ var _this = this;
+ common.defer(function() {
+ _this.onResize();
+ });
+
+ },
+
+ destroy: function() {
+
+ if (this.autoPlace) {
+ auto_place_container.removeChild(this.domElement);
+ }
+
+ },
+
+ /**
+ * @param name
+ * @returns {dat.gui.GUI} The new folder.
+ * @throws {Error} if this GUI already has a folder by the specified
+ * name
+ * @instance
+ */
+ addFolder: function(name) {
+
+ // We have to prevent collisions on names in order to have a key
+ // by which to remember saved values
+ if (this.__folders[name] !== undefined) {
+ throw new Error('You already have a folder in this GUI by the' +
+ ' name "' + name + '"');
+ }
+
+ var new_gui_params = { name: name, parent: this };
+
+ // We need to pass down the autoPlace trait so that we can
+ // attach event listeners to open/close folder actions to
+ // ensure that a scrollbar appears if the window is too short.
+ new_gui_params.autoPlace = this.autoPlace;
+
+ // Do we have saved appearance data for this folder?
+
+ if (this.load && // Anything loaded?
+ this.load.folders && // Was my parent a dead-end?
+ this.load.folders[name]) { // Did daddy remember me?
+
+ // Start me closed if I was closed
+ new_gui_params.closed = this.load.folders[name].closed;
+
+ // Pass down the loaded data
+ new_gui_params.load = this.load.folders[name];
+
+ }
+
+ var gui = new GUI(new_gui_params);
+ this.__folders[name] = gui;
+
+ var li = addRow(this, gui.domElement);
+ dom.addClass(li, 'folder');
+ return gui;
+
+ },
+
+ open: function() {
+ this.closed = false;
+ },
+
+ close: function() {
+ this.closed = true;
+ },
+
+ onResize: function() {
+
+ var root = this.getRoot();
+
+ if (root.scrollable) {
+
+ var top = dom.getOffset(root.__ul).top;
+ var h = 0;
+
+ common.each(root.__ul.childNodes, function(node) {
+ if (! (root.autoPlace && node === root.__save_row))
+ h += dom.getHeight(node);
+ });
+
+ if (window.innerHeight - top - CLOSE_BUTTON_HEIGHT < h) {
+ dom.addClass(root.domElement, GUI.CLASS_TOO_TALL);
+ root.__ul.style.height = window.innerHeight - top - CLOSE_BUTTON_HEIGHT + 'px';
+ } else {
+ dom.removeClass(root.domElement, GUI.CLASS_TOO_TALL);
+ root.__ul.style.height = 'auto';
+ }
+
+ }
+
+ if (root.__resize_handle) {
+ common.defer(function() {
+ root.__resize_handle.style.height = root.__ul.offsetHeight + 'px';
+ });
+ }
+
+ if (root.__closeButton) {
+ root.__closeButton.style.width = root.width + 'px';
+ }
+
+ },
+
+ /**
+ * Mark objects for saving. The order of these objects cannot change as
+ * the GUI grows. When remembering new objects, append them to the end
+ * of the list.
+ *
+ * @param {Object...} objects
+ * @throws {Error} if not called on a top level GUI.
+ * @instance
+ */
+ remember: function() {
+
+ if (common.isUndefined(SAVE_DIALOGUE)) {
+ SAVE_DIALOGUE = new CenteredDiv();
+ SAVE_DIALOGUE.domElement.innerHTML = saveDialogueContents;
+ }
+
+ if (this.parent) {
+ throw new Error("You can only call remember on a top level GUI.");
+ }
+
+ var _this = this;
+
+ common.each(Array.prototype.slice.call(arguments), function(object) {
+ if (_this.__rememberedObjects.length == 0) {
+ addSaveMenu(_this);
+ }
+ if (_this.__rememberedObjects.indexOf(object) == -1) {
+ _this.__rememberedObjects.push(object);
+ }
+ });
+
+ if (this.autoPlace) {
+ // Set save row width
+ setWidth(this, this.width);
+ }
+
+ },
+
+ /**
+ * @returns {dat.gui.GUI} the topmost parent GUI of a nested GUI.
+ * @instance
+ */
+ getRoot: function() {
+ var gui = this;
+ while (gui.parent) {
+ gui = gui.parent;
+ }
+ return gui;
+ },
+
+ /**
+ * @returns {Object} a JSON object representing the current state of
+ * this GUI as well as its remembered properties.
+ * @instance
+ */
+ getSaveObject: function() {
+
+ var toReturn = this.load;
+
+ toReturn.closed = this.closed;
+
+ // Am I remembering any values?
+ if (this.__rememberedObjects.length > 0) {
+
+ toReturn.preset = this.preset;
+
+ if (!toReturn.remembered) {
+ toReturn.remembered = {};
+ }
+
+ toReturn.remembered[this.preset] = getCurrentPreset(this);
+
+ }
+
+ toReturn.folders = {};
+ common.each(this.__folders, function(element, key) {
+ toReturn.folders[key] = element.getSaveObject();
+ });
+
+ return toReturn;
+
+ },
+
+ save: function() {
+
+ if (!this.load.remembered) {
+ this.load.remembered = {};
+ }
+
+ this.load.remembered[this.preset] = getCurrentPreset(this);
+ markPresetModified(this, false);
+
+ },
+
+ saveAs: function(presetName) {
+
+ if (!this.load.remembered) {
+
+ // Retain default values upon first save
+ this.load.remembered = {};
+ this.load.remembered[DEFAULT_DEFAULT_PRESET_NAME] = getCurrentPreset(this, true);
+
+ }
+
+ this.load.remembered[presetName] = getCurrentPreset(this);
+ this.preset = presetName;
+ addPresetOption(this, presetName, true);
+
+ },
+
+ revert: function(gui) {
+
+ common.each(this.__controllers, function(controller) {
+ // Make revert work on Default.
+ if (!this.getRoot().load.remembered) {
+ controller.setValue(controller.initialValue);
+ } else {
+ recallSavedValue(gui || this.getRoot(), controller);
+ }
+ }, this);
+
+ common.each(this.__folders, function(folder) {
+ folder.revert(folder);
+ });
+
+ if (!gui) {
+ markPresetModified(this.getRoot(), false);
+ }
+
+
+ },
+
+ listen: function(controller) {
+
+ var init = this.__listening.length == 0;
+ this.__listening.push(controller);
+ if (init) updateDisplays(this.__listening);
+
+ }
+
+ }
+
+ );
+
+ function add(gui, object, property, params) {
+
+ if (object[property] === undefined) {
+ throw new Error("Object " + object + " has no property \"" + property + "\"");
+ }
+
+ var controller;
+
+ if (params.color) {
+
+ controller = new ColorController(object, property);
+
+ } else {
+
+ var factoryArgs = [object,property].concat(params.factoryArgs);
+ controller = controllerFactory.apply(gui, factoryArgs);
+
+ }
+
+ if (params.before instanceof Controller) {
+ params.before = params.before.__li;
+ }
+
+ recallSavedValue(gui, controller);
+
+ dom.addClass(controller.domElement, 'c');
+
+ var name = document.createElement('span');
+ dom.addClass(name, 'property-name');
+ name.innerHTML = controller.property;
+
+ var container = document.createElement('div');
+ container.appendChild(name);
+ container.appendChild(controller.domElement);
+
+ var li = addRow(gui, container, params.before);
+
+ dom.addClass(li, GUI.CLASS_CONTROLLER_ROW);
+ dom.addClass(li, typeof controller.getValue());
+
+ augmentController(gui, li, controller);
+
+ gui.__controllers.push(controller);
+
+ return controller;
+
+ }
+
+ /**
+ * Add a row to the end of the GUI or before another row.
+ *
+ * @param gui
+ * @param [dom] If specified, inserts the dom content in the new row
+ * @param [liBefore] If specified, places the new row before another row
+ */
+ function addRow(gui, dom, liBefore) {
+ var li = document.createElement('li');
+ if (dom) li.appendChild(dom);
+ if (liBefore) {
+ gui.__ul.insertBefore(li, params.before);
+ } else {
+ gui.__ul.appendChild(li);
+ }
+ gui.onResize();
+ return li;
+ }
+
+ function augmentController(gui, li, controller) {
+
+ controller.__li = li;
+ controller.__gui = gui;
+
+ common.extend(controller, {
+
+ options: function(options) {
+
+ if (arguments.length > 1) {
+ controller.remove();
+
+ return add(
+ gui,
+ controller.object,
+ controller.property,
+ {
+ before: controller.__li.nextElementSibling,
+ factoryArgs: [common.toArray(arguments)]
+ }
+ );
+
+ }
+
+ if (common.isArray(options) || common.isObject(options)) {
+ controller.remove();
+
+ return add(
+ gui,
+ controller.object,
+ controller.property,
+ {
+ before: controller.__li.nextElementSibling,
+ factoryArgs: [options]
+ }
+ );
+
+ }
+
+ },
+
+ name: function(v) {
+ controller.__li.firstElementChild.firstElementChild.innerHTML = v;
+ return controller;
+ },
+
+ listen: function() {
+ controller.__gui.listen(controller);
+ return controller;
+ },
+
+ remove: function() {
+ controller.__gui.remove(controller);
+ return controller;
+ }
+
+ });
+
+ // All sliders should be accompanied by a box.
+ if (controller instanceof NumberControllerSlider) {
+
+ var box = new NumberControllerBox(controller.object, controller.property,
+ { min: controller.__min, max: controller.__max, step: controller.__step });
+
+ common.each(['updateDisplay', 'onChange', 'onFinishChange'], function(method) {
+ var pc = controller[method];
+ var pb = box[method];
+ controller[method] = box[method] = function() {
+ var args = Array.prototype.slice.call(arguments);
+ pc.apply(controller, args);
+ return pb.apply(box, args);
+ }
+ });
+
+ dom.addClass(li, 'has-slider');
+ controller.domElement.insertBefore(box.domElement, controller.domElement.firstElementChild);
+
+ }
+ else if (controller instanceof NumberControllerBox) {
+
+ var r = function(returned) {
+
+ // Have we defined both boundaries?
+ if (common.isNumber(controller.__min) && common.isNumber(controller.__max)) {
+
+ // Well, then lets just replace this with a slider.
+ controller.remove();
+ return add(
+ gui,
+ controller.object,
+ controller.property,
+ {
+ before: controller.__li.nextElementSibling,
+ factoryArgs: [controller.__min, controller.__max, controller.__step]
+ });
+
+ }
+
+ return returned;
+
+ };
+
+ controller.min = common.compose(r, controller.min);
+ controller.max = common.compose(r, controller.max);
+
+ }
+ else if (controller instanceof BooleanController) {
+
+ dom.bind(li, 'click', function() {
+ dom.fakeEvent(controller.__checkbox, 'click');
+ });
+
+ dom.bind(controller.__checkbox, 'click', function(e) {
+ e.stopPropagation(); // Prevents double-toggle
+ })
+
+ }
+ else if (controller instanceof FunctionController) {
+
+ dom.bind(li, 'click', function() {
+ dom.fakeEvent(controller.__button, 'click');
+ });
+
+ dom.bind(li, 'mouseover', function() {
+ dom.addClass(controller.__button, 'hover');
+ });
+
+ dom.bind(li, 'mouseout', function() {
+ dom.removeClass(controller.__button, 'hover');
+ });
+
+ }
+ else if (controller instanceof ColorController) {
+
+ dom.addClass(li, 'color');
+ controller.updateDisplay = common.compose(function(r) {
+ li.style.borderLeftColor = controller.__color.toString();
+ return r;
+ }, controller.updateDisplay);
+
+ controller.updateDisplay();
+
+ }
+
+ controller.setValue = common.compose(function(r) {
+ if (gui.getRoot().__preset_select && controller.isModified()) {
+ markPresetModified(gui.getRoot(), true);
+ }
+ return r;
+ }, controller.setValue);
+
+ }
+
+ function recallSavedValue(gui, controller) {
+
+ // Find the topmost GUI, that's where remembered objects live.
+ var root = gui.getRoot();
+
+ // Does the object we're controlling match anything we've been told to
+ // remember?
+ var matched_index = root.__rememberedObjects.indexOf(controller.object);
+
+ // Why yes, it does!
+ if (matched_index != -1) {
+
+ // Let me fetch a map of controllers for thcommon.isObject.
+ var controller_map =
+ root.__rememberedObjectIndecesToControllers[matched_index];
+
+ // Ohp, I believe this is the first controller we've created for this
+ // object. Lets make the map fresh.
+ if (controller_map === undefined) {
+ controller_map = {};
+ root.__rememberedObjectIndecesToControllers[matched_index] =
+ controller_map;
+ }
+
+ // Keep track of this controller
+ controller_map[controller.property] = controller;
+
+ // Okay, now have we saved any values for this controller?
+ if (root.load && root.load.remembered) {
+
+ var preset_map = root.load.remembered;
+
+ // Which preset are we trying to load?
+ var preset;
+
+ if (preset_map[gui.preset]) {
+
+ preset = preset_map[gui.preset];
+
+ } else if (preset_map[DEFAULT_DEFAULT_PRESET_NAME]) {
+
+ // Uhh, you can have the default instead?
+ preset = preset_map[DEFAULT_DEFAULT_PRESET_NAME];
+
+ } else {
+
+ // Nada.
+
+ return;
+
+ }
+
+
+ // Did the loaded object remember thcommon.isObject?
+ if (preset[matched_index] &&
+
+ // Did we remember this particular property?
+ preset[matched_index][controller.property] !== undefined) {
+
+ // We did remember something for this guy ...
+ var value = preset[matched_index][controller.property];
+
+ // And that's what it is.
+ controller.initialValue = value;
+ controller.setValue(value);
+
+ }
+
+ }
+
+ }
+
+ }
+
+ function getLocalStorageHash(gui, key) {
+ // TODO how does this deal with multiple GUI's?
+ return document.location.href + '.' + key;
+
+ }
+
+ function addSaveMenu(gui) {
+
+ var div = gui.__save_row = document.createElement('li');
+
+ dom.addClass(gui.domElement, 'has-save');
+
+ gui.__ul.insertBefore(div, gui.__ul.firstChild);
+
+ dom.addClass(div, 'save-row');
+
+ var gears = document.createElement('span');
+ gears.innerHTML = ' ';
+ dom.addClass(gears, 'button gears');
+
+ // TODO replace with FunctionController
+ var button = document.createElement('span');
+ button.innerHTML = 'Save';
+ dom.addClass(button, 'button');
+ dom.addClass(button, 'save');
+
+ var button2 = document.createElement('span');
+ button2.innerHTML = 'New';
+ dom.addClass(button2, 'button');
+ dom.addClass(button2, 'save-as');
+
+ var button3 = document.createElement('span');
+ button3.innerHTML = 'Revert';
+ dom.addClass(button3, 'button');
+ dom.addClass(button3, 'revert');
+
+ var select = gui.__preset_select = document.createElement('select');
+
+ if (gui.load && gui.load.remembered) {
+
+ common.each(gui.load.remembered, function(value, key) {
+ addPresetOption(gui, key, key == gui.preset);
+ });
+
+ } else {
+ addPresetOption(gui, DEFAULT_DEFAULT_PRESET_NAME, false);
+ }
+
+ dom.bind(select, 'change', function() {
+
+
+ for (var index = 0; index < gui.__preset_select.length; index++) {
+ gui.__preset_select[index].innerHTML = gui.__preset_select[index].value;
+ }
+
+ gui.preset = this.value;
+
+ });
+
+ div.appendChild(select);
+ div.appendChild(gears);
+ div.appendChild(button);
+ div.appendChild(button2);
+ div.appendChild(button3);
+
+ if (SUPPORTS_LOCAL_STORAGE) {
+
+ var saveLocally = document.getElementById('dg-save-locally');
+ var explain = document.getElementById('dg-local-explain');
+
+ saveLocally.style.display = 'block';
+
+ var localStorageCheckBox = document.getElementById('dg-local-storage');
+
+ if (localStorage.getItem(getLocalStorageHash(gui, 'isLocal')) === 'true') {
+ localStorageCheckBox.setAttribute('checked', 'checked');
+ }
+
+ function showHideExplain() {
+ explain.style.display = gui.useLocalStorage ? 'block' : 'none';
+ }
+
+ showHideExplain();
+
+ // TODO: Use a boolean controller, fool!
+ dom.bind(localStorageCheckBox, 'change', function() {
+ gui.useLocalStorage = !gui.useLocalStorage;
+ showHideExplain();
+ });
+
+ }
+
+ var newConstructorTextArea = document.getElementById('dg-new-constructor');
+
+ dom.bind(newConstructorTextArea, 'keydown', function(e) {
+ if (e.metaKey && (e.which === 67 || e.keyCode == 67)) {
+ SAVE_DIALOGUE.hide();
+ }
+ });
+
+ dom.bind(gears, 'click', function() {
+ newConstructorTextArea.innerHTML = JSON.stringify(gui.getSaveObject(), undefined, 2);
+ SAVE_DIALOGUE.show();
+ newConstructorTextArea.focus();
+ newConstructorTextArea.select();
+ });
+
+ dom.bind(button, 'click', function() {
+ gui.save();
+ });
+
+ dom.bind(button2, 'click', function() {
+ var presetName = prompt('Enter a new preset name.');
+ if (presetName) gui.saveAs(presetName);
+ });
+
+ dom.bind(button3, 'click', function() {
+ gui.revert();
+ });
+
+ // div.appendChild(button2);
+
+ }
+
+ function addResizeHandle(gui) {
+
+ gui.__resize_handle = document.createElement('div');
+
+ common.extend(gui.__resize_handle.style, {
+
+ width: '6px',
+ marginLeft: '-3px',
+ height: '200px',
+ cursor: 'ew-resize',
+ position: 'absolute'
+ // border: '1px solid blue'
+
+ });
+
+ var pmouseX;
+
+ dom.bind(gui.__resize_handle, 'mousedown', dragStart);
+ dom.bind(gui.__closeButton, 'mousedown', dragStart);
+
+ gui.domElement.insertBefore(gui.__resize_handle, gui.domElement.firstElementChild);
+
+ function dragStart(e) {
+
+ e.preventDefault();
+
+ pmouseX = e.clientX;
+
+ dom.addClass(gui.__closeButton, GUI.CLASS_DRAG);
+ dom.bind(window, 'mousemove', drag);
+ dom.bind(window, 'mouseup', dragStop);
+
+ return false;
+
+ }
+
+ function drag(e) {
+
+ e.preventDefault();
+
+ gui.width += pmouseX - e.clientX;
+ gui.onResize();
+ pmouseX = e.clientX;
+
+ return false;
+
+ }
+
+ function dragStop() {
+
+ dom.removeClass(gui.__closeButton, GUI.CLASS_DRAG);
+ dom.unbind(window, 'mousemove', drag);
+ dom.unbind(window, 'mouseup', dragStop);
+
+ }
+
+ }
+
+ function setWidth(gui, w) {
+ gui.domElement.style.width = w + 'px';
+ // Auto placed save-rows are position fixed, so we have to
+ // set the width manually if we want it to bleed to the edge
+ if (gui.__save_row && gui.autoPlace) {
+ gui.__save_row.style.width = w + 'px';
+ }if (gui.__closeButton) {
+ gui.__closeButton.style.width = w + 'px';
+ }
+ }
+
+ function getCurrentPreset(gui, useInitialValues) {
+
+ var toReturn = {};
+
+ // For each object I'm remembering
+ common.each(gui.__rememberedObjects, function(val, index) {
+
+ var saved_values = {};
+
+ // The controllers I've made for thcommon.isObject by property
+ var controller_map =
+ gui.__rememberedObjectIndecesToControllers[index];
+
+ // Remember each value for each property
+ common.each(controller_map, function(controller, property) {
+ saved_values[property] = useInitialValues ? controller.initialValue : controller.getValue();
+ });
+
+ // Save the values for thcommon.isObject
+ toReturn[index] = saved_values;
+
+ });
+
+ return toReturn;
+
+ }
+
+ function addPresetOption(gui, name, setSelected) {
+ var opt = document.createElement('option');
+ opt.innerHTML = name;
+ opt.value = name;
+ gui.__preset_select.appendChild(opt);
+ if (setSelected) {
+ gui.__preset_select.selectedIndex = gui.__preset_select.length - 1;
+ }
+ }
+
+ function setPresetSelectIndex(gui) {
+ for (var index = 0; index < gui.__preset_select.length; index++) {
+ if (gui.__preset_select[index].value == gui.preset) {
+ gui.__preset_select.selectedIndex = index;
+ }
+ }
+ }
+
+ function markPresetModified(gui, modified) {
+ var opt = gui.__preset_select[gui.__preset_select.selectedIndex];
+ // console.log('mark', modified, opt);
+ if (modified) {
+ opt.innerHTML = opt.value + "*";
+ } else {
+ opt.innerHTML = opt.value;
+ }
+ }
+
+ function updateDisplays(controllerArray) {
+
+
+ if (controllerArray.length != 0) {
+
+ requestAnimationFrame(function() {
+ updateDisplays(controllerArray);
+ });
+
+ }
+
+ common.each(controllerArray, function(c) {
+ c.updateDisplay();
+ });
+
+ }
+
+ return GUI;
+
+ })(dat.utils.css,
+ "
\n\n Here's the new load parameter for your GUI's constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI's constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n \n