├── LICENSE
├── README.md
├── demos
├── dat.gui.min.js
├── demo.html
├── demo_1.html
├── demo_2.html
├── demo_3.html
├── demo_4.html
├── demo_5.html
├── demo_6.html
├── demo_7.html
├── style.css
└── style.less
└── particles.js
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 MapleRecall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # html5-particles
2 | A particle generator on HTML 5 Canvas
3 |
4 | 一个性可能能不是很好的js粒子生成器,最初是为了完成某内部技术交流会做一个创意loading的作业而写的,结果越写越偏写出了这个东西,主要用途是在指定canvas里按一定的参数生成粒子。
5 |
6 | (最终loading的成品可以看[Demo_Loading](http://maplerecall.github.io/html5-particles/demos/demo_1.html))
7 |
8 | ##使用方法
9 | 在您的页面中引入这个js,最简单的方法如下
10 | ```js
11 | var mp = mapleParticles(canvas);
12 | ```
13 | 其中的canvas是需要进行绘制的canvas元素。调用该方法将返回一个对象,对象中包含配置参数对象和一些常用的方法,对参数对象进行修改可以直接反应到渲染结果,还是蛮方便的……吧……
14 |
15 | ##参数
16 | 啊参数好多好烦,直接先去[Demo_with_dat.gui](http://maplerecall.github.io/html5-particles/demos/demo.html)
17 | 里瞅瞅看效果吧。
18 |
19 | 参数是一个Object对象,带有参数的调用方法大致如下
20 | ```js
21 | var config={
22 | num: 200,
23 | size: {
24 | minSize: 10,
25 | maxSize: 300
26 | },
27 | }
28 | var mp = mapleParticles(canvas,config);
29 | ```
30 |
31 | 当然可选的参数远不止这些(我是懒鬼不想全写了),其它的去源码里瞅瞅呗,应该都有注释……
32 |
33 | ##关于性能
34 | 说实话写这个的时候是我第一次接触canvas,能画出东西就很开心了,所以没有优化性能(好吧也不知道怎么优化\_(:3」∠)_),当开启模糊效果的时候在非webkit/brink内核的浏览器上性能十分糟糕,如果关闭模糊,不设置太多粒子的话一般使用还是没啥问题的。
35 | ```js
36 | var config={
37 | blur:false//high performance
38 | }
39 | ```
40 | ##Demos
41 | 通过不同的配置可以达到不同的效果,下面列出了几个随便乱写的简单的demo,大多数都开启了模糊效果,建议使用webkit/brink内核浏览器查看。
42 | * 带有配置面板的demo,蛮好玩的:[Demo_with_dat.gui](http://maplerecall.github.io/html5-particles/demos/demo.html)
43 | * 技术交流会的最终Loading:[Demo_Loading](http://maplerecall.github.io/html5-particles/demos/demo_1.html)
44 | * 粉白粉白:[Love Me](http://maplerecall.github.io/html5-particles/demos/demo_2.html)
45 | * 土豪金:[GOLDEN TIME](http://maplerecall.github.io/html5-particles/demos/demo_3.html)
46 | * 天依未来蓝:[SEE THE FUTURE](http://maplerecall.github.io/html5-particles/demos/demo_4.html)
47 | * 鼠标/触摸随动:[MapleRecall](http://maplerecall.github.io/html5-particles/demos/demo_5.html)
48 | * GalGame:[さようなら](http://maplerecall.github.io/html5-particles/demos/demo_7.html)
49 |
50 |
--------------------------------------------------------------------------------
/demos/dat.gui.min.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(Math.abs(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 | this.getValue().call(this.object);
1088 | if (this.__onFinishChange) {
1089 | this.__onFinishChange.call(this, this.getValue());
1090 | }
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.splice(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