muigui 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,11 @@
1
- # muigui
2
-
3
- # NOT READY for USE
4
-
5
- <!---
6
-
7
- <img src="./images/muigui.png" style="max-width: 640px">
1
+ # muigui (⍺)
8
2
 
9
3
  A simple Web UI library.
10
4
 
5
+ [See docs here](https://muigui.org)
6
+
11
7
  muigui is a simple UI library in the spirit of
12
- [dat.gui](https://github.com/dataarts/dat.gui) and/or [lil-gui](https://github.com/georgealways/).
8
+ [dat.gui](https://github.com/dataarts/dat.gui) and/or [lil-gui](https://github.com/georgealways/) and [tweakpane](https://cocopon.github.io/tweakpane/)
13
9
 
14
10
  ## Usage
15
11
 
@@ -28,66 +24,377 @@ Then
28
24
  ```js
29
25
  const s = {
30
26
  someNumber: 123,
31
- someString: "hello",
32
- someOption: "dog",
27
+ someString: 'hello',
28
+ someOption: 'dog',
33
29
  someColor: '#ED3281',
34
30
  someFunction: () => console.log('called')
35
31
  };
36
32
 
37
33
  const gui = new GUI();
38
34
  gui.add(s, 'someNumber', 0, 200); // range 0 to 200
39
- gui.add(s, 'someString);
40
- gui.add(s, 'someOption, ['cat', 'bird', 'dog']);
35
+ gui.add(s, 'someString');
36
+ gui.add(s, 'someOption', ['cat', 'bird', 'dog']);
41
37
  gui.addColor(s, 'someColor');
42
38
  gui.add(s, 'someFunction');
43
39
  ```
44
40
 
45
41
  produces
46
42
 
47
- <img src="./images/muigui-screenshot.png" style="max-width: 275px">
43
+ <img src="./images/muigui-screenshot.png" style="max-width: 250px">
48
44
 
49
- or a shorter version
45
+ ## Why
50
46
 
51
- ```js
52
- const s = {
53
- someNumber: 123,
54
- someString: "hello",
55
- someOption: "dog",
56
- someColor: '#ED3281',
57
- someFunction: () => console.log('called')
58
- };
47
+ So, to be honest, I like [tweakpane](https://cocopon.github.io/tweakpane/) and
48
+ I didn't know about it when I started this library. That said, I've looked
49
+ into using tweakpane and it doesn't meet my needs as of v4.0.0. Examples below
50
+
51
+ * ## Simpler
52
+
53
+ I wanted certain things to be simpler. For example, in dat.gui/lil.gui/tweakpane, if I wanted
54
+ to store radians in code but show degrees in the UI I had to jump through
55
+ hoops. I could either, store degrees and convert 😢
56
+
57
+ ```js
58
+ const settings = {
59
+ angle: 45,
60
+ };
61
+ gui.add(settings, 'angle', -360, 360);
62
+
63
+ ...
64
+ const rotation = settings.angle * Math.PI / 180
65
+ ```
66
+
67
+ That's bad IMO. I shouldn't have to refactor my code to fit the GUI.
68
+
69
+ I can make some proxy class that presents degrees to the GUI
70
+ and stores them in radians like this
71
+
72
+ ```js
73
+ class DegRadHelper {
74
+ constructor( obj, prop ) {
75
+ this.obj = obj;
76
+ this.prop = prop;
77
+ }
78
+ get value() {
79
+ return this.obj[this.prop] * 180 / Math.PI;
80
+ }
81
+ set value(v) {
82
+ this.obj[this.prop] = v * Math.PI / 180;
83
+ }
84
+ }
85
+ const settings = {
86
+ angle: Math.PI * 0.5,
87
+ };
88
+ gui.add(new DegRadHelper(settings, 'angle'), 'value', -360, 360);
89
+ ```
90
+
91
+ But that looks poor to use. What is `'value'`?? 😭
92
+
93
+ So, muigui handles that slightly nicer in the form of converters.
94
+
95
+ ```js
96
+ const settings = {
97
+ angle: Math.PI * 0.5,
98
+ };
99
+ const degToRad = d => d * Math.PI / 180;
100
+ const radToDeg = r => r * 180 / Math.PI;
101
+ gui.add(s, 'angleRad', {
102
+ min: -360, max: 360,
103
+ converters: {
104
+ to: radToDeg,
105
+ from: v => [true, degToRad(v)],
106
+ }});
107
+ ```
108
+
109
+ Typically I'll pull out the settings like this
110
+
111
+ ```js
112
+ const degToRad = d => d * Math.PI / 180;
113
+ const radToDeg = r => r * 180 / Math.PI;
114
+ const radToDegSettings {
115
+ min: -360, max: 360,
116
+ converters: {
117
+ to: radToDeg,
118
+ from: v => [true, degToRad(v)],
119
+ }});
120
+ ```
121
+
122
+ And then I can use it like this
123
+
124
+ ```js
125
+ const settings = {
126
+ angle: Math.PI * 0.5,
127
+ rotation: Math.PI * 0.25,
128
+ };
129
+ gui.add(s, 'angleRad', radToDegSettings);
130
+ gui.add(s, 'rotation', radToDegSettings);
131
+ ```
132
+
133
+ You provide converters where `to` converts to the form the UI wants
134
+ and `from` converts back. The reason `from` returns a tuple is that
135
+ it's used to convert the text and gives you a chance to say the text
136
+ does not match the required format in which case you return `[false]`
137
+
138
+ Other examples of simpler: Want a drop-down for numbers?
139
+
140
+ ```js
141
+ const settings = { speed: 1 }
142
+
143
+ // tweakpane
144
+ pane.addBinding(settings, speed, {
145
+ options: {
146
+ slow: 0,
147
+ medium: 1,
148
+ fast: 2,
149
+ }
150
+ })
151
+
152
+ // muigui
153
+ gui.add(settings, 'speed', ['slow', 'medium', 'fast']);
154
+ ```
155
+
156
+ Want a drop-down for strings
157
+
158
+ ```js
159
+ const settings = { alphaMode: 'opaque' }
160
+
161
+ // tweakpane
162
+ pane.addBindng(settings, 'alphaMode', {
163
+ options: {
164
+ 'opaque': 'opaque',
165
+ 'premultiplied': 'premultiplied',
166
+ }
167
+ });
168
+
169
+ // muigui
170
+ gui.add(settings, 'alphaMode', ['opaque', 'premultiplied']);
171
+ ```
172
+
173
+ Of course you can also pass in key/value settings like tweakpane.
174
+
175
+ * ## Color formats and storage
176
+
177
+ I often work with WebGL and WebGPU. The most common colors are in an array or a typedArray
178
+
179
+ ```js
180
+ const uniforms = {
181
+ color1: [1, 0.5, 0.25], // orange
182
+ color2: new Float32Array([0, 1, 1]); // cyan
183
+ color3: new Uint8Array([0, 128, 0, 128]); // transparent green
184
+ }
185
+ ```
186
+
187
+ Neither dat.gui, lil.gui, nor tweakpane can edit these AFAICT. (2023)
188
+ You'd have to jump
189
+ through the hoops like the `DegRadHelper` example above but it's not
190
+ that easy because, if you're showing the value textually
191
+ then you want that value to be the one you want to show the user,
192
+ not the value the editor wants.
193
+
194
+ This is still an issue in muigui where it uses the browser's built
195
+ in color editor in parts. That editor might show 0-255 values but if it's
196
+ editing 0 to 1 values it'd be nice if the editor showed 0 to 1 values.
197
+
198
+ muigui does handle this in the text part of it's color display. If you
199
+ ask it to edit 0 to 1 values it shows 0 to 1 values in the text part.
200
+ The reason you need the text part is so you can copy and paste colors
59
201
 
60
- const options = {
61
- someNumber: [1, 200], // range 0 to 200
62
- someOption: ['cat', 'bird', 'dog'],
63
- }
202
+ muigui also edits hsl colors. By that I don't mean the editor can
203
+ switch to an HSL editor, I mean the actual value that comes out
204
+ is `hsl(hue, sat, luminance)` and not some RGB value. Like the number
205
+ conversions, it would be easy to add `hsv`, `hsb`, maybe `labch` etc...
206
+
207
+ * ## More Use Cases
208
+
209
+ In some projects, I'd end up writing a small
210
+ app with an HTML form and then have to write all the code to parse
211
+ the form and I'd be thinking "It would be so nice if I could use
212
+ the same API as dat.gui 🙁
213
+
214
+ So, I thought I'd try to write a library that handled that case.
215
+
216
+ I also wanted to explore various things though many of them
217
+ have not made it into muigui yet.
218
+
219
+ * ## PropertyGrid
220
+
221
+ I'm sure the first app to do this was something from the 60s or 70s
222
+ in Smalltalk or something but my first experience was C#. In C#
223
+ the UI library had a `PropertyGrid` which you could pass any class
224
+ and with would auto-magically make UI to edit the public fields
225
+ of that class. If you've ever used Unity, it's the same. You declare
226
+ a class and it immediately shows a UI for all of its public properties.
227
+
228
+ That's easier in a typed language than a more loose language like
229
+ JavaScript.
230
+
231
+ I'm still experimenting with ideas but it sure would be nice to
232
+ get that for JS. Just give it an object and get a UI. You can
233
+ then customize later.
234
+
235
+ To be more concrete. Here's some code to setup a GUI
236
+
237
+ ```js
238
+ const s = {
239
+ someNumber: 123,
240
+ someString: 'hello',
241
+ someOption: 'dog',
242
+ someColor: '#ED3281',
243
+ someFunction: () => console.log('called')
244
+ };
245
+
246
+ const gui = new GUI();
247
+ gui.add(s, 'someNumber', 0, 200); // range 0 to 200
248
+ gui.add(s, 'someString');
249
+ gui.add(s, 'someOption', ['cat', 'bird', 'dog']);
250
+ gui.addColor(s, 'someColor');
251
+ gui.add(s, 'someFunction');
252
+ ```
253
+
254
+ I'd really like it to be this
255
+
256
+ ```js
257
+ const s = {
258
+ someNumber: 123,
259
+ someString: 'hello',
260
+ someOption: 'dog',
261
+ someColor: '#ED3281',
262
+ someFunction: () => console.log('called')
263
+ };
264
+
265
+ const gui = new GUI();
266
+ gui.add(s);
267
+ ```
268
+
269
+ At the moment that won't work. `someNumber` could only become
270
+ a `TextNumber` because there's no range. `someOption` would
271
+ only become a `Text` because there's no info that it's an enum.
272
+ `someColor` would become a `Text` because there's no info that
273
+ it's a color. So in the end only 2 of the 5 would work without
274
+ having to provide more info.
275
+
276
+ It's not clear in JS that adding that info would be a win
277
+ for keeping it simple but it sure would be nice.
278
+
279
+ * ## Modularity
280
+
281
+ Ideally I'd like it to be easy to make UIs based on collections
282
+ of parts. A simple example might be a 3 component vector
283
+ editor that is the combination of 3 number editors.
284
+
285
+ I'm still experimenting. While muigui has components that
286
+ do this I'm not happy with the API ergonomics yet.
287
+
288
+ Similarly I'd like to more easily split layout so it's trivial
289
+ to layout sub components. Again, still experimenting.
290
+
291
+ * ## Don't over specialize
292
+
293
+ This might be ranty, but I find libraries that try to do too much,
294
+ frustrating. In this case, it would
295
+ be a library that graphs data for you. The problem
296
+ with this functionality is that there is no end to
297
+ the number of features that will be requested.
298
+
299
+ You start with "graph an array of numbers".
300
+ Then you'll be asked to be able to supply a range.
301
+ Then you'll be asked to allow more than one array
302
+ for the graph.
303
+ You'll next be asked to let you specify a different
304
+ color for each array. Next you'll be asked to draw
305
+ axes, in different colors, with different units,
306
+ and labels. Then you'll be asked to have an option
307
+ to fill under the graph. Etc, etc, etc... forever.
308
+
309
+ In this case, It's arguably better to provide
310
+ a canvas and let the developer write their
311
+ own graphing code. Maybe provide an example or
312
+ a simple helper for the simplest case.
313
+
314
+ They can even choose when to update vs having to
315
+ choose an interval.
316
+
317
+ Let's compare
318
+
319
+ ```js
320
+ // tweakpane
321
+ const pane = new Pane();
322
+ pane.addBinding(PARAMS, 'wave', {
323
+ readonly: true,
324
+ view: 'graph',
325
+ min: -1,
326
+ max: +1,
327
+ });
328
+
329
+ // muigui
330
+ const gui = new GUI();
331
+ helpers.graph(gui.addCanvas('wave'), waveData, {
332
+ min: -1,
333
+ max: +1,
334
+ });
335
+ ```
336
+
337
+ It wasn't any harder to use, but the fact that we
338
+ just returned a canvas and left the rest outside
339
+ the library made it way more flexible.
340
+
341
+ This problem of providing too specialized a solution
342
+ is endemic throughout the library ecosystem of pretty much
343
+ every language.
344
+
345
+ There's a balance, but in general, if you need
346
+ to add more and more options then it was probably the
347
+ wrong solution. It's better to provide the
348
+ building blocks.
349
+
350
+ ## No Save/Restore
351
+
352
+ The problem with save/restore in lil.gui etc is it assumes the data
353
+ I want to edit can be serialized to JSON
354
+
355
+ Just as the simplest example I can think of
356
+
357
+ ```js
358
+ const material = new THREE.MeshBasicMaterial();
64
359
 
65
360
  const gui = new GUI();
66
- gui.add(s, options);
361
+ gui.addColor(material, 'color');
67
362
  ```
68
363
 
69
- ## What
364
+ It makes no sense to save/restore here. I'm editing a three.js material.
365
+ If I wanted to serialize anything I'd serialize the material.
70
366
 
71
- It is not a general purpose library for every type of GUI.
72
- Rather, it is a small, easy to use library for small apps.
73
- Basically I liked how simple it was to use dat.gui to add
74
- a few sliders and options to a demo.
367
+ Otherwise I can just save the stuff I passed to the GUI.
75
368
 
76
- I thought I'd try to make a CSS/DOM based UI standard elements
77
- only and then require CSS to style it and see how far I got.
369
+ ```js
370
+ const s = {
371
+ someNumber: 123,
372
+ someString: 'hello',
373
+ someOption: 'dog',
374
+ someColor: '#ED3281',
375
+ };
78
376
 
79
- ### Not invented here syndrome
377
+ // save
378
+ const str = JSON.stringify(s);
80
379
 
81
- It's possible this already exists but if so I couldn't find it.
82
- Most UI libraries seem to be giant and require a build step.
83
- I wanted something hopefully not too big and something I could
84
- easily add to any example with 1 file (or 2 if you add CSS).
380
+ // restore
381
+ Object.assign(s, JSON.parse(str));
382
+ gui.updateDisplay();
383
+ ```
85
384
 
86
- ## muigui - wat?
385
+ In other words, the serialization is too specialized. It's trivial
386
+ to call `JSON.stringify` on data that serializable. No need to put
387
+ serialization in the GUI. Note: I get that you might want to save
388
+ some hidden gui state like whether or not a folder is expanded.
389
+ You still run into the issue though that the data being edited
390
+ might not be easily serializable so you'd have to find another solution.
87
391
 
88
- https://user-images.githubusercontent.com/234804/177000460-3449c2dd-da94-4119-903f-cc7460b46e7b.mp4
392
+ ## Future
89
393
 
90
- -->
394
+ I'm under sure how much time I'll continue to put into this.
395
+ I get the feeling other people are far more motivated to make
396
+ UIs. Maybe if I'm lucky they'll take some inspiration from
397
+ the thoughts above and I'll find they've covered it all.
91
398
 
92
399
  ## License
93
400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muigui",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "A Simple GUI",
5
5
  "main": "muigui.js",
6
6
  "module": "src/muigui.js",
@@ -45,7 +45,7 @@
45
45
  "@typescript-eslint/eslint-plugin": "^6.12.0",
46
46
  "@typescript-eslint/parser": "^6.12.0",
47
47
  "chokidar": "^3.5.3",
48
- "eslint": "^8.20.0",
48
+ "eslint": "^8.54.0",
49
49
  "eslint-plugin-html": "^7.1.0",
50
50
  "eslint-plugin-one-variable-per-var": "^0.0.3",
51
51
  "eslint-plugin-optional-comma-spacing": "0.0.4",
@@ -26,6 +26,9 @@ export default class Button extends Controller {
26
26
  }));
27
27
  this.setOptions({name: property, ...options});
28
28
  }
29
+ name(name) {
30
+ this.#buttonElem.textContent = name;
31
+ }
29
32
  setOptions(options) {
30
33
  copyExistingProperties(this.#options, options);
31
34
  const {name} = this.#options;
@@ -5,8 +5,8 @@ import LabelController from './LabelController.js';
5
5
  export default class Canvas extends LabelController {
6
6
  #canvasElem;
7
7
 
8
- constructor() {
9
- super('muigui-canvas');
8
+ constructor(name) {
9
+ super('muigui-canvas', name);
10
10
  this.#canvasElem = this.add(
11
11
  new ElementView('canvas', 'muigui-canvas'),
12
12
  ).domElement;
@@ -16,7 +16,6 @@ export default class ColorChooser extends PopDownController {
16
16
  #colorView;
17
17
  #textView;
18
18
  #to;
19
- #setKnobHelper;
20
19
 
21
20
  constructor(object, property, options = {}) {
22
21
  super(object, property, 'muigui-color-chooser');
@@ -28,20 +27,22 @@ export default class ColorChooser extends PopDownController {
28
27
  this.addTop(this.#textView);
29
28
  this.addBottom(this.#colorView);
30
29
  // WTF! FIX!
31
- this.#setKnobHelper = () => {
32
- if (this.#to) {
33
- const hex6Or8 = this.#to(this.getValue());
34
- const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8));
35
- hsl[2] = (hsl[2] + 50) % 100;
36
- const hex = uint8RGBToHex(hslToRgbUint8(hsl));
37
- this.setKnobColor(`${hex6Or8.substring(0, 7)}FF`, hex);
38
- }
39
- };
30
+ this.___setKnobHelper = true;
40
31
  this.updateDisplay();
41
32
  }
33
+ #setKnobHelper() {
34
+ if (this.#to) {
35
+ const hex6Or8 = this.#to(this.getValue());
36
+ const alpha = hex6Or8.length === 9 ? hex6Or8.substring(7, 9) : 'FF';
37
+ const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8));
38
+ hsl[2] = (hsl[2] + 50) % 100;
39
+ const hex = uint8RGBToHex(hslToRgbUint8(hsl));
40
+ this.setKnobColor(`${hex6Or8.substring(0, 7)}${alpha}`, hex);
41
+ }
42
+ }
42
43
  updateDisplay() {
43
44
  super.updateDisplay();
44
- if (this.#setKnobHelper) {
45
+ if (this.___setKnobHelper) {
45
46
  this.#setKnobHelper();
46
47
  }
47
48
  }
@@ -11,6 +11,7 @@ export default class Folder extends Container {
11
11
  type: 'button',
12
12
  onClick: () => this.toggleOpen(),
13
13
  }, [this.#labelElem]));
14
+ this.pushContainer(new Container('muigui-open-container'));
14
15
  this.pushContainer(new Container());
15
16
  this.name(name);
16
17
  this.open();
@@ -54,7 +54,10 @@ export default class PopDownController extends ValueController {
54
54
  }));
55
55
  this.#checkboxElem = checkboxElem;
56
56
  this.#valuesView = this.#top.add(new ElementView('div', 'muigui-pop-down-values'));
57
- this.#bottom = this.add(new ElementView('div', 'muigui-pop-down-bottom'));
57
+ const container = new ElementView('div', 'muigui-pop-down-bottom muigui-open-container');
58
+ this.#bottom = new ElementView('div');
59
+ container.add(this.#bottom);
60
+ this.add(container);
58
61
  this.setOptions(options);
59
62
  }
60
63
  setKnobColor(bgCssColor/*, fgCssColor*/) {
@@ -3,7 +3,7 @@ import ValueController from './ValueController.js';
3
3
 
4
4
  export default class Text extends ValueController {
5
5
  constructor(object, property) {
6
- super(object, property, 'muigui-checkbox');
6
+ super(object, property, 'muigui-text');
7
7
  this.add(new TextView(this));
8
8
  this.updateDisplay();
9
9
  }
@@ -11,7 +11,7 @@ export default class TextNumber extends ValueController {
11
11
  #step;
12
12
 
13
13
  constructor(object, property, options = {}) {
14
- super(object, property, 'muigui-checkbox');
14
+ super(object, property, 'muigui-text-number');
15
15
  this.#textView = this.add(new NumberView(this, options));
16
16
  this.updateDisplay();
17
17
  }
@@ -24,6 +24,9 @@ export function createController(object, property, ...args) {
24
24
  if (Array.isArray(arg1)) {
25
25
  return new Select(object, property, {keyValues: arg1});
26
26
  }
27
+ if (arg1 && arg1.keyValues) {
28
+ return new Select(object, property, {keyValues: arg1.keyValues});
29
+ }
27
30
 
28
31
  const t = typeof object[property];
29
32
  switch (t) {
package/src/esm.ts CHANGED
@@ -9,4 +9,12 @@ export { default as Slider } from './controllers/Slider.js';
9
9
  export { default as TextNumber } from './controllers/TextNumber.js';
10
10
  export { default as Vec2 } from './controllers/Vec2.js';
11
11
 
12
+ import {graph} from './libs/graph.js';
13
+ import {monitor} from './libs/monitor.js';
14
+
15
+ export const helpers = {
16
+ graph,
17
+ monitor,
18
+ };
19
+
12
20
  export default GUI;
@@ -583,6 +583,50 @@ export const colorFormatConverters = {
583
583
  to: v => Array.from(v).map(v => f3(v)).join(', '),
584
584
  },
585
585
  },
586
+ 'float-hsv': {
587
+ color: {
588
+ from: v => [true, rgbFloatToHSV01(hexToFloatRGB(v))],
589
+ to: v => hsv01ToRGBFloat(floatRGBToHex(v)),
590
+ },
591
+ text: {
592
+ from: strTo3Floats,
593
+ // need Array.from because map of Float32Array makes a Float32Array
594
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
595
+ },
596
+ },
597
+ 'float-hsva': {
598
+ color: {
599
+ from: v => [true, rgbaFloatToHSVA01(hexToFloatRGB(v))],
600
+ to: v => hsva01ToRGBAFloat(floatRGBToHex(v)),
601
+ },
602
+ text: {
603
+ from: strTo4Floats,
604
+ // need Array.from because map of Float32Array makes a Float32Array
605
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
606
+ },
607
+ },
608
+ //'float-hsl': {
609
+ // color: {
610
+ // from: v => [true, rgbFloatToHsl01(hexToFloatRGB(v))],
611
+ // to: v => hsl01ToRGBFloat(floatRGBToHex(v)),
612
+ // },
613
+ // text: {
614
+ // from: strTo3Floats,
615
+ // // need Array.from because map of Float32Array makes a Float32Array
616
+ // to: v => Array.from(v).map(v => f3(v)).join(', '),
617
+ // },
618
+ //},
619
+ //'float-hsla': {
620
+ // color: {
621
+ // from: v => [true, hexToFloatRGBA(v)],
622
+ // to: floatRGBAToHex,
623
+ // },
624
+ // text: {
625
+ // from: strTo4Floats,
626
+ // // need Array.from because map of Float32Array makes a Float32Array
627
+ // to: v => Array.from(v).map(v => f3(v)).join(', '),
628
+ // },
629
+ //},
586
630
  'object-rgb': {
587
631
  color: {
588
632
  from: v => [true, hexToObjectRGB(v)],
@@ -0,0 +1,42 @@
1
+ const darkColors = {
2
+ main: '#ddd',
3
+ };
4
+ const lightColors = {
5
+ main: '#333',
6
+ };
7
+
8
+ const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
9
+
10
+ let colors;
11
+ let isDarkMode;
12
+
13
+ function update() {
14
+ isDarkMode = darkMatcher.matches;
15
+ colors = isDarkMode ? darkColors : lightColors;
16
+ }
17
+ darkMatcher.addEventListener('change', update);
18
+ update();
19
+
20
+ export function graph(canvas, data, {
21
+ min = -1,
22
+ max = 1,
23
+ interval = 16,
24
+ color,
25
+ }) {
26
+ const ctx = canvas.getContext('2d');
27
+
28
+ function render() {
29
+ const {width, height} = canvas;
30
+ ctx.clearRect(0, 0, width, height);
31
+ ctx.beginPath();
32
+ const range = max - min;
33
+ for (let i = 0; i < data.length; ++i) {
34
+ const x = i * width / data.length;
35
+ const y = (data[i] - min) * height / range;
36
+ ctx.lineTo(x, y);
37
+ }
38
+ ctx.strokeStyle = color || colors.main;
39
+ ctx.stroke();
40
+ }
41
+ setInterval(render, interval);
42
+ }
@@ -0,0 +1,5 @@
1
+ export function monitor(label, object, property, {interval = 200} = {}) {
2
+ setInterval(() => {
3
+ label.text(JSON.stringify(object[property], null, 2));
4
+ }, interval);
5
+ }
package/src/muigui.js CHANGED
@@ -61,6 +61,10 @@ export class GUIFolder extends Folder {
61
61
  addLabel(text) {
62
62
  return this.addController(new Label(text));
63
63
  }
64
+ addButton(name, fn) {
65
+ const o = {fn};
66
+ return this.add(o, 'fn').name(name);
67
+ }
64
68
  }
65
69
 
66
70
  class MuiguiElement extends HTMLElement {
@@ -73,7 +77,7 @@ class MuiguiElement extends HTMLElement {
73
77
  customElements.define('muigui-element', MuiguiElement);
74
78
 
75
79
  const baseStyleSheet = new CSSStyleSheet();
76
- baseStyleSheet.replaceSync(css.default);
80
+ //baseStyleSheet.replaceSync(css.default);
77
81
  const userStyleSheet = new CSSStyleSheet();
78
82
 
79
83
  function makeStyleSheetUpdater(styleSheet) {
@@ -100,6 +104,11 @@ function makeStyleSheetUpdater(styleSheet) {
100
104
  const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet);
101
105
  const updateUserStyle = makeStyleSheetUpdater(userStyleSheet);
102
106
 
107
+ function getTheme(name) {
108
+ const { include, css: cssStr } = css.themes[name];
109
+ return `${include.map(m => css[m]).join('\n')} : css.default}\n${cssStr || ''}`;
110
+ }
111
+
103
112
  export class GUI extends GUIFolder {
104
113
  static converters = converters;
105
114
  static mapRange = mapRange;
@@ -131,13 +140,14 @@ export class GUI extends GUIFolder {
131
140
  }
132
141
  if (parent) {
133
142
  const muiguiElement = createElem('muigui-element');
134
- muiguiElement.shadowRoot.adoptedStyleSheets = [baseStyleSheet, userStyleSheet, this.#localStyleSheet];
143
+ muiguiElement.shadowRoot.adoptedStyleSheets = [this.#localStyleSheet, baseStyleSheet, userStyleSheet];
135
144
  muiguiElement.shadow.appendChild(this.domElement);
136
145
  parent.appendChild(muiguiElement);
137
146
  }
138
147
  if (title) {
139
148
  this.title(title);
140
149
  }
150
+ this.#localStyleSheet.replaceSync(css.default);
141
151
  this.domElement.classList.add('muigui', 'muigui-colors');
142
152
  }
143
153
  setStyle(css) {
@@ -155,8 +165,11 @@ export class GUI extends GUIFolder {
155
165
  static getUserStyleSheet() {
156
166
  return userStyleSheet;
157
167
  }
168
+ setTheme(name) {
169
+ this.setStyle(getTheme(name));
170
+ }
158
171
  static setTheme(name) {
159
- GUI.setBaseStyles(`${css.default}\n${css.themes[name] || ''}`);
172
+ GUI.setBaseStyles(getTheme(name));
160
173
  }
161
174
  }
162
175
 
@@ -10,16 +10,24 @@ export default {
10
10
  --menu-bg-color: #f8f8f8;
11
11
  --menu-sep-color: #bbb;
12
12
  --hover-bg-color: #999;
13
- --focus-color: #68C;
14
- --range-color: #888888;
13
+ --focus-color: #8BF;
14
+ --range-color: #AAA;
15
15
  --invalid-color: #FF0000;
16
16
  --selected-color: rgb(255, 255, 255, 0.9);
17
17
 
18
18
  --button-bg-color: var(--value-bg-color);
19
19
 
20
+ --image-open: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDQ0OyIgeD0iMjAlIiB5PSI0NSUiIHdpZHRoPSI2MCUiIGhlaWdodD0iMTAlIj48L3JlY3Q+Cjwvc3ZnPg==);
21
+ --image-closed: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDQ0OyIgeD0iNDUlIiB5PSIyMCUiIHdpZHRoPSIxMCUiIGhlaWdodD0iNjAlIj48L3JlY3Q+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6ICM0NDQ7IiB4PSIyMCUiIHk9IjQ1JSIgd2lkdGg9IjYwJSIgaGVpZ2h0PSIxMCUiPjwvcmVjdD4KPC9zdmc+);
22
+ --image-checkerboard: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDA0MDQwOyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSI+PC9yZWN0PgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjODA4MDgwOyIgeD0iMCIgeT0iMCIgd2lkdGg9IjUwJSIgaGVpZ2h0PSI1MCUiPjwvcmVjdD4KICA8cmVjdCBzdHlsZT0iZmlsbDogIzgwODA4MDsiIHg9IjUwJSIgeT0iNTAlIiB3aWR0aD0iNTAlIiBoZWlnaHQ9IjUwJSI+PC9yZWN0Pgo8L3N2Zz4=);
23
+
20
24
  --range-left-color: var(--value-color);
21
25
  --range-right-color: var(--value-bg-color);
22
26
  --range-right-hover-color: var(--hover-bg-color);
27
+ --button-image:
28
+ linear-gradient(
29
+ rgba(255, 255, 255, 1), rgba(0, 0, 0, 0.2)
30
+ );
23
31
 
24
32
  color: var(--color);
25
33
  background-color: var(--bg-color);
@@ -36,7 +44,7 @@ export default {
36
44
  --menu-bg-color: #080808;
37
45
  --menu-sep-color: #444444;
38
46
  --hover-bg-color: #666666;
39
- --focus-color: #88AAFF;
47
+ --focus-color: #458; /*#88AAFF*/;
40
48
  --range-color: #888888;
41
49
  --invalid-color: #FF6666;
42
50
  --selected-color: rgba(255, 255, 255, 0.3);
@@ -46,9 +54,15 @@ export default {
46
54
  --range-left-color: var(--value-color);
47
55
  --range-right-color: var(--value-bg-color);
48
56
  --range-right-hover-color: var(--hover-bg-color);
57
+ --button-image: linear-gradient(
58
+ rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.4)
59
+ );
49
60
 
50
61
  color: var(--color);
51
62
  background-color: var(--bg-color);
63
+
64
+ --image-closed: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjREREOyIgeD0iMjAlIiB5PSI0NSUiIHdpZHRoPSI2MCUiIGhlaWdodD0iMTAlIj48L3JlY3Q+Cjwvc3ZnPg==);
65
+ --image-open: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjREREOyIgeD0iNDUlIiB5PSIyMCUiIHdpZHRoPSIxMCUiIGhlaWdodD0iNjAlIj48L3JlY3Q+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6ICNEREQ7IiB4PSIyMCUiIHk9IjQ1JSIgd2lkdGg9IjYwJSIgaGVpZ2h0PSIxMCUiPjwvcmVjdD4KPC9zdmc+);
52
66
  }
53
67
  }
54
68
 
@@ -57,7 +71,6 @@ export default {
57
71
  --label-width: 45%;
58
72
  --number-width: 40%;
59
73
 
60
-
61
74
  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
62
75
  --font-size: 11px;
63
76
  --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;
@@ -150,6 +163,9 @@ export default {
150
163
  min-width: 0;
151
164
  min-height: var(--line-height);
152
165
  }
166
+ .muigui-root {
167
+ z-index: 1;
168
+ }
153
169
  .muigui-root,
154
170
  .muigui-menu {
155
171
  display: flex;
@@ -174,8 +190,7 @@ export default {
174
190
  color: var(--color);
175
191
  background-color: var(--menu-bg-color);
176
192
  min-height: var(--line-height);
177
- padding-top: 0.2em;
178
- padding-bottom: 0.2em;
193
+ padding: 0.2em;
179
194
  cursor: pointer;
180
195
  border-radius: var(--border-radius);
181
196
  }
@@ -196,7 +211,7 @@ export default {
196
211
  .muigui-controller>*:nth-child(1) {
197
212
  flex: 1 0 var(--label-width);
198
213
  min-width: 0;
199
- white-space: pre;
214
+ /* white-space: pre; why?? */
200
215
  }
201
216
  .muigui-controller>label:nth-child(1) {
202
217
  place-content: center start;
@@ -238,32 +253,41 @@ export default {
238
253
  /* fix! */
239
254
  .muigui-open>button>label::before,
240
255
  .muigui-closed>button>label::before {
256
+ content: "X";
257
+ color: rgba(0, 0, 0, 0);
258
+ background-color: var(--range-color);
259
+ border-radius: 0.2em;
241
260
  width: 1.25em;
242
- height: var(--line-height);
261
+ margin-right: 0.25em;
262
+ height: 1.25em; /*var(--line-height);*/
243
263
  display: inline-grid;
244
264
  place-content: center start;
245
265
  pointer-events: none;
246
266
  }
247
267
  .muigui-open>button>label::before {
248
- content: "ⓧ"; /*"▼";*/
268
+ background-image: var(--image-open);
249
269
  }
250
270
  .muigui-closed>button>label::before {
251
- content: "⨁"; /*"▶";*/
271
+ background-image: var(--image-closed);
252
272
  }
253
- .muigui-open>*:nth-child(2) {
254
- transition: max-height 0.2s ease-out,
255
- opacity 0.5s ease-out;
256
- max-height: 100vh;
273
+
274
+ .muigui-open>.muigui-open-container {
275
+ transition: all 0.1s ease-out;
257
276
  overflow: auto;
258
- opacity: 1;
277
+ height: 100%;
259
278
  }
260
-
261
- .muigui-closed>*:nth-child(2) {
262
- transition: max-height 0.2s ease-out,
263
- opacity 1s;
264
- max-height: 0;
265
- opacity: 0;
279
+ .muigui-closed>.muigui-open-container {
280
+ transition: all 0.1s ease-out;
266
281
  overflow: hidden;
282
+ min-height: 0;
283
+ }
284
+ .muigui-open>.muigui-open-container>* {
285
+ transition: all 0.1s ease-out;
286
+ margin-top: 0px;
287
+ }
288
+ .muigui-closed>.muigui-open-container>* {
289
+ transition: all 0.1s ease-out;
290
+ margin-top: -100%;
267
291
  }
268
292
 
269
293
  /* ---- popdown ---- */
@@ -275,8 +299,12 @@ export default {
275
299
  .muigui-value>*:nth-child(1).muigui-pop-down-top {
276
300
  flex: 0;
277
301
  }
278
- .muigui-pop-down-bottom {
302
+ .muigui-closed .muigui-pop-down-bottom {
303
+ max-height: 0;
304
+ }
279
305
 
306
+ .muigui-value .muigui-pop-down-bottom {
307
+ margin: 0;
280
308
  }
281
309
 
282
310
  .muigui-pop-down-values {
@@ -298,6 +326,10 @@ export default {
298
326
  width: auto;
299
327
  color: var(--value-color);
300
328
  background-color: var(--value-bg-color);
329
+ background-image: var(--image-checkerboard);
330
+ background-size: 10px 10px;
331
+ background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
332
+
301
333
  cursor: pointer;
302
334
 
303
335
  display: grid;
@@ -389,14 +421,16 @@ export default {
389
421
 
390
422
  .muigui-button {
391
423
  display: grid;
392
-
424
+ padding: 2px 0 2px 0;
393
425
  }
394
426
  .muigui-button button {
395
427
  border: none;
396
428
  color: var(--value-color);
397
429
  background-color: var(--button-bg-color);
430
+ background-image: var(--button-image);
398
431
  cursor: pointer;
399
432
  place-content: center center;
433
+ height: var(--line-height);
400
434
  }
401
435
 
402
436
  /* ------ [ color ] ------ */
@@ -608,9 +642,9 @@ export default {
608
642
  border-bottom: 1px solid rgba(0,0,0,0.2);
609
643
  border-right: 1px solid rgba(0,0,0,0.2);
610
644
  background-color: var(--range-color);
611
- margin-top: calc((var(--line-height) - 2px) / -2);
612
- width: calc(var(--line-height) - 2px);
613
- height: calc(var(--line-height) - 2px);
645
+ margin-top: calc((var(--line-height) - 6px) / -2);
646
+ width: calc(var(--line-height) - 6px);
647
+ height: calc(var(--line-height) - 6px);
614
648
  }
615
649
 
616
650
  .muigui-range input[type=range]::-webkit-slider-runnable-track {
@@ -694,8 +728,14 @@ export default {
694
728
 
695
729
  `,
696
730
  themes: {
697
- default: '',
698
- float: `
731
+ default: {
732
+ include: ['default'],
733
+ css: `
734
+ `,
735
+ },
736
+ float: {
737
+ include: ['default'],
738
+ css: `
699
739
  :root {
700
740
  color-scheme: light dark,
701
741
  }
@@ -752,5 +792,57 @@ themes: {
752
792
  --range-color: rgba(0, 0, 0, 0.125);
753
793
  }
754
794
  `,
795
+ },
796
+ form: {
797
+ include: [],
798
+ css: `
799
+ .muigui {
800
+ --width: 100%;
801
+ --label-width: 45%;
802
+ --number-width: 40%;
803
+ }
804
+ .muigui-root>button {
805
+ display: none;
806
+ }
807
+ .muigui-controller {
808
+ margin-top: 1em;
809
+ }
810
+ .muigui-label-controller {
811
+ display: flex;
812
+ flex-direction: column;
813
+ align-items: stretch;
814
+ margin-top: 1em;
815
+ }
816
+ .muigui-label-controller:has(.muigui-checkbox) {
817
+ flex-direction: row;
818
+ }
819
+ .muigui-value {
820
+ display: flex;
821
+ align-items: stretch;
822
+ }
823
+ .muigui-value>* {
824
+ flex: 1 1 auto;
825
+ min-width: 0;
826
+ }
827
+ .muigui-controller>*:nth-child(1) {
828
+ flex: 1 0 var(--label-width);
829
+ min-width: 0;
830
+ white-space: pre;
831
+ }
832
+ .muigui-controller>label:nth-child(1) {
833
+ place-content: center start;
834
+ display: inline-grid;
835
+ overflow: hidden;
836
+ }
837
+ .muigui-controller>*:nth-child(2) {
838
+ flex: 1 1 75%;
839
+ min-width: 0;
840
+ }
841
+ `,
842
+ },
843
+ none: {
844
+ include: [],
845
+ css: '',
846
+ },
755
847
  },
756
848
  };
@@ -22,15 +22,22 @@ export default class NumberView extends EditView {
22
22
  const wheelHelper = createWheelHelper();
23
23
  super(createElem('input', {
24
24
  type: 'number',
25
- onInput: () => this.#handleInput(setValue, true),
26
- onChange: () => this.#handleInput(setFinalValue, false),
25
+ onInput: () => {
26
+ this.#handleInput(setValue, true);
27
+ },
28
+ onChange: () => {
29
+ this.#handleInput(setFinalValue, false);
30
+ },
27
31
  onWheel: e => {
28
32
  e.preventDefault();
29
33
  const {min, max, step} = this.#options;
30
34
  const delta = wheelHelper(e, step);
31
35
  const v = parseFloat(this.domElement.value);
32
36
  const newV = clamp(stepify(v + delta, v => v, step), min, max);
33
- setter.setValue(newV);
37
+ const [valid, outV] = this.#from(newV);
38
+ if (valid) {
39
+ setter.setValue(outV);
40
+ }
34
41
  },
35
42
  }));
36
43
  this.setOptions(options);
@@ -16,8 +16,12 @@ export default class TextView extends EditView {
16
16
  const setFinalValue = setter.setFinalValue.bind(setter);
17
17
  super(createElem('input', {
18
18
  type: 'text',
19
- onInput: () => this.#handleInput(setValue, true),
20
- onChange: () => this.#handleInput(setFinalValue, false),
19
+ onInput: () => {
20
+ this.#handleInput(setValue, true);
21
+ },
22
+ onChange: () => {
23
+ this.#handleInput(setFinalValue, false);
24
+ },
21
25
  }));
22
26
  this.setOptions(options);
23
27
  }