// // particleExplorer.js // // Created by James B. Pollack @imgntn on 9/26/2015 // Copyright 2015 High Fidelity, Inc. // Web app side of the App - contains GUI. // This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* global window, alert, EventBridge, dat, listenForSettingsUpdates, createVec3Folder, createQuatFolder, writeVec3ToInterface, writeDataToInterface, $, document, _, openEventBridge */ var Settings = function () { this.exportSettings = function () { // copyExportSettingsToClipboard(); showPreselectedPrompt(); }; this.importSettings = function () { importSettings(); }; }; // 2-way bindings-aren't quite ready yet. see bottom of file. var AUTO_UPDATE = false; var UPDATE_ALL_FREQUENCY = 100; var controllers = []; var colpickKeys = []; var folders = []; var gui = null; var settings = new Settings(); var updateInterval; var active = false; var currentInputField; var storedController; // CHANGE TO WHITELIST var keysToAllow = [ 'isEmitting', 'maxParticles', 'lifespan', 'emitRate', 'emitSpeed', 'speedSpread', 'emitOrientation', 'emitDimensions', 'polarStart', 'polarFinish', 'azimuthStart', 'azimuthFinish', 'emitAcceleration', 'accelerationSpread', 'particleRadius', 'radiusSpread', 'radiusStart', 'radiusFinish', 'color', 'colorSpread', 'colorStart', 'colorFinish', 'alpha', 'alphaSpread', 'alphaStart', 'alphaFinish', 'emitterShouldTrail', 'textures' ]; var individualKeys = []; var vec3Keys = []; var quatKeys = []; var colorKeys = []; window.onload = function () { openEventBridge(function () { var stringifiedData = JSON.stringify({ messageType: 'page_loaded' }); EventBridge.emitWebEvent( stringifiedData ); listenForSettingsUpdates(); window.onresize = setGUIWidthToWindowWidth; }); }; function loadGUI() { // whether or not to autoplace gui = new dat.GUI({ autoPlace: false }); // if not autoplacing, put gui in a custom container if (gui.autoPlace === false) { var customContainer = document.getElementById('my-gui-container'); customContainer.appendChild(gui.domElement); } // presets for the GUI itself. a little confusing and import/export is mostly what we want to do at the moment. // gui.remember(settings); colpickKeys = []; var keys = _.keys(settings); _.each(keys, function(key) { var shouldAllow = _.contains(keysToAllow, key); if (shouldAllow) { var subKeys = _.keys(settings[key]); var hasX = _.contains(subKeys, 'x'); var hasY = _.contains(subKeys, 'y'); var hasZ = _.contains(subKeys, 'z'); var hasW = _.contains(subKeys, 'w'); var hasRed = _.contains(subKeys, 'red'); var hasGreen = _.contains(subKeys, 'green'); var hasBlue = _.contains(subKeys, 'blue'); if ((hasX && hasY && hasZ) && hasW === false) { vec3Keys.push(key); } else if (hasX && hasY && hasZ && hasW) { quatKeys.push(key); } else if (hasRed || hasGreen || hasBlue) { colorKeys.push(key); } else { individualKeys.push(key); } } }); // alphabetize our keys individualKeys.sort(); vec3Keys.sort(); quatKeys.sort(); colorKeys.sort(); // add to gui in the order they should appear gui.add(settings, 'importSettings'); gui.add(settings, 'exportSettings'); addIndividualKeys(); addFolders(); // set the gui width to match the web window width gui.width = window.innerWidth; // 2-way binding stuff // if (AUTO_UPDATE) { // setInterval(manuallyUpdateDisplay, UPDATE_ALL_FREQUENCY); // registerDOMElementsForListenerBlocking(); // } } function addIndividualKeys() { _.each(individualKeys, function(key) { // temporary patch for not crashing when this goes below 0 var controller; if (key.indexOf('emitRate') > -1) { controller = gui.add(settings, key).min(0); } else { controller = gui.add(settings, key); } // 2-way - need to fix not being able to input exact values if constantly listening // controller.listen(); // keep track of our controller controllers.push(controller); // hook into change events for this gui controller controller.onChange(function(value) { // Fires on every change, drag, keypress, etc. writeDataToInterface(this.property, value); }); }); } function addFolders() { _.each(colorKeys, function(key) { createColorPicker(key); }); _.each(vec3Keys, function(key) { createVec3Folder(key); }); _.each(quatKeys, function(key) { createQuatFolder(key); }); } function createColorPicker(key) { var colorObject = settings[key]; // Embed colpick's color picker into dat.GUI var name = document.createElement('span'); name.className = 'property-name'; name.innerHTML = key; var container = document.createElement('div'); container.appendChild(name); var $colPickContainer = $('
', { id: key.toString(), class: "color-box" }); $colPickContainer.css('background-color', "rgb(" + colorObject.red + "," + colorObject.green + "," + colorObject.blue + ")"); container.appendChild($colPickContainer[0]); var $li = $('
  • ', { class: 'cr object color' }); $li.append(container); gui.__ul.appendChild($li[0]); gui.onResize(); $colPickContainer.colpick({ colorScheme: 'dark', layout: 'hex', color: { r: colorObject.red, g: colorObject.green, b: colorObject.blue }, onSubmit: function (hsb, hex, rgb, el) { $(el).css('background-color', '#' + hex); $(el).colpickHide(); var obj = {}; obj[key] = { red: rgb.r, green: rgb.g, blue: rgb.b }; writeVec3ToInterface(obj); } }); colpickKeys.push(key); } function createVec3Folder(category) { var folder = gui.addFolder(category); folder.add(settings[category], 'x').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category][this.property] = value; obj[category].y = settings[category].y; obj[category].z = settings[category].z; writeVec3ToInterface(obj); }); folder.add(settings[category], 'y').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category].x = settings[category].x; obj[category][this.property] = value; obj[category].z = settings[category].z; writeVec3ToInterface(obj); }); folder.add(settings[category], 'z').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category].y = settings[category].y; obj[category].x = settings[category].x; obj[category][this.property] = value; writeVec3ToInterface(obj); }); folders.push(folder); folder.open(); } function createQuatFolder(category) { var folder = gui.addFolder(category); folder.add(settings[category], 'x').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category][this.property] = value; obj[category].y = settings[category].y; obj[category].z = settings[category].z; obj[category].w = settings[category].w; writeVec3ToInterface(obj); }); folder.add(settings[category], 'y').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category].x = settings[category].x; obj[category][this.property] = value; obj[category].z = settings[category].z; obj[category].w = settings[category].w; writeVec3ToInterface(obj); }); folder.add(settings[category], 'z').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category].x = settings[category].x; obj[category].y = settings[category].y; obj[category][this.property] = value; obj[category].w = settings[category].w; writeVec3ToInterface(obj); }); folder.add(settings[category], 'w').step(0.1).onChange(function(value) { // Fires when a controller loses focus. var obj = {}; obj[category] = {}; obj[category].x = settings[category].x; obj[category].y = settings[category].y; obj[category].z = settings[category].z; obj[category][this.property] = value; writeVec3ToInterface(obj); }); folders.push(folder); folder.open(); } function convertColorObjectToArray(colorObject) { var colorArray = []; _.each(colorObject, function(singleColor) { colorArray.push(singleColor); }); return colorArray; } function convertColorArrayToObject(colorArray) { var colorObject = { red: colorArray[0], green: colorArray[1], blue: colorArray[2] }; return colorObject; } function writeDataToInterface(property, value) { var data = {}; data[property] = value; var sendData = { messageType: "settings_update", updatedSettings: data }; var stringifiedData = JSON.stringify(sendData); EventBridge.emitWebEvent(stringifiedData); } function writeVec3ToInterface(obj) { var sendData = { messageType: "settings_update", updatedSettings: obj }; var stringifiedData = JSON.stringify(sendData); EventBridge.emitWebEvent(stringifiedData); } function listenForSettingsUpdates() { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.messageType === 'particle_settings') { _.each(data.currentProperties, function(value, key) { settings[key] = {}; settings[key] = value; }); if (gui) { manuallyUpdateDisplay(); } else { loadGUI(); } if (!active) { // gui.toggleHide(); gui.closed = false; } active = true; } else if (data.messageType === "particle_close") { // none of this seems to work. // if (active) { // gui.toggleHide(); // } active = false; gui.closed = true; } }); } function manuallyUpdateDisplay() { // Iterate over all controllers // this is expensive, write a method for indiviudal controllers and use it when the value is different than a cached value, perhaps. var i; for (i in gui.__controllers) { gui.__controllers[i].updateDisplay(); } // Update color pickers for (i in colpickKeys) { var color = settings[colpickKeys[i]]; var $object = $('#' + colpickKeys[i]); $object.css('background-color', "rgb(" + color.red + "," + color.green + "," + color.blue + ")"); $object.colpickSetColor({ r: color.red, g: color.green, b: color.blue }, true); } } function setGUIWidthToWindowWidth() { if (gui !== null) { gui.width = window.innerWidth; } } function handleInputKeyPress(e) { if (e.keyCode === 13) { importSettings(); } return false; } function importSettings() { var importInput = document.getElementById('importer-input'); try { var importedSettings = JSON.parse(importInput.value); var keys = _.keys(importedSettings); _.each(keys, function(key) { var shouldAllow = _.contains(keysToAllow, key); if (!shouldAllow) { return; } settings[key] = importedSettings[key]; }); writeVec3ToInterface(settings); manuallyUpdateDisplay(); } catch (e) { alert('Not properly formatted JSON'); } } function prepareSettingsForExport() { var keys = _.keys(settings); var exportSettings = {}; _.each(keys, function(key) { var shouldAllow = _.contains(keysToAllow, key); if (!shouldAllow) { return; } if (key.indexOf('color') > -1) { var colorObject = convertColorArrayToObject(settings[key]); settings[key] = colorObject; } exportSettings[key] = settings[key]; }); return JSON.stringify(exportSettings, null, 4); } function showPreselectedPrompt() { var elem = document.getElementById("exported-props"); var exportSettings = prepareSettingsForExport(); elem.innerHTML = ""; var buttonnode = document.createElement('input'); buttonnode.setAttribute('type', 'button'); buttonnode.setAttribute('value', 'close'); elem.appendChild(document.createTextNode("COPY THE BELOW FIELD TO CLIPBOARD:")); elem.appendChild(document.createElement("br")); var textAreaNode = document.createElement("textarea"); textAreaNode.value = exportSettings; elem.appendChild(textAreaNode); elem.appendChild(document.createElement("br")); elem.appendChild(buttonnode); buttonnode.onclick = function() { console.log("click"); elem.innerHTML = ""; }; // window.alert("Ctrl-C to copy, then Enter.", prepareSettingsForExport()); } function removeContainerDomElement() { var elem = document.getElementById("my-gui-container"); elem.parentNode.removeChild(elem); } function removeListenerFromGUI(key) { _.each(gui.__listening, function(controller, index) { if (controller.property === key) { storedController = controller; gui.__listening.splice(index, 1); } }); } // the section below is to try to work at achieving two way bindings; function addListenersBackToGUI() { gui.__listening.push(storedController); storedController = null; } function registerDOMElementsForListenerBlocking() { _.each(gui.__controllers, function(controller) { var input = controller.domElement.childNodes[0]; input.addEventListener('focus', function() { console.log('INPUT ELEMENT GOT FOCUS!' + controller.property); removeListenerFromGUI(controller.property); }); }); _.each(gui.__controllers, function(controller) { var input = controller.domElement.childNodes[0]; input.addEventListener('blur', function() { console.log('INPUT ELEMENT GOT BLUR!' + controller.property); addListenersBackToGUI(); }); }); // also listen to inputs inside of folders _.each(gui.__folders, function(folder) { _.each(folder.__controllers, function(controller) { var input = controller.__input; input.addEventListener('focus', function() { console.log('FOLDER ELEMENT GOT FOCUS!' + controller.property); }); }); }); }