diff --git a/README.md b/README.md
index 342e5a3..817dbb3 100644
--- a/README.md
+++ b/README.md
@@ -1,105 +1,29 @@
-# Project 6: Implicit surfaces - Marching cubes
+# Project 6: Marching Cubes
-**Goal:** Implement an isosurface created from metaballs using the marching cubes algorithm.
+[Demo](http://josephgao.me/Project6-MarchingCubes-Implicit-Surfaces/)
-Metaballs are organic-looking n-dimensional objects. We will be implementing a 3-dimensional metaballs. They are great to make bloppy shapes. An isosurface is created whenever the metaball function crosses a certain threshold, called isolevel. The metaball function describes the total influences of each metaball to a given points. A metaball influence is a function between its radius and distance to the point:
+* Author: Joseph Gao
+* PennKey: gaoj
-`f(point) = (radius * radius) / (distance * distance)`
+**Goal:** Create some cool shaders using WebGL.
-By summing up all these influences, you effectively describes all the points that are greater than the isolevel as inside, and less than the isolevel as outside (or vice versa). As an observation, the bigger the metaball's radius is, the bigger its influence is.
+* DISCLAIMER - The metaballs run very slowly. This could be due to my own local environment. The performance is bad enough that I am unable to work on the custom shader part. I will make my pull request now and continue to try and optimize. In addition, the some GUI buttons are still broken! (WIP).
-Marching cubes essentially voxelize the space, then generate triangles based on the density function distribution at the corners of each voxel. By increasing the voxelized grid's resolution, the surface eventually gets that blobby, organic look of the metaballs. Marching cubes can achieve a similar effect to ray marching for rendering implicit surfaces, but in addition to the rendered image, you also retain actual geometries.
+## 90 points worth of materials implemented.
-Marching cubes are commonly used in MRI scanning, where you can generate geometries for the scans. Marching cubes are also used to generate complex terrains with caves in games. The additional geometry information can handily support collision and other physical calculation for game engines. For example, their bounding boxes can then be computed to construct the acceleration data structure for collisions.
+
-**Warning**: this assignment option requires more effort than the ray marching option. The two base codes diverge significantly, so switching options midway can be costly for your time and effort.
+### Animation of Metaballs
+This step was done by simply adding to each metaball position the current velocity components.
-## Resources
-We suggest reading the following resources before starting your assignment:
+### Sampling at Corners
+To accomplish this, I defined some new inspection points and then ran my sampling function at each of these corners. Each voxel box has eight possible corner points.
-- [Generating complex terrain](https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch01.html) from [GPU Gems 3](https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_pref01.html).
-- [Polygonising a scalar field](http://paulbourke.net/geometry/polygonise/) by Paul Bourke.
-- [Marching squares](http://jamie-wong.com/2014/08/19/metaballs-and-marching-squares/) by Jamie Wong.
+### Polygonization
+My code and ideas are heavily based off of Paul Bourke's ideas. I used the tables provided to correctly find the triangles and edges that would be intersected, and wrote my own interpolation function based off of Paul's to figure out exactly where each edge will be cut.
-## Base code framework
+### Meshing
+To do meshing, I first created the mesh with the appropriate shader material, and then I would call polygonize on each voxel and figure out what vertices need to be changed for the mesh to have its adaptive look and feel.
-We have provided a basecode as a reference. You are welcome to modify the framework for your project. The basecode implements metaballs on the CPU.
-
-_main.js_:
-
- - `App`:
-
-This is a global configuration object. All information for the marching cubes are stored here.
-
-**Note**: `App.visualDebug` is a global control of all the visual debugging components. Even though it is helpful for development, it could be memory intensive. Toggle this flag off for better perforamance at high resolution.
-
-_marching_cubes.js_:
-
- - `class MarchingCubes`:
- This class encapsulates everything about the metaballs, grid, voxels, and sampling information.
-
- - `class Voxel`:
- This class contains information about a single voxel, and its sample points. Polygonization happens here.
-
-_inspect_point.js_:
-
- - `class InspectPoint`:
- This class simply contains a single sample point that can output its value on the screen at its pixel location.
-
-_metaball.js_:
-
- - `class Metaball`:
- This class represents a single metaball.
-
-_marching_cube_LUT.js_:
-
-This file contains the edge table and the triangle table for the marching cubes.
-
-## Animate metaballs (5 points)
-Implement the `update` for metaballs to move its position based velocity. Reverse the velocity whenever the metaball goes out of bounds. Since the metaball function is not well defined at the boundaries, maintain an additional small margin so that the metaball can reverse its moving direction before reaching the bounds.
-
-## Metaball function (2 points)
-Implement the metaball function inside `sample` of `MarchingCubes`. This function should return the total influences of all moving metaballs with respect to a given point.
-
-## Sampling at corners (15 points)
-In order to polygonize a voxel, generate new samples at each corner of the voxel. Their isovalues must be updated as the metaball function changes due of metaballs moving.
-
-## Polygonization (50 points)
-Implement `polygonize` inside `Cell` class. This function should return the list of **vertices** and **normals** of the triangles polygonized in the voxel.
-
-### Vertices (30 points out of 50)
-To compute the vertices, we have provided the look-up tables from Paul Bourke's. The table assumes the following indexing scheme:
-
-
-- _The eight corners can be represented as an 8-bit number, where 1 means the isovalue is above or below the isolevel based on your implementation._
-- _The twelve edges can be represented as a 12-bit number, where 1 means that the isosurface intersects with this edge._
-
-- **EDGE_TABLE**: This table returns a 12-bit number that represents the edges intersected by the isosurface. For each intersected edge, compute the linearly interpolated vertex position on the edge according to the isovalue at each end corner of the edge.
-
-- **TRI_TABLE**: This table acts as the triangle indices. Every 16 elements in the table represents a possible polygonizing configuration. Within each configuration, every three consecutive elements represents the indices of a triangle that should be created from the edges above.
-
-### Normals (20 points out of 50)
-Compute the normals using the gradient of the vertex with respect to the x, y, and z. The normals are then used for shading different materials.
-
-## Meshing (18 points)
-The mesh for the metaball's isosurface should be created once. At each frame, using the list of **vertices** and **normals** polygonized from the voxels, update the mesh's geometry for the isosurface. Notice that the total volume of the mesh does change.
-
-## Materials and post-processing (10 points)
-Interesting shader materials beyond just the provided threejs materials. We encourage using your previous shaders assignment for this part.
-
-## Extra credits (Up to 30 points)
-- Metaball can be positive or negative. A negative metaball will substract from the surface, which pushed the surface inward. **Implement a scene with both positive and negative metaballs. (10 points)**
-- **More implicit surfaces!** For example: planes, mesh, etc.). Some very interesting ideas are to blend your metaballs into those surfaces. **(5 points for each)**
-
-## Submission
-
-- Update `README.md` to contain a solid description of your project
-- Publish your project to gh-pages. `npm run deploy`. It should now be visible at http://username.github.io/repo-name
-- Create a [pull request](https://help.github.com/articles/creating-a-pull-request/) to this repository, and in the comment, include a link to your published project.
-- Submit the link to your pull request on Canvas.
-
-## Deploy
-- `npm run build`
-- Add and commit all changes
-- `npm run deploy`
-- If you're having problems with assets not linking correctly, make sure you wrap you're filepaths in `require()`. This will make the bundler package and your static assets as well. So, instead of `loadTexture('./images/thing.bmp')`, do `loadTexture(require('./images/thing.bmp'))`.
\ No newline at end of file
+### Materials and post-processing
+This part I had some trouble with, and it is still a WIP. I will submit a pull request now as to not be too late, and if I have any cool updates I will push to my github pages profile.
diff --git a/build/bundle.js b/build/bundle.js
new file mode 100644
index 0000000..7fda81b
--- /dev/null
+++ b/build/bundle.js
@@ -0,0 +1,48971 @@
+/******/ (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 _marching_cube_LUT = __webpack_require__(8);
+
+ var _marching_cube_LUT2 = _interopRequireDefault(_marching_cube_LUT);
+
+ var _marching_cubes = __webpack_require__(9);
+
+ var _marching_cubes2 = _interopRequireDefault(_marching_cubes);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+ __webpack_require__(14);
+
+ // Credit:
+ // http://jamie-wong.com/2014/08/19/metaballs-and-marching-squares/
+ // http://paulbourke.net/geometry/polygonise/
+
+ var THREE = __webpack_require__(6); // older modules are imported like this. You shouldn't have to worry about this much
+
+ var DEFAULT_VISUAL_DEBUG = true;
+ var DEFAULT_ISO_LEVEL = 0.4;
+ var DEFAULT_GRID_RES = 12;
+ var DEFAULT_GRID_WIDTH = 10;
+ var DEFAULT_NUM_METABALLS = 4;
+ var DEFAULT_MIN_RADIUS = 0.5;
+ var DEFAULT_MAX_RADIUS = 1;
+ var DEFAULT_MAX_SPEED = 0.07;
+ var DEFAULT_SHADER = 'lambert';
+
+ // options
+ var options = {
+ lightColor: '#ffffff',
+ lightIntensity: 2,
+ albedo: '#dddddd',
+ ambient: '#111111'
+ };
+
+ var App = {
+ //
+ marchingCubes: undefined,
+ config: {
+ // Global control of all visual debugging.
+ // This can be set to false to disallow any memory allocation of visual debugging components.
+ // **Note**: If your application experiences performance drop, disable this flag.
+ visualDebug: DEFAULT_VISUAL_DEBUG,
+
+ // The isolevel for marching cubes
+ isolevel: DEFAULT_ISO_LEVEL,
+
+ // Grid resolution in each dimension. If gridRes = 4, then we have a 4x4x4 grid
+ gridRes: DEFAULT_GRID_RES,
+
+ // Total width of grid
+ gridWidth: DEFAULT_GRID_WIDTH,
+
+ // Width of each voxel
+ // Ideally, we want the voxel to be small (higher resolution)
+ gridCellWidth: DEFAULT_GRID_WIDTH / DEFAULT_GRID_RES,
+
+ // Number of metaballs
+ numMetaballs: DEFAULT_NUM_METABALLS,
+
+ // Minimum radius of a metaball
+ minRadius: DEFAULT_MIN_RADIUS,
+
+ // Maxium radius of a metaball
+ maxRadius: DEFAULT_MAX_RADIUS,
+
+ // Maximum speed of a metaball
+ maxSpeed: DEFAULT_MAX_SPEED,
+
+ shader: DEFAULT_SHADER
+ },
+
+ // Scene's framework objects
+ camera: undefined,
+ scene: undefined,
+ renderer: undefined,
+
+ // Play/pause control for the simulation
+ isPaused: false
+ };
+
+ // called after the scene loads
+ function onLoad(framework) {
+ var scene = framework.scene,
+ camera = framework.camera,
+ renderer = framework.renderer,
+ gui = framework.gui,
+ stats = framework.stats;
+
+ App.scene = scene;
+ App.camera = camera;
+ App.renderer = renderer;
+
+ renderer.setClearColor(0xbfd1e5);
+ scene.add(new THREE.AxisHelper(20));
+
+ setupCamera(App.camera);
+ setupLights(App.scene);
+ setupScene(App.scene);
+ setupGUI(gui);
+ }
+
+ // called on frame updates
+ function onUpdate(framework) {
+
+ if (App.marchingCubes) {
+ App.marchingCubes.update();
+ }
+ }
+
+ function setupCamera(camera) {
+ // set camera position
+ camera.position.set(5, 5, 30);
+ camera.lookAt(new THREE.Vector3(0, 0, 0));
+ }
+
+ function setupLights(scene) {
+
+ // Directional light
+ var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.color.setHSL(0.1, 1, 0.95);
+ directionalLight.position.set(1, 10, 2);
+ directionalLight.position.multiplyScalar(10);
+
+ scene.add(directionalLight);
+ }
+
+ function setupScene(scene) {
+ App.marchingCubes = new _marching_cubes2.default(App);
+ }
+
+ function setupGUI(gui) {
+
+ // more information here: https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
+
+ // --- CONFIG ---
+ gui.add(App, 'isPaused').onChange(function (value) {
+ App.isPaused = value;
+ if (value) {
+ App.marchingCubes.pause();
+ } else {
+ App.marchingCubes.play();
+ }
+ });
+
+ gui.add(App.config, 'numMetaballs', 1, 10).onChange(function (value) {
+ App.config.numMetaballs = value;
+ App.marchingCubes.init(App);
+ });
+
+ gui.addColor(options, 'lightColor').onChange(function (val) {
+ _marching_cubes.Shaders[App.config.shader].material.uniforms.u_lightCol.value = new THREE.Color(val);
+ });
+
+ gui.add(options, 'lightIntensity').onChange(function (val) {
+ _marching_cubes.Shaders[App.config.shader].material.uniforms.u_lightIntensity.value = val;
+ });
+
+ gui.addColor(options, 'albedo').onChange(function (val) {
+ _marching_cubes.Shaders[App.config.shader].material.uniforms.u_albedo.value = new THREE.Color(val);
+ });
+
+ gui.addColor(options, 'ambient').onChange(function (val) {
+ _marching_cubes.Shaders[App.config.shader].material.uniforms.u_ambient.value = new THREE.Color(val);
+ });
+
+ gui.add(App.config, 'shader', Object.keys(_marching_cubes.Shaders)).onChange(function (value) {
+ App.marchingCubes.init(App);
+ switch (value) {
+ case 'lambert':
+ App.config.shader = 'lambert';
+ break;
+ case 'toon':
+ App.config.shader = 'lambert';
+ _marching_cubes.Shaders[App.config.shader].uniforms.u_viewPos.value = App.camera.position;
+ break;
+ }
+ });
+
+ // --- DEBUG ---
+
+ var debugFolder = gui.addFolder('Debug');
+ debugFolder.add(App.marchingCubes, 'showGrid').onChange(function (value) {
+ App.marchingCubes.showGrid = value;
+ if (value) {
+ App.marchingCubes.show();
+ } else {
+ App.marchingCubes.hide();
+ }
+ });
+
+ debugFolder.add(App.marchingCubes, 'showSpheres').onChange(function (value) {
+ App.marchingCubes.showSpheres = value;
+ if (value) {
+ for (var i = 0; i < App.config.numMetaballs; i++) {
+ App.marchingCubes.balls[i].show();
+ }
+ } else {
+ for (var i = 0; i < App.config.numMetaballs; i++) {
+ App.marchingCubes.balls[i].hide();
+ }
+ }
+ });
+ debugFolder.open();
+ }
+
+ // 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(0, 0, 0);
+ controls.rotateSpeed = 0.3;
+ controls.zoomSpeed = 1.0;
+ controls.panSpeed = 2.0;
+ controls.addEventListener('change', function () {
+ camera.hasMoved = true;
+ });
+
+ 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;
+
+ // 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