├── .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 | [](http://github.com/badges/stability-badges)
2 |
3 | # img2dds
4 |
5 | 
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 | * remember
ing
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 | "
GUI
's constructor:\n\n \n\n localStorage
on exit.\n\n 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 File information: " + file.name +
45 | // "
type: " + file.type +
46 | "
size: " + file.size +" bytes