├── .gitignore ├── README.md ├── app ├── DDSLoader.js ├── dat.gui.js ├── index.css ├── index.html ├── renderer.js └── three.js ├── main.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 2 | 3 | # img2dds 4 | 5 | ![Super Duplicate Screenshot](http://makiopolis.com/img2dds.jpg) 6 | 7 | Convert your images into dds texture files and test it into a threejs view directly. 8 | 9 | ## Install 10 | 11 | ```bash 12 | # Install dependencies 13 | npm install 14 | # Start the app 15 | npm start 16 | ``` 17 | 18 | ## Use it 19 | 20 | Drag'n drop your image(s) into the app and it will convert it to the dds (the file is generated into the same folder but the extention change to .dds, you can change the folder path) 21 | 22 | ## Technical note 23 | 24 | The images ~~dimensions need to a power of two and minimum size is 256 : 256x256 / 512x512 / 256x512 / etc..~~ 25 | If the image is not a power of 2 it will be resized to the next power of 2 before convert as DDS texture. 26 | If the image is smaller than 256, it will be resized to 256. 27 | Example : 967x128 -> 1024x256 28 | 29 | The compression of the dds is terrible but on my test I figured out the gzipped version are very close, here my results on 59images with a size from 256 to 2048 (no alpha) : 30 | - jpegs files optimized (imageoptim) : 12.9mo 31 | - jpegs files optimized (imageoptim) gzipped : 12.2mo 32 | - dds files dxt1 : 48mo 33 | - dds files dxt1 gzipped : 12.3mo 34 | - dds files dxt5 : 89.7mo 35 | - dds files dxt5 gzipped : 14.9mo 36 | 37 | The app is build with nodejs / electron / threejs / datgui 38 | 39 | The dds header is build into the app but the dxt compression use node-dxt(squish algorithm) https://github.com/Morhaus/node-dxt 40 | 41 | The dds files are load with the THREE.DDSLoader: https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/DDSLoader.js 42 | 43 | ## Compression dxt options: 44 | - low : super fast poor quality 45 | - normal : fast & good quality 46 | - hight : super slow & best quality 47 | 48 | - DXT1 : this format don't manage the transparency 49 | - DXT3 : transparent and 16bits 50 | - DXT5 : transparent and 16bits (recommended for transparent texture) 51 | 52 | - ColorMetric : perceptual(default) / uniform 53 | - weightColourByAlpha : true / false (default is false) 54 | 55 | ## Trouble shooting 56 | 57 | If you have a problem while compiling, please install the latest nodejs and then "npm rebuild", if the problem persist, thanks to open an issue. 58 | If you have a bug while using the tool, press "d" to open the devTool panel to see the error message, if you can't figure out the problem, thanks to open an issue. 59 | 60 | ## Learn more about DDS & DXT Compressed Texture 61 | 62 | Great article on DDS & DXT : http://beyondskyrim.org/2015/05/26/working-with-dds-files/ 63 | 64 | Microsoft docs : https://msdn.microsoft.com/en-us/library/windows/desktop/bb943991(v=vs.85).aspx 65 | -------------------------------------------------------------------------------- /app/DDSLoader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | const THREE = require('./three.js') 6 | 7 | THREE.DDSLoader = function () { 8 | 9 | this._parser = THREE.DDSLoader.parse; 10 | 11 | }; 12 | 13 | THREE.DDSLoader.prototype = Object.create( THREE.CompressedTextureLoader.prototype ); 14 | THREE.DDSLoader.prototype.constructor = THREE.DDSLoader; 15 | 16 | THREE.DDSLoader.parse = function ( buffer, loadMipmaps ) { 17 | 18 | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 }; 19 | 20 | // Adapted from @toji's DDS utils 21 | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js 22 | 23 | // All values and structures referenced from: 24 | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/ 25 | 26 | var DDS_MAGIC = 0x20534444; 27 | 28 | var DDSD_CAPS = 0x1, 29 | DDSD_HEIGHT = 0x2, 30 | DDSD_WIDTH = 0x4, 31 | DDSD_PITCH = 0x8, 32 | DDSD_PIXELFORMAT = 0x1000, 33 | DDSD_MIPMAPCOUNT = 0x20000, 34 | DDSD_LINEARSIZE = 0x80000, 35 | DDSD_DEPTH = 0x800000; 36 | 37 | var DDSCAPS_COMPLEX = 0x8, 38 | DDSCAPS_MIPMAP = 0x400000, 39 | DDSCAPS_TEXTURE = 0x1000; 40 | 41 | var DDSCAPS2_CUBEMAP = 0x200, 42 | DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, 43 | DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, 44 | DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, 45 | DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, 46 | DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, 47 | DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, 48 | DDSCAPS2_VOLUME = 0x200000; 49 | 50 | var DDPF_ALPHAPIXELS = 0x1, 51 | DDPF_ALPHA = 0x2, 52 | DDPF_FOURCC = 0x4, 53 | DDPF_RGB = 0x40, 54 | DDPF_YUV = 0x200, 55 | DDPF_LUMINANCE = 0x20000; 56 | 57 | function fourCCToInt32( value ) { 58 | 59 | return value.charCodeAt( 0 ) + 60 | ( value.charCodeAt( 1 ) << 8 ) + 61 | ( value.charCodeAt( 2 ) << 16 ) + 62 | ( value.charCodeAt( 3 ) << 24 ); 63 | 64 | } 65 | 66 | function int32ToFourCC( value ) { 67 | 68 | return String.fromCharCode( 69 | value & 0xff, 70 | ( value >> 8 ) & 0xff, 71 | ( value >> 16 ) & 0xff, 72 | ( value >> 24 ) & 0xff 73 | ); 74 | 75 | } 76 | 77 | function loadARGBMip( buffer, dataOffset, width, height ) { 78 | 79 | var dataLength = width * height * 4; 80 | var srcBuffer = new Uint8Array( buffer, dataOffset, dataLength ); 81 | var byteArray = new Uint8Array( dataLength ); 82 | var dst = 0; 83 | var src = 0; 84 | for ( var y = 0; y < height; y ++ ) { 85 | 86 | for ( var x = 0; x < width; x ++ ) { 87 | 88 | var b = srcBuffer[ src ]; src ++; 89 | var g = srcBuffer[ src ]; src ++; 90 | var r = srcBuffer[ src ]; src ++; 91 | var a = srcBuffer[ src ]; src ++; 92 | byteArray[ dst ] = r; dst ++; //r 93 | byteArray[ dst ] = g; dst ++; //g 94 | byteArray[ dst ] = b; dst ++; //b 95 | byteArray[ dst ] = a; dst ++; //a 96 | 97 | } 98 | 99 | } 100 | return byteArray; 101 | 102 | } 103 | 104 | var FOURCC_DXT1 = fourCCToInt32( "DXT1" ); 105 | var FOURCC_DXT3 = fourCCToInt32( "DXT3" ); 106 | var FOURCC_DXT5 = fourCCToInt32( "DXT5" ); 107 | var FOURCC_ETC1 = fourCCToInt32( "ETC1" ); 108 | 109 | var headerLengthInt = 31; // The header length in 32 bit ints 110 | 111 | // Offsets into the header array 112 | 113 | var off_magic = 0; 114 | 115 | var off_size = 1; 116 | var off_flags = 2; 117 | var off_height = 3; 118 | var off_width = 4; 119 | 120 | var off_mipmapCount = 7; 121 | 122 | var off_pfFlags = 20; 123 | var off_pfFourCC = 21; 124 | var off_RGBBitCount = 22; 125 | var off_RBitMask = 23; 126 | var off_GBitMask = 24; 127 | var off_BBitMask = 25; 128 | var off_ABitMask = 26; 129 | 130 | var off_caps = 27; 131 | var off_caps2 = 28; 132 | var off_caps3 = 29; 133 | var off_caps4 = 30; 134 | 135 | // Parse header 136 | 137 | var header = new Int32Array( buffer, 0, headerLengthInt ); 138 | for(var h of header){ 139 | // console.log(h) 140 | } 141 | if ( header[ off_magic ] !== DDS_MAGIC ) { 142 | 143 | console.error( 'THREE.DDSLoader.parse: Invalid magic number in DDS header.' ); 144 | return dds; 145 | 146 | } 147 | 148 | if ( ! header[ off_pfFlags ] & DDPF_FOURCC ) { 149 | 150 | console.error( 'THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.' ); 151 | return dds; 152 | 153 | } 154 | 155 | var blockBytes; 156 | 157 | var fourCC = header[ off_pfFourCC ]; 158 | 159 | var isRGBAUncompressed = false; 160 | 161 | switch ( fourCC ) { 162 | 163 | case FOURCC_DXT1: 164 | 165 | blockBytes = 8; 166 | dds.format = THREE.RGB_S3TC_DXT1_Format; 167 | break; 168 | 169 | case FOURCC_DXT3: 170 | 171 | blockBytes = 16; 172 | dds.format = THREE.RGBA_S3TC_DXT3_Format; 173 | break; 174 | 175 | case FOURCC_DXT5: 176 | 177 | blockBytes = 16; 178 | dds.format = THREE.RGBA_S3TC_DXT5_Format; 179 | break; 180 | 181 | case FOURCC_ETC1: 182 | 183 | blockBytes = 8; 184 | dds.format = THREE.RGB_ETC1_Format; 185 | break; 186 | 187 | default: 188 | 189 | if ( header[ off_RGBBitCount ] === 32 190 | && header[ off_RBitMask ] & 0xff0000 191 | && header[ off_GBitMask ] & 0xff00 192 | && header[ off_BBitMask ] & 0xff 193 | && header[ off_ABitMask ] & 0xff000000 ) { 194 | 195 | isRGBAUncompressed = true; 196 | blockBytes = 64; 197 | dds.format = THREE.RGBAFormat; 198 | 199 | } else { 200 | 201 | console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) ); 202 | return dds; 203 | 204 | } 205 | } 206 | 207 | dds.mipmapCount = 1; 208 | 209 | if ( header[ off_flags ] & DDSD_MIPMAPCOUNT && loadMipmaps !== false ) { 210 | 211 | dds.mipmapCount = Math.max( 1, header[ off_mipmapCount ] ); 212 | 213 | } 214 | 215 | // console.log("mipmap:",header[ off_flags ] & DDSD_MIPMAPCOUNT) 216 | 217 | var caps2 = header[ off_caps2 ]; 218 | 219 | // console.log("isCubemap",caps2 & DDSCAPS2_CUBEMAP) 220 | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false; 221 | if ( dds.isCubemap && ( 222 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEX ) || 223 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX ) || 224 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEY ) || 225 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY ) || 226 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ ) || 227 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ ) 228 | ) ) { 229 | 230 | console.error( 'THREE.DDSLoader.parse: Incomplete cubemap faces' ); 231 | return dds; 232 | 233 | } 234 | 235 | dds.width = header[ off_width ]; 236 | dds.height = header[ off_height ]; 237 | 238 | // console.log(dds.width) 239 | // console.log(dds.height) 240 | var dataOffset = header[ off_size ] + 4; 241 | 242 | // Extract mipmaps buffers 243 | 244 | var faces = dds.isCubemap ? 6 : 1; 245 | 246 | // console.log("isRGBAUncompressed",isRGBAUncompressed) 247 | 248 | for ( var face = 0; face < faces; face ++ ) { 249 | 250 | var width = dds.width; 251 | var height = dds.height; 252 | 253 | for ( var i = 0; i < dds.mipmapCount; i ++ ) { 254 | 255 | if ( isRGBAUncompressed ) { 256 | 257 | var byteArray = loadARGBMip( buffer, dataOffset, width, height ); 258 | var dataLength = byteArray.length; 259 | 260 | } else { 261 | 262 | var dataLength = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4 * blockBytes; 263 | // console.log("dataLength",dataLength) 264 | // console.log("blockBytes",blockBytes) 265 | // console.log("dataOffset",dataOffset) 266 | // console.log(buffer) 267 | var byteArray = new Uint8Array( buffer, dataOffset, dataLength ); 268 | 269 | } 270 | 271 | var mipmap = { "data": byteArray, "width": width, "height": height }; 272 | dds.mipmaps.push( mipmap ); 273 | 274 | dataOffset += dataLength; 275 | 276 | width = Math.max( width >> 1, 1 ); 277 | height = Math.max( height >> 1, 1 ); 278 | 279 | } 280 | 281 | } 282 | 283 | return dds; 284 | 285 | }; 286 | -------------------------------------------------------------------------------- /app/dat.gui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dat-gui JavaScript Controller Library 3 | * http://code.google.com/p/dat-gui 4 | * 5 | * Copyright 2011 Data Arts Team, Google Creative Lab 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | */ 13 | 14 | /** @namespace */ 15 | var dat = dat || {}; 16 | 17 | /** @namespace */ 18 | dat.gui = dat.gui || {}; 19 | 20 | /** @namespace */ 21 | dat.utils = dat.utils || {}; 22 | 23 | /** @namespace */ 24 | dat.controllers = dat.controllers || {}; 25 | 26 | /** @namespace */ 27 | dat.dom = dat.dom || {}; 28 | 29 | /** @namespace */ 30 | dat.color = dat.color || {}; 31 | 32 | dat.utils.css = (function () { 33 | return { 34 | load: function (url, doc) { 35 | doc = doc || document; 36 | var link = doc.createElement('link'); 37 | link.type = 'text/css'; 38 | link.rel = 'stylesheet'; 39 | link.href = url; 40 | doc.getElementsByTagName('head')[0].appendChild(link); 41 | }, 42 | inject: function(css, doc) { 43 | doc = doc || document; 44 | var injected = document.createElement('style'); 45 | injected.type = 'text/css'; 46 | injected.innerHTML = css; 47 | doc.getElementsByTagName('head')[0].appendChild(injected); 48 | } 49 | } 50 | })(); 51 | 52 | 53 | dat.utils.common = (function () { 54 | 55 | var ARR_EACH = Array.prototype.forEach; 56 | var ARR_SLICE = Array.prototype.slice; 57 | 58 | /** 59 | * Band-aid methods for things that should be a lot easier in JavaScript. 60 | * Implementation and structure inspired by underscore.js 61 | * http://documentcloud.github.com/underscore/ 62 | */ 63 | 64 | return { 65 | 66 | BREAK: {}, 67 | 68 | extend: function(target) { 69 | 70 | this.each(ARR_SLICE.call(arguments, 1), function(obj) { 71 | 72 | for (var key in obj) 73 | if (!this.isUndefined(obj[key])) 74 | target[key] = obj[key]; 75 | 76 | }, this); 77 | 78 | return target; 79 | 80 | }, 81 | 82 | defaults: function(target) { 83 | 84 | this.each(ARR_SLICE.call(arguments, 1), function(obj) { 85 | 86 | for (var key in obj) 87 | if (this.isUndefined(target[key])) 88 | target[key] = obj[key]; 89 | 90 | }, this); 91 | 92 | return target; 93 | 94 | }, 95 | 96 | compose: function() { 97 | var toCall = ARR_SLICE.call(arguments); 98 | return function() { 99 | var args = ARR_SLICE.call(arguments); 100 | for (var i = toCall.length -1; i >= 0; i--) { 101 | args = [toCall[i].apply(this, args)]; 102 | } 103 | return args[0]; 104 | } 105 | }, 106 | 107 | each: function(obj, itr, scope) { 108 | 109 | if (!obj) return; 110 | 111 | if (ARR_EACH && obj.forEach && obj.forEach === ARR_EACH) { 112 | 113 | obj.forEach(itr, scope); 114 | 115 | } else if (obj.length === obj.length + 0) { // Is number but not NaN 116 | 117 | for (var key = 0, l = obj.length; key < l; key++) 118 | if (key in obj && itr.call(scope, obj[key], key) === this.BREAK) 119 | return; 120 | 121 | } else { 122 | 123 | for (var key in obj) 124 | if (itr.call(scope, obj[key], key) === this.BREAK) 125 | return; 126 | 127 | } 128 | 129 | }, 130 | 131 | defer: function(fnc) { 132 | setTimeout(fnc, 0); 133 | }, 134 | 135 | toArray: function(obj) { 136 | if (obj.toArray) return obj.toArray(); 137 | return ARR_SLICE.call(obj); 138 | }, 139 | 140 | isUndefined: function(obj) { 141 | return obj === undefined; 142 | }, 143 | 144 | isNull: function(obj) { 145 | return obj === null; 146 | }, 147 | 148 | isNaN: function(obj) { 149 | return obj !== obj; 150 | }, 151 | 152 | isArray: Array.isArray || function(obj) { 153 | return obj.constructor === Array; 154 | }, 155 | 156 | isObject: function(obj) { 157 | return obj === Object(obj); 158 | }, 159 | 160 | isNumber: function(obj) { 161 | return obj === obj+0; 162 | }, 163 | 164 | isString: function(obj) { 165 | return obj === obj+''; 166 | }, 167 | 168 | isBoolean: function(obj) { 169 | return obj === false || obj === true; 170 | }, 171 | 172 | isFunction: function(obj) { 173 | return Object.prototype.toString.call(obj) === '[object Function]'; 174 | } 175 | 176 | }; 177 | 178 | })(); 179 | 180 | 181 | dat.controllers.Controller = (function (common) { 182 | 183 | /** 184 | * @class An "abstract" class that represents a given property of an object. 185 | * 186 | * @param {Object} object The object to be manipulated 187 | * @param {string} property The name of the property to be manipulated 188 | * 189 | * @member dat.controllers 190 | */ 191 | var Controller = function(object, property) { 192 | 193 | this.initialValue = object[property]; 194 | 195 | /** 196 | * Those who extend this class will put their DOM elements in here. 197 | * @type {DOMElement} 198 | */ 199 | this.domElement = document.createElement('div'); 200 | 201 | /** 202 | * The object to manipulate 203 | * @type {Object} 204 | */ 205 | this.object = object; 206 | 207 | /** 208 | * The name of the property to manipulate 209 | * @type {String} 210 | */ 211 | this.property = property; 212 | 213 | /** 214 | * The function to be called on change. 215 | * @type {Function} 216 | * @ignore 217 | */ 218 | this.__onChange = undefined; 219 | 220 | /** 221 | * The function to be called on finishing change. 222 | * @type {Function} 223 | * @ignore 224 | */ 225 | this.__onFinishChange = undefined; 226 | 227 | }; 228 | 229 | common.extend( 230 | 231 | Controller.prototype, 232 | 233 | /** @lends dat.controllers.Controller.prototype */ 234 | { 235 | 236 | /** 237 | * Specify that a function fire every time someone changes the value with 238 | * this Controller. 239 | * 240 | * @param {Function} fnc This function will be called whenever the value 241 | * is modified via this Controller. 242 | * @returns {dat.controllers.Controller} this 243 | */ 244 | onChange: function(fnc) { 245 | this.__onChange = fnc; 246 | return this; 247 | }, 248 | 249 | /** 250 | * Specify that a function fire every time someone "finishes" changing 251 | * the value wih this Controller. Useful for values that change 252 | * incrementally like numbers or strings. 253 | * 254 | * @param {Function} fnc This function will be called whenever 255 | * someone "finishes" changing the value via this Controller. 256 | * @returns {dat.controllers.Controller} this 257 | */ 258 | onFinishChange: function(fnc) { 259 | this.__onFinishChange = fnc; 260 | return this; 261 | }, 262 | 263 | /** 264 | * Change the value of object[property] 265 | * 266 | * @param {Object} newValue The new value of object[property] 267 | */ 268 | setValue: function(newValue) { 269 | this.object[this.property] = newValue; 270 | if (this.__onChange) { 271 | this.__onChange.call(this, newValue); 272 | } 273 | this.updateDisplay(); 274 | return this; 275 | }, 276 | 277 | /** 278 | * Gets the value of object[property] 279 | * 280 | * @returns {Object} The current value of object[property] 281 | */ 282 | getValue: function() { 283 | return this.object[this.property]; 284 | }, 285 | 286 | /** 287 | * Refreshes the visual display of a Controller in order to keep sync 288 | * with the object's current value. 289 | * @returns {dat.controllers.Controller} this 290 | */ 291 | updateDisplay: function() { 292 | return this; 293 | }, 294 | 295 | /** 296 | * @returns {Boolean} true if the value has deviated from initialValue 297 | */ 298 | isModified: function() { 299 | return this.initialValue !== this.getValue() 300 | } 301 | 302 | } 303 | 304 | ); 305 | 306 | return Controller; 307 | 308 | 309 | })(dat.utils.common); 310 | 311 | 312 | dat.dom.dom = (function (common) { 313 | 314 | var EVENT_MAP = { 315 | 'HTMLEvents': ['change'], 316 | 'MouseEvents': ['click','mousemove','mousedown','mouseup', 'mouseover'], 317 | 'KeyboardEvents': ['keydown'] 318 | }; 319 | 320 | var EVENT_MAP_INV = {}; 321 | common.each(EVENT_MAP, function(v, k) { 322 | common.each(v, function(e) { 323 | EVENT_MAP_INV[e] = k; 324 | }); 325 | }); 326 | 327 | var CSS_VALUE_PIXELS = /(\d+(\.\d+)?)px/; 328 | 329 | function cssValueToPixels(val) { 330 | 331 | if (val === '0' || common.isUndefined(val)) return 0; 332 | 333 | var match = val.match(CSS_VALUE_PIXELS); 334 | 335 | if (!common.isNull(match)) { 336 | return parseFloat(match[1]); 337 | } 338 | 339 | // TODO ...ems? %? 340 | 341 | return 0; 342 | 343 | } 344 | 345 | /** 346 | * @namespace 347 | * @member dat.dom 348 | */ 349 | var dom = { 350 | 351 | /** 352 | * 353 | * @param elem 354 | * @param selectable 355 | */ 356 | makeSelectable: function(elem, selectable) { 357 | 358 | if (elem === undefined || elem.style === undefined) return; 359 | 360 | elem.onselectstart = selectable ? function() { 361 | return false; 362 | } : function() { 363 | }; 364 | 365 | elem.style.MozUserSelect = selectable ? 'auto' : 'none'; 366 | elem.style.KhtmlUserSelect = selectable ? 'auto' : 'none'; 367 | elem.unselectable = selectable ? 'on' : 'off'; 368 | 369 | }, 370 | 371 | /** 372 | * 373 | * @param elem 374 | * @param horizontal 375 | * @param vertical 376 | */ 377 | makeFullscreen: function(elem, horizontal, vertical) { 378 | 379 | if (common.isUndefined(horizontal)) horizontal = true; 380 | if (common.isUndefined(vertical)) vertical = true; 381 | 382 | elem.style.position = 'absolute'; 383 | 384 | if (horizontal) { 385 | elem.style.left = 0; 386 | elem.style.right = 0; 387 | } 388 | if (vertical) { 389 | elem.style.top = 0; 390 | elem.style.bottom = 0; 391 | } 392 | 393 | }, 394 | 395 | /** 396 | * 397 | * @param elem 398 | * @param eventType 399 | * @param params 400 | */ 401 | fakeEvent: function(elem, eventType, params, aux) { 402 | params = params || {}; 403 | var className = EVENT_MAP_INV[eventType]; 404 | if (!className) { 405 | throw new Error('Event type ' + eventType + ' not supported.'); 406 | } 407 | var evt = document.createEvent(className); 408 | switch (className) { 409 | case 'MouseEvents': 410 | var clientX = params.x || params.clientX || 0; 411 | var clientY = params.y || params.clientY || 0; 412 | evt.initMouseEvent(eventType, params.bubbles || false, 413 | params.cancelable || true, window, params.clickCount || 1, 414 | 0, //screen X 415 | 0, //screen Y 416 | clientX, //client X 417 | clientY, //client Y 418 | false, false, false, false, 0, null); 419 | break; 420 | case 'KeyboardEvents': 421 | var init = evt.initKeyboardEvent || evt.initKeyEvent; // webkit || moz 422 | common.defaults(params, { 423 | cancelable: true, 424 | ctrlKey: false, 425 | altKey: false, 426 | shiftKey: false, 427 | metaKey: false, 428 | keyCode: undefined, 429 | charCode: undefined 430 | }); 431 | init(eventType, params.bubbles || false, 432 | params.cancelable, window, 433 | params.ctrlKey, params.altKey, 434 | params.shiftKey, params.metaKey, 435 | params.keyCode, params.charCode); 436 | break; 437 | default: 438 | evt.initEvent(eventType, params.bubbles || false, 439 | params.cancelable || true); 440 | break; 441 | } 442 | common.defaults(evt, aux); 443 | elem.dispatchEvent(evt); 444 | }, 445 | 446 | /** 447 | * 448 | * @param elem 449 | * @param event 450 | * @param func 451 | * @param bool 452 | */ 453 | bind: function(elem, event, func, bool) { 454 | bool = bool || false; 455 | if (elem.addEventListener) 456 | elem.addEventListener(event, func, bool); 457 | else if (elem.attachEvent) 458 | elem.attachEvent('on' + event, func); 459 | return dom; 460 | }, 461 | 462 | /** 463 | * 464 | * @param elem 465 | * @param event 466 | * @param func 467 | * @param bool 468 | */ 469 | unbind: function(elem, event, func, bool) { 470 | bool = bool || false; 471 | if (elem.removeEventListener) 472 | elem.removeEventListener(event, func, bool); 473 | else if (elem.detachEvent) 474 | elem.detachEvent('on' + event, func); 475 | return dom; 476 | }, 477 | 478 | /** 479 | * 480 | * @param elem 481 | * @param className 482 | */ 483 | addClass: function(elem, className) { 484 | if (elem.className === undefined) { 485 | elem.className = className; 486 | } else if (elem.className !== className) { 487 | var classes = elem.className.split(/ +/); 488 | if (classes.indexOf(className) == -1) { 489 | classes.push(className); 490 | elem.className = classes.join(' ').replace(/^\s+/, '').replace(/\s+$/, ''); 491 | } 492 | } 493 | return dom; 494 | }, 495 | 496 | /** 497 | * 498 | * @param elem 499 | * @param className 500 | */ 501 | removeClass: function(elem, className) { 502 | if (className) { 503 | if (elem.className === undefined) { 504 | // elem.className = className; 505 | } else if (elem.className === className) { 506 | elem.removeAttribute('class'); 507 | } else { 508 | var classes = elem.className.split(/ +/); 509 | var index = classes.indexOf(className); 510 | if (index != -1) { 511 | classes.splice(index, 1); 512 | elem.className = classes.join(' '); 513 | } 514 | } 515 | } else { 516 | elem.className = undefined; 517 | } 518 | return dom; 519 | }, 520 | 521 | hasClass: function(elem, className) { 522 | return new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)').test(elem.className) || false; 523 | }, 524 | 525 | /** 526 | * 527 | * @param elem 528 | */ 529 | getWidth: function(elem) { 530 | 531 | var style = getComputedStyle(elem); 532 | 533 | return cssValueToPixels(style['border-left-width']) + 534 | cssValueToPixels(style['border-right-width']) + 535 | cssValueToPixels(style['padding-left']) + 536 | cssValueToPixels(style['padding-right']) + 537 | cssValueToPixels(style['width']); 538 | }, 539 | 540 | /** 541 | * 542 | * @param elem 543 | */ 544 | getHeight: function(elem) { 545 | 546 | var style = getComputedStyle(elem); 547 | 548 | return cssValueToPixels(style['border-top-width']) + 549 | cssValueToPixels(style['border-bottom-width']) + 550 | cssValueToPixels(style['padding-top']) + 551 | cssValueToPixels(style['padding-bottom']) + 552 | cssValueToPixels(style['height']); 553 | }, 554 | 555 | /** 556 | * 557 | * @param elem 558 | */ 559 | getOffset: function(elem) { 560 | var offset = {left: 0, top:0}; 561 | if (elem.offsetParent) { 562 | do { 563 | offset.left += elem.offsetLeft; 564 | offset.top += elem.offsetTop; 565 | } while (elem = elem.offsetParent); 566 | } 567 | return offset; 568 | }, 569 | 570 | // http://stackoverflow.com/posts/2684561/revisions 571 | /** 572 | * 573 | * @param elem 574 | */ 575 | isActive: function(elem) { 576 | return elem === document.activeElement && ( elem.type || elem.href ); 577 | } 578 | 579 | }; 580 | 581 | return dom; 582 | 583 | })(dat.utils.common); 584 | 585 | 586 | dat.controllers.OptionController = (function (Controller, dom, common) { 587 | 588 | /** 589 | * @class Provides a select input to alter the property of an object, using a 590 | * list of accepted values. 591 | * 592 | * @extends dat.controllers.Controller 593 | * 594 | * @param {Object} object The object to be manipulated 595 | * @param {string} property The name of the property to be manipulated 596 | * @param {Object|string[]} options A map of labels to acceptable values, or 597 | * a list of acceptable string values. 598 | * 599 | * @member dat.controllers 600 | */ 601 | var OptionController = function(object, property, options) { 602 | 603 | OptionController.superclass.call(this, object, property); 604 | 605 | var _this = this; 606 | 607 | /** 608 | * The drop down menu 609 | * @ignore 610 | */ 611 | this.__select = document.createElement('select'); 612 | 613 | if (common.isArray(options)) { 614 | var map = {}; 615 | common.each(options, function(element) { 616 | map[element] = element; 617 | }); 618 | options = map; 619 | } 620 | 621 | common.each(options, function(value, key) { 622 | 623 | var opt = document.createElement('option'); 624 | opt.innerHTML = key; 625 | opt.setAttribute('value', value); 626 | _this.__select.appendChild(opt); 627 | 628 | }); 629 | 630 | // Acknowledge original value 631 | this.updateDisplay(); 632 | 633 | dom.bind(this.__select, 'change', function() { 634 | var desiredValue = this.options[this.selectedIndex].value; 635 | _this.setValue(desiredValue); 636 | }); 637 | 638 | this.domElement.appendChild(this.__select); 639 | 640 | }; 641 | 642 | OptionController.superclass = Controller; 643 | 644 | common.extend( 645 | 646 | OptionController.prototype, 647 | Controller.prototype, 648 | 649 | { 650 | 651 | setValue: function(v) { 652 | var toReturn = OptionController.superclass.prototype.setValue.call(this, v); 653 | if (this.__onFinishChange) { 654 | this.__onFinishChange.call(this, this.getValue()); 655 | } 656 | return toReturn; 657 | }, 658 | 659 | updateDisplay: function() { 660 | this.__select.value = this.getValue(); 661 | return OptionController.superclass.prototype.updateDisplay.call(this); 662 | } 663 | 664 | } 665 | 666 | ); 667 | 668 | return OptionController; 669 | 670 | })(dat.controllers.Controller, 671 | dat.dom.dom, 672 | dat.utils.common); 673 | 674 | 675 | dat.controllers.NumberController = (function (Controller, common) { 676 | 677 | /** 678 | * @class Represents a given property of an object that is a number. 679 | * 680 | * @extends dat.controllers.Controller 681 | * 682 | * @param {Object} object The object to be manipulated 683 | * @param {string} property The name of the property to be manipulated 684 | * @param {Object} [params] Optional parameters 685 | * @param {Number} [params.min] Minimum allowed value 686 | * @param {Number} [params.max] Maximum allowed value 687 | * @param {Number} [params.step] Increment by which to change value 688 | * 689 | * @member dat.controllers 690 | */ 691 | var NumberController = function(object, property, params) { 692 | 693 | NumberController.superclass.call(this, object, property); 694 | 695 | params = params || {}; 696 | 697 | this.__min = params.min; 698 | this.__max = params.max; 699 | this.__step = params.step; 700 | 701 | if (common.isUndefined(this.__step)) { 702 | 703 | if (this.initialValue == 0) { 704 | this.__impliedStep = 1; // What are we, psychics? 705 | } else { 706 | // Hey Doug, check this out. 707 | this.__impliedStep = Math.pow(10, Math.floor(Math.log(this.initialValue)/Math.LN10))/10; 708 | } 709 | 710 | } else { 711 | 712 | this.__impliedStep = this.__step; 713 | 714 | } 715 | 716 | this.__precision = numDecimals(this.__impliedStep); 717 | 718 | 719 | }; 720 | 721 | NumberController.superclass = Controller; 722 | 723 | common.extend( 724 | 725 | NumberController.prototype, 726 | Controller.prototype, 727 | 728 | /** @lends dat.controllers.NumberController.prototype */ 729 | { 730 | 731 | setValue: function(v) { 732 | 733 | if (this.__min !== undefined && v < this.__min) { 734 | v = this.__min; 735 | } else if (this.__max !== undefined && v > this.__max) { 736 | v = this.__max; 737 | } 738 | 739 | if (this.__step !== undefined && v % this.__step != 0) { 740 | v = Math.round(v / this.__step) * this.__step; 741 | } 742 | 743 | return NumberController.superclass.prototype.setValue.call(this, v); 744 | 745 | }, 746 | 747 | /** 748 | * Specify a minimum value for object[property]. 749 | * 750 | * @param {Number} minValue The minimum value for 751 | * object[property] 752 | * @returns {dat.controllers.NumberController} this 753 | */ 754 | min: function(v) { 755 | this.__min = v; 756 | return this; 757 | }, 758 | 759 | /** 760 | * Specify a maximum value for object[property]. 761 | * 762 | * @param {Number} maxValue The maximum value for 763 | * object[property] 764 | * @returns {dat.controllers.NumberController} this 765 | */ 766 | max: function(v) { 767 | this.__max = v; 768 | return this; 769 | }, 770 | 771 | /** 772 | * Specify a step value that dat.controllers.NumberController 773 | * increments by. 774 | * 775 | * @param {Number} stepValue The step value for 776 | * dat.controllers.NumberController 777 | * @default if minimum and maximum specified increment is 1% of the 778 | * difference otherwise stepValue is 1 779 | * @returns {dat.controllers.NumberController} this 780 | */ 781 | step: function(v) { 782 | this.__step = v; 783 | this.__impliedStep = v; 784 | this.__precision = numDecimals(v); 785 | return this; 786 | } 787 | 788 | } 789 | 790 | ); 791 | 792 | function numDecimals(x) { 793 | x = x.toString(); 794 | if (x.indexOf('.') > -1) { 795 | return x.length - x.indexOf('.') - 1; 796 | } else { 797 | return 0; 798 | } 799 | } 800 | 801 | return NumberController; 802 | 803 | })(dat.controllers.Controller, 804 | dat.utils.common); 805 | 806 | 807 | dat.controllers.NumberControllerBox = (function (NumberController, dom, common) { 808 | 809 | /** 810 | * @class Represents a given property of an object that is a number and 811 | * provides an input element with which to manipulate it. 812 | * 813 | * @extends dat.controllers.Controller 814 | * @extends dat.controllers.NumberController 815 | * 816 | * @param {Object} object The object to be manipulated 817 | * @param {string} property The name of the property to be manipulated 818 | * @param {Object} [params] Optional parameters 819 | * @param {Number} [params.min] Minimum allowed value 820 | * @param {Number} [params.max] Maximum allowed value 821 | * @param {Number} [params.step] Increment by which to change value 822 | * 823 | * @member dat.controllers 824 | */ 825 | var NumberControllerBox = function(object, property, params) { 826 | 827 | this.__truncationSuspended = false; 828 | 829 | NumberControllerBox.superclass.call(this, object, property, params); 830 | 831 | var _this = this; 832 | 833 | /** 834 | * {Number} Previous mouse y position 835 | * @ignore 836 | */ 837 | var prev_y; 838 | 839 | this.__input = document.createElement('input'); 840 | this.__input.setAttribute('type', 'text'); 841 | 842 | // Makes it so manually specified values are not truncated. 843 | 844 | dom.bind(this.__input, 'change', onChange); 845 | dom.bind(this.__input, 'blur', onBlur); 846 | dom.bind(this.__input, 'mousedown', onMouseDown); 847 | dom.bind(this.__input, 'keydown', function(e) { 848 | 849 | // When pressing entire, you can be as precise as you want. 850 | if (e.keyCode === 13) { 851 | _this.__truncationSuspended = true; 852 | this.blur(); 853 | _this.__truncationSuspended = false; 854 | } 855 | 856 | }); 857 | 858 | function onChange() { 859 | var attempted = parseFloat(_this.__input.value); 860 | if (!common.isNaN(attempted)) _this.setValue(attempted); 861 | } 862 | 863 | function onBlur() { 864 | onChange(); 865 | if (_this.__onFinishChange) { 866 | _this.__onFinishChange.call(_this, _this.getValue()); 867 | } 868 | } 869 | 870 | function onMouseDown(e) { 871 | dom.bind(window, 'mousemove', onMouseDrag); 872 | dom.bind(window, 'mouseup', onMouseUp); 873 | prev_y = e.clientY; 874 | } 875 | 876 | function onMouseDrag(e) { 877 | 878 | var diff = prev_y - e.clientY; 879 | _this.setValue(_this.getValue() + diff * _this.__impliedStep); 880 | 881 | prev_y = e.clientY; 882 | 883 | } 884 | 885 | function onMouseUp() { 886 | dom.unbind(window, 'mousemove', onMouseDrag); 887 | dom.unbind(window, 'mouseup', onMouseUp); 888 | } 889 | 890 | this.updateDisplay(); 891 | 892 | this.domElement.appendChild(this.__input); 893 | 894 | }; 895 | 896 | NumberControllerBox.superclass = NumberController; 897 | 898 | common.extend( 899 | 900 | NumberControllerBox.prototype, 901 | NumberController.prototype, 902 | 903 | { 904 | 905 | updateDisplay: function() { 906 | 907 | this.__input.value = this.__truncationSuspended ? this.getValue() : roundToDecimal(this.getValue(), this.__precision); 908 | return NumberControllerBox.superclass.prototype.updateDisplay.call(this); 909 | } 910 | 911 | } 912 | 913 | ); 914 | 915 | function roundToDecimal(value, decimals) { 916 | var tenTo = Math.pow(10, decimals); 917 | return Math.round(value * tenTo) / tenTo; 918 | } 919 | 920 | return NumberControllerBox; 921 | 922 | })(dat.controllers.NumberController, 923 | dat.dom.dom, 924 | dat.utils.common); 925 | 926 | 927 | dat.controllers.NumberControllerSlider = (function (NumberController, dom, css, common, styleSheet) { 928 | 929 | /** 930 | * @class Represents a given property of an object that is a number, contains 931 | * a minimum and maximum, and provides a slider element with which to 932 | * manipulate it. It should be noted that the slider element is made up of 933 | * <div> tags, not the html5 934 | * <slider> element. 935 | * 936 | * @extends dat.controllers.Controller 937 | * @extends dat.controllers.NumberController 938 | * 939 | * @param {Object} object The object to be manipulated 940 | * @param {string} property The name of the property to be manipulated 941 | * @param {Number} minValue Minimum allowed value 942 | * @param {Number} maxValue Maximum allowed value 943 | * @param {Number} stepValue Increment by which to change value 944 | * 945 | * @member dat.controllers 946 | */ 947 | var NumberControllerSlider = function(object, property, min, max, step) { 948 | 949 | NumberControllerSlider.superclass.call(this, object, property, { min: min, max: max, step: step }); 950 | 951 | var _this = this; 952 | 953 | this.__background = document.createElement('div'); 954 | this.__foreground = document.createElement('div'); 955 | 956 | 957 | 958 | dom.bind(this.__background, 'mousedown', onMouseDown); 959 | 960 | dom.addClass(this.__background, 'slider'); 961 | dom.addClass(this.__foreground, 'slider-fg'); 962 | 963 | function onMouseDown(e) { 964 | 965 | dom.bind(window, 'mousemove', onMouseDrag); 966 | dom.bind(window, 'mouseup', onMouseUp); 967 | 968 | onMouseDrag(e); 969 | } 970 | 971 | function onMouseDrag(e) { 972 | 973 | e.preventDefault(); 974 | 975 | var offset = dom.getOffset(_this.__background); 976 | var width = dom.getWidth(_this.__background); 977 | 978 | _this.setValue( 979 | map(e.clientX, offset.left, offset.left + width, _this.__min, _this.__max) 980 | ); 981 | 982 | return false; 983 | 984 | } 985 | 986 | function onMouseUp() { 987 | dom.unbind(window, 'mousemove', onMouseDrag); 988 | dom.unbind(window, 'mouseup', onMouseUp); 989 | if (_this.__onFinishChange) { 990 | _this.__onFinishChange.call(_this, _this.getValue()); 991 | } 992 | } 993 | 994 | this.updateDisplay(); 995 | 996 | this.__background.appendChild(this.__foreground); 997 | this.domElement.appendChild(this.__background); 998 | 999 | }; 1000 | 1001 | NumberControllerSlider.superclass = NumberController; 1002 | 1003 | /** 1004 | * Injects default stylesheet for slider elements. 1005 | */ 1006 | NumberControllerSlider.useDefaultStyles = function() { 1007 | css.inject(styleSheet); 1008 | }; 1009 | 1010 | common.extend( 1011 | 1012 | NumberControllerSlider.prototype, 1013 | NumberController.prototype, 1014 | 1015 | { 1016 | 1017 | updateDisplay: function() { 1018 | var pct = (this.getValue() - this.__min)/(this.__max - this.__min); 1019 | this.__foreground.style.width = pct*100+'%'; 1020 | return NumberControllerSlider.superclass.prototype.updateDisplay.call(this); 1021 | } 1022 | 1023 | } 1024 | 1025 | 1026 | 1027 | ); 1028 | 1029 | function map(v, i1, i2, o1, o2) { 1030 | return o1 + (o2 - o1) * ((v - i1) / (i2 - i1)); 1031 | } 1032 | 1033 | return NumberControllerSlider; 1034 | 1035 | })(dat.controllers.NumberController, 1036 | dat.dom.dom, 1037 | dat.utils.css, 1038 | dat.utils.common, 1039 | "/**\n * dat-gui JavaScript Controller Library\n * http://code.google.com/p/dat-gui\n *\n * Copyright 2011 Data Arts Team, Google Creative Lab\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\n.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}"); 1040 | 1041 | 1042 | dat.controllers.FunctionController = (function (Controller, dom, common) { 1043 | 1044 | /** 1045 | * @class Provides a GUI interface to fire a specified method, a property of an object. 1046 | * 1047 | * @extends dat.controllers.Controller 1048 | * 1049 | * @param {Object} object The object to be manipulated 1050 | * @param {string} property The name of the property to be manipulated 1051 | * 1052 | * @member dat.controllers 1053 | */ 1054 | var FunctionController = function(object, property, text) { 1055 | 1056 | FunctionController.superclass.call(this, object, property); 1057 | 1058 | var _this = this; 1059 | 1060 | this.__button = document.createElement('div'); 1061 | this.__button.innerHTML = text === undefined ? 'Fire' : text; 1062 | dom.bind(this.__button, 'click', function(e) { 1063 | e.preventDefault(); 1064 | _this.fire(); 1065 | return false; 1066 | }); 1067 | 1068 | dom.addClass(this.__button, 'button'); 1069 | 1070 | this.domElement.appendChild(this.__button); 1071 | 1072 | 1073 | }; 1074 | 1075 | FunctionController.superclass = Controller; 1076 | 1077 | common.extend( 1078 | 1079 | FunctionController.prototype, 1080 | Controller.prototype, 1081 | { 1082 | 1083 | fire: function() { 1084 | if (this.__onChange) { 1085 | this.__onChange.call(this); 1086 | } 1087 | if (this.__onFinishChange) { 1088 | this.__onFinishChange.call(this, this.getValue()); 1089 | } 1090 | this.getValue().call(this.object); 1091 | } 1092 | } 1093 | 1094 | ); 1095 | 1096 | return FunctionController; 1097 | 1098 | })(dat.controllers.Controller, 1099 | dat.dom.dom, 1100 | dat.utils.common); 1101 | 1102 | 1103 | dat.controllers.BooleanController = (function (Controller, dom, common) { 1104 | 1105 | /** 1106 | * @class Provides a checkbox input to alter the boolean property of an object. 1107 | * @extends dat.controllers.Controller 1108 | * 1109 | * @param {Object} object The object to be manipulated 1110 | * @param {string} property The name of the property to be manipulated 1111 | * 1112 | * @member dat.controllers 1113 | */ 1114 | var BooleanController = function(object, property) { 1115 | 1116 | BooleanController.superclass.call(this, object, property); 1117 | 1118 | var _this = this; 1119 | this.__prev = this.getValue(); 1120 | 1121 | this.__checkbox = document.createElement('input'); 1122 | this.__checkbox.setAttribute('type', 'checkbox'); 1123 | 1124 | 1125 | dom.bind(this.__checkbox, 'change', onChange, false); 1126 | 1127 | this.domElement.appendChild(this.__checkbox); 1128 | 1129 | // Match original value 1130 | this.updateDisplay(); 1131 | 1132 | function onChange() { 1133 | _this.setValue(!_this.__prev); 1134 | } 1135 | 1136 | }; 1137 | 1138 | BooleanController.superclass = Controller; 1139 | 1140 | common.extend( 1141 | 1142 | BooleanController.prototype, 1143 | Controller.prototype, 1144 | 1145 | { 1146 | 1147 | setValue: function(v) { 1148 | var toReturn = BooleanController.superclass.prototype.setValue.call(this, v); 1149 | if (this.__onFinishChange) { 1150 | this.__onFinishChange.call(this, this.getValue()); 1151 | } 1152 | this.__prev = this.getValue(); 1153 | return toReturn; 1154 | }, 1155 | 1156 | updateDisplay: function() { 1157 | 1158 | if (this.getValue() === true) { 1159 | this.__checkbox.setAttribute('checked', 'checked'); 1160 | this.__checkbox.checked = true; 1161 | } else { 1162 | this.__checkbox.checked = false; 1163 | } 1164 | 1165 | return BooleanController.superclass.prototype.updateDisplay.call(this); 1166 | 1167 | } 1168 | 1169 | 1170 | } 1171 | 1172 | ); 1173 | 1174 | return BooleanController; 1175 | 1176 | })(dat.controllers.Controller, 1177 | dat.dom.dom, 1178 | dat.utils.common); 1179 | 1180 | 1181 | dat.color.toString = (function (common) { 1182 | 1183 | return function(color) { 1184 | 1185 | if (color.a == 1 || common.isUndefined(color.a)) { 1186 | 1187 | var s = color.hex.toString(16); 1188 | while (s.length < 6) { 1189 | s = '0' + s; 1190 | } 1191 | 1192 | return '#' + s; 1193 | 1194 | } else { 1195 | 1196 | return 'rgba(' + Math.round(color.r) + ',' + Math.round(color.g) + ',' + Math.round(color.b) + ',' + color.a + ')'; 1197 | 1198 | } 1199 | 1200 | } 1201 | 1202 | })(dat.utils.common); 1203 | 1204 | 1205 | dat.color.interpret = (function (toString, common) { 1206 | 1207 | var result, toReturn; 1208 | 1209 | var interpret = function() { 1210 | 1211 | toReturn = false; 1212 | 1213 | var original = arguments.length > 1 ? common.toArray(arguments) : arguments[0]; 1214 | 1215 | common.each(INTERPRETATIONS, function(family) { 1216 | 1217 | if (family.litmus(original)) { 1218 | 1219 | common.each(family.conversions, function(conversion, conversionName) { 1220 | 1221 | result = conversion.read(original); 1222 | 1223 | if (toReturn === false && result !== false) { 1224 | toReturn = result; 1225 | result.conversionName = conversionName; 1226 | result.conversion = conversion; 1227 | return common.BREAK; 1228 | 1229 | } 1230 | 1231 | }); 1232 | 1233 | return common.BREAK; 1234 | 1235 | } 1236 | 1237 | }); 1238 | 1239 | return toReturn; 1240 | 1241 | }; 1242 | 1243 | var INTERPRETATIONS = [ 1244 | 1245 | // Strings 1246 | { 1247 | 1248 | litmus: common.isString, 1249 | 1250 | conversions: { 1251 | 1252 | THREE_CHAR_HEX: { 1253 | 1254 | read: function(original) { 1255 | 1256 | var test = original.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i); 1257 | if (test === null) return false; 1258 | 1259 | return { 1260 | space: 'HEX', 1261 | hex: parseInt( 1262 | '0x' + 1263 | test[1].toString() + test[1].toString() + 1264 | test[2].toString() + test[2].toString() + 1265 | test[3].toString() + test[3].toString()) 1266 | }; 1267 | 1268 | }, 1269 | 1270 | write: toString 1271 | 1272 | }, 1273 | 1274 | SIX_CHAR_HEX: { 1275 | 1276 | read: function(original) { 1277 | 1278 | var test = original.match(/^#([A-F0-9]{6})$/i); 1279 | if (test === null) return false; 1280 | 1281 | return { 1282 | space: 'HEX', 1283 | hex: parseInt('0x' + test[1].toString()) 1284 | }; 1285 | 1286 | }, 1287 | 1288 | write: toString 1289 | 1290 | }, 1291 | 1292 | CSS_RGB: { 1293 | 1294 | read: function(original) { 1295 | 1296 | var test = original.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/); 1297 | if (test === null) return false; 1298 | 1299 | return { 1300 | space: 'RGB', 1301 | r: parseFloat(test[1]), 1302 | g: parseFloat(test[2]), 1303 | b: parseFloat(test[3]) 1304 | }; 1305 | 1306 | }, 1307 | 1308 | write: toString 1309 | 1310 | }, 1311 | 1312 | CSS_RGBA: { 1313 | 1314 | read: function(original) { 1315 | 1316 | var test = original.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/); 1317 | if (test === null) return false; 1318 | 1319 | return { 1320 | space: 'RGB', 1321 | r: parseFloat(test[1]), 1322 | g: parseFloat(test[2]), 1323 | b: parseFloat(test[3]), 1324 | a: parseFloat(test[4]) 1325 | }; 1326 | 1327 | }, 1328 | 1329 | write: toString 1330 | 1331 | } 1332 | 1333 | } 1334 | 1335 | }, 1336 | 1337 | // Numbers 1338 | { 1339 | 1340 | litmus: common.isNumber, 1341 | 1342 | conversions: { 1343 | 1344 | HEX: { 1345 | read: function(original) { 1346 | return { 1347 | space: 'HEX', 1348 | hex: original, 1349 | conversionName: 'HEX' 1350 | } 1351 | }, 1352 | 1353 | write: function(color) { 1354 | return color.hex; 1355 | } 1356 | } 1357 | 1358 | } 1359 | 1360 | }, 1361 | 1362 | // Arrays 1363 | { 1364 | 1365 | litmus: common.isArray, 1366 | 1367 | conversions: { 1368 | 1369 | RGB_ARRAY: { 1370 | read: function(original) { 1371 | if (original.length != 3) return false; 1372 | return { 1373 | space: 'RGB', 1374 | r: original[0], 1375 | g: original[1], 1376 | b: original[2] 1377 | }; 1378 | }, 1379 | 1380 | write: function(color) { 1381 | return [color.r, color.g, color.b]; 1382 | } 1383 | 1384 | }, 1385 | 1386 | RGBA_ARRAY: { 1387 | read: function(original) { 1388 | if (original.length != 4) return false; 1389 | return { 1390 | space: 'RGB', 1391 | r: original[0], 1392 | g: original[1], 1393 | b: original[2], 1394 | a: original[3] 1395 | }; 1396 | }, 1397 | 1398 | write: function(color) { 1399 | return [color.r, color.g, color.b, color.a]; 1400 | } 1401 | 1402 | } 1403 | 1404 | } 1405 | 1406 | }, 1407 | 1408 | // Objects 1409 | { 1410 | 1411 | litmus: common.isObject, 1412 | 1413 | conversions: { 1414 | 1415 | RGBA_OBJ: { 1416 | read: function(original) { 1417 | if (common.isNumber(original.r) && 1418 | common.isNumber(original.g) && 1419 | common.isNumber(original.b) && 1420 | common.isNumber(original.a)) { 1421 | return { 1422 | space: 'RGB', 1423 | r: original.r, 1424 | g: original.g, 1425 | b: original.b, 1426 | a: original.a 1427 | } 1428 | } 1429 | return false; 1430 | }, 1431 | 1432 | write: function(color) { 1433 | return { 1434 | r: color.r, 1435 | g: color.g, 1436 | b: color.b, 1437 | a: color.a 1438 | } 1439 | } 1440 | }, 1441 | 1442 | RGB_OBJ: { 1443 | read: function(original) { 1444 | if (common.isNumber(original.r) && 1445 | common.isNumber(original.g) && 1446 | common.isNumber(original.b)) { 1447 | return { 1448 | space: 'RGB', 1449 | r: original.r, 1450 | g: original.g, 1451 | b: original.b 1452 | } 1453 | } 1454 | return false; 1455 | }, 1456 | 1457 | write: function(color) { 1458 | return { 1459 | r: color.r, 1460 | g: color.g, 1461 | b: color.b 1462 | } 1463 | } 1464 | }, 1465 | 1466 | HSVA_OBJ: { 1467 | read: function(original) { 1468 | if (common.isNumber(original.h) && 1469 | common.isNumber(original.s) && 1470 | common.isNumber(original.v) && 1471 | common.isNumber(original.a)) { 1472 | return { 1473 | space: 'HSV', 1474 | h: original.h, 1475 | s: original.s, 1476 | v: original.v, 1477 | a: original.a 1478 | } 1479 | } 1480 | return false; 1481 | }, 1482 | 1483 | write: function(color) { 1484 | return { 1485 | h: color.h, 1486 | s: color.s, 1487 | v: color.v, 1488 | a: color.a 1489 | } 1490 | } 1491 | }, 1492 | 1493 | HSV_OBJ: { 1494 | read: function(original) { 1495 | if (common.isNumber(original.h) && 1496 | common.isNumber(original.s) && 1497 | common.isNumber(original.v)) { 1498 | return { 1499 | space: 'HSV', 1500 | h: original.h, 1501 | s: original.s, 1502 | v: original.v 1503 | } 1504 | } 1505 | return false; 1506 | }, 1507 | 1508 | write: function(color) { 1509 | return { 1510 | h: color.h, 1511 | s: color.s, 1512 | v: color.v 1513 | } 1514 | } 1515 | 1516 | } 1517 | 1518 | } 1519 | 1520 | } 1521 | 1522 | 1523 | ]; 1524 | 1525 | return interpret; 1526 | 1527 | 1528 | })(dat.color.toString, 1529 | dat.utils.common); 1530 | 1531 | 1532 | dat.GUI = dat.gui.GUI = (function (css, saveDialogueContents, styleSheet, controllerFactory, Controller, BooleanController, FunctionController, NumberControllerBox, NumberControllerSlider, OptionController, ColorController, requestAnimationFrame, CenteredDiv, dom, common) { 1533 | 1534 | css.inject(styleSheet); 1535 | 1536 | /** Outer-most className for GUI's */ 1537 | var CSS_NAMESPACE = 'dg'; 1538 | 1539 | var HIDE_KEY_CODE = 72; 1540 | 1541 | /** The only value shared between the JS and SCSS. Use caution. */ 1542 | var CLOSE_BUTTON_HEIGHT = 20; 1543 | 1544 | var DEFAULT_DEFAULT_PRESET_NAME = 'Default'; 1545 | 1546 | var SUPPORTS_LOCAL_STORAGE = (function() { 1547 | try { 1548 | return 'localStorage' in window && window['localStorage'] !== null; 1549 | } catch (e) { 1550 | return false; 1551 | } 1552 | })(); 1553 | 1554 | var SAVE_DIALOGUE; 1555 | 1556 | /** Have we yet to create an autoPlace GUI? */ 1557 | var auto_place_virgin = true; 1558 | 1559 | /** Fixed position div that auto place GUI's go inside */ 1560 | var auto_place_container; 1561 | 1562 | /** Are we hiding the GUI's ? */ 1563 | var hide = false; 1564 | 1565 | /** GUI's which should be hidden */ 1566 | var hideable_guis = []; 1567 | 1568 | /** 1569 | * A lightweight controller library for JavaScript. It allows you to easily 1570 | * manipulate variables and fire functions on the fly. 1571 | * @class 1572 | * 1573 | * @member dat.gui 1574 | * 1575 | * @param {Object} [params] 1576 | * @param {String} [params.name] The name of this GUI. 1577 | * @param {Object} [params.load] JSON object representing the saved state of 1578 | * this GUI. 1579 | * @param {Boolean} [params.auto=true] 1580 | * @param {dat.gui.GUI} [params.parent] The GUI I'm nested in. 1581 | * @param {Boolean} [params.closed] If true, starts closed 1582 | */ 1583 | var GUI = function(params) { 1584 | 1585 | var _this = this; 1586 | 1587 | /** 1588 | * Outermost DOM Element 1589 | * @type DOMElement 1590 | */ 1591 | this.domElement = document.createElement('div'); 1592 | this.__ul = document.createElement('ul'); 1593 | this.domElement.appendChild(this.__ul); 1594 | 1595 | dom.addClass(this.domElement, CSS_NAMESPACE); 1596 | 1597 | /** 1598 | * Nested GUI's by name 1599 | * @ignore 1600 | */ 1601 | this.__folders = {}; 1602 | 1603 | this.__controllers = []; 1604 | 1605 | /** 1606 | * List of objects I'm remembering for save, only used in top level GUI 1607 | * @ignore 1608 | */ 1609 | this.__rememberedObjects = []; 1610 | 1611 | /** 1612 | * Maps the index of remembered objects to a map of controllers, only used 1613 | * in top level GUI. 1614 | * 1615 | * @private 1616 | * @ignore 1617 | * 1618 | * @example 1619 | * [ 1620 | * { 1621 | * propertyName: Controller, 1622 | * anotherPropertyName: Controller 1623 | * }, 1624 | * { 1625 | * propertyName: Controller 1626 | * } 1627 | * ] 1628 | */ 1629 | this.__rememberedObjectIndecesToControllers = []; 1630 | 1631 | this.__listening = []; 1632 | 1633 | params = params || {}; 1634 | 1635 | // Default parameters 1636 | params = common.defaults(params, { 1637 | autoPlace: true, 1638 | width: GUI.DEFAULT_WIDTH 1639 | }); 1640 | 1641 | params = common.defaults(params, { 1642 | resizable: params.autoPlace, 1643 | hideable: params.autoPlace 1644 | }); 1645 | 1646 | 1647 | if (!common.isUndefined(params.load)) { 1648 | 1649 | // Explicit preset 1650 | if (params.preset) params.load.preset = params.preset; 1651 | 1652 | } else { 1653 | 1654 | params.load = { preset: DEFAULT_DEFAULT_PRESET_NAME }; 1655 | 1656 | } 1657 | 1658 | if (common.isUndefined(params.parent) && params.hideable) { 1659 | hideable_guis.push(this); 1660 | } 1661 | 1662 | // Only root level GUI's are resizable. 1663 | params.resizable = common.isUndefined(params.parent) && params.resizable; 1664 | 1665 | 1666 | if (params.autoPlace && common.isUndefined(params.scrollable)) { 1667 | params.scrollable = true; 1668 | } 1669 | // params.scrollable = common.isUndefined(params.parent) && params.scrollable === true; 1670 | 1671 | // Not part of params because I don't want people passing this in via 1672 | // constructor. Should be a 'remembered' value. 1673 | var use_local_storage = 1674 | SUPPORTS_LOCAL_STORAGE && 1675 | localStorage.getItem(getLocalStorageHash(this, 'isLocal')) === 'true'; 1676 | 1677 | var saveToLocalStorage; 1678 | 1679 | Object.defineProperties(this, 1680 | 1681 | /** @lends dat.gui.GUI.prototype */ 1682 | { 1683 | 1684 | /** 1685 | * The parent GUI 1686 | * @type dat.gui.GUI 1687 | */ 1688 | parent: { 1689 | get: function() { 1690 | return params.parent; 1691 | } 1692 | }, 1693 | 1694 | scrollable: { 1695 | get: function() { 1696 | return params.scrollable; 1697 | } 1698 | }, 1699 | 1700 | /** 1701 | * Handles GUI's element placement for you 1702 | * @type Boolean 1703 | */ 1704 | autoPlace: { 1705 | get: function() { 1706 | return params.autoPlace; 1707 | } 1708 | }, 1709 | 1710 | /** 1711 | * The identifier for a set of saved values 1712 | * @type String 1713 | */ 1714 | preset: { 1715 | 1716 | get: function() { 1717 | if (_this.parent) { 1718 | return _this.getRoot().preset; 1719 | } else { 1720 | return params.load.preset; 1721 | } 1722 | }, 1723 | 1724 | set: function(v) { 1725 | if (_this.parent) { 1726 | _this.getRoot().preset = v; 1727 | } else { 1728 | params.load.preset = v; 1729 | } 1730 | setPresetSelectIndex(this); 1731 | _this.revert(); 1732 | } 1733 | 1734 | }, 1735 | 1736 | /** 1737 | * The width of GUI element 1738 | * @type Number 1739 | */ 1740 | width: { 1741 | get: function() { 1742 | return params.width; 1743 | }, 1744 | set: function(v) { 1745 | params.width = v; 1746 | setWidth(_this, v); 1747 | } 1748 | }, 1749 | 1750 | /** 1751 | * The name of GUI. Used for folders. i.e 1752 | * a folder's name 1753 | * @type String 1754 | */ 1755 | name: { 1756 | get: function() { 1757 | return params.name; 1758 | }, 1759 | set: function(v) { 1760 | // TODO Check for collisions among sibling folders 1761 | params.name = v; 1762 | if (title_row_name) { 1763 | title_row_name.innerHTML = params.name; 1764 | } 1765 | } 1766 | }, 1767 | 1768 | /** 1769 | * Whether the GUI is collapsed or not 1770 | * @type Boolean 1771 | */ 1772 | closed: { 1773 | get: function() { 1774 | return params.closed; 1775 | }, 1776 | set: function(v) { 1777 | params.closed = v; 1778 | if (params.closed) { 1779 | dom.addClass(_this.__ul, GUI.CLASS_CLOSED); 1780 | } else { 1781 | dom.removeClass(_this.__ul, GUI.CLASS_CLOSED); 1782 | } 1783 | // For browsers that aren't going to respect the CSS transition, 1784 | // Lets just check our height against the window height right off 1785 | // the bat. 1786 | this.onResize(); 1787 | 1788 | if (_this.__closeButton) { 1789 | _this.__closeButton.innerHTML = v ? GUI.TEXT_OPEN : GUI.TEXT_CLOSED; 1790 | } 1791 | } 1792 | }, 1793 | 1794 | /** 1795 | * Contains all presets 1796 | * @type Object 1797 | */ 1798 | load: { 1799 | get: function() { 1800 | return params.load; 1801 | } 1802 | }, 1803 | 1804 | /** 1805 | * Determines whether or not to use localStorage as the means for 1806 | * remembering 1807 | * @type Boolean 1808 | */ 1809 | useLocalStorage: { 1810 | 1811 | get: function() { 1812 | return use_local_storage; 1813 | }, 1814 | set: function(bool) { 1815 | if (SUPPORTS_LOCAL_STORAGE) { 1816 | use_local_storage = bool; 1817 | if (bool) { 1818 | dom.bind(window, 'unload', saveToLocalStorage); 1819 | } else { 1820 | dom.unbind(window, 'unload', saveToLocalStorage); 1821 | } 1822 | localStorage.setItem(getLocalStorageHash(_this, 'isLocal'), bool); 1823 | } 1824 | } 1825 | 1826 | } 1827 | 1828 | }); 1829 | 1830 | // Are we a root level GUI? 1831 | if (common.isUndefined(params.parent)) { 1832 | 1833 | params.closed = false; 1834 | 1835 | dom.addClass(this.domElement, GUI.CLASS_MAIN); 1836 | dom.makeSelectable(this.domElement, false); 1837 | 1838 | // Are we supposed to be loading locally? 1839 | if (SUPPORTS_LOCAL_STORAGE) { 1840 | 1841 | if (use_local_storage) { 1842 | 1843 | _this.useLocalStorage = true; 1844 | 1845 | var saved_gui = localStorage.getItem(getLocalStorageHash(this, 'gui')); 1846 | 1847 | if (saved_gui) { 1848 | params.load = JSON.parse(saved_gui); 1849 | } 1850 | 1851 | } 1852 | 1853 | } 1854 | 1855 | this.__closeButton = document.createElement('div'); 1856 | this.__closeButton.innerHTML = GUI.TEXT_CLOSED; 1857 | dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BUTTON); 1858 | this.domElement.appendChild(this.__closeButton); 1859 | 1860 | dom.bind(this.__closeButton, 'click', function() { 1861 | 1862 | _this.closed = !_this.closed; 1863 | 1864 | 1865 | }); 1866 | 1867 | 1868 | // Oh, you're a nested GUI! 1869 | } else { 1870 | 1871 | if (params.closed === undefined) { 1872 | params.closed = true; 1873 | } 1874 | 1875 | var title_row_name = document.createTextNode(params.name); 1876 | dom.addClass(title_row_name, 'controller-name'); 1877 | 1878 | var title_row = addRow(_this, title_row_name); 1879 | 1880 | var on_click_title = function(e) { 1881 | e.preventDefault(); 1882 | _this.closed = !_this.closed; 1883 | return false; 1884 | }; 1885 | 1886 | dom.addClass(this.__ul, GUI.CLASS_CLOSED); 1887 | 1888 | dom.addClass(title_row, 'title'); 1889 | dom.bind(title_row, 'click', on_click_title); 1890 | 1891 | if (!params.closed) { 1892 | this.closed = false; 1893 | } 1894 | 1895 | } 1896 | 1897 | if (params.autoPlace) { 1898 | 1899 | if (common.isUndefined(params.parent)) { 1900 | 1901 | if (auto_place_virgin) { 1902 | auto_place_container = document.createElement('div'); 1903 | dom.addClass(auto_place_container, CSS_NAMESPACE); 1904 | dom.addClass(auto_place_container, GUI.CLASS_AUTO_PLACE_CONTAINER); 1905 | document.body.appendChild(auto_place_container); 1906 | auto_place_virgin = false; 1907 | } 1908 | 1909 | // Put it in the dom for you. 1910 | auto_place_container.appendChild(this.domElement); 1911 | 1912 | // Apply the auto styles 1913 | dom.addClass(this.domElement, GUI.CLASS_AUTO_PLACE); 1914 | 1915 | } 1916 | 1917 | 1918 | // Make it not elastic. 1919 | if (!this.parent) setWidth(_this, params.width); 1920 | 1921 | } 1922 | 1923 | dom.bind(window, 'resize', function() { _this.onResize() }); 1924 | dom.bind(this.__ul, 'webkitTransitionEnd', function() { _this.onResize(); }); 1925 | dom.bind(this.__ul, 'transitionend', function() { _this.onResize() }); 1926 | dom.bind(this.__ul, 'oTransitionEnd', function() { _this.onResize() }); 1927 | this.onResize(); 1928 | 1929 | 1930 | if (params.resizable) { 1931 | addResizeHandle(this); 1932 | } 1933 | 1934 | saveToLocalStorage = function () { 1935 | if (SUPPORTS_LOCAL_STORAGE && localStorage.getItem(getLocalStorageHash(_this, 'isLocal')) === 'true') { 1936 | localStorage.setItem(getLocalStorageHash(_this, 'gui'), JSON.stringify(_this.getSaveObject())); 1937 | } 1938 | } 1939 | 1940 | // expose this method publicly 1941 | this.saveToLocalStorageIfPossible = saveToLocalStorage; 1942 | 1943 | var root = _this.getRoot(); 1944 | function resetWidth() { 1945 | var root = _this.getRoot(); 1946 | root.width += 1; 1947 | common.defer(function() { 1948 | root.width -= 1; 1949 | }); 1950 | } 1951 | 1952 | if (!params.parent) { 1953 | resetWidth(); 1954 | } 1955 | 1956 | }; 1957 | 1958 | GUI.toggleHide = function() { 1959 | 1960 | hide = !hide; 1961 | common.each(hideable_guis, function(gui) { 1962 | gui.domElement.style.zIndex = hide ? -999 : 999; 1963 | gui.domElement.style.opacity = hide ? 0 : 1; 1964 | }); 1965 | }; 1966 | 1967 | GUI.CLASS_AUTO_PLACE = 'a'; 1968 | GUI.CLASS_AUTO_PLACE_CONTAINER = 'ac'; 1969 | GUI.CLASS_MAIN = 'main'; 1970 | GUI.CLASS_CONTROLLER_ROW = 'cr'; 1971 | GUI.CLASS_TOO_TALL = 'taller-than-window'; 1972 | GUI.CLASS_CLOSED = 'closed'; 1973 | GUI.CLASS_CLOSE_BUTTON = 'close-button'; 1974 | GUI.CLASS_DRAG = 'drag'; 1975 | 1976 | GUI.DEFAULT_WIDTH = 245; 1977 | GUI.TEXT_CLOSED = 'Close Controls'; 1978 | GUI.TEXT_OPEN = 'Open Controls'; 1979 | 1980 | dom.bind(window, 'keydown', function(e) { 1981 | 1982 | if (document.activeElement.type !== 'text' && 1983 | (e.which === HIDE_KEY_CODE || e.keyCode == HIDE_KEY_CODE)) { 1984 | GUI.toggleHide(); 1985 | } 1986 | 1987 | }, false); 1988 | 1989 | common.extend( 1990 | 1991 | GUI.prototype, 1992 | 1993 | /** @lends dat.gui.GUI */ 1994 | { 1995 | 1996 | /** 1997 | * @param object 1998 | * @param property 1999 | * @returns {dat.controllers.Controller} The new controller that was added. 2000 | * @instance 2001 | */ 2002 | add: function(object, property) { 2003 | 2004 | return add( 2005 | this, 2006 | object, 2007 | property, 2008 | { 2009 | factoryArgs: Array.prototype.slice.call(arguments, 2) 2010 | } 2011 | ); 2012 | 2013 | }, 2014 | 2015 | /** 2016 | * @param object 2017 | * @param property 2018 | * @returns {dat.controllers.ColorController} The new controller that was added. 2019 | * @instance 2020 | */ 2021 | addColor: function(object, property) { 2022 | 2023 | return add( 2024 | this, 2025 | object, 2026 | property, 2027 | { 2028 | color: true 2029 | } 2030 | ); 2031 | 2032 | }, 2033 | 2034 | /** 2035 | * @param controller 2036 | * @instance 2037 | */ 2038 | remove: function(controller) { 2039 | 2040 | // TODO listening? 2041 | this.__ul.removeChild(controller.__li); 2042 | this.__controllers.slice(this.__controllers.indexOf(controller), 1); 2043 | var _this = this; 2044 | common.defer(function() { 2045 | _this.onResize(); 2046 | }); 2047 | 2048 | }, 2049 | 2050 | destroy: function() { 2051 | 2052 | if (this.autoPlace) { 2053 | auto_place_container.removeChild(this.domElement); 2054 | } 2055 | 2056 | }, 2057 | 2058 | /** 2059 | * @param name 2060 | * @returns {dat.gui.GUI} The new folder. 2061 | * @throws {Error} if this GUI already has a folder by the specified 2062 | * name 2063 | * @instance 2064 | */ 2065 | addFolder: function(name) { 2066 | 2067 | // We have to prevent collisions on names in order to have a key 2068 | // by which to remember saved values 2069 | if (this.__folders[name] !== undefined) { 2070 | throw new Error('You already have a folder in this GUI by the' + 2071 | ' name "' + name + '"'); 2072 | } 2073 | 2074 | var new_gui_params = { name: name, parent: this }; 2075 | 2076 | // We need to pass down the autoPlace trait so that we can 2077 | // attach event listeners to open/close folder actions to 2078 | // ensure that a scrollbar appears if the window is too short. 2079 | new_gui_params.autoPlace = this.autoPlace; 2080 | 2081 | // Do we have saved appearance data for this folder? 2082 | 2083 | if (this.load && // Anything loaded? 2084 | this.load.folders && // Was my parent a dead-end? 2085 | this.load.folders[name]) { // Did daddy remember me? 2086 | 2087 | // Start me closed if I was closed 2088 | new_gui_params.closed = this.load.folders[name].closed; 2089 | 2090 | // Pass down the loaded data 2091 | new_gui_params.load = this.load.folders[name]; 2092 | 2093 | } 2094 | 2095 | var gui = new GUI(new_gui_params); 2096 | this.__folders[name] = gui; 2097 | 2098 | var li = addRow(this, gui.domElement); 2099 | dom.addClass(li, 'folder'); 2100 | return gui; 2101 | 2102 | }, 2103 | 2104 | open: function() { 2105 | this.closed = false; 2106 | }, 2107 | 2108 | close: function() { 2109 | this.closed = true; 2110 | }, 2111 | 2112 | onResize: function() { 2113 | 2114 | var root = this.getRoot(); 2115 | 2116 | if (root.scrollable) { 2117 | 2118 | var top = dom.getOffset(root.__ul).top; 2119 | var h = 0; 2120 | 2121 | common.each(root.__ul.childNodes, function(node) { 2122 | if (! (root.autoPlace && node === root.__save_row)) 2123 | h += dom.getHeight(node); 2124 | }); 2125 | 2126 | if (window.innerHeight - top - CLOSE_BUTTON_HEIGHT < h) { 2127 | dom.addClass(root.domElement, GUI.CLASS_TOO_TALL); 2128 | root.__ul.style.height = window.innerHeight - top - CLOSE_BUTTON_HEIGHT + 'px'; 2129 | } else { 2130 | dom.removeClass(root.domElement, GUI.CLASS_TOO_TALL); 2131 | root.__ul.style.height = 'auto'; 2132 | } 2133 | 2134 | } 2135 | 2136 | if (root.__resize_handle) { 2137 | common.defer(function() { 2138 | root.__resize_handle.style.height = root.__ul.offsetHeight + 'px'; 2139 | }); 2140 | } 2141 | 2142 | if (root.__closeButton) { 2143 | root.__closeButton.style.width = root.width + 'px'; 2144 | } 2145 | 2146 | }, 2147 | 2148 | /** 2149 | * Mark objects for saving. The order of these objects cannot change as 2150 | * the GUI grows. When remembering new objects, append them to the end 2151 | * of the list. 2152 | * 2153 | * @param {Object...} objects 2154 | * @throws {Error} if not called on a top level GUI. 2155 | * @instance 2156 | */ 2157 | remember: function() { 2158 | 2159 | if (common.isUndefined(SAVE_DIALOGUE)) { 2160 | SAVE_DIALOGUE = new CenteredDiv(); 2161 | SAVE_DIALOGUE.domElement.innerHTML = saveDialogueContents; 2162 | } 2163 | 2164 | if (this.parent) { 2165 | throw new Error("You can only call remember on a top level GUI."); 2166 | } 2167 | 2168 | var _this = this; 2169 | 2170 | common.each(Array.prototype.slice.call(arguments), function(object) { 2171 | if (_this.__rememberedObjects.length == 0) { 2172 | addSaveMenu(_this); 2173 | } 2174 | if (_this.__rememberedObjects.indexOf(object) == -1) { 2175 | _this.__rememberedObjects.push(object); 2176 | } 2177 | }); 2178 | 2179 | if (this.autoPlace) { 2180 | // Set save row width 2181 | setWidth(this, this.width); 2182 | } 2183 | 2184 | }, 2185 | 2186 | /** 2187 | * @returns {dat.gui.GUI} the topmost parent GUI of a nested GUI. 2188 | * @instance 2189 | */ 2190 | getRoot: function() { 2191 | var gui = this; 2192 | while (gui.parent) { 2193 | gui = gui.parent; 2194 | } 2195 | return gui; 2196 | }, 2197 | 2198 | /** 2199 | * @returns {Object} a JSON object representing the current state of 2200 | * this GUI as well as its remembered properties. 2201 | * @instance 2202 | */ 2203 | getSaveObject: function() { 2204 | 2205 | var toReturn = this.load; 2206 | 2207 | toReturn.closed = this.closed; 2208 | 2209 | // Am I remembering any values? 2210 | if (this.__rememberedObjects.length > 0) { 2211 | 2212 | toReturn.preset = this.preset; 2213 | 2214 | if (!toReturn.remembered) { 2215 | toReturn.remembered = {}; 2216 | } 2217 | 2218 | toReturn.remembered[this.preset] = getCurrentPreset(this); 2219 | 2220 | } 2221 | 2222 | toReturn.folders = {}; 2223 | common.each(this.__folders, function(element, key) { 2224 | toReturn.folders[key] = element.getSaveObject(); 2225 | }); 2226 | 2227 | return toReturn; 2228 | 2229 | }, 2230 | 2231 | save: function() { 2232 | 2233 | if (!this.load.remembered) { 2234 | this.load.remembered = {}; 2235 | } 2236 | 2237 | this.load.remembered[this.preset] = getCurrentPreset(this); 2238 | markPresetModified(this, false); 2239 | this.saveToLocalStorageIfPossible(); 2240 | 2241 | }, 2242 | 2243 | saveAs: function(presetName) { 2244 | 2245 | if (!this.load.remembered) { 2246 | 2247 | // Retain default values upon first save 2248 | this.load.remembered = {}; 2249 | this.load.remembered[DEFAULT_DEFAULT_PRESET_NAME] = getCurrentPreset(this, true); 2250 | 2251 | } 2252 | 2253 | this.load.remembered[presetName] = getCurrentPreset(this); 2254 | this.preset = presetName; 2255 | addPresetOption(this, presetName, true); 2256 | this.saveToLocalStorageIfPossible(); 2257 | 2258 | }, 2259 | 2260 | revert: function(gui) { 2261 | 2262 | common.each(this.__controllers, function(controller) { 2263 | // Make revert work on Default. 2264 | if (!this.getRoot().load.remembered) { 2265 | controller.setValue(controller.initialValue); 2266 | } else { 2267 | recallSavedValue(gui || this.getRoot(), controller); 2268 | } 2269 | }, this); 2270 | 2271 | common.each(this.__folders, function(folder) { 2272 | folder.revert(folder); 2273 | }); 2274 | 2275 | if (!gui) { 2276 | markPresetModified(this.getRoot(), false); 2277 | } 2278 | 2279 | 2280 | }, 2281 | 2282 | listen: function(controller) { 2283 | 2284 | var init = this.__listening.length == 0; 2285 | this.__listening.push(controller); 2286 | if (init) updateDisplays(this.__listening); 2287 | 2288 | } 2289 | 2290 | } 2291 | 2292 | ); 2293 | 2294 | function add(gui, object, property, params) { 2295 | 2296 | if (object[property] === undefined) { 2297 | throw new Error("Object " + object + " has no property \"" + property + "\""); 2298 | } 2299 | 2300 | var controller; 2301 | 2302 | if (params.color) { 2303 | 2304 | controller = new ColorController(object, property); 2305 | 2306 | } else { 2307 | 2308 | var factoryArgs = [object,property].concat(params.factoryArgs); 2309 | controller = controllerFactory.apply(gui, factoryArgs); 2310 | 2311 | } 2312 | 2313 | if (params.before instanceof Controller) { 2314 | params.before = params.before.__li; 2315 | } 2316 | 2317 | recallSavedValue(gui, controller); 2318 | 2319 | dom.addClass(controller.domElement, 'c'); 2320 | 2321 | var name = document.createElement('span'); 2322 | dom.addClass(name, 'property-name'); 2323 | name.innerHTML = controller.property; 2324 | 2325 | var container = document.createElement('div'); 2326 | container.appendChild(name); 2327 | container.appendChild(controller.domElement); 2328 | 2329 | var li = addRow(gui, container, params.before); 2330 | 2331 | dom.addClass(li, GUI.CLASS_CONTROLLER_ROW); 2332 | dom.addClass(li, typeof controller.getValue()); 2333 | 2334 | augmentController(gui, li, controller); 2335 | 2336 | gui.__controllers.push(controller); 2337 | 2338 | return controller; 2339 | 2340 | } 2341 | 2342 | /** 2343 | * Add a row to the end of the GUI or before another row. 2344 | * 2345 | * @param gui 2346 | * @param [dom] If specified, inserts the dom content in the new row 2347 | * @param [liBefore] If specified, places the new row before another row 2348 | */ 2349 | function addRow(gui, dom, liBefore) { 2350 | var li = document.createElement('li'); 2351 | if (dom) li.appendChild(dom); 2352 | if (liBefore) { 2353 | gui.__ul.insertBefore(li, params.before); 2354 | } else { 2355 | gui.__ul.appendChild(li); 2356 | } 2357 | gui.onResize(); 2358 | return li; 2359 | } 2360 | 2361 | function augmentController(gui, li, controller) { 2362 | 2363 | controller.__li = li; 2364 | controller.__gui = gui; 2365 | 2366 | common.extend(controller, { 2367 | 2368 | options: function(options) { 2369 | 2370 | if (arguments.length > 1) { 2371 | controller.remove(); 2372 | 2373 | return add( 2374 | gui, 2375 | controller.object, 2376 | controller.property, 2377 | { 2378 | before: controller.__li.nextElementSibling, 2379 | factoryArgs: [common.toArray(arguments)] 2380 | } 2381 | ); 2382 | 2383 | } 2384 | 2385 | if (common.isArray(options) || common.isObject(options)) { 2386 | controller.remove(); 2387 | 2388 | return add( 2389 | gui, 2390 | controller.object, 2391 | controller.property, 2392 | { 2393 | before: controller.__li.nextElementSibling, 2394 | factoryArgs: [options] 2395 | } 2396 | ); 2397 | 2398 | } 2399 | 2400 | }, 2401 | 2402 | name: function(v) { 2403 | controller.__li.firstElementChild.firstElementChild.innerHTML = v; 2404 | return controller; 2405 | }, 2406 | 2407 | listen: function() { 2408 | controller.__gui.listen(controller); 2409 | return controller; 2410 | }, 2411 | 2412 | remove: function() { 2413 | controller.__gui.remove(controller); 2414 | return controller; 2415 | } 2416 | 2417 | }); 2418 | 2419 | // All sliders should be accompanied by a box. 2420 | if (controller instanceof NumberControllerSlider) { 2421 | 2422 | var box = new NumberControllerBox(controller.object, controller.property, 2423 | { min: controller.__min, max: controller.__max, step: controller.__step }); 2424 | 2425 | common.each(['updateDisplay', 'onChange', 'onFinishChange'], function(method) { 2426 | var pc = controller[method]; 2427 | var pb = box[method]; 2428 | controller[method] = box[method] = function() { 2429 | var args = Array.prototype.slice.call(arguments); 2430 | pc.apply(controller, args); 2431 | return pb.apply(box, args); 2432 | } 2433 | }); 2434 | 2435 | dom.addClass(li, 'has-slider'); 2436 | controller.domElement.insertBefore(box.domElement, controller.domElement.firstElementChild); 2437 | 2438 | } 2439 | else if (controller instanceof NumberControllerBox) { 2440 | 2441 | var r = function(returned) { 2442 | 2443 | // Have we defined both boundaries? 2444 | if (common.isNumber(controller.__min) && common.isNumber(controller.__max)) { 2445 | 2446 | // Well, then lets just replace this with a slider. 2447 | controller.remove(); 2448 | return add( 2449 | gui, 2450 | controller.object, 2451 | controller.property, 2452 | { 2453 | before: controller.__li.nextElementSibling, 2454 | factoryArgs: [controller.__min, controller.__max, controller.__step] 2455 | }); 2456 | 2457 | } 2458 | 2459 | return returned; 2460 | 2461 | }; 2462 | 2463 | controller.min = common.compose(r, controller.min); 2464 | controller.max = common.compose(r, controller.max); 2465 | 2466 | } 2467 | else if (controller instanceof BooleanController) { 2468 | 2469 | dom.bind(li, 'click', function() { 2470 | dom.fakeEvent(controller.__checkbox, 'click'); 2471 | }); 2472 | 2473 | dom.bind(controller.__checkbox, 'click', function(e) { 2474 | e.stopPropagation(); // Prevents double-toggle 2475 | }) 2476 | 2477 | } 2478 | else if (controller instanceof FunctionController) { 2479 | 2480 | dom.bind(li, 'click', function() { 2481 | dom.fakeEvent(controller.__button, 'click'); 2482 | }); 2483 | 2484 | dom.bind(li, 'mouseover', function() { 2485 | dom.addClass(controller.__button, 'hover'); 2486 | }); 2487 | 2488 | dom.bind(li, 'mouseout', function() { 2489 | dom.removeClass(controller.__button, 'hover'); 2490 | }); 2491 | 2492 | } 2493 | else if (controller instanceof ColorController) { 2494 | 2495 | dom.addClass(li, 'color'); 2496 | controller.updateDisplay = common.compose(function(r) { 2497 | li.style.borderLeftColor = controller.__color.toString(); 2498 | return r; 2499 | }, controller.updateDisplay); 2500 | 2501 | controller.updateDisplay(); 2502 | 2503 | } 2504 | 2505 | controller.setValue = common.compose(function(r) { 2506 | if (gui.getRoot().__preset_select && controller.isModified()) { 2507 | markPresetModified(gui.getRoot(), true); 2508 | } 2509 | return r; 2510 | }, controller.setValue); 2511 | 2512 | } 2513 | 2514 | function recallSavedValue(gui, controller) { 2515 | 2516 | // Find the topmost GUI, that's where remembered objects live. 2517 | var root = gui.getRoot(); 2518 | 2519 | // Does the object we're controlling match anything we've been told to 2520 | // remember? 2521 | var matched_index = root.__rememberedObjects.indexOf(controller.object); 2522 | 2523 | // Why yes, it does! 2524 | if (matched_index != -1) { 2525 | 2526 | // Let me fetch a map of controllers for thcommon.isObject. 2527 | var controller_map = 2528 | root.__rememberedObjectIndecesToControllers[matched_index]; 2529 | 2530 | // Ohp, I believe this is the first controller we've created for this 2531 | // object. Lets make the map fresh. 2532 | if (controller_map === undefined) { 2533 | controller_map = {}; 2534 | root.__rememberedObjectIndecesToControllers[matched_index] = 2535 | controller_map; 2536 | } 2537 | 2538 | // Keep track of this controller 2539 | controller_map[controller.property] = controller; 2540 | 2541 | // Okay, now have we saved any values for this controller? 2542 | if (root.load && root.load.remembered) { 2543 | 2544 | var preset_map = root.load.remembered; 2545 | 2546 | // Which preset are we trying to load? 2547 | var preset; 2548 | 2549 | if (preset_map[gui.preset]) { 2550 | 2551 | preset = preset_map[gui.preset]; 2552 | 2553 | } else if (preset_map[DEFAULT_DEFAULT_PRESET_NAME]) { 2554 | 2555 | // Uhh, you can have the default instead? 2556 | preset = preset_map[DEFAULT_DEFAULT_PRESET_NAME]; 2557 | 2558 | } else { 2559 | 2560 | // Nada. 2561 | 2562 | return; 2563 | 2564 | } 2565 | 2566 | 2567 | // Did the loaded object remember thcommon.isObject? 2568 | if (preset[matched_index] && 2569 | 2570 | // Did we remember this particular property? 2571 | preset[matched_index][controller.property] !== undefined) { 2572 | 2573 | // We did remember something for this guy ... 2574 | var value = preset[matched_index][controller.property]; 2575 | 2576 | // And that's what it is. 2577 | controller.initialValue = value; 2578 | controller.setValue(value); 2579 | 2580 | } 2581 | 2582 | } 2583 | 2584 | } 2585 | 2586 | } 2587 | 2588 | function getLocalStorageHash(gui, key) { 2589 | // TODO how does this deal with multiple GUI's? 2590 | return document.location.href + '.' + key; 2591 | 2592 | } 2593 | 2594 | function addSaveMenu(gui) { 2595 | 2596 | var div = gui.__save_row = document.createElement('li'); 2597 | 2598 | dom.addClass(gui.domElement, 'has-save'); 2599 | 2600 | gui.__ul.insertBefore(div, gui.__ul.firstChild); 2601 | 2602 | dom.addClass(div, 'save-row'); 2603 | 2604 | var gears = document.createElement('span'); 2605 | gears.innerHTML = ' '; 2606 | dom.addClass(gears, 'button gears'); 2607 | 2608 | // TODO replace with FunctionController 2609 | var button = document.createElement('span'); 2610 | button.innerHTML = 'Save'; 2611 | dom.addClass(button, 'button'); 2612 | dom.addClass(button, 'save'); 2613 | 2614 | var button2 = document.createElement('span'); 2615 | button2.innerHTML = 'New'; 2616 | dom.addClass(button2, 'button'); 2617 | dom.addClass(button2, 'save-as'); 2618 | 2619 | var button3 = document.createElement('span'); 2620 | button3.innerHTML = 'Revert'; 2621 | dom.addClass(button3, 'button'); 2622 | dom.addClass(button3, 'revert'); 2623 | 2624 | var select = gui.__preset_select = document.createElement('select'); 2625 | 2626 | if (gui.load && gui.load.remembered) { 2627 | 2628 | common.each(gui.load.remembered, function(value, key) { 2629 | addPresetOption(gui, key, key == gui.preset); 2630 | }); 2631 | 2632 | } else { 2633 | addPresetOption(gui, DEFAULT_DEFAULT_PRESET_NAME, false); 2634 | } 2635 | 2636 | dom.bind(select, 'change', function() { 2637 | 2638 | 2639 | for (var index = 0; index < gui.__preset_select.length; index++) { 2640 | gui.__preset_select[index].innerHTML = gui.__preset_select[index].value; 2641 | } 2642 | 2643 | gui.preset = this.value; 2644 | 2645 | }); 2646 | 2647 | div.appendChild(select); 2648 | div.appendChild(gears); 2649 | div.appendChild(button); 2650 | div.appendChild(button2); 2651 | div.appendChild(button3); 2652 | 2653 | if (SUPPORTS_LOCAL_STORAGE) { 2654 | 2655 | var saveLocally = document.getElementById('dg-save-locally'); 2656 | var explain = document.getElementById('dg-local-explain'); 2657 | 2658 | saveLocally.style.display = 'block'; 2659 | 2660 | var localStorageCheckBox = document.getElementById('dg-local-storage'); 2661 | 2662 | if (localStorage.getItem(getLocalStorageHash(gui, 'isLocal')) === 'true') { 2663 | localStorageCheckBox.setAttribute('checked', 'checked'); 2664 | } 2665 | 2666 | function showHideExplain() { 2667 | explain.style.display = gui.useLocalStorage ? 'block' : 'none'; 2668 | } 2669 | 2670 | showHideExplain(); 2671 | 2672 | // TODO: Use a boolean controller, fool! 2673 | dom.bind(localStorageCheckBox, 'change', function() { 2674 | gui.useLocalStorage = !gui.useLocalStorage; 2675 | showHideExplain(); 2676 | }); 2677 | 2678 | } 2679 | 2680 | var newConstructorTextArea = document.getElementById('dg-new-constructor'); 2681 | 2682 | dom.bind(newConstructorTextArea, 'keydown', function(e) { 2683 | if (e.metaKey && (e.which === 67 || e.keyCode == 67)) { 2684 | SAVE_DIALOGUE.hide(); 2685 | } 2686 | }); 2687 | 2688 | dom.bind(gears, 'click', function() { 2689 | newConstructorTextArea.innerHTML = JSON.stringify(gui.getSaveObject(), undefined, 2); 2690 | SAVE_DIALOGUE.show(); 2691 | newConstructorTextArea.focus(); 2692 | newConstructorTextArea.select(); 2693 | }); 2694 | 2695 | dom.bind(button, 'click', function() { 2696 | gui.save(); 2697 | }); 2698 | 2699 | dom.bind(button2, 'click', function() { 2700 | var presetName = prompt('Enter a new preset name.'); 2701 | if (presetName) gui.saveAs(presetName); 2702 | }); 2703 | 2704 | dom.bind(button3, 'click', function() { 2705 | gui.revert(); 2706 | }); 2707 | 2708 | // div.appendChild(button2); 2709 | 2710 | } 2711 | 2712 | function addResizeHandle(gui) { 2713 | 2714 | gui.__resize_handle = document.createElement('div'); 2715 | 2716 | common.extend(gui.__resize_handle.style, { 2717 | 2718 | width: '6px', 2719 | marginLeft: '-3px', 2720 | height: '200px', 2721 | cursor: 'ew-resize', 2722 | position: 'absolute' 2723 | // border: '1px solid blue' 2724 | 2725 | }); 2726 | 2727 | var pmouseX; 2728 | 2729 | dom.bind(gui.__resize_handle, 'mousedown', dragStart); 2730 | dom.bind(gui.__closeButton, 'mousedown', dragStart); 2731 | 2732 | gui.domElement.insertBefore(gui.__resize_handle, gui.domElement.firstElementChild); 2733 | 2734 | function dragStart(e) { 2735 | 2736 | e.preventDefault(); 2737 | 2738 | pmouseX = e.clientX; 2739 | 2740 | dom.addClass(gui.__closeButton, GUI.CLASS_DRAG); 2741 | dom.bind(window, 'mousemove', drag); 2742 | dom.bind(window, 'mouseup', dragStop); 2743 | 2744 | return false; 2745 | 2746 | } 2747 | 2748 | function drag(e) { 2749 | 2750 | e.preventDefault(); 2751 | 2752 | gui.width += pmouseX - e.clientX; 2753 | gui.onResize(); 2754 | pmouseX = e.clientX; 2755 | 2756 | return false; 2757 | 2758 | } 2759 | 2760 | function dragStop() { 2761 | 2762 | dom.removeClass(gui.__closeButton, GUI.CLASS_DRAG); 2763 | dom.unbind(window, 'mousemove', drag); 2764 | dom.unbind(window, 'mouseup', dragStop); 2765 | 2766 | } 2767 | 2768 | } 2769 | 2770 | function setWidth(gui, w) { 2771 | gui.domElement.style.width = w + 'px'; 2772 | // Auto placed save-rows are position fixed, so we have to 2773 | // set the width manually if we want it to bleed to the edge 2774 | if (gui.__save_row && gui.autoPlace) { 2775 | gui.__save_row.style.width = w + 'px'; 2776 | }if (gui.__closeButton) { 2777 | gui.__closeButton.style.width = w + 'px'; 2778 | } 2779 | } 2780 | 2781 | function getCurrentPreset(gui, useInitialValues) { 2782 | 2783 | var toReturn = {}; 2784 | 2785 | // For each object I'm remembering 2786 | common.each(gui.__rememberedObjects, function(val, index) { 2787 | 2788 | var saved_values = {}; 2789 | 2790 | // The controllers I've made for thcommon.isObject by property 2791 | var controller_map = 2792 | gui.__rememberedObjectIndecesToControllers[index]; 2793 | 2794 | // Remember each value for each property 2795 | common.each(controller_map, function(controller, property) { 2796 | saved_values[property] = useInitialValues ? controller.initialValue : controller.getValue(); 2797 | }); 2798 | 2799 | // Save the values for thcommon.isObject 2800 | toReturn[index] = saved_values; 2801 | 2802 | }); 2803 | 2804 | return toReturn; 2805 | 2806 | } 2807 | 2808 | function addPresetOption(gui, name, setSelected) { 2809 | var opt = document.createElement('option'); 2810 | opt.innerHTML = name; 2811 | opt.value = name; 2812 | gui.__preset_select.appendChild(opt); 2813 | if (setSelected) { 2814 | gui.__preset_select.selectedIndex = gui.__preset_select.length - 1; 2815 | } 2816 | } 2817 | 2818 | function setPresetSelectIndex(gui) { 2819 | for (var index = 0; index < gui.__preset_select.length; index++) { 2820 | if (gui.__preset_select[index].value == gui.preset) { 2821 | gui.__preset_select.selectedIndex = index; 2822 | } 2823 | } 2824 | } 2825 | 2826 | function markPresetModified(gui, modified) { 2827 | var opt = gui.__preset_select[gui.__preset_select.selectedIndex]; 2828 | // console.log('mark', modified, opt); 2829 | if (modified) { 2830 | opt.innerHTML = opt.value + "*"; 2831 | } else { 2832 | opt.innerHTML = opt.value; 2833 | } 2834 | } 2835 | 2836 | function updateDisplays(controllerArray) { 2837 | 2838 | 2839 | if (controllerArray.length != 0) { 2840 | 2841 | requestAnimationFrame(function() { 2842 | updateDisplays(controllerArray); 2843 | }); 2844 | 2845 | } 2846 | 2847 | common.each(controllerArray, function(c) { 2848 | c.updateDisplay(); 2849 | }); 2850 | 2851 | } 2852 | 2853 | return GUI; 2854 | 2855 | })(dat.utils.css, 2856 | "
\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
\n \n
\n\n
", 2857 | ".dg {\n /** Clear list styles */\n /* Auto-place container */\n /* Auto-placed GUI's */\n /* Line items that don't contain folders. */\n /** Folder names */\n /** Hides closed items */\n /** Controller row */\n /** Name-half (left) */\n /** Controller-half (right) */\n /** Controller placement */\n /** Shorter number boxes when slider is present. */\n /** Ensure the entire boolean and function row shows a hand */ }\n .dg ul {\n list-style: none;\n margin: 0;\n padding: 0;\n width: 100%;\n clear: both; }\n .dg.ac {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 0;\n z-index: 0; }\n .dg:not(.ac) .main {\n /** Exclude mains in ac so that we don't hide close button */\n overflow: hidden; }\n .dg.main {\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear; }\n .dg.main.taller-than-window {\n overflow-y: auto; }\n .dg.main.taller-than-window .close-button {\n opacity: 1;\n /* TODO, these are style notes */\n margin-top: -1px;\n border-top: 1px solid #2c2c2c; }\n .dg.main ul.closed .close-button {\n opacity: 1 !important; }\n .dg.main:hover .close-button,\n .dg.main .close-button.drag {\n opacity: 1; }\n .dg.main .close-button {\n /*opacity: 0;*/\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear;\n border: 0;\n position: absolute;\n line-height: 19px;\n height: 20px;\n /* TODO, these are style notes */\n cursor: pointer;\n text-align: center;\n background-color: #000; }\n .dg.main .close-button:hover {\n background-color: #111; }\n .dg.a {\n float: right;\n margin-right: 15px;\n overflow-x: hidden; }\n .dg.a.has-save > ul {\n margin-top: 27px; }\n .dg.a.has-save > ul.closed {\n margin-top: 0; }\n .dg.a .save-row {\n position: fixed;\n top: 0;\n z-index: 1002; }\n .dg li {\n -webkit-transition: height 0.1s ease-out;\n -o-transition: height 0.1s ease-out;\n -moz-transition: height 0.1s ease-out;\n transition: height 0.1s ease-out; }\n .dg li:not(.folder) {\n cursor: auto;\n height: 27px;\n line-height: 27px;\n overflow: hidden;\n padding: 0 4px 0 5px; }\n .dg li.folder {\n padding: 0;\n border-left: 4px solid rgba(0, 0, 0, 0); }\n .dg li.title {\n cursor: pointer;\n margin-left: -4px; }\n .dg .closed li:not(.title),\n .dg .closed ul li,\n .dg .closed ul li > * {\n height: 0;\n overflow: hidden;\n border: 0; }\n .dg .cr {\n clear: both;\n padding-left: 3px;\n height: 27px; }\n .dg .property-name {\n cursor: default;\n float: left;\n clear: left;\n width: 40%;\n overflow: hidden;\n text-overflow: ellipsis; }\n .dg .c {\n float: left;\n width: 60%; }\n .dg .c input[type=text] {\n border: 0;\n margin-top: 4px;\n padding: 3px;\n width: 100%;\n float: right; }\n .dg .has-slider input[type=text] {\n width: 30%;\n /*display: none;*/\n margin-left: 0; }\n .dg .slider {\n float: left;\n width: 66%;\n margin-left: -5px;\n margin-right: 0;\n height: 19px;\n margin-top: 4px; }\n .dg .slider-fg {\n height: 100%; }\n .dg .c input[type=checkbox] {\n margin-top: 9px; }\n .dg .c select {\n margin-top: 5px; }\n .dg .cr.function,\n .dg .cr.function .property-name,\n .dg .cr.function *,\n .dg .cr.boolean,\n .dg .cr.boolean * {\n cursor: pointer; }\n .dg .selector {\n display: none;\n position: absolute;\n margin-left: -9px;\n margin-top: 23px;\n z-index: 10; }\n .dg .c:hover .selector,\n .dg .selector.drag {\n display: block; }\n .dg li.save-row {\n padding: 0; }\n .dg li.save-row .button {\n display: inline-block;\n padding: 0px 6px; }\n .dg.dialogue {\n background-color: #222;\n width: 460px;\n padding: 15px;\n font-size: 13px;\n line-height: 15px; }\n\n/* TODO Separate style and structure */\n#dg-new-constructor {\n padding: 10px;\n color: #222;\n font-family: Monaco, monospace;\n font-size: 10px;\n border: 0;\n resize: none;\n box-shadow: inset 1px 1px 1px #888;\n word-wrap: break-word;\n margin: 12px 0;\n display: block;\n width: 440px;\n overflow-y: scroll;\n height: 100px;\n position: relative; }\n\n#dg-local-explain {\n display: none;\n font-size: 11px;\n line-height: 17px;\n border-radius: 3px;\n background-color: #333;\n padding: 8px;\n margin-top: 10px; }\n #dg-local-explain code {\n font-size: 10px; }\n\n#dat-gui-save-locally {\n display: none; }\n\n/** Main type */\n.dg {\n color: #eee;\n font: 11px 'Lucida Grande', sans-serif;\n text-shadow: 0 -1px 0 #111;\n /** Auto place */\n /* Controller row,
  • */\n /** Controllers */ }\n .dg.main {\n /** Scrollbar */ }\n .dg.main::-webkit-scrollbar {\n width: 5px;\n background: #1a1a1a; }\n .dg.main::-webkit-scrollbar-corner {\n height: 0;\n display: none; }\n .dg.main::-webkit-scrollbar-thumb {\n border-radius: 5px;\n background: #676767; }\n .dg li:not(.folder) {\n background: #1a1a1a;\n border-bottom: 1px solid #2c2c2c; }\n .dg li.save-row {\n line-height: 25px;\n background: #dad5cb;\n border: 0; }\n .dg li.save-row select {\n margin-left: 5px;\n width: 108px; }\n .dg li.save-row .button {\n margin-left: 5px;\n margin-top: 1px;\n border-radius: 2px;\n font-size: 9px;\n line-height: 7px;\n padding: 4px 4px 5px 4px;\n background: #c5bdad;\n color: #fff;\n text-shadow: 0 1px 0 #b0a58f;\n box-shadow: 0 -1px 0 #b0a58f;\n cursor: pointer; }\n .dg li.save-row .button.gears {\n background: #c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;\n height: 7px;\n width: 8px; }\n .dg li.save-row .button:hover {\n background-color: #bab19e;\n box-shadow: 0 -1px 0 #b0a58f; }\n .dg li.folder {\n border-bottom: 0; }\n .dg li.title {\n padding-left: 16px;\n background: black url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.2); }\n .dg .closed li.title {\n background-image: url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==); }\n .dg .cr.boolean {\n border-left: 3px solid #806787; }\n .dg .cr.function {\n border-left: 3px solid #e61d5f; }\n .dg .cr.number {\n border-left: 3px solid #2fa1d6; }\n .dg .cr.number input[type=text] {\n color: #2fa1d6; }\n .dg .cr.string {\n border-left: 3px solid #1ed36f; }\n .dg .cr.string input[type=text] {\n color: #1ed36f; }\n .dg .cr.function:hover, .dg .cr.boolean:hover {\n background: #111; }\n .dg .c input[type=text] {\n background: #303030;\n outline: none; }\n .dg .c input[type=text]:hover {\n background: #3c3c3c; }\n .dg .c input[type=text]:focus {\n background: #494949;\n color: #fff; }\n .dg .c .slider {\n background: #303030;\n cursor: ew-resize; }\n .dg .c .slider-fg {\n background: #2fa1d6; }\n .dg .c .slider:hover {\n background: #3c3c3c; }\n .dg .c .slider:hover .slider-fg {\n background: #44abda; }\n", 2858 | dat.controllers.factory = (function (OptionController, NumberControllerBox, NumberControllerSlider, StringController, FunctionController, BooleanController, common) { 2859 | 2860 | return function(object, property) { 2861 | 2862 | var initialValue = object[property]; 2863 | 2864 | // Providing options? 2865 | if (common.isArray(arguments[2]) || common.isObject(arguments[2])) { 2866 | return new OptionController(object, property, arguments[2]); 2867 | } 2868 | 2869 | // Providing a map? 2870 | 2871 | if (common.isNumber(initialValue)) { 2872 | 2873 | if (common.isNumber(arguments[2]) && common.isNumber(arguments[3])) { 2874 | 2875 | // Has min and max. 2876 | return new NumberControllerSlider(object, property, arguments[2], arguments[3]); 2877 | 2878 | } else { 2879 | 2880 | return new NumberControllerBox(object, property, { min: arguments[2], max: arguments[3] }); 2881 | 2882 | } 2883 | 2884 | } 2885 | 2886 | if (common.isString(initialValue)) { 2887 | return new StringController(object, property); 2888 | } 2889 | 2890 | if (common.isFunction(initialValue)) { 2891 | return new FunctionController(object, property, ''); 2892 | } 2893 | 2894 | if (common.isBoolean(initialValue)) { 2895 | return new BooleanController(object, property); 2896 | } 2897 | 2898 | } 2899 | 2900 | })(dat.controllers.OptionController, 2901 | dat.controllers.NumberControllerBox, 2902 | dat.controllers.NumberControllerSlider, 2903 | dat.controllers.StringController = (function (Controller, dom, common) { 2904 | 2905 | /** 2906 | * @class Provides a text input to alter the string property of an object. 2907 | * 2908 | * @extends dat.controllers.Controller 2909 | * 2910 | * @param {Object} object The object to be manipulated 2911 | * @param {string} property The name of the property to be manipulated 2912 | * 2913 | * @member dat.controllers 2914 | */ 2915 | var StringController = function(object, property) { 2916 | 2917 | StringController.superclass.call(this, object, property); 2918 | 2919 | var _this = this; 2920 | 2921 | this.__input = document.createElement('input'); 2922 | this.__input.setAttribute('type', 'text'); 2923 | 2924 | dom.bind(this.__input, 'keyup', onChange); 2925 | dom.bind(this.__input, 'change', onChange); 2926 | dom.bind(this.__input, 'blur', onBlur); 2927 | dom.bind(this.__input, 'keydown', function(e) { 2928 | if (e.keyCode === 13) { 2929 | this.blur(); 2930 | } 2931 | }); 2932 | 2933 | 2934 | function onChange() { 2935 | _this.setValue(_this.__input.value); 2936 | } 2937 | 2938 | function onBlur() { 2939 | if (_this.__onFinishChange) { 2940 | _this.__onFinishChange.call(_this, _this.getValue()); 2941 | } 2942 | } 2943 | 2944 | this.updateDisplay(); 2945 | 2946 | this.domElement.appendChild(this.__input); 2947 | 2948 | }; 2949 | 2950 | StringController.superclass = Controller; 2951 | 2952 | common.extend( 2953 | 2954 | StringController.prototype, 2955 | Controller.prototype, 2956 | 2957 | { 2958 | 2959 | updateDisplay: function() { 2960 | // Stops the caret from moving on account of: 2961 | // keyup -> setValue -> updateDisplay 2962 | if (!dom.isActive(this.__input)) { 2963 | this.__input.value = this.getValue(); 2964 | } 2965 | return StringController.superclass.prototype.updateDisplay.call(this); 2966 | } 2967 | 2968 | } 2969 | 2970 | ); 2971 | 2972 | return StringController; 2973 | 2974 | })(dat.controllers.Controller, 2975 | dat.dom.dom, 2976 | dat.utils.common), 2977 | dat.controllers.FunctionController, 2978 | dat.controllers.BooleanController, 2979 | dat.utils.common), 2980 | dat.controllers.Controller, 2981 | dat.controllers.BooleanController, 2982 | dat.controllers.FunctionController, 2983 | dat.controllers.NumberControllerBox, 2984 | dat.controllers.NumberControllerSlider, 2985 | dat.controllers.OptionController, 2986 | dat.controllers.ColorController = (function (Controller, dom, Color, interpret, common) { 2987 | 2988 | var ColorController = function(object, property) { 2989 | 2990 | ColorController.superclass.call(this, object, property); 2991 | 2992 | this.__color = new Color(this.getValue()); 2993 | this.__temp = new Color(0); 2994 | 2995 | var _this = this; 2996 | 2997 | this.domElement = document.createElement('div'); 2998 | 2999 | dom.makeSelectable(this.domElement, false); 3000 | 3001 | this.__selector = document.createElement('div'); 3002 | this.__selector.className = 'selector'; 3003 | 3004 | this.__saturation_field = document.createElement('div'); 3005 | this.__saturation_field.className = 'saturation-field'; 3006 | 3007 | this.__field_knob = document.createElement('div'); 3008 | this.__field_knob.className = 'field-knob'; 3009 | this.__field_knob_border = '2px solid '; 3010 | 3011 | this.__hue_knob = document.createElement('div'); 3012 | this.__hue_knob.className = 'hue-knob'; 3013 | 3014 | this.__hue_field = document.createElement('div'); 3015 | this.__hue_field.className = 'hue-field'; 3016 | 3017 | this.__input = document.createElement('input'); 3018 | this.__input.type = 'text'; 3019 | this.__input_textShadow = '0 1px 1px '; 3020 | 3021 | dom.bind(this.__input, 'keydown', function(e) { 3022 | if (e.keyCode === 13) { // on enter 3023 | onBlur.call(this); 3024 | } 3025 | }); 3026 | 3027 | dom.bind(this.__input, 'blur', onBlur); 3028 | 3029 | dom.bind(this.__selector, 'mousedown', function(e) { 3030 | 3031 | dom 3032 | .addClass(this, 'drag') 3033 | .bind(window, 'mouseup', function(e) { 3034 | dom.removeClass(_this.__selector, 'drag'); 3035 | }); 3036 | 3037 | }); 3038 | 3039 | var value_field = document.createElement('div'); 3040 | 3041 | common.extend(this.__selector.style, { 3042 | width: '122px', 3043 | height: '102px', 3044 | padding: '3px', 3045 | backgroundColor: '#222', 3046 | boxShadow: '0px 1px 3px rgba(0,0,0,0.3)' 3047 | }); 3048 | 3049 | common.extend(this.__field_knob.style, { 3050 | position: 'absolute', 3051 | width: '12px', 3052 | height: '12px', 3053 | border: this.__field_knob_border + (this.__color.v < .5 ? '#fff' : '#000'), 3054 | boxShadow: '0px 1px 3px rgba(0,0,0,0.5)', 3055 | borderRadius: '12px', 3056 | zIndex: 1 3057 | }); 3058 | 3059 | common.extend(this.__hue_knob.style, { 3060 | position: 'absolute', 3061 | width: '15px', 3062 | height: '2px', 3063 | borderRight: '4px solid #fff', 3064 | zIndex: 1 3065 | }); 3066 | 3067 | common.extend(this.__saturation_field.style, { 3068 | width: '100px', 3069 | height: '100px', 3070 | border: '1px solid #555', 3071 | marginRight: '3px', 3072 | display: 'inline-block', 3073 | cursor: 'pointer' 3074 | }); 3075 | 3076 | common.extend(value_field.style, { 3077 | width: '100%', 3078 | height: '100%', 3079 | background: 'none' 3080 | }); 3081 | 3082 | linearGradient(value_field, 'top', 'rgba(0,0,0,0)', '#000'); 3083 | 3084 | common.extend(this.__hue_field.style, { 3085 | width: '15px', 3086 | height: '100px', 3087 | display: 'inline-block', 3088 | border: '1px solid #555', 3089 | cursor: 'ns-resize' 3090 | }); 3091 | 3092 | hueGradient(this.__hue_field); 3093 | 3094 | common.extend(this.__input.style, { 3095 | outline: 'none', 3096 | // width: '120px', 3097 | textAlign: 'center', 3098 | // padding: '4px', 3099 | // marginBottom: '6px', 3100 | color: '#fff', 3101 | border: 0, 3102 | fontWeight: 'bold', 3103 | textShadow: this.__input_textShadow + 'rgba(0,0,0,0.7)' 3104 | }); 3105 | 3106 | dom.bind(this.__saturation_field, 'mousedown', fieldDown); 3107 | dom.bind(this.__field_knob, 'mousedown', fieldDown); 3108 | 3109 | dom.bind(this.__hue_field, 'mousedown', function(e) { 3110 | setH(e); 3111 | dom.bind(window, 'mousemove', setH); 3112 | dom.bind(window, 'mouseup', unbindH); 3113 | }); 3114 | 3115 | function fieldDown(e) { 3116 | setSV(e); 3117 | // document.body.style.cursor = 'none'; 3118 | dom.bind(window, 'mousemove', setSV); 3119 | dom.bind(window, 'mouseup', unbindSV); 3120 | } 3121 | 3122 | function unbindSV() { 3123 | dom.unbind(window, 'mousemove', setSV); 3124 | dom.unbind(window, 'mouseup', unbindSV); 3125 | // document.body.style.cursor = 'default'; 3126 | } 3127 | 3128 | function onBlur() { 3129 | var i = interpret(this.value); 3130 | if (i !== false) { 3131 | _this.__color.__state = i; 3132 | _this.setValue(_this.__color.toOriginal()); 3133 | } else { 3134 | this.value = _this.__color.toString(); 3135 | } 3136 | } 3137 | 3138 | function unbindH() { 3139 | dom.unbind(window, 'mousemove', setH); 3140 | dom.unbind(window, 'mouseup', unbindH); 3141 | } 3142 | 3143 | this.__saturation_field.appendChild(value_field); 3144 | this.__selector.appendChild(this.__field_knob); 3145 | this.__selector.appendChild(this.__saturation_field); 3146 | this.__selector.appendChild(this.__hue_field); 3147 | this.__hue_field.appendChild(this.__hue_knob); 3148 | 3149 | this.domElement.appendChild(this.__input); 3150 | this.domElement.appendChild(this.__selector); 3151 | 3152 | this.updateDisplay(); 3153 | 3154 | function setSV(e) { 3155 | 3156 | e.preventDefault(); 3157 | 3158 | var w = dom.getWidth(_this.__saturation_field); 3159 | var o = dom.getOffset(_this.__saturation_field); 3160 | var s = (e.clientX - o.left + document.body.scrollLeft) / w; 3161 | var v = 1 - (e.clientY - o.top + document.body.scrollTop) / w; 3162 | 3163 | if (v > 1) v = 1; 3164 | else if (v < 0) v = 0; 3165 | 3166 | if (s > 1) s = 1; 3167 | else if (s < 0) s = 0; 3168 | 3169 | _this.__color.v = v; 3170 | _this.__color.s = s; 3171 | 3172 | _this.setValue(_this.__color.toOriginal()); 3173 | 3174 | 3175 | return false; 3176 | 3177 | } 3178 | 3179 | function setH(e) { 3180 | 3181 | e.preventDefault(); 3182 | 3183 | var s = dom.getHeight(_this.__hue_field); 3184 | var o = dom.getOffset(_this.__hue_field); 3185 | var h = 1 - (e.clientY - o.top + document.body.scrollTop) / s; 3186 | 3187 | if (h > 1) h = 1; 3188 | else if (h < 0) h = 0; 3189 | 3190 | _this.__color.h = h * 360; 3191 | 3192 | _this.setValue(_this.__color.toOriginal()); 3193 | 3194 | return false; 3195 | 3196 | } 3197 | 3198 | }; 3199 | 3200 | ColorController.superclass = Controller; 3201 | 3202 | common.extend( 3203 | 3204 | ColorController.prototype, 3205 | Controller.prototype, 3206 | 3207 | { 3208 | 3209 | updateDisplay: function() { 3210 | 3211 | var i = interpret(this.getValue()); 3212 | 3213 | if (i !== false) { 3214 | 3215 | var mismatch = false; 3216 | 3217 | // Check for mismatch on the interpreted value. 3218 | 3219 | common.each(Color.COMPONENTS, function(component) { 3220 | if (!common.isUndefined(i[component]) && 3221 | !common.isUndefined(this.__color.__state[component]) && 3222 | i[component] !== this.__color.__state[component]) { 3223 | mismatch = true; 3224 | return {}; // break 3225 | } 3226 | }, this); 3227 | 3228 | // If nothing diverges, we keep our previous values 3229 | // for statefulness, otherwise we recalculate fresh 3230 | if (mismatch) { 3231 | common.extend(this.__color.__state, i); 3232 | } 3233 | 3234 | } 3235 | 3236 | common.extend(this.__temp.__state, this.__color.__state); 3237 | 3238 | this.__temp.a = 1; 3239 | 3240 | var flip = (this.__color.v < .5 || this.__color.s > .5) ? 255 : 0; 3241 | var _flip = 255 - flip; 3242 | 3243 | common.extend(this.__field_knob.style, { 3244 | marginLeft: 100 * this.__color.s - 7 + 'px', 3245 | marginTop: 100 * (1 - this.__color.v) - 7 + 'px', 3246 | backgroundColor: this.__temp.toString(), 3247 | border: this.__field_knob_border + 'rgb(' + flip + ',' + flip + ',' + flip +')' 3248 | }); 3249 | 3250 | this.__hue_knob.style.marginTop = (1 - this.__color.h / 360) * 100 + 'px' 3251 | 3252 | this.__temp.s = 1; 3253 | this.__temp.v = 1; 3254 | 3255 | linearGradient(this.__saturation_field, 'left', '#fff', this.__temp.toString()); 3256 | 3257 | common.extend(this.__input.style, { 3258 | backgroundColor: this.__input.value = this.__color.toString(), 3259 | color: 'rgb(' + flip + ',' + flip + ',' + flip +')', 3260 | textShadow: this.__input_textShadow + 'rgba(' + _flip + ',' + _flip + ',' + _flip +',.7)' 3261 | }); 3262 | 3263 | } 3264 | 3265 | } 3266 | 3267 | ); 3268 | 3269 | var vendors = ['-moz-','-o-','-webkit-','-ms-','']; 3270 | 3271 | function linearGradient(elem, x, a, b) { 3272 | elem.style.background = ''; 3273 | common.each(vendors, function(vendor) { 3274 | elem.style.cssText += 'background: ' + vendor + 'linear-gradient('+x+', '+a+' 0%, ' + b + ' 100%); '; 3275 | }); 3276 | } 3277 | 3278 | function hueGradient(elem) { 3279 | elem.style.background = ''; 3280 | elem.style.cssText += 'background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);' 3281 | elem.style.cssText += 'background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);' 3282 | elem.style.cssText += 'background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);' 3283 | elem.style.cssText += 'background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);' 3284 | elem.style.cssText += 'background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);' 3285 | } 3286 | 3287 | 3288 | return ColorController; 3289 | 3290 | })(dat.controllers.Controller, 3291 | dat.dom.dom, 3292 | dat.color.Color = (function (interpret, math, toString, common) { 3293 | 3294 | var Color = function() { 3295 | 3296 | this.__state = interpret.apply(this, arguments); 3297 | 3298 | if (this.__state === false) { 3299 | throw 'Failed to interpret color arguments'; 3300 | } 3301 | 3302 | this.__state.a = this.__state.a || 1; 3303 | 3304 | 3305 | }; 3306 | 3307 | Color.COMPONENTS = ['r','g','b','h','s','v','hex','a']; 3308 | 3309 | common.extend(Color.prototype, { 3310 | 3311 | toString: function() { 3312 | return toString(this); 3313 | }, 3314 | 3315 | toOriginal: function() { 3316 | return this.__state.conversion.write(this); 3317 | } 3318 | 3319 | }); 3320 | 3321 | defineRGBComponent(Color.prototype, 'r', 2); 3322 | defineRGBComponent(Color.prototype, 'g', 1); 3323 | defineRGBComponent(Color.prototype, 'b', 0); 3324 | 3325 | defineHSVComponent(Color.prototype, 'h'); 3326 | defineHSVComponent(Color.prototype, 's'); 3327 | defineHSVComponent(Color.prototype, 'v'); 3328 | 3329 | Object.defineProperty(Color.prototype, 'a', { 3330 | 3331 | get: function() { 3332 | return this.__state.a; 3333 | }, 3334 | 3335 | set: function(v) { 3336 | this.__state.a = v; 3337 | } 3338 | 3339 | }); 3340 | 3341 | Object.defineProperty(Color.prototype, 'hex', { 3342 | 3343 | get: function() { 3344 | 3345 | if (!this.__state.space !== 'HEX') { 3346 | this.__state.hex = math.rgb_to_hex(this.r, this.g, this.b); 3347 | } 3348 | 3349 | return this.__state.hex; 3350 | 3351 | }, 3352 | 3353 | set: function(v) { 3354 | 3355 | this.__state.space = 'HEX'; 3356 | this.__state.hex = v; 3357 | 3358 | } 3359 | 3360 | }); 3361 | 3362 | function defineRGBComponent(target, component, componentHexIndex) { 3363 | 3364 | Object.defineProperty(target, component, { 3365 | 3366 | get: function() { 3367 | 3368 | if (this.__state.space === 'RGB') { 3369 | return this.__state[component]; 3370 | } 3371 | 3372 | recalculateRGB(this, component, componentHexIndex); 3373 | 3374 | return this.__state[component]; 3375 | 3376 | }, 3377 | 3378 | set: function(v) { 3379 | 3380 | if (this.__state.space !== 'RGB') { 3381 | recalculateRGB(this, component, componentHexIndex); 3382 | this.__state.space = 'RGB'; 3383 | } 3384 | 3385 | this.__state[component] = v; 3386 | 3387 | } 3388 | 3389 | }); 3390 | 3391 | } 3392 | 3393 | function defineHSVComponent(target, component) { 3394 | 3395 | Object.defineProperty(target, component, { 3396 | 3397 | get: function() { 3398 | 3399 | if (this.__state.space === 'HSV') 3400 | return this.__state[component]; 3401 | 3402 | recalculateHSV(this); 3403 | 3404 | return this.__state[component]; 3405 | 3406 | }, 3407 | 3408 | set: function(v) { 3409 | 3410 | if (this.__state.space !== 'HSV') { 3411 | recalculateHSV(this); 3412 | this.__state.space = 'HSV'; 3413 | } 3414 | 3415 | this.__state[component] = v; 3416 | 3417 | } 3418 | 3419 | }); 3420 | 3421 | } 3422 | 3423 | function recalculateRGB(color, component, componentHexIndex) { 3424 | 3425 | if (color.__state.space === 'HEX') { 3426 | 3427 | color.__state[component] = math.component_from_hex(color.__state.hex, componentHexIndex); 3428 | 3429 | } else if (color.__state.space === 'HSV') { 3430 | 3431 | common.extend(color.__state, math.hsv_to_rgb(color.__state.h, color.__state.s, color.__state.v)); 3432 | 3433 | } else { 3434 | 3435 | throw 'Corrupted color state'; 3436 | 3437 | } 3438 | 3439 | } 3440 | 3441 | function recalculateHSV(color) { 3442 | 3443 | var result = math.rgb_to_hsv(color.r, color.g, color.b); 3444 | 3445 | common.extend(color.__state, 3446 | { 3447 | s: result.s, 3448 | v: result.v 3449 | } 3450 | ); 3451 | 3452 | if (!common.isNaN(result.h)) { 3453 | color.__state.h = result.h; 3454 | } else if (common.isUndefined(color.__state.h)) { 3455 | color.__state.h = 0; 3456 | } 3457 | 3458 | } 3459 | 3460 | return Color; 3461 | 3462 | })(dat.color.interpret, 3463 | dat.color.math = (function () { 3464 | 3465 | var tmpComponent; 3466 | 3467 | return { 3468 | 3469 | hsv_to_rgb: function(h, s, v) { 3470 | 3471 | var hi = Math.floor(h / 60) % 6; 3472 | 3473 | var f = h / 60 - Math.floor(h / 60); 3474 | var p = v * (1.0 - s); 3475 | var q = v * (1.0 - (f * s)); 3476 | var t = v * (1.0 - ((1.0 - f) * s)); 3477 | var c = [ 3478 | [v, t, p], 3479 | [q, v, p], 3480 | [p, v, t], 3481 | [p, q, v], 3482 | [t, p, v], 3483 | [v, p, q] 3484 | ][hi]; 3485 | 3486 | return { 3487 | r: c[0] * 255, 3488 | g: c[1] * 255, 3489 | b: c[2] * 255 3490 | }; 3491 | 3492 | }, 3493 | 3494 | rgb_to_hsv: function(r, g, b) { 3495 | 3496 | var min = Math.min(r, g, b), 3497 | max = Math.max(r, g, b), 3498 | delta = max - min, 3499 | h, s; 3500 | 3501 | if (max != 0) { 3502 | s = delta / max; 3503 | } else { 3504 | return { 3505 | h: NaN, 3506 | s: 0, 3507 | v: 0 3508 | }; 3509 | } 3510 | 3511 | if (r == max) { 3512 | h = (g - b) / delta; 3513 | } else if (g == max) { 3514 | h = 2 + (b - r) / delta; 3515 | } else { 3516 | h = 4 + (r - g) / delta; 3517 | } 3518 | h /= 6; 3519 | if (h < 0) { 3520 | h += 1; 3521 | } 3522 | 3523 | return { 3524 | h: h * 360, 3525 | s: s, 3526 | v: max / 255 3527 | }; 3528 | }, 3529 | 3530 | rgb_to_hex: function(r, g, b) { 3531 | var hex = this.hex_with_component(0, 2, r); 3532 | hex = this.hex_with_component(hex, 1, g); 3533 | hex = this.hex_with_component(hex, 0, b); 3534 | return hex; 3535 | }, 3536 | 3537 | component_from_hex: function(hex, componentIndex) { 3538 | return (hex >> (componentIndex * 8)) & 0xFF; 3539 | }, 3540 | 3541 | hex_with_component: function(hex, componentIndex, value) { 3542 | return value << (tmpComponent = componentIndex * 8) | (hex & ~ (0xFF << tmpComponent)); 3543 | } 3544 | 3545 | } 3546 | 3547 | })(), 3548 | dat.color.toString, 3549 | dat.utils.common), 3550 | dat.color.interpret, 3551 | dat.utils.common), 3552 | dat.utils.requestAnimationFrame = (function () { 3553 | 3554 | /** 3555 | * requirejs version of Paul Irish's RequestAnimationFrame 3556 | * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 3557 | */ 3558 | 3559 | return window.requestAnimationFrame || 3560 | window.webkitRequestAnimationFrame || 3561 | window.mozRequestAnimationFrame || 3562 | window.oRequestAnimationFrame || 3563 | window.msRequestAnimationFrame || 3564 | function(callback, element) { 3565 | 3566 | window.setTimeout(callback, 1000 / 60); 3567 | 3568 | }; 3569 | })(), 3570 | dat.dom.CenteredDiv = (function (dom, common) { 3571 | 3572 | 3573 | var CenteredDiv = function() { 3574 | 3575 | this.backgroundElement = document.createElement('div'); 3576 | common.extend(this.backgroundElement.style, { 3577 | backgroundColor: 'rgba(0,0,0,0.8)', 3578 | top: 0, 3579 | left: 0, 3580 | display: 'none', 3581 | zIndex: '1000', 3582 | opacity: 0, 3583 | WebkitTransition: 'opacity 0.2s linear' 3584 | }); 3585 | 3586 | dom.makeFullscreen(this.backgroundElement); 3587 | this.backgroundElement.style.position = 'fixed'; 3588 | 3589 | this.domElement = document.createElement('div'); 3590 | common.extend(this.domElement.style, { 3591 | position: 'fixed', 3592 | display: 'none', 3593 | zIndex: '1001', 3594 | opacity: 0, 3595 | WebkitTransition: '-webkit-transform 0.2s ease-out, opacity 0.2s linear' 3596 | }); 3597 | 3598 | 3599 | document.body.appendChild(this.backgroundElement); 3600 | document.body.appendChild(this.domElement); 3601 | 3602 | var _this = this; 3603 | dom.bind(this.backgroundElement, 'click', function() { 3604 | _this.hide(); 3605 | }); 3606 | 3607 | 3608 | }; 3609 | 3610 | CenteredDiv.prototype.show = function() { 3611 | 3612 | var _this = this; 3613 | 3614 | 3615 | 3616 | this.backgroundElement.style.display = 'block'; 3617 | 3618 | this.domElement.style.display = 'block'; 3619 | this.domElement.style.opacity = 0; 3620 | // this.domElement.style.top = '52%'; 3621 | this.domElement.style.webkitTransform = 'scale(1.1)'; 3622 | 3623 | this.layout(); 3624 | 3625 | common.defer(function() { 3626 | _this.backgroundElement.style.opacity = 1; 3627 | _this.domElement.style.opacity = 1; 3628 | _this.domElement.style.webkitTransform = 'scale(1)'; 3629 | }); 3630 | 3631 | }; 3632 | 3633 | CenteredDiv.prototype.hide = function() { 3634 | 3635 | var _this = this; 3636 | 3637 | var hide = function() { 3638 | 3639 | _this.domElement.style.display = 'none'; 3640 | _this.backgroundElement.style.display = 'none'; 3641 | 3642 | dom.unbind(_this.domElement, 'webkitTransitionEnd', hide); 3643 | dom.unbind(_this.domElement, 'transitionend', hide); 3644 | dom.unbind(_this.domElement, 'oTransitionEnd', hide); 3645 | 3646 | }; 3647 | 3648 | dom.bind(this.domElement, 'webkitTransitionEnd', hide); 3649 | dom.bind(this.domElement, 'transitionend', hide); 3650 | dom.bind(this.domElement, 'oTransitionEnd', hide); 3651 | 3652 | this.backgroundElement.style.opacity = 0; 3653 | // this.domElement.style.top = '48%'; 3654 | this.domElement.style.opacity = 0; 3655 | this.domElement.style.webkitTransform = 'scale(1.1)'; 3656 | 3657 | }; 3658 | 3659 | CenteredDiv.prototype.layout = function() { 3660 | this.domElement.style.left = window.innerWidth/2 - dom.getWidth(this.domElement) / 2 + 'px'; 3661 | this.domElement.style.top = window.innerHeight/2 - dom.getHeight(this.domElement) / 2 + 'px'; 3662 | }; 3663 | 3664 | function lockScroll(e) { 3665 | console.log(e); 3666 | } 3667 | 3668 | return CenteredDiv; 3669 | 3670 | })(dat.dom.dom, 3671 | dat.utils.common), 3672 | dat.dom.dom, 3673 | dat.utils.common); 3674 | 3675 | module.exports = dat 3676 | -------------------------------------------------------------------------------- /app/index.css: -------------------------------------------------------------------------------- 1 | #infos{ 2 | color:#FFF; 3 | position:absolute; 4 | top:10px; 5 | left:10px; 6 | font-size:11px; 7 | z-index:1; 8 | } 9 | 10 | canvas{ 11 | position:absolute; 12 | top:0; 13 | left:0; 14 | z-index:-1; 15 | } 16 | 17 | html,body{ 18 | padding:0; 19 | margin:0; 20 | } 21 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | img2dds 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/renderer.js: -------------------------------------------------------------------------------- 1 | const dxt = require('dxt') 2 | const fs = require('fs') 3 | const electron = require('electron') 4 | const isPo2 = require('is-power-of-two') 5 | const nextPo2 = require('next-power-of-two') 6 | 7 | const THREE = require('./three.js') 8 | const dat = require('./dat.gui.js') 9 | require('./DDSLoader.js') 10 | 11 | //------------------------------------------------------------------------------ CONFIG 12 | 13 | let config = { 14 | algo:'DXT1', 15 | compressionQuality:'normal', 16 | colorMetric:'perceptual', 17 | folderDDS:'./', 18 | weightColourByAlpha:false, 19 | transparent:true, 20 | materials:{}, 21 | rotatePreview:false, 22 | flipY:true 23 | } 24 | 25 | gui = new dat.GUI() 26 | gui.add(config,"folderDDS") 27 | gui.add(config,'rotatePreview') 28 | gui.add(config,'flipY') 29 | gui.add(config,'algo',['DXT1','DXT3','DXT5']) 30 | gui.add(config,'compressionQuality',['low','normal','hight']) 31 | gui.add(config,'colorMetric',['perceptual','uniform']) 32 | gui.add(config,'weightColourByAlpha') 33 | // let c = gui.add(config,'transparent') 34 | let guiMaterials = gui.add(config,'materials',{}).onChange(function(){}) 35 | let materials = [] 36 | let currentMaterial = 0 37 | 38 | let infos = document.querySelector("#infos"); 39 | 40 | //------------------------------------------------------------------------------ DDS Generation 41 | 42 | function ParseFile(file) { 43 | infos.innerHTML = 44 | "

    File information: " + file.name + 45 | // "
    type: " + file.type + 46 | "
    size: " + file.size +" bytes

    " 47 | 48 | let reader = new FileReader() 49 | reader.onload = function (e) { 50 | let img = new Image() 51 | infos.innerHTML+='---------------
    compressing, please wait.
    ' 52 | img.onload = function(){ 53 | infos.innerHTML+="dimension: " + img.width +"x"+ img.height +" px
    " 54 | const start = Date.now() 55 | 56 | let canvas = document.createElement('canvas') 57 | let w = img.width 58 | let h = img.height 59 | if(!isPo2(w)) { w = nextPo2(w) } 60 | if(!isPo2(h)) { h = nextPo2(h) } 61 | // the dds algo need 256 as minimum size 62 | w = Math.max( 256, w ) 63 | h = Math.max( 256, w ) 64 | 65 | canvas.width = w 66 | canvas.height = h 67 | let ctx = canvas.getContext('2d') 68 | ctx.scale(1,-1) 69 | ctx.drawImage(img,0,0,img.width,img.height,0,0,w,config.flipY?-h:h) 70 | let data = ctx.getImageData(0, 0, w, h).data 71 | 72 | if(w != img.width || h != img.height) { 73 | infos.innerHTML+="dimension: " + img.width +"x"+ img.height +" px
    " 74 | } 75 | 76 | if(!config.transparent){ data = getRGB(data) } 77 | 78 | let header = "" 79 | header += "DDS "//magic number 80 | header += int32ToFourCC(124)//size header, have to be 124 81 | header += int32ToFourCC(calculatePitch(w*h,config.transparent?32:24))//pitch 82 | header += int32ToFourCC(w)//width 83 | header += int32ToFourCC(h)//height 84 | header += int32ToFourCC(0)// ? 85 | header += int32ToFourCC(0)// ? 86 | header += int32ToFourCC(0)//mipmapCount 87 | header += int32ToFourCC(0)// unused 88 | header += int32ToFourCC(0)// unused 89 | header += int32ToFourCC(0)// unused 90 | header += int32ToFourCC(0)// unused 91 | header += int32ToFourCC(0)// unused 92 | header += int32ToFourCC(0)// unused 93 | header += int32ToFourCC(0)// unused 94 | header += int32ToFourCC(0)// unused 95 | header += int32ToFourCC(0)// unused 96 | header += int32ToFourCC(0)// unused 97 | header += int32ToFourCC(0)// unused 98 | header += int32ToFourCC(0)// unused 99 | let pfFlags = 0x4 100 | if(config.transparent){ pfFlags |= 0x1 } 101 | header += int32ToFourCC(pfFlags)//off_pfFlags 102 | header += config.algo//off_pfFourCC 103 | if(config.transparent){ 104 | header += int32ToFourCC(32)//off_RGBBitCount 105 | header += int32ToFourCC(0xFF000000)//off_RBitMask 106 | header += int32ToFourCC(0xFF0000)//off_GBitMask 107 | header += int32ToFourCC(0xFF00)//off_BBitMask 108 | header += int32ToFourCC(0xFF)//off_ABitMask 109 | } 110 | else{ 111 | header += int32ToFourCC(24)//off_RGBBitCount 112 | header += int32ToFourCC(0xFF0000)//off_RBitMask 113 | header += int32ToFourCC(0xFF00)//off_GBitMask 114 | header += int32ToFourCC(0xFF)//off_BBitMask 115 | header += int32ToFourCC(0)//off_ABitMask 116 | } 117 | header += int32ToFourCC(0x1000)//off_caps : for special texture 118 | header += int32ToFourCC(0)//off_caps2 : for cubemap 119 | header += int32ToFourCC(0)//off_caps3 120 | header += int32ToFourCC(0)//off_caps4 121 | let headerBuffer = Buffer.from(header) 122 | 123 | let flag = 0 124 | switch(config.algo){ 125 | case "DXT1": flag |= dxt.kDxt1; break; 126 | case "DXT3": flag |= dxt.kDxt3; break; 127 | case "DXT5": flag |= dxt.kDxt5; break; 128 | } 129 | switch(config.compressionQuality){ 130 | case "low": flag |= dxt.kColourRangeFit; break; 131 | case "normal": flag |= dxt.kColourClusterFit; break; 132 | case "hight": flag |= dxt.kColourIterativeClusterFit; break; 133 | } 134 | switch(config.colorMetric){ 135 | case "perceptual": flag |= dxt.kColourMetricPerceptual; break; 136 | case "uniform": flag |= dxt.kColourMetricUniform; break; 137 | } 138 | if(config.weightColourByAlpha){ 139 | flag |= dxt.weightColourByAlpha; 140 | } 141 | 142 | let compressed = dxt.compress(Buffer.from(data),w,h,flag) 143 | let finalBuffer = Buffer.concat([headerBuffer,compressed]) 144 | let folder = config.folderDDS 145 | if(folder.substr(folder.length - 1) != '/'){ 146 | folder+='/' 147 | } 148 | infos.innerHTML+='---------------
    compression completed
    duration :'+(Date.now()-start)+"ms
    " 149 | let path = (file.path.replace(/[^\/]*$/, ""))+folder 150 | if(!fs.existsSync(path)){ 151 | fs.mkdirSync(path); 152 | } 153 | path+=file.name.replace(/\.[^/.]+$/,"")+".dds" 154 | let fd = fs.openSync(path, 'w') 155 | fs.write(fd, finalBuffer, 0, finalBuffer.length, 0, function(err,written){ 156 | infos.innerHTML+='size: '+finalBuffer.length+"bytes
    " 157 | infos.innerHTML+='size change: '+Math.ceil(parseInt(finalBuffer.length)/parseInt(file.size)*100)+"%
    " 158 | loadTexture(path,file.name) 159 | setTimeout(function(){ 160 | setTimeout(loadNext,1500) 161 | }) 162 | }) 163 | 164 | } 165 | img.src = e.target.result 166 | } 167 | reader.readAsDataURL(file) 168 | } 169 | 170 | let loadList = [] 171 | function loadNext(){ 172 | if(loadList.length>0){ 173 | ParseFile(loadList.pop()) 174 | } 175 | } 176 | 177 | function getRGB(data){ 178 | const l = data.length/4 179 | let rgb = new Uint8Array(l*3) 180 | for(let i=0;i> 8 ) & 0xff, 196 | ( value >> 16 ) & 0xff, 197 | ( value >> 24 ) & 0xff 198 | ) 199 | } 200 | 201 | //------------------------------------------------------------------------------ WEBGL 202 | 203 | let camera 204 | let scene 205 | let renderer 206 | let mesh 207 | let loader = new THREE.DDSLoader() 208 | 209 | function initWebgl(){ 210 | camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 2000 ) 211 | camera.position.z = 500 212 | camera.lookAt(new THREE.Vector3()) 213 | scene = new THREE.Scene() 214 | renderer = new THREE.WebGLRenderer( { antialias: true } ) 215 | renderer.setPixelRatio( window.devicePixelRatio ) 216 | renderer.setSize( window.innerWidth, window.innerHeight ) 217 | document.body.appendChild( renderer.domElement ) 218 | let geometry = new THREE.BoxGeometry( 200, 200, 200 ) 219 | let material = new THREE.MeshBasicMaterial( { color:Math.random()*0xFFFFFF, transparent: true } ) 220 | config.materials['base']=0 221 | materials.push(material) 222 | updateMaterialGUI() 223 | mesh = new THREE.Mesh( geometry,material ) 224 | scene.add( mesh ) 225 | } 226 | 227 | function animate() { 228 | requestAnimationFrame( animate ) 229 | if(config.rotatePreview){ 230 | mesh.rotation.x += 0.03 231 | mesh.rotation.y += 0.03 232 | } 233 | renderer.render( scene, camera ) 234 | } 235 | 236 | // TODO find better way to do so. 237 | function updateMaterialGUI(){ 238 | guiMaterials.remove() 239 | guiMaterials = gui.add(config,'materials',config.materials).onChange(function(value){ 240 | mesh.material = materials[value] 241 | }) 242 | } 243 | 244 | initWebgl() 245 | animate() 246 | 247 | function loadTexture(url,name){ 248 | const start = Date.now() 249 | url = url || __dirname+"/tmp/disturb_dxt1_mip.dds" 250 | let map = loader.load( url ) 251 | map.minFilter = map.magFilter = THREE.LinearFilter 252 | map.anisotropy = 4 253 | mesh.material = new THREE.MeshBasicMaterial( { map:map, transparent: true, side:THREE.DoubleSide } ) 254 | config.materials[name] = materials.length 255 | currentMaterial = materials.length 256 | materials.push(mesh.material) 257 | updateMaterialGUI() 258 | } 259 | 260 | //------------------------------------------------------------------------------ EVENT LISTENER 261 | 262 | document.addEventListener('dragover', function (e) { 263 | e.preventDefault() 264 | return false 265 | }, false) 266 | 267 | document.addEventListener('keydown', function (e) { 268 | // e.preventDefault() 269 | 270 | electron.ipcRenderer.send('invokeAction', e.keyCode) 271 | 272 | if(e.keyCode==39||e.keyCode==37){ 273 | if(e.keyCode==39){ currentMaterial++ } 274 | else if(e.keyCode==37){ currentMaterial-- } 275 | if(currentMaterial<0){currentMaterial=materials.length-1} 276 | currentMaterial%=materials.length 277 | mesh.material = materials[currentMaterial] 278 | } 279 | return false 280 | }, false) 281 | 282 | document.addEventListener('drop', function (e) { 283 | e.preventDefault() 284 | let files = e.target.files || e.dataTransfer.files 285 | for (let i = 0, f; f = files[i]; i++) { 286 | if(f.type.search("image")!=-1){ 287 | loadList.push(f) 288 | } 289 | } 290 | loadNext() 291 | return false 292 | }, false) 293 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const app = electron.app 3 | const BrowserWindow = electron.BrowserWindow 4 | 5 | let mainWindow 6 | 7 | function createWindow () { 8 | mainWindow = new BrowserWindow({width: 1024, height: 768}) 9 | mainWindow.loadURL(`file://${__dirname}/app/index.html`) 10 | // mainWindow.webContents.openDevTools() 11 | mainWindow.on('closed', function () { 12 | mainWindow = null 13 | }) 14 | } 15 | 16 | app.on('ready', createWindow) 17 | app.on('window-all-closed', function () { 18 | if (process.platform !== 'darwin') { 19 | app.quit() 20 | } 21 | }) 22 | 23 | app.on('activate', function () { 24 | if (mainWindow === null) { 25 | createWindow() 26 | } 27 | }) 28 | 29 | let devTools = false 30 | 31 | electron.ipcMain.on('invokeAction', function(event, keyCode){ 32 | if(keyCode!=68) 33 | return; 34 | 35 | devTools =! devTools 36 | 37 | if(devTools){ 38 | mainWindow.webContents.openDevTools() 39 | }else{ 40 | mainWindow.webContents.closeDevTools() 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "img2dds", 3 | "version": "1.1.0", 4 | "description": "Convert image to dds", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron main.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/makio64/img2dds.git" 12 | }, 13 | "keywords": [ 14 | "dxt", 15 | "dds", 16 | "compression", 17 | "texture", 18 | "electron" 19 | ], 20 | "author": "GitHub", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/makio64/img2dds/issues" 24 | }, 25 | "homepage": "https://github.com/makio64/img2dds#readme", 26 | "devDependencies": { 27 | "dxt": "^1.0.0", 28 | "electron-prebuilt": "^1.1.1", 29 | "is-power-of-two": "^1.0.0", 30 | "next-power-of-two": "^1.0.0" 31 | } 32 | } 33 | --------------------------------------------------------------------------------