muigui 0.0.14 → 0.0.16

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.
Files changed (70) hide show
  1. package/README.md +49 -4
  2. package/dist/0.x/controllers/Button.d.ts +7 -0
  3. package/dist/0.x/controllers/Canvas.d.ts +6 -0
  4. package/dist/0.x/controllers/Checkbox.d.ts +4 -0
  5. package/dist/0.x/controllers/Color.d.ts +6 -0
  6. package/dist/0.x/controllers/ColorChooser.d.ts +6 -0
  7. package/dist/0.x/controllers/Container.d.ts +13 -0
  8. package/dist/0.x/controllers/Controller.d.ts +20 -0
  9. package/dist/0.x/controllers/Direction.d.ts +5 -0
  10. package/dist/0.x/controllers/Divider.d.ts +4 -0
  11. package/dist/0.x/controllers/Folder.d.ts +10 -0
  12. package/dist/0.x/controllers/Label.d.ts +4 -0
  13. package/dist/0.x/controllers/LabelController.d.ts +8 -0
  14. package/dist/0.x/controllers/PopDownController.d.ts +10 -0
  15. package/dist/0.x/controllers/RadioGrid.d.ts +4 -0
  16. package/dist/0.x/controllers/Range.d.ts +4 -0
  17. package/dist/0.x/controllers/Select.d.ts +4 -0
  18. package/dist/0.x/controllers/Slider.d.ts +4 -0
  19. package/dist/0.x/controllers/Text.d.ts +4 -0
  20. package/dist/0.x/controllers/TextNumber.d.ts +5 -0
  21. package/dist/0.x/controllers/ValueController.d.ts +17 -0
  22. package/dist/0.x/controllers/Vec2.d.ts +4 -0
  23. package/dist/0.x/controllers/create-controller.d.ts +13 -0
  24. package/dist/0.x/esm.d.ts +16 -0
  25. package/dist/0.x/layout/Column.d.ts +4 -0
  26. package/dist/0.x/layout/Frame.d.ts +5 -0
  27. package/dist/0.x/layout/Grid.d.ts +4 -0
  28. package/dist/0.x/layout/Layout.d.ts +5 -0
  29. package/dist/0.x/layout/Row.d.ts +4 -0
  30. package/dist/0.x/libs/assert.d.ts +1 -0
  31. package/dist/0.x/libs/color-utils.d.ts +235 -0
  32. package/dist/0.x/libs/conversions.d.ts +16 -0
  33. package/dist/0.x/libs/elem.d.ts +4 -0
  34. package/dist/0.x/libs/graph.d.ts +6 -0
  35. package/dist/0.x/libs/ids.d.ts +1 -0
  36. package/dist/0.x/libs/key-values.d.ts +1 -0
  37. package/dist/0.x/libs/keyboard.d.ts +6 -0
  38. package/dist/0.x/libs/monitor.d.ts +3 -0
  39. package/dist/0.x/libs/resize-helpers.d.ts +3 -0
  40. package/dist/0.x/libs/svg.d.ts +1 -0
  41. package/dist/0.x/libs/taskrunner.d.ts +2 -0
  42. package/dist/0.x/libs/touch.d.ts +17 -0
  43. package/dist/0.x/libs/utils.d.ts +27 -0
  44. package/dist/0.x/libs/wheel.d.ts +1 -0
  45. package/dist/0.x/muigui.d.ts +48 -0
  46. package/dist/0.x/muigui.js +4006 -0
  47. package/dist/0.x/muigui.js.map +1 -0
  48. package/dist/0.x/muigui.min.js +2 -0
  49. package/dist/0.x/muigui.min.js.map +1 -0
  50. package/dist/0.x/muigui.module.js +4043 -0
  51. package/dist/0.x/muigui.module.js.map +1 -0
  52. package/dist/0.x/muigui.module.min.js +2 -0
  53. package/dist/0.x/muigui.module.min.js.map +1 -0
  54. package/dist/0.x/styles/muigui.css.d.ts +30 -0
  55. package/dist/0.x/views/CheckboxView.d.ts +6 -0
  56. package/dist/0.x/views/ColorChooserView.d.ts +7 -0
  57. package/dist/0.x/views/ColorView.d.ts +7 -0
  58. package/dist/0.x/views/DirectionView.d.ts +7 -0
  59. package/dist/0.x/views/EditView.d.ts +6 -0
  60. package/dist/0.x/views/ElementView.d.ts +4 -0
  61. package/dist/0.x/views/NumberView.d.ts +7 -0
  62. package/dist/0.x/views/RadioGridView.d.ts +7 -0
  63. package/dist/0.x/views/RangeView.d.ts +7 -0
  64. package/dist/0.x/views/SelectView.d.ts +6 -0
  65. package/dist/0.x/views/SliderView.d.ts +7 -0
  66. package/dist/0.x/views/TextView.d.ts +7 -0
  67. package/dist/0.x/views/ValueView.d.ts +4 -0
  68. package/dist/0.x/views/Vec2View.d.ts +6 -0
  69. package/dist/0.x/views/View.d.ts +16 -0
  70. package/package.json +4 -4
@@ -0,0 +1,4043 @@
1
+ /* muigui@0.0.16, license MIT */
2
+ var css = {
3
+ default: `
4
+ .muigui {
5
+ --bg-color: #ddd;
6
+ --color: #222;
7
+ --contrast-color: #eee;
8
+ --value-color: #145 ;
9
+ --value-bg-color: #eeee;
10
+ --disabled-color: #999;
11
+ --menu-bg-color: #f8f8f8;
12
+ --menu-sep-color: #bbb;
13
+ --hover-bg-color: #999;
14
+ --focus-color: #8BF;
15
+ --range-color: #AAA;
16
+ --invalid-color: #FF0000;
17
+ --selected-color: rgb(255, 255, 255, 0.9);
18
+
19
+ --button-bg-color: var(--value-bg-color);
20
+
21
+ --image-open: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDQ0OyIgeD0iMjAlIiB5PSI0NSUiIHdpZHRoPSI2MCUiIGhlaWdodD0iMTAlIj48L3JlY3Q+Cjwvc3ZnPg==);
22
+ --image-closed: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDQ0OyIgeD0iNDUlIiB5PSIyMCUiIHdpZHRoPSIxMCUiIGhlaWdodD0iNjAlIj48L3JlY3Q+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6ICM0NDQ7IiB4PSIyMCUiIHk9IjQ1JSIgd2lkdGg9IjYwJSIgaGVpZ2h0PSIxMCUiPjwvcmVjdD4KPC9zdmc+);
23
+ --image-checkerboard: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjNDA0MDQwOyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSI+PC9yZWN0PgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjODA4MDgwOyIgeD0iMCIgeT0iMCIgd2lkdGg9IjUwJSIgaGVpZ2h0PSI1MCUiPjwvcmVjdD4KICA8cmVjdCBzdHlsZT0iZmlsbDogIzgwODA4MDsiIHg9IjUwJSIgeT0iNTAlIiB3aWR0aD0iNTAlIiBoZWlnaHQ9IjUwJSI+PC9yZWN0Pgo8L3N2Zz4=);
24
+
25
+ --range-left-color: var(--value-color);
26
+ --range-right-color: var(--value-bg-color);
27
+ --range-right-hover-color: var(--hover-bg-color);
28
+ --button-image:
29
+ linear-gradient(
30
+ rgba(255, 255, 255, 1), rgba(0, 0, 0, 0.2)
31
+ );
32
+
33
+ color: var(--color);
34
+ background-color: var(--bg-color);
35
+ }
36
+
37
+ @media (prefers-color-scheme: dark) {
38
+ .muigui {
39
+ --bg-color: #222222;
40
+ --color: #dddddd;
41
+ --contrast-color: #000;
42
+ --value-color: #43e5f7;
43
+ --value-bg-color: #444444;
44
+ --disabled-color: #666666;
45
+ --menu-bg-color: #080808;
46
+ --menu-sep-color: #444444;
47
+ --hover-bg-color: #666666;
48
+ --focus-color: #458; /*#88AAFF*/;
49
+ --range-color: #888888;
50
+ --invalid-color: #FF6666;
51
+ --selected-color: rgba(255, 255, 255, 0.3);
52
+
53
+ --button-bg-color: var(--value-bg-color);
54
+
55
+ --range-left-color: var(--value-color);
56
+ --range-right-color: var(--value-bg-color);
57
+ --range-right-hover-color: var(--hover-bg-color);
58
+ --button-image: linear-gradient(
59
+ rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.4)
60
+ );
61
+
62
+ color: var(--color);
63
+ background-color: var(--bg-color);
64
+
65
+ --image-closed: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjREREOyIgeD0iMjAlIiB5PSI0NSUiIHdpZHRoPSI2MCUiIGhlaWdodD0iMTAlIj48L3JlY3Q+Cjwvc3ZnPg==);
66
+ --image-open: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjREREOyIgeD0iNDUlIiB5PSIyMCUiIHdpZHRoPSIxMCUiIGhlaWdodD0iNjAlIj48L3JlY3Q+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6ICNEREQ7IiB4PSIyMCUiIHk9IjQ1JSIgd2lkdGg9IjYwJSIgaGVpZ2h0PSIxMCUiPjwvcmVjdD4KPC9zdmc+);
67
+ }
68
+ }
69
+
70
+ .muigui {
71
+ --width: 250px;
72
+ --label-width: 45%;
73
+ --number-width: 40%;
74
+
75
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
76
+ --font-size: 11px;
77
+ --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;
78
+ --font-size-mono: 11px;
79
+
80
+ --line-height: 1.7em;
81
+ --border-radius: 0px;
82
+
83
+ width: var(--width);
84
+ font-family: var(--font-family);
85
+ font-size: var(--font-size);
86
+ box-sizing: border-box;
87
+ line-height: 100%;
88
+ }
89
+ .muigui * {
90
+ box-sizing: inherit;
91
+ }
92
+
93
+ .muigui-no-scroll {
94
+ touch-action: none;
95
+ }
96
+ .muigui-no-h-scroll {
97
+ touch-action: pan-y;
98
+ }
99
+ .muigui-no-v-scroll {
100
+ touch-action: pan-x;
101
+ }
102
+
103
+ .muigui-invalid-value {
104
+ background-color: red !important;
105
+ color: white !important;
106
+ }
107
+
108
+ .muigui-grid {
109
+ display: grid;
110
+ }
111
+ .muigui-rows {
112
+ display: flex;
113
+ flex-direction: column;
114
+
115
+ min-height: 20px;
116
+ border: 2px solid red;
117
+ }
118
+ .muigui-columns {
119
+ display: flex;
120
+ flex-direction: row;
121
+
122
+ height: 20px;
123
+ border: 2px solid green;
124
+ }
125
+ .muigui-rows>*,
126
+ .muigui-columns>* {
127
+ flex: 1 1 auto;
128
+ align-items: stretch;
129
+ min-height: 0;
130
+ min-width: 0;
131
+ }
132
+
133
+ .muigui-row {
134
+ border: 2px solid yellow;
135
+ min-height: 10px
136
+ }
137
+ .muigui-column {
138
+ border: 2px solid lightgreen;
139
+ }
140
+
141
+ /* -------- */
142
+
143
+ .muigui-show { /* */ }
144
+ .muigui-hide {
145
+ display: none !important;
146
+ }
147
+ .muigui-disabled {
148
+ pointer-events: none;
149
+ --color: var(--disabled-color) !important;
150
+ --value-color: var(--disabled-color) !important;
151
+ --range-left-color: var(--disabled-color) !important;
152
+ }
153
+
154
+ .muigui canvas,
155
+ .muigui svg {
156
+ display: block;
157
+ border-radius: var(--border-radius);
158
+ }
159
+ .muigui canvas {
160
+ background-color: var(--value-bg-color);
161
+ }
162
+
163
+ .muigui-controller {
164
+ min-width: 0;
165
+ min-height: var(--line-height);
166
+ }
167
+ .muigui-root {
168
+ z-index: 1;
169
+ }
170
+ .muigui-root,
171
+ .muigui-menu {
172
+ display: flex;
173
+ flex-direction: column;
174
+ position: relative;
175
+ user-select: none;
176
+ height: fit-content;
177
+ margin: 0;
178
+ padding-bottom: 0.1em;
179
+ border-radius: var(--border-radius);
180
+ }
181
+ .muigui-menu {
182
+ border-bottom: 1px solid var(--menu-sep-color);
183
+ }
184
+
185
+ .muigui-root>button:nth-child(1),
186
+ .muigui-menu>button:nth-child(1) {
187
+ border-top: 1px solid var(--menu-sep-color);
188
+ border-bottom: 1px solid var(--menu-sep-color);
189
+ position: relative;
190
+ text-align: left;
191
+ color: var(--color);
192
+ background-color: var(--menu-bg-color);
193
+ min-height: var(--line-height);
194
+ padding: 0.2em;
195
+ cursor: pointer;
196
+ border-radius: var(--border-radius);
197
+ }
198
+ .muigui-root>div:nth-child(2),
199
+ .muigui-menu>div:nth-child(2) {
200
+ flex: 1 1 auto;
201
+ }
202
+
203
+ .muigui-controller {
204
+ margin-left: 0.2em;
205
+ margin-right: 0.2em;
206
+ }
207
+ .muigui-root.muigui-controller,
208
+ .muigui-menu.muigui-controller {
209
+ margin-left: 0;
210
+ margin-right: 0;
211
+ }
212
+ .muigui-controller>*:nth-child(1) {
213
+ flex: 1 0 var(--label-width);
214
+ min-width: 0;
215
+ /* white-space: pre; why?? */
216
+ }
217
+ .muigui-controller>label:nth-child(1) {
218
+ place-content: center start;
219
+ display: inline-grid;
220
+ overflow: hidden;
221
+ }
222
+ .muigui-controller>*:nth-child(2) {
223
+ flex: 1 1 75%;
224
+ min-width: 0;
225
+ }
226
+
227
+ /* -----------------------------------------
228
+ a label controller is [[label][value]]
229
+ */
230
+
231
+ .muigui-label-controller {
232
+ display: flex;
233
+ margin: 0.4em 0 0.4em 0;
234
+ word-wrap: initial;
235
+ align-items: stretch;
236
+ }
237
+
238
+ .muigui-value {
239
+ display: flex;
240
+ align-items: stretch;
241
+ }
242
+ .muigui-value>* {
243
+ flex: 1 1 auto;
244
+ min-width: 0;
245
+ }
246
+ .muigui-value>*:nth-child(1) {
247
+ flex: 1 1 calc(100% - var(--number-width));
248
+ }
249
+ .muigui-value>*:nth-child(2) {
250
+ flex: 1 1 var(--number-width);
251
+ margin-left: 0.2em;
252
+ }
253
+
254
+ /* fix! */
255
+ .muigui-open>button>label::before,
256
+ .muigui-closed>button>label::before {
257
+ content: "X";
258
+ color: rgba(0, 0, 0, 0);
259
+ background-color: var(--range-color);
260
+ border-radius: 0.2em;
261
+ width: 1.25em;
262
+ margin-right: 0.25em;
263
+ height: 1.25em; /*var(--line-height);*/
264
+ display: inline-grid;
265
+ place-content: center start;
266
+ pointer-events: none;
267
+ }
268
+ .muigui-open>button>label::before {
269
+ background-image: var(--image-open);
270
+ }
271
+ .muigui-closed>button>label::before {
272
+ background-image: var(--image-closed);
273
+ }
274
+
275
+ .muigui-open>.muigui-open-container {
276
+ transition: all 0.1s ease-out;
277
+ overflow: auto;
278
+ height: 100%;
279
+ }
280
+ .muigui-closed>.muigui-open-container {
281
+ transition: all 0.1s ease-out;
282
+ overflow: hidden;
283
+ min-height: 0;
284
+ }
285
+ .muigui-open>.muigui-open-container>* {
286
+ transition: all 0.1s ease-out;
287
+ margin-top: 0px;
288
+ }
289
+ .muigui-closed>.muigui-open-container>* {
290
+ transition: all 0.1s ease-out;
291
+ margin-top: -100%;
292
+ }
293
+
294
+ /* ---- popdown ---- */
295
+
296
+ .muigui-pop-down-top {
297
+ display: flex;
298
+ }
299
+ /* fix? */
300
+ .muigui-value>*:nth-child(1).muigui-pop-down-top {
301
+ flex: 0;
302
+ }
303
+ .muigui-closed .muigui-pop-down-bottom {
304
+ max-height: 0;
305
+ }
306
+
307
+ .muigui-value .muigui-pop-down-bottom {
308
+ margin: 0;
309
+ }
310
+
311
+ .muigui-pop-down-values {
312
+ min-width: 0;
313
+ display: flex;
314
+ }
315
+ .muigui-pop-down-values>* {
316
+ flex: 1 1 auto;
317
+ min-width: 0;
318
+ }
319
+
320
+ .muigui-value.muigui-pop-down-controller {
321
+ flex-direction: column;
322
+ }
323
+
324
+ .muigui-pop-down-top input[type=checkbox] {
325
+ -webkit-appearance: none;
326
+ appearance: none;
327
+ width: auto;
328
+ color: var(--value-color);
329
+ background-color: var(--value-bg-color);
330
+ background-image: var(--image-checkerboard);
331
+ background-size: 10px 10px;
332
+ background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
333
+
334
+ cursor: pointer;
335
+
336
+ display: grid;
337
+ place-content: center;
338
+ margin: 0;
339
+ font: inherit;
340
+ color: currentColor;
341
+ width: 1.7em;
342
+ height: 1.7em;
343
+ transform: translateY(-0.075em);
344
+ }
345
+
346
+ .muigui-pop-down-top input[type=checkbox]::before {
347
+ content: "+";
348
+ display: grid;
349
+ place-content: center;
350
+ border-radius: calc(var(--border-radius) + 2px);
351
+ border-left: 1px solid rgba(255,255,255,0.3);
352
+ border-top: 1px solid rgba(255,255,255,0.3);
353
+ border-bottom: 1px solid rgba(0,0,0,0.2);
354
+ border-right: 1px solid rgba(0,0,0,0.2);
355
+ background-color: var(--range-color);
356
+ color: var(--value-bg-color);
357
+ width: calc(var(--line-height) - 4px);
358
+ height: calc(var(--line-height) - 4px);
359
+ }
360
+
361
+ .muigui-pop-down-top input[type=checkbox]:checked::before {
362
+ content: "X";
363
+ }
364
+
365
+
366
+ /* ---- select ---- */
367
+
368
+ .muigui select,
369
+ .muigui option,
370
+ .muigui input,
371
+ .muigui button {
372
+ color: var(--value-color);
373
+ background-color: var(--value-bg-color);
374
+ font-family: var(--font-family);
375
+ font-size: var(--font-size);
376
+ border: none;
377
+ margin: 0;
378
+ border-radius: var(--border-radius);
379
+ }
380
+ .muigui select {
381
+ appearance: none;
382
+ margin: 0;
383
+ margin-left: 0; /*?*/
384
+ overflow: hidden; /* Safari */
385
+ }
386
+
387
+ .muigui select:focus,
388
+ .muigui input:focus,
389
+ .muigui button:focus {
390
+ outline: 1px solid var(--focus-color);
391
+ }
392
+
393
+ .muigui select:hover,
394
+ .muigui option:hover,
395
+ .muigui input:hover,
396
+ .muigui button:hover {
397
+ background-color: var(--hover-bg-color);
398
+ }
399
+
400
+ /* ------ [ label ] ------ */
401
+
402
+ .muigui-label {
403
+ border-top: 1px solid var(--menu-sep-color);
404
+ border-bottom: 1px solid var(--menu-sep-color);
405
+ padding-top: 0.4em;
406
+ padding-bottom: 0.3em;
407
+ place-content: center start;
408
+ background-color: var(--menu-bg-color);
409
+ white-space: pre;
410
+ border-radius: var(--border-radius);
411
+ }
412
+
413
+ /* ------ [ divider] ------ */
414
+
415
+ .muigui-divider {
416
+ min-height: 6px;
417
+ border-top: 2px solid var(--menu-sep-color);
418
+ margin-top: 6px;
419
+ }
420
+
421
+ /* ------ [ button ] ------ */
422
+
423
+ .muigui-button {
424
+ display: grid;
425
+ padding: 2px 0 2px 0;
426
+ }
427
+ .muigui-button button {
428
+ border: none;
429
+ color: var(--value-color);
430
+ background-color: var(--button-bg-color);
431
+ background-image: var(--button-image);
432
+ cursor: pointer;
433
+ place-content: center center;
434
+ height: var(--line-height);
435
+ }
436
+
437
+ /* ------ [ color ] ------ */
438
+
439
+ .muigui-color>div {
440
+ overflow: hidden;
441
+ position: relative;
442
+ margin-left: 0;
443
+ margin-right: 0; /* why? */
444
+ max-width: var(--line-height);
445
+ border-radius: var(--border-radius);
446
+ }
447
+
448
+ .muigui-color>div:focus-within {
449
+ outline: 1px solid var(--focus-color);
450
+ }
451
+
452
+ .muigui-color input[type=color] {
453
+ border: none;
454
+ padding: 0;
455
+ background: inherit;
456
+ cursor: pointer;
457
+ position: absolute;
458
+ width: 200%;
459
+ left: -10px;
460
+ top: -10px;
461
+ height: 200%;
462
+ }
463
+ .muigui-disabled canvas,
464
+ .muigui-disabled svg,
465
+ .muigui-disabled img,
466
+ .muigui-disabled .muigui-color input[type=color] {
467
+ opacity: 0.2;
468
+ }
469
+
470
+ /* ------ [ checkbox ] ------ */
471
+
472
+ .muigui-checkbox>label:nth-child(2) {
473
+ display: grid;
474
+ place-content: center start;
475
+ margin: 0;
476
+ }
477
+
478
+ .muigui-checkbox input[type=checkbox] {
479
+ -webkit-appearance: none;
480
+ appearance: none;
481
+ width: auto;
482
+ color: var(--value-color);
483
+ background-color: var(--value-bg-color);
484
+ cursor: pointer;
485
+
486
+ display: grid;
487
+ place-content: center;
488
+ margin: 0;
489
+ font: inherit;
490
+ color: currentColor;
491
+ width: 1.7em;
492
+ height: 1.7em;
493
+ transform: translateY(-0.075em);
494
+ }
495
+
496
+ .muigui-checkbox input[type=checkbox]::before {
497
+ content: "";
498
+ color: var(--value-color);
499
+ display: grid;
500
+ place-content: center;
501
+ }
502
+
503
+ .muigui-checkbox input[type=checkbox]:checked::before {
504
+ content: "✔";
505
+ }
506
+
507
+ .muigui input[type=number]::-webkit-inner-spin-button,
508
+ .muigui input[type=number]::-webkit-outer-spin-button {
509
+ -webkit-appearance: none;
510
+ appearance: none;
511
+ margin: 0;
512
+ }
513
+ .muigui input[type=number] {
514
+ -moz-appearance: textfield;
515
+ }
516
+
517
+ /* ------ [ radio grid ] ------ */
518
+
519
+ .muigui-radio-grid>div {
520
+ display: grid;
521
+ gap: 2px;
522
+ }
523
+
524
+ .muigui-radio-grid input {
525
+ appearance: none;
526
+ display: none;
527
+ }
528
+
529
+ .muigui-radio-grid button {
530
+ color: var(--color);
531
+ width: 100%;
532
+ text-align: left;
533
+ }
534
+
535
+ .muigui-radio-grid input:checked + button {
536
+ color: var(--value-color);
537
+ background-color: var(--selected-color);
538
+ }
539
+
540
+ /* ------ [ color-chooser ] ------ */
541
+
542
+ .muigui-color-chooser-cursor {
543
+ stroke-width: 1px;
544
+ stroke: white;
545
+ fill: none;
546
+ }
547
+ .muigui-color-chooser-circle {
548
+ stroke-width: 1px;
549
+ stroke: white;
550
+ fill: none;
551
+ }
552
+
553
+
554
+ /* ------ [ vec2 ] ------ */
555
+
556
+ .muigui-vec2 svg {
557
+ background-color: var(--value-bg-color);
558
+ }
559
+
560
+ .muigui-vec2-axis {
561
+ stroke: 1px;
562
+ stroke: var(--focus-color);
563
+ }
564
+
565
+ .muigui-vec2-line {
566
+ stroke-width: 1px;
567
+ stroke: var(--value-color);
568
+ fill: var(--value-color);
569
+ }
570
+
571
+ /* ------ [ direction ] ------ */
572
+
573
+ .muigui-direction svg {
574
+ background-color: rgba(0,0,0,0.2);
575
+ }
576
+
577
+ .muigui-direction:focus-within svg {
578
+ outline: none;
579
+ }
580
+ .muigui-direction-range {
581
+ fill: var(--value-bg-color);
582
+ }
583
+ .muigui-direction svg:focus {
584
+ outline: none;
585
+ }
586
+ .muigui-direction svg:focus .muigui-direction-range {
587
+ stroke-width: 0.5px;
588
+ stroke: var(--focus-color);
589
+ }
590
+
591
+ .muigui-direction-arrow {
592
+ fill: var(--value-color);
593
+ }
594
+
595
+ /* ------ [ slider ] ------ */
596
+
597
+ .muigui-slider>div {
598
+ display: flex;
599
+ align-items: stretch;
600
+ height: var(--line-height);
601
+ }
602
+ .muigui-slider svg {
603
+ flex: 1 1 auto;
604
+ }
605
+ .muigui-slider .muigui-slider-up #muigui-orientation {
606
+ transform: scale(1, -1) translateY(-100%);
607
+ }
608
+
609
+ .muigui-slider .muigui-slider-up #muigui-number-orientation {
610
+ transform: scale(1,-1);
611
+ }
612
+
613
+ .muigui-ticks {
614
+ stroke: var(--range-color);
615
+ }
616
+ .muigui-thicks {
617
+ stroke: var(--color);
618
+ stroke-width: 2px;
619
+ }
620
+ .muigui-svg-text {
621
+ fill: var(--color);
622
+ font-size: 7px;
623
+ }
624
+ .muigui-mark {
625
+ fill: var(--value-color);
626
+ }
627
+
628
+ /* ------ [ range ] ------ */
629
+
630
+
631
+ .muigui-range input[type=range] {
632
+ -webkit-appearance: none;
633
+ appearance: none;
634
+ background-color: transparent;
635
+ }
636
+
637
+ .muigui-range input[type=range]::-webkit-slider-thumb {
638
+ -webkit-appearance: none;
639
+ appearance: none;
640
+ border-radius: calc(var(--border-radius) + 2px);
641
+ border-left: 1px solid rgba(255,255,255,0.3);
642
+ border-top: 1px solid rgba(255,255,255,0.3);
643
+ border-bottom: 1px solid rgba(0,0,0,0.2);
644
+ border-right: 1px solid rgba(0,0,0,0.2);
645
+ background-color: var(--range-color);
646
+ margin-top: calc((var(--line-height) - 6px) / -2);
647
+ width: calc(var(--line-height) - 6px);
648
+ height: calc(var(--line-height) - 6px);
649
+ }
650
+
651
+ .muigui-range input[type=range]::-webkit-slider-runnable-track {
652
+ -webkit-appearance: none;
653
+ appearance: none;
654
+ border: 1px solid var(--menu-sep-color);
655
+ height: 2px;
656
+ }
657
+
658
+
659
+ /* dat.gui style - doesn't work on Safari iOS */
660
+
661
+ /*
662
+ .muigui-range input[type=range] {
663
+ cursor: ew-resize;
664
+ overflow: hidden;
665
+ }
666
+
667
+ .muigui-range input[type=range] {
668
+ -webkit-appearance: none;
669
+ appearance: none;
670
+ background-color: var(--range-right-color);
671
+ margin: 0;
672
+ }
673
+ .muigui-range input[type=range]:hover {
674
+ background-color: var(--range-right-hover-color);
675
+ }
676
+
677
+ .muigui-range input[type=range]::-webkit-slider-runnable-track {
678
+ -webkit-appearance: none;
679
+ appearance: none;
680
+ height: max-content;
681
+ color: var(--range-left-color);
682
+ margin-top: -1px;
683
+ }
684
+
685
+ .muigui-range input[type=range]::-webkit-slider-thumb {
686
+ -webkit-appearance: none;
687
+ appearance: none;
688
+ width: 0px;
689
+ height: max-content;
690
+ box-shadow: -1000px 0 0 1000px var(--range-left-color);
691
+ }
692
+ */
693
+
694
+ /* FF */
695
+ /*
696
+ .muigui-range input[type=range]::-moz-slider-progress {
697
+ background-color: var(--range-left-color);
698
+ }
699
+ .muigui-range input[type=range]::-moz-slider-thumb {
700
+ height: max-content;
701
+ width: 0;
702
+ border: none;
703
+ box-shadow: -1000px 0 0 1000px var(--range-left-color);
704
+ box-sizing: border-box;
705
+ }
706
+ */
707
+
708
+ .muigui-checkered-background {
709
+ background-color: #404040;
710
+ background-image:
711
+ linear-gradient(45deg, #808080 25%, transparent 25%),
712
+ linear-gradient(-45deg, #808080 25%, transparent 25%),
713
+ linear-gradient(45deg, transparent 75%, #808080 75%),
714
+ linear-gradient(-45deg, transparent 75%, #808080 75%);
715
+ background-size: 16px 16px;
716
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
717
+ }
718
+
719
+ /* ---------------------------------------------------------- */
720
+
721
+ /* needs to be at bottom to take precedence */
722
+ .muigui-auto-place {
723
+ max-height: 100%;
724
+ position: fixed;
725
+ top: 0;
726
+ right: 15px;
727
+ z-index: 100001;
728
+ }
729
+
730
+ `,
731
+ themes: {
732
+ default: {
733
+ include: ['default'],
734
+ css: `
735
+ `,
736
+ },
737
+ float: {
738
+ include: ['default'],
739
+ css: `
740
+ :root {
741
+ color-scheme: light dark,
742
+ }
743
+
744
+ .muigui {
745
+ --width: 400px;
746
+ --bg-color: initial;
747
+ --label-width: 25%;
748
+ --number-width: 20%;
749
+ }
750
+
751
+ input,
752
+ .muigui-label-controller>label {
753
+ text-shadow:
754
+ -1px -1px 0 var(--contrast-color),
755
+ 1px -1px 0 var(--contrast-color),
756
+ -1px 1px 0 var(--contrast-color),
757
+ 1px 1px 0 var(--contrast-color);
758
+ }
759
+
760
+ .muigui-controller > label:nth-child(1) {
761
+ place-content: center end;
762
+ margin-right: 1em;
763
+ }
764
+
765
+ .muigui-value > :nth-child(2) {
766
+ margin-left: 1em;
767
+ }
768
+
769
+ .muigui-root>*:nth-child(1) {
770
+ display: none;
771
+ }
772
+
773
+ .muigui-range input[type=range]::-webkit-slider-thumb {
774
+ border-radius: 1em;
775
+ }
776
+
777
+ .muigui-range input[type=range]::-webkit-slider-runnable-track {
778
+ -webkit-appearance: initial;
779
+ appearance: none;
780
+ border: 1px solid rgba(0, 0, 0, 0.25);
781
+ height: 2px;
782
+ }
783
+
784
+ .muigui-colors {
785
+ --value-color: var(--color );
786
+ --value-bg-color: rgba(0, 0, 0, 0.1);
787
+ --disabled-color: #cccccc;
788
+ --menu-bg-color: rgba(0, 0, 0, 0.1);
789
+ --menu-sep-color: #bbbbbb;
790
+ --hover-bg-color: rgba(0, 0, 0, 0);
791
+ --invalid-color: #FF0000;
792
+ --selected-color: rgba(0, 0, 0, 0.3);
793
+ --range-color: rgba(0, 0, 0, 0.125);
794
+ }
795
+ `,
796
+ },
797
+ form: {
798
+ include: [],
799
+ css: `
800
+ .muigui {
801
+ --width: 100%;
802
+ --label-width: 45%;
803
+ --number-width: 40%;
804
+ }
805
+ .muigui-root>button {
806
+ display: none;
807
+ }
808
+ .muigui-controller {
809
+ margin-top: 1em;
810
+ }
811
+ .muigui-label-controller {
812
+ display: flex;
813
+ flex-direction: column;
814
+ align-items: stretch;
815
+ margin-top: 1em;
816
+ }
817
+ .muigui-label-controller:has(.muigui-checkbox) {
818
+ flex-direction: row;
819
+ }
820
+ .muigui-value {
821
+ display: flex;
822
+ align-items: stretch;
823
+ }
824
+ .muigui-value>* {
825
+ flex: 1 1 auto;
826
+ min-width: 0;
827
+ }
828
+ .muigui-controller>*:nth-child(1) {
829
+ flex: 1 0 var(--label-width);
830
+ min-width: 0;
831
+ white-space: pre;
832
+ }
833
+ .muigui-controller>label:nth-child(1) {
834
+ place-content: center start;
835
+ display: inline-grid;
836
+ overflow: hidden;
837
+ }
838
+ .muigui-controller>*:nth-child(2) {
839
+ flex: 1 1 75%;
840
+ min-width: 0;
841
+ }
842
+ `,
843
+ },
844
+ none: {
845
+ include: [],
846
+ css: '',
847
+ },
848
+ },
849
+ };
850
+
851
+ function setElemProps(elem, attrs, children) {
852
+ for (const [key, value] of Object.entries(attrs)) {
853
+ if (typeof value === 'function' && key.startsWith('on')) {
854
+ const eventName = key.substring(2).toLowerCase();
855
+ elem.addEventListener(eventName, value, {passive: false});
856
+ } else if (typeof value === 'object') {
857
+ for (const [k, v] of Object.entries(value)) {
858
+ elem[key][k] = v;
859
+ }
860
+ } else if (elem[key] === undefined) {
861
+ elem.setAttribute(key, value);
862
+ } else {
863
+ elem[key] = value;
864
+ }
865
+ }
866
+ for (const child of children) {
867
+ elem.appendChild(child);
868
+ }
869
+ return elem;
870
+ }
871
+
872
+ function createElem(tag, attrs = {}, children = []) {
873
+ const elem = document.createElement(tag);
874
+ setElemProps(elem, attrs, children);
875
+ return elem;
876
+ }
877
+
878
+ function addElem(tag, parent, attrs = {}, children = []) {
879
+ const elem = createElem(tag, attrs, children);
880
+ parent.appendChild(elem);
881
+ return elem;
882
+ }
883
+
884
+ let nextId = 0;
885
+ function getNewId() {
886
+ return `muigui-id-${nextId++}`;
887
+ }
888
+
889
+ function removeArrayElem(array, value) {
890
+ const ndx = array.indexOf(value);
891
+ if (ndx) {
892
+ array.splice(ndx, 1);
893
+ }
894
+ return array;
895
+ }
896
+
897
+ /**
898
+ * Converts an camelCase or snake_case id to "camel case" or "snake case"
899
+ * @param {string} id
900
+ */
901
+ const underscoreRE = /_/g;
902
+ const upperLowerRE = /([A-Z])([a-z])/g;
903
+ function idToLabel(id) {
904
+ return id.replace(underscoreRE, ' ')
905
+ .replace(upperLowerRE, (m, m1, m2) => `${m1.toLowerCase()} ${m2}`);
906
+ }
907
+
908
+ function clamp$1(v, min, max) {
909
+ return Math.max(min, Math.min(max, v));
910
+ }
911
+
912
+ const isTypedArray = typeof SharedArrayBuffer !== 'undefined'
913
+ ? function isArrayBufferOrSharedArrayBuffer(a) {
914
+ return a && a.buffer && (a.buffer instanceof ArrayBuffer || a.buffer instanceof SharedArrayBuffer);
915
+ }
916
+ : function isArrayBuffer(a) {
917
+ return a && a.buffer && a.buffer instanceof ArrayBuffer;
918
+ };
919
+
920
+ const isArrayOrTypedArray = v => Array.isArray(v) || isTypedArray(v);
921
+
922
+ // Yea, I know this should be `Math.round(v / step) * step
923
+ // but try step = 0.1, newV = 19.95
924
+ //
925
+ // I get
926
+ // Math.round(19.95 / 0.1) * 0.1
927
+ // 19.900000000000002
928
+ // vs
929
+ // Math.round(19.95 / 0.1) / (1 / 0.1)
930
+ // 19.9
931
+ //
932
+ const stepify = (v, from, step) => Math.round(from(v) / step) / (1 / step);
933
+
934
+ const euclideanModulo$1 = (v, n) => ((v % n) + n) % n;
935
+ const lerp$1 = (a, b, t) => a + (b - a) * t;
936
+ function copyExistingProperties(dst, src) {
937
+ for (const key in src) {
938
+ if (key in dst) {
939
+ dst[key] = src[key];
940
+ }
941
+ }
942
+ return dst;
943
+ }
944
+
945
+ const mapRange = (v, inMin, inMax, outMin, outMax) => (v - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
946
+
947
+ const makeRangeConverters = ({from, to}) => {
948
+ return {
949
+ to: v => mapRange(v, ...from, ...to),
950
+ from: v => [true, mapRange(v, ...to, ...from)],
951
+ };
952
+ };
953
+
954
+ const makeRangeOptions = ({from, to, step}) => {
955
+ return {
956
+ min: to[0],
957
+ max: to[1],
958
+ ...(step && {step}),
959
+ converters: makeRangeConverters({from, to}),
960
+ };
961
+ };
962
+
963
+ // TODO: remove an use one in conversions. Move makeRangeConverters there?
964
+ const identity$1 = {
965
+ to: v => v,
966
+ from: v => [true, v],
967
+ };
968
+ function makeMinMaxPair(gui, properties, minPropName, maxPropName, options) {
969
+ const { converters: { from } = identity$1 } = options;
970
+ const { min, max } = options;
971
+ const guiMinRange = options.minRange || 0;
972
+ const valueMinRange = from(guiMinRange)[1];
973
+ const minGui = gui
974
+ .add(properties, minPropName, {
975
+ ...options,
976
+ min,
977
+ max: max - guiMinRange,
978
+ })
979
+ .onChange(v => {
980
+ maxGui.setValue(Math.min(max, Math.max(v + valueMinRange, properties[maxPropName])));
981
+ });
982
+ const maxGui = gui
983
+ .add(properties, maxPropName, {
984
+ ...options,
985
+ min: min + guiMinRange,
986
+ max,
987
+ })
988
+ .onChange(v => {
989
+ minGui.setValue(Math.max(min, Math.min(v - valueMinRange, properties[minPropName])));
990
+ });
991
+ return [ minGui, maxGui ];
992
+ }
993
+
994
+ class View {
995
+ domElement;
996
+ #childDestElem;
997
+ #views = [];
998
+ constructor(elem) {
999
+ this.domElement = elem;
1000
+ this.#childDestElem = elem;
1001
+ }
1002
+ addElem(elem) {
1003
+ this.#childDestElem.appendChild(elem);
1004
+ return elem;
1005
+ }
1006
+ removeElem(elem) {
1007
+ this.#childDestElem.removeChild(elem);
1008
+ return elem;
1009
+ }
1010
+ pushSubElem(elem) {
1011
+ this.#childDestElem.appendChild(elem);
1012
+ this.#childDestElem = elem;
1013
+ }
1014
+ popSubElem() {
1015
+ this.#childDestElem = this.#childDestElem.parentElement;
1016
+ }
1017
+ add(view) {
1018
+ this.#views.push(view);
1019
+ this.addElem(view.domElement);
1020
+ return view;
1021
+ }
1022
+ remove(view) {
1023
+ this.removeElem(view.domElement);
1024
+ removeArrayElem(this.#views, view);
1025
+ return view;
1026
+ }
1027
+ pushSubView(view) {
1028
+ this.pushSubElem(view.domElement);
1029
+ }
1030
+ popSubView() {
1031
+ this.popSubElem();
1032
+ }
1033
+ setOptions(options) {
1034
+ for (const view of this.#views) {
1035
+ view.setOptions(options);
1036
+ }
1037
+ }
1038
+ updateDisplayIfNeeded(newV, ignoreCache) {
1039
+ for (const view of this.#views) {
1040
+ view.updateDisplayIfNeeded(newV, ignoreCache);
1041
+ }
1042
+ return this;
1043
+ }
1044
+ $(selector) {
1045
+ return this.domElement.querySelector(selector);
1046
+ }
1047
+ }
1048
+
1049
+ class Controller extends View {
1050
+ #changeFns;
1051
+ #finishChangeFns;
1052
+ #parent;
1053
+
1054
+ constructor(className) {
1055
+ super(createElem('div', {className: 'muigui-controller'}));
1056
+ this.#changeFns = [];
1057
+ this.#finishChangeFns = [];
1058
+ // we need the specialization to come last so it takes precedence.
1059
+ if (className) {
1060
+ this.domElement.classList.add(className);
1061
+ }
1062
+ }
1063
+ get parent() {
1064
+ return this.#parent;
1065
+ }
1066
+ setParent(parent) {
1067
+ this.#parent = parent;
1068
+ this.enable(!this.disabled());
1069
+ }
1070
+ show(show = true) {
1071
+ this.domElement.classList.toggle('muigui-hide', !show);
1072
+ this.domElement.classList.toggle('muigui-show', show);
1073
+ return this;
1074
+ }
1075
+ hide() {
1076
+ return this.show(false);
1077
+ }
1078
+ disabled() {
1079
+ return !!this.domElement.closest('.muigui-disabled');
1080
+ }
1081
+
1082
+ enable(enable = true) {
1083
+ this.domElement.classList.toggle('muigui-disabled', !enable);
1084
+
1085
+ // If disabled we need to set the attribute 'disabled=true' to all
1086
+ // input/select/button/textarea's below
1087
+ //
1088
+ // If enabled we need to set the attribute 'disabled=false' to all below
1089
+ // until we hit a disabled controller.
1090
+ //
1091
+ // ATM the problem is we can find the input/select/button/textarea elements
1092
+ // but we can't easily find which controller they belong do.
1093
+ // But we don't need to? We can just check up if it or parent has
1094
+ // '.muigui-disabled'
1095
+ ['input', 'button', 'select', 'textarea'].forEach(tag => {
1096
+ this.domElement.querySelectorAll(tag).forEach(elem => {
1097
+ const disabled = !!elem.closest('.muigui-disabled');
1098
+ elem.disabled = disabled;
1099
+ });
1100
+ });
1101
+
1102
+ return this;
1103
+ }
1104
+ disable(disable = true) {
1105
+ return this.enable(!disable);
1106
+ }
1107
+ onChange(fn) {
1108
+ this.removeChange(fn);
1109
+ this.#changeFns.push(fn);
1110
+ return this;
1111
+ }
1112
+ removeChange(fn) {
1113
+ removeArrayElem(this.#changeFns, fn);
1114
+ return this;
1115
+ }
1116
+ onFinishChange(fn) {
1117
+ this.removeFinishChange(fn);
1118
+ this.#finishChangeFns.push(fn);
1119
+ return this;
1120
+ }
1121
+ removeFinishChange(fn) {
1122
+ removeArrayElem(this.#finishChangeFns, fn);
1123
+ return this;
1124
+ }
1125
+ #callListeners(fns, newV) {
1126
+ for (const fn of fns) {
1127
+ fn.call(this, newV);
1128
+ }
1129
+ }
1130
+ emitChange(value, object, property) {
1131
+ this.#callListeners(this.#changeFns, value);
1132
+ if (this.#parent) {
1133
+ if (object === undefined) {
1134
+ this.#parent.emitChange(value);
1135
+ } else {
1136
+ this.#parent.emitChange({
1137
+ object,
1138
+ property,
1139
+ value,
1140
+ controller: this,
1141
+ });
1142
+ }
1143
+ }
1144
+ }
1145
+ emitFinalChange(value, object, property) {
1146
+ this.#callListeners(this.#finishChangeFns, value);
1147
+ if (this.#parent) {
1148
+ if (object === undefined) {
1149
+ this.#parent.emitChange(value);
1150
+ } else {
1151
+ this.#parent.emitFinalChange({
1152
+ object,
1153
+ property,
1154
+ value,
1155
+ controller: this,
1156
+ });
1157
+ }
1158
+ }
1159
+ }
1160
+ updateDisplay() {
1161
+ // placeholder. override
1162
+ }
1163
+ getColors() {
1164
+ const toCamelCase = s => s.replace(/-([a-z])/g, (m, m1) => m1.toUpperCase());
1165
+ const keys = [
1166
+ 'color',
1167
+ 'bg-color',
1168
+ 'value-color',
1169
+ 'value-bg-color',
1170
+ 'hover-bg-color',
1171
+ 'menu-bg-color',
1172
+ 'menu-sep-color',
1173
+ 'disabled-color',
1174
+ ];
1175
+ const div = createElem('div');
1176
+ this.domElement.appendChild(div);
1177
+ const colors = Object.fromEntries(keys.map(key => {
1178
+ div.style.color = `var(--${key})`;
1179
+ const s = getComputedStyle(div);
1180
+ return [toCamelCase(key), s.color];
1181
+ }));
1182
+ div.remove();
1183
+ return colors;
1184
+ }
1185
+ }
1186
+
1187
+ class Button extends Controller {
1188
+ #object;
1189
+ #property;
1190
+ #buttonElem;
1191
+ #options = {
1192
+ name: '',
1193
+ };
1194
+
1195
+ constructor(object, property, options = {}) {
1196
+ super('muigui-button', '');
1197
+ this.#object = object;
1198
+ this.#property = property;
1199
+
1200
+ this.#buttonElem = this.addElem(
1201
+ createElem('button', {
1202
+ type: 'button',
1203
+ onClick: () => {
1204
+ this.#object[this.#property](this);
1205
+ },
1206
+ }));
1207
+ this.setOptions({name: property, ...options});
1208
+ }
1209
+ name(name) {
1210
+ this.#buttonElem.textContent = name;
1211
+ }
1212
+ setOptions(options) {
1213
+ copyExistingProperties(this.#options, options);
1214
+ const {name} = this.#options;
1215
+ this.#buttonElem.textContent = name;
1216
+ }
1217
+ }
1218
+
1219
+ function arraysEqual(a, b) {
1220
+ if (a.length !== b.length) {
1221
+ return false;
1222
+ }
1223
+ for (let i = 0; i < a.length; ++i) {
1224
+ if (a[i] !== b[i]) {
1225
+ return false;
1226
+ }
1227
+ }
1228
+ return true;
1229
+ }
1230
+
1231
+ function copyArrayElementsFromTo(src, dst) {
1232
+ dst.length = src.length;
1233
+ for (let i = 0; i < src.length; ++i) {
1234
+ dst[i] = src[i];
1235
+ }
1236
+ }
1237
+
1238
+ class EditView extends View {
1239
+ #oldV;
1240
+ #updateCheck;
1241
+
1242
+ #checkArrayNeedsUpdate(newV) {
1243
+ // It's an array, we need to compare all elements
1244
+ // Example, vec2, [r,g,b], ...
1245
+ const needUpdate = !arraysEqual(newV, this.#oldV);
1246
+ if (needUpdate) {
1247
+ copyArrayElementsFromTo(newV, this.#oldV);
1248
+ }
1249
+ return needUpdate;
1250
+ }
1251
+
1252
+ #checkTypedArrayNeedsUpdate() {
1253
+ let once = true;
1254
+ return function checkTypedArrayNeedsUpdateImpl(newV) {
1255
+ // It's a typedarray, we need to compare all elements
1256
+ // Example: Float32Array([r, g, b])
1257
+ let needUpdate = once;
1258
+ once = false;
1259
+ if (!needUpdate) {
1260
+ needUpdate = !arraysEqual(newV, this.#oldV);
1261
+ }
1262
+ return needUpdate;
1263
+ };
1264
+ }
1265
+
1266
+ #checkObjectNeedsUpdate(newV) {
1267
+ let needUpdate = false;
1268
+ for (const key in newV) {
1269
+ if (newV[key] !== this.#oldV[key]) {
1270
+ needUpdate = true;
1271
+ this.#oldV[key] = newV[key];
1272
+ }
1273
+ }
1274
+ return needUpdate;
1275
+ }
1276
+
1277
+ #checkValueNeedsUpdate(newV) {
1278
+ const needUpdate = newV !== this.#oldV;
1279
+ this.#oldV = newV;
1280
+ return needUpdate;
1281
+ }
1282
+
1283
+ #getUpdateCheckForType(newV) {
1284
+ if (Array.isArray(newV)) {
1285
+ this.#oldV = [];
1286
+ return this.#checkArrayNeedsUpdate.bind(this);
1287
+ } else if (isTypedArray(newV)) {
1288
+ this.#oldV = new newV.constructor(newV);
1289
+ return this.#checkTypedArrayNeedsUpdate(this);
1290
+ } else if (typeof newV === 'object') {
1291
+ this.#oldV = {};
1292
+ return this.#checkObjectNeedsUpdate.bind(this);
1293
+ } else {
1294
+ return this.#checkValueNeedsUpdate.bind(this);
1295
+ }
1296
+ }
1297
+
1298
+ // The point of this is updating DOM elements
1299
+ // is slow but if we've called `listen` then
1300
+ // every frame we're going to try to update
1301
+ // things with the current value so if nothing
1302
+ // has changed then skip it.
1303
+ updateDisplayIfNeeded(newV, ignoreCache) {
1304
+ this.#updateCheck = this.#updateCheck || this.#getUpdateCheckForType(newV);
1305
+ // Note: We call #updateCheck first because it updates
1306
+ // the cache
1307
+ if (this.#updateCheck(newV) || ignoreCache) {
1308
+ this.updateDisplay(newV);
1309
+ }
1310
+ }
1311
+ setOptions(/*options*/) {
1312
+ // override this
1313
+ return this;
1314
+ }
1315
+ }
1316
+
1317
+ class CheckboxView extends EditView {
1318
+ #checkboxElem;
1319
+ constructor(setter, id) {
1320
+ const checkboxElem = createElem('input', {
1321
+ type: 'checkbox',
1322
+ id,
1323
+ onInput: () => {
1324
+ setter.setValue(checkboxElem.checked);
1325
+ },
1326
+ onChange: () => {
1327
+ setter.setFinalValue(checkboxElem.checked);
1328
+ },
1329
+ });
1330
+ super(createElem('label', {}, [checkboxElem]));
1331
+ this.#checkboxElem = checkboxElem;
1332
+ }
1333
+ updateDisplay(v) {
1334
+ this.#checkboxElem.checked = v;
1335
+ }
1336
+ }
1337
+
1338
+ const tasks = [];
1339
+ const tasksToRemove = new Set();
1340
+
1341
+ let requestId;
1342
+ let processing;
1343
+
1344
+ function removeTasks() {
1345
+ if (!tasksToRemove.size) {
1346
+ return;
1347
+ }
1348
+
1349
+ if (processing) {
1350
+ queueProcessing();
1351
+ return;
1352
+ }
1353
+
1354
+ tasksToRemove.forEach(task => {
1355
+ removeArrayElem(tasks, task);
1356
+ });
1357
+ tasksToRemove.clear();
1358
+ }
1359
+
1360
+ function processTasks() {
1361
+ requestId = undefined;
1362
+ processing = true;
1363
+ for (const task of tasks) {
1364
+ if (!tasksToRemove.has(task)) {
1365
+ task();
1366
+ }
1367
+ }
1368
+ processing = false;
1369
+ removeTasks();
1370
+ queueProcessing();
1371
+ }
1372
+
1373
+ function queueProcessing() {
1374
+ if (!requestId && tasks.length) {
1375
+ requestId = requestAnimationFrame(processTasks);
1376
+ }
1377
+ }
1378
+
1379
+ function addTask(fn) {
1380
+ tasks.push(fn);
1381
+ queueProcessing();
1382
+ }
1383
+
1384
+ function removeTask(fn) {
1385
+ tasksToRemove.set(fn);
1386
+
1387
+ const ndx = tasks.indexOf(fn);
1388
+ if (ndx >= 0) {
1389
+ tasks.splice(ndx, 1);
1390
+ }
1391
+ }
1392
+
1393
+ let id = 0;
1394
+
1395
+ function makeId() {
1396
+ return `muigui-${++id}`;
1397
+ }
1398
+
1399
+ class ValueView extends View {
1400
+ constructor(className = '') {
1401
+ super(createElem('div', {className: 'muigui-value'}));
1402
+ if (className) {
1403
+ this.domElement.classList.add(className);
1404
+ }
1405
+ }
1406
+ }
1407
+
1408
+ class LabelController extends Controller {
1409
+ #id;
1410
+ #nameElem;
1411
+
1412
+ constructor(className = '', name = '') {
1413
+ super('muigui-label-controller');
1414
+ this.#id = makeId();
1415
+ this.#nameElem = createElem('label', {for: this.#id});
1416
+ this.domElement.appendChild(this.#nameElem);
1417
+ this.pushSubView(new ValueView(className));
1418
+ this.name(name);
1419
+ }
1420
+ get id() {
1421
+ return this.#id;
1422
+ }
1423
+ name(name) {
1424
+ if (this.#nameElem.title === this.#nameElem.textContent) {
1425
+ this.#nameElem.title = name;
1426
+ }
1427
+ this.#nameElem.textContent = name;
1428
+ return this;
1429
+ }
1430
+ tooltip(tip) {
1431
+ this.#nameElem.title = tip;
1432
+ }
1433
+ }
1434
+
1435
+ class ValueController extends LabelController {
1436
+ #object;
1437
+ #property;
1438
+ #initialValue;
1439
+ #listening;
1440
+ #views;
1441
+ #updateFn;
1442
+
1443
+ constructor(object, property, className = '') {
1444
+ super(className, property);
1445
+ this.#object = object;
1446
+ this.#property = property;
1447
+ this.#initialValue = this.getValue();
1448
+ this.#listening = false;
1449
+ this.#views = [];
1450
+ }
1451
+ get initialValue() {
1452
+ return this.#initialValue;
1453
+ }
1454
+ get object() {
1455
+ return this.#object;
1456
+ }
1457
+ get property() {
1458
+ return this.#property;
1459
+ }
1460
+ add(view) {
1461
+ this.#views.push(view);
1462
+ super.add(view);
1463
+ this.updateDisplay();
1464
+ return view;
1465
+ }
1466
+ #setValueImpl(v, ignoreCache) {
1467
+ let isDifferent = false;
1468
+ if (typeof v === 'object') {
1469
+ const dst = this.#object[this.#property];
1470
+ // don't replace objects, just their values.
1471
+ if (Array.isArray(v) || isTypedArray(v)) {
1472
+ for (let i = 0; i < v.length; ++i) {
1473
+ isDifferent ||= dst[i] !== v[i];
1474
+ dst[i] = v[i];
1475
+ }
1476
+ } else {
1477
+ for (const key of Object.keys(v)) {
1478
+ isDifferent ||= dst[key] !== v[key];
1479
+ }
1480
+ Object.assign(dst, v);
1481
+ }
1482
+ } else {
1483
+ isDifferent = this.#object[this.#property] !== v;
1484
+ this.#object[this.#property] = v;
1485
+ }
1486
+ this.updateDisplay(ignoreCache);
1487
+ if (isDifferent) {
1488
+ this.emitChange(this.getValue(), this.#object, this.#property);
1489
+ }
1490
+ return isDifferent;
1491
+ }
1492
+ setValue(v) {
1493
+ this.#setValueImpl(v);
1494
+ }
1495
+ setFinalValue(v) {
1496
+ const isDifferent = this.#setValueImpl(v, true);
1497
+ if (isDifferent) {
1498
+ this.emitFinalChange(this.getValue(), this.#object, this.#property);
1499
+ }
1500
+ return this;
1501
+ }
1502
+ updateDisplay(ignoreCache) {
1503
+ const newV = this.getValue();
1504
+ for (const view of this.#views) {
1505
+ view.updateDisplayIfNeeded(newV, ignoreCache);
1506
+ }
1507
+ return this;
1508
+ }
1509
+ setOptions(options) {
1510
+ for (const view of this.#views) {
1511
+ view.setOptions(options);
1512
+ }
1513
+ this.updateDisplay();
1514
+ return this;
1515
+ }
1516
+ getValue() {
1517
+ return this.#object[this.#property];
1518
+ }
1519
+ value(v) {
1520
+ this.setValue(v);
1521
+ return this;
1522
+ }
1523
+ reset() {
1524
+ this.setValue(this.#initialValue);
1525
+ return this;
1526
+ }
1527
+ listen(listen = true) {
1528
+ if (!this.#updateFn) {
1529
+ this.#updateFn = this.updateDisplay.bind(this);
1530
+ }
1531
+ if (listen) {
1532
+ if (!this.#listening) {
1533
+ this.#listening = true;
1534
+ addTask(this.#updateFn);
1535
+ }
1536
+ } else {
1537
+ if (this.#listening) {
1538
+ this.#listening = false;
1539
+ removeTask(this.#updateFn);
1540
+ }
1541
+ }
1542
+ return this;
1543
+ }
1544
+ }
1545
+
1546
+ class Checkbox extends ValueController {
1547
+ constructor(object, property) {
1548
+ super(object, property, 'muigui-checkbox');
1549
+ const id = this.id;
1550
+ this.add(new CheckboxView(this, id));
1551
+ this.updateDisplay();
1552
+ }
1553
+ }
1554
+
1555
+ const identity = {
1556
+ to: v => v,
1557
+ from: v => [true, v],
1558
+ };
1559
+
1560
+ // from: from string to value
1561
+ // to: from value to string
1562
+ const strToNumber = {
1563
+ to: v => v.toString(),
1564
+ from: v => {
1565
+ const newV = parseFloat(v);
1566
+ return [!Number.isNaN(newV), newV];
1567
+ },
1568
+ };
1569
+
1570
+ const converters = {
1571
+ radToDeg: makeRangeConverters({to: [0, 180], from: [0, Math.PI]}),
1572
+ };
1573
+
1574
+ function createWheelHelper() {
1575
+ let wheelAccum = 0;
1576
+ return function (e, step, wheelScale = 5) {
1577
+ wheelAccum -= e.deltaY * step / wheelScale;
1578
+ const wheelSteps = Math.floor(Math.abs(wheelAccum) / step) * Math.sign(wheelAccum);
1579
+ const delta = wheelSteps * step;
1580
+ wheelAccum -= delta;
1581
+ return delta;
1582
+ };
1583
+ }
1584
+
1585
+ class NumberView extends EditView {
1586
+ #to;
1587
+ #from;
1588
+ #step;
1589
+ #skipUpdate;
1590
+ #options = {
1591
+ step: 0.01,
1592
+ converters: strToNumber,
1593
+ min: Number.NEGATIVE_INFINITY,
1594
+ max: Number.POSITIVE_INFINITY,
1595
+ };
1596
+
1597
+ constructor(setter, options) {
1598
+ const setValue = setter.setValue.bind(setter);
1599
+ const setFinalValue = setter.setFinalValue.bind(setter);
1600
+ const wheelHelper = createWheelHelper();
1601
+ super(createElem('input', {
1602
+ type: 'number',
1603
+ onInput: () => {
1604
+ this.#handleInput(setValue, true);
1605
+ },
1606
+ onChange: () => {
1607
+ this.#handleInput(setFinalValue, false);
1608
+ },
1609
+ onWheel: e => {
1610
+ e.preventDefault();
1611
+ const {min, max, step} = this.#options;
1612
+ const delta = wheelHelper(e, step);
1613
+ const v = parseFloat(this.domElement.value);
1614
+ const newV = clamp$1(stepify(v + delta, v => v, step), min, max);
1615
+ const [valid, outV] = this.#from(newV);
1616
+ if (valid) {
1617
+ setter.setValue(outV);
1618
+ }
1619
+ },
1620
+ }));
1621
+ this.setOptions(options);
1622
+ }
1623
+ #handleInput(setFn, skipUpdate) {
1624
+ const v = parseFloat(this.domElement.value);
1625
+ const [valid, newV] = this.#from(v);
1626
+ let inRange;
1627
+ if (valid && !Number.isNaN(v)) {
1628
+ const {min, max} = this.#options;
1629
+ inRange = newV >= min && newV <= max;
1630
+ this.#skipUpdate = skipUpdate;
1631
+ setFn(clamp$1(newV, min, max));
1632
+ }
1633
+ this.domElement.classList.toggle('muigui-invalid-value', !valid || !inRange);
1634
+ }
1635
+ updateDisplay(v) {
1636
+ if (!this.#skipUpdate) {
1637
+ this.domElement.value = stepify(v, this.#to, this.#step);
1638
+ }
1639
+ this.#skipUpdate = false;
1640
+ }
1641
+ setOptions(options) {
1642
+ copyExistingProperties(this.#options, options);
1643
+ const {
1644
+ step,
1645
+ converters: {to, from},
1646
+ } = this.#options;
1647
+ this.#to = to;
1648
+ this.#from = from;
1649
+ this.#step = step;
1650
+ return this;
1651
+ }
1652
+ }
1653
+
1654
+ // Wanted to name this `Number` but it conflicts with
1655
+ // JavaScript `Number`. It most likely wouldn't be
1656
+ // an issue? But users might `import {Number} ...` and
1657
+ // things would break.
1658
+ class TextNumber extends ValueController {
1659
+ #textView;
1660
+ #step;
1661
+
1662
+ constructor(object, property, options = {}) {
1663
+ super(object, property, 'muigui-text-number');
1664
+ this.#textView = this.add(new NumberView(this, options));
1665
+ this.updateDisplay();
1666
+ }
1667
+ }
1668
+
1669
+ class SelectView extends EditView {
1670
+ #values;
1671
+
1672
+ constructor(setter, keyValues) {
1673
+ const values = [];
1674
+ super(createElem('select', {
1675
+ onChange: () => {
1676
+ setter.setFinalValue(this.#values[this.domElement.selectedIndex]);
1677
+ },
1678
+ }, keyValues.map(([key, value]) => {
1679
+ values.push(value);
1680
+ return createElem('option', {textContent: key});
1681
+ })));
1682
+ this.#values = values;
1683
+ }
1684
+ updateDisplay(v) {
1685
+ const ndx = this.#values.indexOf(v);
1686
+ this.domElement.selectedIndex = ndx;
1687
+ }
1688
+ }
1689
+
1690
+ // 4 cases
1691
+ // (a) keyValues is array of arrays, each sub array is key value
1692
+ // (b) keyValues is array and value is number then keys = array contents, value = index
1693
+ // (c) keyValues is array and value is not number, key = array contents, value = array contents
1694
+ // (d) keyValues is object then key->value
1695
+ function convertToKeyValues(keyValues, valueIsNumber) {
1696
+ if (Array.isArray(keyValues)) {
1697
+ if (Array.isArray(keyValues[0])) {
1698
+ // (a) keyValues is array of arrays, each sub array is key value
1699
+ return keyValues;
1700
+ } else {
1701
+ if (valueIsNumber) {
1702
+ // (b) keyValues is array and value is number then keys = array contents, value = index
1703
+ return keyValues.map((v, ndx) => [v, ndx]);
1704
+ } else {
1705
+ // (c) keyValues is array and value is not number, key = array contents, value = array contents
1706
+ return keyValues.map(v => [v, v]);
1707
+ }
1708
+ }
1709
+ } else {
1710
+ // (d)
1711
+ return [...Object.entries(keyValues)];
1712
+ }
1713
+ }
1714
+
1715
+ class Select extends ValueController {
1716
+ constructor(object, property, options) {
1717
+ super(object, property, 'muigui-select');
1718
+ const valueIsNumber = typeof this.getValue() === 'number';
1719
+ const {keyValues: keyValuesInput} = options;
1720
+ const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber);
1721
+ this.add(new SelectView(this, keyValues));
1722
+ this.updateDisplay();
1723
+ }
1724
+ }
1725
+
1726
+ class RangeView extends EditView {
1727
+ #to;
1728
+ #from;
1729
+ #step;
1730
+ #skipUpdate;
1731
+ #options = {
1732
+ step: 0.01,
1733
+ min: 0,
1734
+ max: 1,
1735
+ converters: identity,
1736
+ };
1737
+
1738
+ constructor(setter, options) {
1739
+ const wheelHelper = createWheelHelper();
1740
+ super(createElem('input', {
1741
+ type: 'range',
1742
+ onInput: () => {
1743
+ this.#skipUpdate = true;
1744
+ const {min, max, step} = this.#options;
1745
+ const v = parseFloat(this.domElement.value);
1746
+ const newV = clamp$1(stepify(v, v => v, step), min, max);
1747
+ const [valid, validV] = this.#from(newV);
1748
+ if (valid) {
1749
+ setter.setValue(validV);
1750
+ }
1751
+ },
1752
+ onChange: () => {
1753
+ this.#skipUpdate = true;
1754
+ const {min, max, step} = this.#options;
1755
+ const v = parseFloat(this.domElement.value);
1756
+ const newV = clamp$1(stepify(v, v => v, step), min, max);
1757
+ const [valid, validV] = this.#from(newV);
1758
+ if (valid) {
1759
+ setter.setFinalValue(validV);
1760
+ }
1761
+ },
1762
+ onWheel: e => {
1763
+ e.preventDefault();
1764
+ const [valid, v] = this.#from(parseFloat(this.domElement.value));
1765
+ if (!valid) {
1766
+ return;
1767
+ }
1768
+ const {min, max, step} = this.#options;
1769
+ const delta = wheelHelper(e, step);
1770
+ const newV = clamp$1(stepify(v + delta, v => v, step), min, max);
1771
+ setter.setValue(newV);
1772
+ },
1773
+ }));
1774
+ this.setOptions(options);
1775
+ }
1776
+ updateDisplay(v) {
1777
+ if (!this.#skipUpdate) {
1778
+ this.domElement.value = stepify(v, this.#to, this.#step);
1779
+ }
1780
+ this.#skipUpdate = false;
1781
+ }
1782
+ setOptions(options) {
1783
+ copyExistingProperties(this.#options, options);
1784
+ const {
1785
+ step,
1786
+ min,
1787
+ max,
1788
+ converters: {to, from},
1789
+ } = this.#options;
1790
+ this.#to = to;
1791
+ this.#from = from;
1792
+ this.#step = step;
1793
+ this.domElement.step = step;
1794
+ this.domElement.min = min;
1795
+ this.domElement.max = max;
1796
+ return this;
1797
+ }
1798
+ }
1799
+
1800
+ class Range extends ValueController {
1801
+ constructor(object, property, options) {
1802
+ super(object, property, 'muigui-range');
1803
+ this.add(new RangeView(this, options));
1804
+ this.add(new NumberView(this, options));
1805
+ }
1806
+ }
1807
+
1808
+ class TextView extends EditView {
1809
+ #to;
1810
+ #from;
1811
+ #skipUpdate;
1812
+ #options = {
1813
+ converters: identity,
1814
+ };
1815
+
1816
+ constructor(setter, options) {
1817
+ const setValue = setter.setValue.bind(setter);
1818
+ const setFinalValue = setter.setFinalValue.bind(setter);
1819
+ super(createElem('input', {
1820
+ type: 'text',
1821
+ onInput: () => {
1822
+ this.#handleInput(setValue, true);
1823
+ },
1824
+ onChange: () => {
1825
+ this.#handleInput(setFinalValue, false);
1826
+ },
1827
+ }));
1828
+ this.setOptions(options);
1829
+ }
1830
+ #handleInput(setFn, skipUpdate) {
1831
+ const [valid, newV] = this.#from(this.domElement.value);
1832
+ if (valid) {
1833
+ this.#skipUpdate = skipUpdate;
1834
+ setFn(newV);
1835
+ }
1836
+ this.domElement.style.color = valid ? '' : 'var(--invalid-color)';
1837
+
1838
+ }
1839
+ updateDisplay(v) {
1840
+ if (!this.#skipUpdate) {
1841
+ this.domElement.value = this.#to(v);
1842
+ this.domElement.style.color = '';
1843
+ }
1844
+ this.#skipUpdate = false;
1845
+ }
1846
+ setOptions(options) {
1847
+ copyExistingProperties(this.#options, options);
1848
+ const {
1849
+ converters: {to, from},
1850
+ } = this.#options;
1851
+ this.#to = to;
1852
+ this.#from = from;
1853
+ return this;
1854
+ }
1855
+ }
1856
+
1857
+ class Text extends ValueController {
1858
+ constructor(object, property) {
1859
+ super(object, property, 'muigui-text');
1860
+ this.add(new TextView(this));
1861
+ this.updateDisplay();
1862
+ }
1863
+ }
1864
+
1865
+ // const isConversion = o => typeof o.to === 'function' && typeof o.from === 'function';
1866
+
1867
+ /**
1868
+ * possible inputs
1869
+ * add(o, p, min: number, max: number)
1870
+ * add(o, p, min: number, max: number, step: number)
1871
+ * add(o, p, array: [value])
1872
+ * add(o, p, array: [[key, value]])
1873
+ *
1874
+ * @param {*} object
1875
+ * @param {string} property
1876
+ * @param {...any} args
1877
+ * @returns {Controller}
1878
+ */
1879
+ function createController(object, property, ...args) {
1880
+ const [arg1] = args;
1881
+ if (Array.isArray(arg1)) {
1882
+ return new Select(object, property, {keyValues: arg1});
1883
+ }
1884
+ if (arg1 && arg1.keyValues) {
1885
+ return new Select(object, property, {keyValues: arg1.keyValues});
1886
+ }
1887
+
1888
+ const t = typeof object[property];
1889
+ switch (t) {
1890
+ case 'number':
1891
+ if (typeof args[0] === 'number' && typeof args[1] === 'number') {
1892
+ const min = args[0];
1893
+ const max = args[1];
1894
+ const step = args[2];
1895
+ return new Range(object, property, {min, max, ...(step && {step})});
1896
+ }
1897
+ return args.length === 0
1898
+ ? new TextNumber(object, property, ...args)
1899
+ : new Range(object, property, ...args);
1900
+ case 'boolean':
1901
+ return new Checkbox(object, property, ...args);
1902
+ case 'function':
1903
+ return new Button(object, property, ...args);
1904
+ case 'string':
1905
+ return new Text(object, property, ...args);
1906
+ case 'undefined':
1907
+ throw new Error(`no property named ${property}`);
1908
+ default:
1909
+ throw new Error(`unhandled type ${t} for property ${property}`);
1910
+ }
1911
+ }
1912
+
1913
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
1914
+ const lerp = (a, b, t) => a + (b - a) * t;
1915
+ const fract = v => v >= 0 ? v % 1 : 1 - (v % 1);
1916
+
1917
+ const f0 = v => +v.toFixed(0); // converts to string (eg 1.2 => "1"), then converts back to number (eg, "1.200" => 1.2)
1918
+ const f3 = v => +v.toFixed(3); // converts to string (eg 1.2 => "1.200"), then converts back to number (eg, "1.200" => 1.2)
1919
+
1920
+ const hexToUint32RGB = v => (parseInt(v.substring(1, 3), 16) << 16) |
1921
+ (parseInt(v.substring(3, 5), 16) << 8 ) |
1922
+ (parseInt(v.substring(5, 7), 16) );
1923
+ const uint32RGBToHex = v => `#${(Math.round(v)).toString(16).padStart(6, '0')}`;
1924
+ const hexToUint32RGBA = v => (parseInt(v.substring(1, 3), 16) * 2 ** 24) +
1925
+ (parseInt(v.substring(3, 5), 16) * 2 ** 16) +
1926
+ (parseInt(v.substring(5, 7), 16) * 2 ** 8) +
1927
+ (parseInt(v.substring(7, 9), 16) );
1928
+ const uint32RGBAToHex = v => `#${(Math.round(v)).toString(16).padStart(8, '0')}`;
1929
+
1930
+ const hexToUint8RGB = v => [
1931
+ parseInt(v.substring(1, 3), 16),
1932
+ parseInt(v.substring(3, 5), 16),
1933
+ parseInt(v.substring(5, 7), 16),
1934
+ ];
1935
+ const uint8RGBToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`;
1936
+
1937
+ const hexToUint8RGBA = v => [
1938
+ parseInt(v.substring(1, 3), 16),
1939
+ parseInt(v.substring(3, 5), 16),
1940
+ parseInt(v.substring(5, 7), 16),
1941
+ parseInt(v.substring(7, 9), 16),
1942
+ ];
1943
+ const uint8RGBAToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`;
1944
+
1945
+ const hexToFloatRGB = v => hexToUint8RGB(v).map(v => f3(v / 255));
1946
+ const floatRGBToHex = v => uint8RGBToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255))));
1947
+
1948
+ const hexToFloatRGBA = v => hexToUint8RGBA(v).map(v => f3(v / 255));
1949
+ const floatRGBAToHex = v => uint8RGBAToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255))));
1950
+
1951
+ const scaleAndClamp = v => clamp(Math.round(v * 255), 0, 255).toString(16).padStart(2, '0');
1952
+
1953
+ const hexToObjectRGB = v => ({
1954
+ r: parseInt(v.substring(1, 3), 16) / 255,
1955
+ g: parseInt(v.substring(3, 5), 16) / 255,
1956
+ b: parseInt(v.substring(5, 7), 16) / 255,
1957
+ });
1958
+ const objectRGBToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}`;
1959
+ const hexToObjectRGBA = v => ({
1960
+ r: parseInt(v.substring(1, 3), 16) / 255,
1961
+ g: parseInt(v.substring(3, 5), 16) / 255,
1962
+ b: parseInt(v.substring(5, 7), 16) / 255,
1963
+ a: parseInt(v.substring(7, 9), 16) / 255,
1964
+ });
1965
+ const objectRGBAToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}${scaleAndClamp(v.a)}`;
1966
+
1967
+ const hexToCssRGB = v => `rgb(${hexToUint8RGB(v).join(', ')})`;
1968
+ const cssRGBRegex = /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/;
1969
+ const cssRGBToHex = v => {
1970
+ const m = cssRGBRegex.exec(v);
1971
+ return uint8RGBToHex([m[1], m[2], m[3]].map(v => parseInt(v)));
1972
+ };
1973
+ const hexToCssRGBA = v => `rgba(${hexToUint8RGBA(v).map((v, i) => i === 3 ? v / 255 : v).join(', ')})`;
1974
+ const cssRGBARegex = /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+\.\d+|\d+)\s*\)\s*$/;
1975
+ const cssRGBAToHex = v => {
1976
+ const m = cssRGBARegex.exec(v);
1977
+ return uint8RGBAToHex([m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? (parseFloat(v) * 255 | 0) : parseInt(v)));
1978
+ };
1979
+
1980
+ const hexToCssHSL = v => {
1981
+ const hsl = rgbUint8ToHsl(hexToUint8RGB(v)).map(v => f0(v));
1982
+ return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
1983
+ };
1984
+ const hexToCssHSLA = v => {
1985
+ const hsla = rgbaUint8ToHsla(hexToUint8RGBA(v)).map((v, i) => i === 3 ? f3(v) : f0(v));
1986
+ return `hsl(${hsla[0]} ${hsla[1]}% ${hsla[2]}% / ${hsla[3]})`;
1987
+ };
1988
+ const cssHSLRegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\)\s*$/;
1989
+ const cssHSLARegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\/\s*(\d+\.\d+|\d+)\s*\)\s*$/;
1990
+
1991
+ const hex3DigitTo6Digit = v => `${v[0]}${v[0]}${v[1]}${v[1]}${v[2]}${v[2]}`;
1992
+ const cssHSLToHex = v => {
1993
+ const m = cssHSLRegex.exec(v);
1994
+ const rgb = hslToRgbUint8([m[1], m[2], m[3]].map(v => parseFloat(v)));
1995
+ return uint8RGBToHex(rgb);
1996
+ };
1997
+ const cssHSLAToHex = v => {
1998
+ const m = cssHSLARegex.exec(v);
1999
+ const rgba = hslaToRgbaUint8([m[1], m[2], m[3], m[4]].map(v => parseFloat(v)));
2000
+ return uint8RGBAToHex(rgba);
2001
+ };
2002
+
2003
+ const euclideanModulo = (v, n) => ((v % n) + n) % n;
2004
+
2005
+ function hslToRgbUint8([h, s, l]) {
2006
+ h = euclideanModulo(h, 360);
2007
+ s = clamp(s / 100, 0, 1);
2008
+ l = clamp(l / 100, 0, 1);
2009
+
2010
+ const a = s * Math.min(l, 1 - l);
2011
+
2012
+ function f(n) {
2013
+ const k = (n + h / 30) % 12;
2014
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
2015
+ }
2016
+
2017
+ return [f(0), f(8), f(4)].map(v => Math.round(v * 255));
2018
+ }
2019
+
2020
+ function hslaToRgbaUint8([h, s, l, a]) {
2021
+ const rgb = hslToRgbUint8([h, s, l]);
2022
+ return [...rgb, a * 255 | 0];
2023
+ }
2024
+
2025
+ function rgbFloatToHsl01([r, g, b]) {
2026
+ const max = Math.max(r, g, b);
2027
+ const min = Math.min(r, g, b);
2028
+ const l = (min + max) * 0.5;
2029
+ const d = max - min;
2030
+ let h = 0;
2031
+ let s = 0;
2032
+
2033
+ if (d !== 0) {
2034
+ s = (l === 0 || l === 1)
2035
+ ? 0
2036
+ : (max - l) / Math.min(l, 1 - l);
2037
+
2038
+ switch (max) {
2039
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
2040
+ case g: h = (b - r) / d + 2; break;
2041
+ case b: h = (r - g) / d + 4;
2042
+ }
2043
+ }
2044
+
2045
+ return [h / 6, s, l];
2046
+ }
2047
+
2048
+ function rgbaFloatToHsla01([r, g, b, a]) {
2049
+ const hsl = rgbFloatToHsl01([r, g, b]);
2050
+ return [...hsl, a];
2051
+ }
2052
+
2053
+ const rgbUint8ToHsl = (rgb) => {
2054
+ const [h, s, l] = rgbFloatToHsl01(rgb.map(v => v / 255));
2055
+ return [h * 360, s * 100, l * 100];
2056
+ };
2057
+
2058
+ const rgbaUint8ToHsla = (rgba) => {
2059
+ const [h, s, l, a] = rgbaFloatToHsla01(rgba.map(v => v / 255));
2060
+ return [h * 360, s * 100, l * 100, a];
2061
+ };
2062
+
2063
+ function hsv01ToRGBFloat([hue, sat, val]) {
2064
+ sat = clamp(sat, 0, 1);
2065
+ val = clamp(val, 0, 1);
2066
+ return [hue, hue + 2 / 3, hue + 1 / 3].map(
2067
+ v => lerp(1, clamp(Math.abs(fract(v) * 6 - 3.0) - 1, 0, 1), sat) * val
2068
+ );
2069
+ }
2070
+
2071
+ function hsva01ToRGBAFloat([hue, sat, val, alpha]) {
2072
+ const rgb = hsv01ToRGBFloat([hue, sat, val]);
2073
+ return [...rgb, alpha];
2074
+ }
2075
+
2076
+ const round3 = v => Math.round(v * 1000) / 1000;
2077
+
2078
+ function rgbFloatToHSV01([r, g, b]) {
2079
+ const p = b > g
2080
+ ? [b, g, -1, 2 / 3]
2081
+ : [g, b, 0, -1 / 3];
2082
+ const q = p[0] > r
2083
+ ? [p[0], p[1], p[3], r]
2084
+ : [r, p[1], p[2], p[0]];
2085
+ const d = q[0] - Math.min(q[3], q[1]);
2086
+ return [
2087
+ Math.abs(q[2] + (q[3] - q[1]) / (6 * d + Number.EPSILON)),
2088
+ d / (q[0] + Number.EPSILON),
2089
+ q[0],
2090
+ ].map(round3);
2091
+ }
2092
+
2093
+ function rgbaFloatToHSVA01([r, g, b, a]) {
2094
+ const hsv = rgbFloatToHSV01([r, g, b]);
2095
+ return [...hsv, a];
2096
+ }
2097
+
2098
+ // window.hsv01ToRGBFloat = hsv01ToRGBFloat;
2099
+ // window.rgbFloatToHSV01 = rgbFloatToHSV01;
2100
+
2101
+ // Yea, meh!
2102
+ const hasAlpha = format => format.endsWith('a') || format.startsWith('hex8');
2103
+
2104
+ const cssStringFormats = [
2105
+ { re: /^#(?:[0-9a-f]){6}$/i, format: 'hex6' },
2106
+ { re: /^(?:[0-9a-f]){6}$/i, format: 'hex6-no-hash' },
2107
+ { re: /^#(?:[0-9a-f]){8}$/i, format: 'hex8' },
2108
+ { re: /^(?:[0-9a-f]){8}$/i, format: 'hex8-no-hash' },
2109
+ { re: /^#(?:[0-9a-f]){3}$/i, format: 'hex3' },
2110
+ { re: /^(?:[0-9a-f]){3}$/i, format: 'hex3-no-hash' },
2111
+ { re: cssRGBRegex, format: 'css-rgb' },
2112
+ { re: cssHSLRegex, format: 'css-hsl' },
2113
+ { re: cssRGBARegex, format: 'css-rgba' },
2114
+ { re: cssHSLARegex, format: 'css-hsla' },
2115
+ ];
2116
+
2117
+ function guessStringColorFormat(v) {
2118
+ for (const formatInfo of cssStringFormats) {
2119
+ if (formatInfo.re.test(v)) {
2120
+ return formatInfo;
2121
+ }
2122
+ }
2123
+ return undefined;
2124
+ }
2125
+
2126
+ function guessFormat(v) {
2127
+ switch (typeof v) {
2128
+ case 'number':
2129
+ console.warn('can not reliably guess format based on a number. You should pass in a format like {format: "uint32-rgb"} or {format: "uint32-rgb"}');
2130
+ return v <= 0xFFFFFF ? 'uint32-rgb' : 'uint32-rgba';
2131
+ case 'string': {
2132
+ const formatInfo = guessStringColorFormat(v.trim());
2133
+ if (formatInfo) {
2134
+ return formatInfo.format;
2135
+ }
2136
+ break;
2137
+ }
2138
+ case 'object':
2139
+ if (v instanceof Uint8Array || v instanceof Uint8ClampedArray) {
2140
+ if (v.length === 3) {
2141
+ return 'uint8-rgb';
2142
+ } else if (v.length === 4) {
2143
+ return 'uint8-rgba';
2144
+ }
2145
+ } else if (v instanceof Float32Array) {
2146
+ if (v.length === 3) {
2147
+ return 'float-rgb';
2148
+ } else if (v.length === 4) {
2149
+ return 'float-rgba';
2150
+ }
2151
+ } else if (Array.isArray(v)) {
2152
+ if (v.length === 3) {
2153
+ return 'float-rgb';
2154
+ } else if (v.length === 4) {
2155
+ return 'float-rgba';
2156
+ }
2157
+ } else {
2158
+ if ('r' in v && 'g' in v && 'b' in v) {
2159
+ if ('a' in v) {
2160
+ return 'object-rgba';
2161
+ } else {
2162
+ return 'object-rgb';
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+ throw new Error(`unknown color format: ${v}`);
2168
+ }
2169
+
2170
+ function fixHex6(v) {
2171
+ return v.trim(v);
2172
+ //const formatInfo = guessStringColorFormat(v.trim());
2173
+ //const fix = formatInfo ? formatInfo.fix : v => v;
2174
+ //return fix(v.trim());
2175
+ }
2176
+
2177
+ function fixHex8(v) {
2178
+ return v.trim(v);
2179
+ //const formatInfo = guessStringColorFormat(v.trim());
2180
+ //const fix = formatInfo ? formatInfo.fix : v => v;
2181
+ //return fix(v.trim());
2182
+ }
2183
+
2184
+ function hex6ToHex3(hex6) {
2185
+ return (hex6[1] === hex6[2] &&
2186
+ hex6[3] === hex6[4] &&
2187
+ hex6[5] === hex6[6])
2188
+ ? `#${hex6[1]}${hex6[3]}${hex6[5]}`
2189
+ : hex6;
2190
+ }
2191
+
2192
+ const hex3RE = /^(#|)([0-9a-f]{3})$/i;
2193
+ function hex3ToHex6(hex3) {
2194
+ const m = hex3RE.exec(hex3);
2195
+ if (m) {
2196
+ const [, , m2] = m;
2197
+ return `#${hex3DigitTo6Digit(m2)}`;
2198
+ }
2199
+ return hex3;
2200
+ }
2201
+
2202
+ function fixHex3(v) {
2203
+ return hex6ToHex3(fixHex6(v));
2204
+ }
2205
+
2206
+ const strToRGBObject = (s) => {
2207
+ try {
2208
+ const json = s.replace(/([a-z])/g, '"$1"');
2209
+ const rgb = JSON.parse(json);
2210
+ if (Number.isNaN(rgb.r) || Number.isNaN(rgb.g) || Number.isNaN(rgb.b)) {
2211
+ throw new Error('not {r, g, b}');
2212
+ }
2213
+ return [true, rgb];
2214
+ } catch (e) {
2215
+ return [false];
2216
+ }
2217
+ };
2218
+
2219
+ const strToRGBAObject = (s) => {
2220
+ try {
2221
+ const json = s.replace(/([a-z])/g, '"$1"');
2222
+ const rgba = JSON.parse(json);
2223
+ if (Number.isNaN(rgba.r) || Number.isNaN(rgba.g) || Number.isNaN(rgba.b) || Number.isNaN(rgba.a)) {
2224
+ throw new Error('not {r, g, b, a}');
2225
+ }
2226
+ return [true, rgba];
2227
+ } catch (e) {
2228
+ return [false];
2229
+ }
2230
+ };
2231
+
2232
+ const strToCssRGB = s => {
2233
+ const m = cssRGBRegex.exec(s);
2234
+ if (!m) {
2235
+ return [false];
2236
+ }
2237
+ const v = [m[1], m[2], m[3]].map(v => parseInt(v));
2238
+ const outOfRange = v.find(v => v > 255);
2239
+ return [!outOfRange, `rgb(${v.join(', ')})`];
2240
+ };
2241
+
2242
+ const strToCssRGBA = s => {
2243
+ const m = cssRGBARegex.exec(s);
2244
+ if (!m) {
2245
+ return [false];
2246
+ }
2247
+ const v = [m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? parseFloat(v) : parseInt(v));
2248
+ const outOfRange = v.find(v => v > 255);
2249
+ return [!outOfRange, `rgba(${v.join(', ')})`];
2250
+ };
2251
+
2252
+ const strToCssHSL = s => {
2253
+ const m = cssHSLRegex.exec(s);
2254
+ if (!m) {
2255
+ return [false];
2256
+ }
2257
+ const v = [m[1], m[2], m[3]].map(v => parseFloat(v));
2258
+ const outOfRange = v.find(v => Number.isNaN(v));
2259
+ return [!outOfRange, `hsl(${v[0]}, ${v[1]}%, ${v[2]}%)`];
2260
+ };
2261
+
2262
+ const strToCssHSLA = s => {
2263
+ const m = cssHSLARegex.exec(s);
2264
+ if (!m) {
2265
+ return [false];
2266
+ }
2267
+ const v = [m[1], m[2], m[3], m[4]].map(v => parseFloat(v));
2268
+ const outOfRange = v.find(v => Number.isNaN(v));
2269
+ return [!outOfRange, `hsl(${v[0]} ${v[1]}% ${v[2]}% / ${v[3]})`];
2270
+ };
2271
+
2272
+ const rgbObjectToStr = rgb => {
2273
+ return `{r:${f3(rgb.r)}, g:${f3(rgb.g)}, b:${f3(rgb.b)}}`;
2274
+ };
2275
+ const rgbaObjectToStr = rgba => {
2276
+ return `{r:${f3(rgba.r)}, g:${f3(rgba.g)}, b:${f3(rgba.b)}}, a:${f3(rgba.a)}}`;
2277
+ };
2278
+
2279
+ const strTo3IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/;
2280
+ const strTo3Ints = s => {
2281
+ const m = strTo3IntsRE.exec(s);
2282
+ if (!m) {
2283
+ return [false];
2284
+ }
2285
+ const v = [m[1], m[2], m[3]].map(v => parseInt(v));
2286
+ const outOfRange = v.find(v => v > 255);
2287
+ return [!outOfRange, v];
2288
+ };
2289
+
2290
+ const strTo4IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/;
2291
+ const strTo4Ints = s => {
2292
+ const m = strTo4IntsRE.exec(s);
2293
+ if (!m) {
2294
+ return [false];
2295
+ }
2296
+ const v = [m[1], m[2], m[3], m[4]].map(v => parseInt(v));
2297
+ const outOfRange = v.find(v => v > 255);
2298
+ return [!outOfRange, v];
2299
+ };
2300
+
2301
+ const strTo3Floats = s => {
2302
+ const numbers = s.split(',').map(s => s.trim());
2303
+ const v = numbers.map(v => parseFloat(v));
2304
+ if (v.length !== 3) {
2305
+ return [false];
2306
+ }
2307
+ // Note: using isNaN not Number.isNaN
2308
+ const badNdx = numbers.findIndex(v => isNaN(v));
2309
+ return [badNdx < 0, v.map(v => f3(v))];
2310
+ };
2311
+
2312
+ const strTo4Floats = s => {
2313
+ const numbers = s.split(',').map(s => s.trim());
2314
+ const v = numbers.map(v => parseFloat(v));
2315
+ if (v.length !== 4) {
2316
+ return [false];
2317
+ }
2318
+ // Note: using isNaN not Number.isNaN
2319
+ const badNdx = numbers.findIndex(v => isNaN(v));
2320
+ return [badNdx < 0, v.map(v => f3(v))];
2321
+ };
2322
+
2323
+ const strToUint32RGBRegex = /^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i;
2324
+ const strToUint32RGB = s => {
2325
+ const m = strToUint32RGBRegex.exec(s);
2326
+ if (!m) {
2327
+ return [false];
2328
+ }
2329
+ return [true, parseInt(m[1], 16)];
2330
+ };
2331
+
2332
+ const strToUint32RGBARegex = /^\s*(?:0x){0,1}([0-9a-z]{1,8})\s*$/i;
2333
+ const strToUint32RGBA = s => {
2334
+ const m = strToUint32RGBARegex.exec(s);
2335
+ if (!m) {
2336
+ return [false];
2337
+ }
2338
+ return [true, parseInt(m[1], 16)];
2339
+ };
2340
+
2341
+ const hex6RE = /^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i;
2342
+ const hexNoHash6RE = /^\s*[a-f0-9]{6}\s*$/i;
2343
+ const hex8RE = /^\s*#[a-f0-9]{8}\s*$/i;
2344
+ const hexNoHash8RE = /^\s*[a-f0-9]{8}\s*$/i;
2345
+
2346
+ // For each format converter
2347
+ //
2348
+ // fromHex/toHex convert from/to '#RRGGBB'
2349
+ //
2350
+ // fromHex converts from the string '#RRBBGG' to the format
2351
+ // (eg: for uint32-rgb, '#123456' becomes 0x123456)
2352
+ //
2353
+ // toHex converts from the format to '#RRGGBB'
2354
+ // (eg: for uint8-rgb, [16, 33, 50] becomes '#102132')
2355
+ //
2356
+ //
2357
+ // fromStr/toStr convert from/to what's in the input[type=text] element
2358
+ //
2359
+ // toStr converts from the format to its string representation
2360
+ // (eg, for object-rgb, {r: 1, g: 0.5, b:0} becomes "{r: 1, g: 0.5, b:0}")
2361
+ // ^object ^string
2362
+ //
2363
+ // fromStr converts its string representation to its format
2364
+ // (eg, for object-rgb) "{r: 1, g: 0.5, b:0}" becomes {r: 1, g: 0.5, b:0})
2365
+ // ^string ^object
2366
+ // fromString returns an array which is [valid, v]
2367
+ // where valid is true if the string was a valid and v is the converted
2368
+ // format if v is true.
2369
+ //
2370
+ // Note: toStr should convert to "ideal" form (whatever that is).
2371
+ // (eg, for css-rgb
2372
+ // "{ r: 0.10000, g: 001, b: 0}" becomes "{r: 0.1, g: 1, b: 0}"
2373
+ // notice that css-rgb is a string to a string
2374
+ // )
2375
+ const colorFormatConverters = {
2376
+ 'hex6': {
2377
+ color: {
2378
+ from: v => [true, v],
2379
+ to: fixHex6,
2380
+ },
2381
+ text: {
2382
+ from: v => [hex6RE.test(v), v.trim()],
2383
+ to: v => v,
2384
+ },
2385
+ },
2386
+ 'hex8': {
2387
+ color: {
2388
+ from: v => [true, v],
2389
+ to: fixHex8,
2390
+ },
2391
+ text: {
2392
+ from: v => [hex8RE.test(v), v.trim()],
2393
+ to: v => v,
2394
+ },
2395
+ },
2396
+ 'hex3': {
2397
+ color: {
2398
+ from: v => [true, fixHex3(v)],
2399
+ to: hex3ToHex6,
2400
+ },
2401
+ text: {
2402
+ from: v => [hex6RE.test(v), hex6ToHex3(v.trim())],
2403
+ to: v => v,
2404
+ },
2405
+ },
2406
+ 'hex6-no-hash': {
2407
+ color: {
2408
+ from: v => [true, v.substring(1)],
2409
+ to: v => `#${fixHex6(v)}`,
2410
+ },
2411
+ text: {
2412
+ from: v => [hexNoHash6RE.test(v), v.trim()],
2413
+ to: v => v,
2414
+ },
2415
+ },
2416
+ 'hex8-no-hash': {
2417
+ color: {
2418
+ from: v => [true, v.substring(1)],
2419
+ to: v => `#${fixHex8(v)}`,
2420
+ },
2421
+ text: {
2422
+ from: v => [hexNoHash8RE.test(v), v.trim()],
2423
+ to: v => v,
2424
+ },
2425
+ },
2426
+ 'hex3-no-hash': {
2427
+ color: {
2428
+ from: v => [true, fixHex3(v).substring(1)],
2429
+ to: hex3ToHex6,
2430
+ },
2431
+ text: {
2432
+ from: v => [hexNoHash6RE.test(v), hex6ToHex3(v.trim())],
2433
+ to: v => v,
2434
+ },
2435
+ },
2436
+ 'uint32-rgb': {
2437
+ color: {
2438
+ from: v => [true, hexToUint32RGB(v)],
2439
+ to: uint32RGBToHex,
2440
+ },
2441
+ text: {
2442
+ from: v => strToUint32RGB(v),
2443
+ to: v => `0x${v.toString(16).padStart(6, '0')}`,
2444
+ },
2445
+ },
2446
+ 'uint32-rgba': {
2447
+ color: {
2448
+ from: v => [true, hexToUint32RGBA(v)],
2449
+ to: uint32RGBAToHex,
2450
+ },
2451
+ text: {
2452
+ from: v => strToUint32RGBA(v),
2453
+ to: v => `0x${v.toString(16).padStart(8, '0')}`,
2454
+ },
2455
+ },
2456
+ 'uint8-rgb': {
2457
+ color: {
2458
+ from: v => [true, hexToUint8RGB(v)],
2459
+ to: uint8RGBToHex,
2460
+ },
2461
+ text: {
2462
+ from: strTo3Ints,
2463
+ to: v => v.join(', '),
2464
+ },
2465
+ },
2466
+ 'uint8-rgba': {
2467
+ color: {
2468
+ from: v => [true, hexToUint8RGBA(v)],
2469
+ to: uint8RGBAToHex,
2470
+ },
2471
+ text: {
2472
+ from: strTo4Ints,
2473
+ to: v => v.join(', '),
2474
+ },
2475
+ },
2476
+ 'float-rgb': {
2477
+ color: {
2478
+ from: v => [true, hexToFloatRGB(v)],
2479
+ to: floatRGBToHex,
2480
+ },
2481
+ text: {
2482
+ from: strTo3Floats,
2483
+ // need Array.from because map of Float32Array makes a Float32Array
2484
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
2485
+ },
2486
+ },
2487
+ 'float-rgba': {
2488
+ color: {
2489
+ from: v => [true, hexToFloatRGBA(v)],
2490
+ to: floatRGBAToHex,
2491
+ },
2492
+ text: {
2493
+ from: strTo4Floats,
2494
+ // need Array.from because map of Float32Array makes a Float32Array
2495
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
2496
+ },
2497
+ },
2498
+ 'float-hsv': {
2499
+ color: {
2500
+ from: v => [true, rgbFloatToHSV01(hexToFloatRGB(v))],
2501
+ to: v => hsv01ToRGBFloat(floatRGBToHex(v)),
2502
+ },
2503
+ text: {
2504
+ from: strTo3Floats,
2505
+ // need Array.from because map of Float32Array makes a Float32Array
2506
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
2507
+ },
2508
+ },
2509
+ 'float-hsva': {
2510
+ color: {
2511
+ from: v => [true, rgbaFloatToHSVA01(hexToFloatRGB(v))],
2512
+ to: v => hsva01ToRGBAFloat(floatRGBToHex(v)),
2513
+ },
2514
+ text: {
2515
+ from: strTo4Floats,
2516
+ // need Array.from because map of Float32Array makes a Float32Array
2517
+ to: v => Array.from(v).map(v => f3(v)).join(', '),
2518
+ },
2519
+ },
2520
+ //'float-hsl': {
2521
+ // color: {
2522
+ // from: v => [true, rgbFloatToHsl01(hexToFloatRGB(v))],
2523
+ // to: v => hsl01ToRGBFloat(floatRGBToHex(v)),
2524
+ // },
2525
+ // text: {
2526
+ // from: strTo3Floats,
2527
+ // // need Array.from because map of Float32Array makes a Float32Array
2528
+ // to: v => Array.from(v).map(v => f3(v)).join(', '),
2529
+ // },
2530
+ //},
2531
+ //'float-hsla': {
2532
+ // color: {
2533
+ // from: v => [true, hexToFloatRGBA(v)],
2534
+ // to: floatRGBAToHex,
2535
+ // },
2536
+ // text: {
2537
+ // from: strTo4Floats,
2538
+ // // need Array.from because map of Float32Array makes a Float32Array
2539
+ // to: v => Array.from(v).map(v => f3(v)).join(', '),
2540
+ // },
2541
+ //},
2542
+ 'object-rgb': {
2543
+ color: {
2544
+ from: v => [true, hexToObjectRGB(v)],
2545
+ to: objectRGBToHex,
2546
+ },
2547
+ text: {
2548
+ from: strToRGBObject,
2549
+ to: rgbObjectToStr,
2550
+ },
2551
+ },
2552
+ 'object-rgba': {
2553
+ color: {
2554
+ from: v => [true, hexToObjectRGBA(v)],
2555
+ to: objectRGBAToHex,
2556
+ },
2557
+ text: {
2558
+ from: strToRGBAObject,
2559
+ to: rgbaObjectToStr,
2560
+ },
2561
+ },
2562
+ 'css-rgb': {
2563
+ color: {
2564
+ from: v => [true, hexToCssRGB(v)],
2565
+ to: cssRGBToHex,
2566
+ },
2567
+ text: {
2568
+ from: strToCssRGB,
2569
+ to: v => strToCssRGB(v)[1],
2570
+ },
2571
+ },
2572
+ 'css-rgba': {
2573
+ color: {
2574
+ from: v => [true, hexToCssRGBA(v)],
2575
+ to: cssRGBAToHex,
2576
+ },
2577
+ text: {
2578
+ from: strToCssRGBA,
2579
+ to: v => strToCssRGBA(v)[1],
2580
+ },
2581
+ },
2582
+ 'css-hsl': {
2583
+ color: {
2584
+ from: v => [true, hexToCssHSL(v)],
2585
+ to: cssHSLToHex,
2586
+ },
2587
+ text: {
2588
+ from: strToCssHSL,
2589
+ to: v => strToCssHSL(v)[1],
2590
+ },
2591
+ },
2592
+ 'css-hsla': {
2593
+ color: {
2594
+ from: v => [true, hexToCssHSLA(v)],
2595
+ to: cssHSLAToHex,
2596
+ },
2597
+ text: {
2598
+ from: strToCssHSLA,
2599
+ to: v => strToCssHSLA(v)[1],
2600
+ },
2601
+ },
2602
+ };
2603
+
2604
+ class ElementView extends View {
2605
+ constructor(tag, className) {
2606
+ super(createElem(tag, {className}));
2607
+ }
2608
+ }
2609
+
2610
+ // TODO: remove this? Should just be user side
2611
+ class Canvas extends LabelController {
2612
+ #canvasElem;
2613
+
2614
+ constructor(name) {
2615
+ super('muigui-canvas', name);
2616
+ this.#canvasElem = this.add(
2617
+ new ElementView('canvas', 'muigui-canvas'),
2618
+ ).domElement;
2619
+ }
2620
+ get canvas() {
2621
+ return this.#canvasElem;
2622
+ }
2623
+ }
2624
+
2625
+ class ColorView extends EditView {
2626
+ #to;
2627
+ #from;
2628
+ #colorElem;
2629
+ #skipUpdate;
2630
+ #options = {
2631
+ converters: identity,
2632
+ };
2633
+
2634
+ constructor(setter, options) {
2635
+ const colorElem = createElem('input', {
2636
+ type: 'color',
2637
+ onInput: () => {
2638
+ const [valid, newV] = this.#from(colorElem.value);
2639
+ if (valid) {
2640
+ this.#skipUpdate = true;
2641
+ setter.setValue(newV);
2642
+ }
2643
+ },
2644
+ onChange: () => {
2645
+ const [valid, newV] = this.#from(colorElem.value);
2646
+ if (valid) {
2647
+ this.#skipUpdate = true;
2648
+ setter.setFinalValue(newV);
2649
+ }
2650
+ },
2651
+ });
2652
+ super(createElem('div', {}, [colorElem]));
2653
+ this.setOptions(options);
2654
+ this.#colorElem = colorElem;
2655
+ }
2656
+ updateDisplay(v) {
2657
+ if (!this.#skipUpdate) {
2658
+ this.#colorElem.value = this.#to(v);
2659
+ }
2660
+ this.#skipUpdate = false;
2661
+ }
2662
+ setOptions(options) {
2663
+ copyExistingProperties(this.#options, options);
2664
+ const {converters: {to, from}} = this.#options;
2665
+ this.#to = to;
2666
+ this.#from = from;
2667
+ return this;
2668
+ }
2669
+ }
2670
+
2671
+ class Color extends ValueController {
2672
+ #colorView;
2673
+ #textView;
2674
+
2675
+ constructor(object, property, options = {}) {
2676
+ super(object, property, 'muigui-color');
2677
+ const format = options.format || guessFormat(this.getValue());
2678
+ const {color, text} = colorFormatConverters[format];
2679
+ this.#colorView = this.add(new ColorView(this, {converters: color}));
2680
+ this.#textView = this.add(new TextView(this, {converters: text}));
2681
+ this.updateDisplay();
2682
+ }
2683
+ setOptions(options) {
2684
+ const {format} = options;
2685
+ if (format) {
2686
+ const {color, text} = colorFormatConverters[format];
2687
+ this.#colorView.setOptions({converters: color});
2688
+ this.#textView.setOptions({converters: text});
2689
+ }
2690
+ super.setOptions(options);
2691
+ return this;
2692
+ }
2693
+ }
2694
+
2695
+ // This feels like it should be something else like
2696
+ // gui.addController({className: 'muigui-divider')};
2697
+ class Divider extends Controller {
2698
+ constructor() {
2699
+ super('muigui-divider');
2700
+ }
2701
+ }
2702
+
2703
+ class Container extends Controller {
2704
+ #controllers;
2705
+ #childDestController;
2706
+
2707
+ constructor(className) {
2708
+ super(className);
2709
+ this.#controllers = [];
2710
+ this.#childDestController = this;
2711
+ }
2712
+ get children() {
2713
+ return this.#controllers; // should we return a copy?
2714
+ }
2715
+ get controllers() {
2716
+ return this.#controllers.filter(c => !(c instanceof Container));
2717
+ }
2718
+ get folders() {
2719
+ return this.#controllers.filter(c => c instanceof Container);
2720
+ }
2721
+ reset(recursive = true) {
2722
+ for (const controller of this.#controllers) {
2723
+ if (!(controller instanceof Container) || recursive) {
2724
+ controller.reset(recursive);
2725
+ }
2726
+ }
2727
+ return this;
2728
+ }
2729
+ updateDisplay() {
2730
+ for (const controller of this.#controllers) {
2731
+ controller.updateDisplay();
2732
+ }
2733
+ return this;
2734
+ }
2735
+ remove(controller) {
2736
+ const ndx = this.#controllers.indexOf(controller);
2737
+ if (ndx >= 0) {
2738
+ const c = this.#controllers.splice(ndx, 1);
2739
+ const c0 = c[0];
2740
+ const elem = c0.domElement;
2741
+ elem.remove();
2742
+ c0.setParent(null);
2743
+ }
2744
+ return this;
2745
+ }
2746
+ #addControllerImpl(controller) {
2747
+ this.domElement.appendChild(controller.domElement);
2748
+ this.#controllers.push(controller);
2749
+ controller.setParent(this);
2750
+ return controller;
2751
+ }
2752
+ addController(controller) {
2753
+ return this.#childDestController.#addControllerImpl(controller);
2754
+ }
2755
+ pushContainer(container) {
2756
+ this.addController(container);
2757
+ this.#childDestController = container;
2758
+ return container;
2759
+ }
2760
+ popContainer() {
2761
+ this.#childDestController = this.#childDestController.parent;
2762
+ return this;
2763
+ }
2764
+ }
2765
+
2766
+ class Folder extends Container {
2767
+ #labelElem;
2768
+
2769
+ constructor(name = 'Controls', className = 'muigui-menu') {
2770
+ super(className);
2771
+ this.#labelElem = createElem('label');
2772
+ this.addElem(createElem('button', {
2773
+ type: 'button',
2774
+ onClick: () => this.toggleOpen(),
2775
+ }, [this.#labelElem]));
2776
+ this.pushContainer(new Container('muigui-open-container'));
2777
+ this.pushContainer(new Container());
2778
+ this.name(name);
2779
+ this.open();
2780
+ }
2781
+ open(open = true) {
2782
+ this.domElement.classList.toggle('muigui-closed', !open);
2783
+ this.domElement.classList.toggle('muigui-open', open);
2784
+ return this;
2785
+ }
2786
+ close() {
2787
+ return this.open(false);
2788
+ }
2789
+ name(name) {
2790
+ this.#labelElem.textContent = name;
2791
+ return this;
2792
+ }
2793
+ title(title) {
2794
+ return this.name(title);
2795
+ }
2796
+ toggleOpen() {
2797
+ this.open(!this.domElement.classList.contains('muigui-open'));
2798
+ return this;
2799
+ }
2800
+ }
2801
+
2802
+ // This feels like it should be something else like
2803
+ // gui.addDividing = new Controller()
2804
+ class Label extends Controller {
2805
+ constructor(text) {
2806
+ super('muigui-label');
2807
+ this.text(text);
2808
+ }
2809
+ text(text) {
2810
+ this.domElement.textContent = text;
2811
+ return this;
2812
+ }
2813
+ }
2814
+
2815
+ function noop$1() {
2816
+ }
2817
+
2818
+ function computeRelativePosition(elem, event, start) {
2819
+ const rect = elem.getBoundingClientRect();
2820
+ const x = event.clientX - rect.left;
2821
+ const y = event.clientY - rect.top;
2822
+ const nx = x / rect.width;
2823
+ const ny = y / rect.height;
2824
+ start = start || [x, y];
2825
+ const dx = x - start[0];
2826
+ const dy = y - start[1];
2827
+ const ndx = dx / rect.width;
2828
+ const ndy = dy / rect.width;
2829
+ return {x, y, nx, ny, dx, dy, ndx, ndy};
2830
+ }
2831
+
2832
+ function addTouchEvents(elem, {onDown = noop$1, onMove = noop$1, onUp = noop$1}) {
2833
+ let start;
2834
+ const pointerMove = function (event) {
2835
+ const e = {
2836
+ type: 'move',
2837
+ ...computeRelativePosition(elem, event, start),
2838
+ };
2839
+ onMove(e);
2840
+ };
2841
+
2842
+ const pointerUp = function (event) {
2843
+ elem.releasePointerCapture(event.pointerId);
2844
+ elem.removeEventListener('pointermove', pointerMove);
2845
+ elem.removeEventListener('pointerup', pointerUp);
2846
+
2847
+ document.body.style.backgroundColor = '';
2848
+
2849
+ onUp('up');
2850
+ };
2851
+
2852
+ const pointerDown = function (event) {
2853
+ elem.addEventListener('pointermove', pointerMove);
2854
+ elem.addEventListener('pointerup', pointerUp);
2855
+ elem.setPointerCapture(event.pointerId);
2856
+
2857
+ const rel = computeRelativePosition(elem, event);
2858
+ start = [rel.x, rel.y];
2859
+ onDown({
2860
+ type: 'down',
2861
+ ...rel,
2862
+ });
2863
+ };
2864
+
2865
+ elem.addEventListener('pointerdown', pointerDown);
2866
+
2867
+ return function () {
2868
+ elem.removeEventListener('pointerdown', pointerDown);
2869
+ };
2870
+ }
2871
+
2872
+ const svg$3 = `
2873
+ <svg class="muigui-checkered-background" tabindex="0" viewBox="0 0 64 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
2874
+ <linearGradient data-src="muigui-color-chooser-light-dark" x1="0" x2="0" y1="0" y2="1">
2875
+ <stop stop-color="rgba(0,0,0,0)" offset="0%"/>
2876
+ <stop stop-color="#000" offset="100%"/>
2877
+ </linearGradient>
2878
+ <linearGradient data-src="muigui-color-chooser-hue">
2879
+ <stop stop-color="hsl(60 0% 100% / 1)" offset="0%"/>
2880
+ <stop stop-color="hsl(60 100% 50% / 1)" offset="100%"/>
2881
+ </linearGradient>
2882
+
2883
+ <rect width="64" height="48" data-target="muigui-color-chooser-hue"/>
2884
+ <rect width="64" height="48" data-target="muigui-color-chooser-light-dark"/>
2885
+ <circle r="4" class="muigui-color-chooser-circle"/>
2886
+ </svg>
2887
+ <svg tabindex="0" viewBox="0 0 64 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
2888
+ <linearGradient data-src="muigui-color-chooser-hues" x1="0" x2="1" y1="0" y2="0">
2889
+ <stop stop-color="hsl(0,100%,50%)" offset="0%"/>
2890
+ <stop stop-color="hsl(60,100%,50%)" offset="16.666%"/>
2891
+ <stop stop-color="hsl(120,100%,50%)" offset="33.333%"/>
2892
+ <stop stop-color="hsl(180,100%,50%)" offset="50%"/>
2893
+ <stop stop-color="hsl(240,100%,50%)" offset="66.666%"/>
2894
+ <stop stop-color="hsl(300,100%,50%)" offset="83.333%"/>
2895
+ <stop stop-color="hsl(360,100%,50%)" offset="100%"/>
2896
+ </linearGradient>
2897
+ <rect y="1" width="64" height="4" data-target="muigui-color-chooser-hues"/>
2898
+ <g class="muigui-color-chooser-hue-cursor">
2899
+ <rect x="-3" width="6" height="6" />
2900
+ </g>
2901
+ </svg>
2902
+ <svg class="muigui-checkered-background" tabindex="0" viewBox="0 0 64 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
2903
+ <linearGradient data-src="muigui-color-chooser-alpha" x1="0" x2="1" y1="0" y2="0">
2904
+ <stop stop-color="hsla(0,100%,100%,0)" offset="0%"/>
2905
+ <stop stop-color="hsla(0,100%,100%,1)" offset="100%"/>
2906
+ </linearGradient>
2907
+ <rect y="1" width="64" height="4" data-target="muigui-color-chooser-alpha"/>
2908
+ <g class="muigui-color-chooser-alpha-cursor">
2909
+ <rect x="-3" width="6" height="6" />
2910
+ </g>
2911
+ </svg>
2912
+ `;
2913
+
2914
+ function connectFillTargets(elem) {
2915
+ elem.querySelectorAll('[data-src]').forEach(srcElem => {
2916
+ const id = getNewId();
2917
+ srcElem.id = id;
2918
+ elem.querySelectorAll(`[data-target=${srcElem.dataset.src}]`).forEach(targetElem => {
2919
+ targetElem.setAttribute('fill', `url(#${id})`);
2920
+ });
2921
+ });
2922
+ return elem;
2923
+ }
2924
+
2925
+ // Was originally going to make alpha an option. Issue is
2926
+ // hard coded conversions?
2927
+ class ColorChooserView extends EditView {
2928
+ #to;
2929
+ #from;
2930
+ #satLevelElem;
2931
+ #circleElem;
2932
+ #hueUIElem;
2933
+ #hueElem;
2934
+ #hueCursorElem;
2935
+ #alphaUIElem;
2936
+ #alphaElem;
2937
+ #alphaCursorElem;
2938
+ #hsva;
2939
+ #skipHueUpdate;
2940
+ #skipSatLevelUpdate;
2941
+ #skipAlphaUpdate;
2942
+ #options = {
2943
+ converters: identity,
2944
+ alpha: false,
2945
+ };
2946
+ #convertInternalToHex;
2947
+ #convertHexToInternal;
2948
+
2949
+ constructor(setter, options) {
2950
+ super(createElem('div', {
2951
+ innerHTML: svg$3,
2952
+ className: 'muigui-no-scroll',
2953
+ }));
2954
+ this.#satLevelElem = this.domElement.children[0];
2955
+ this.#hueUIElem = this.domElement.children[1];
2956
+ this.#alphaUIElem = this.domElement.children[2];
2957
+ connectFillTargets(this.#satLevelElem);
2958
+ connectFillTargets(this.#hueUIElem);
2959
+ connectFillTargets(this.#alphaUIElem);
2960
+ this.#circleElem = this.$('.muigui-color-chooser-circle');
2961
+ this.#hueElem = this.$('[data-src=muigui-color-chooser-hue]');
2962
+ this.#hueCursorElem = this.$('.muigui-color-chooser-hue-cursor');
2963
+ this.#alphaElem = this.$('[data-src=muigui-color-chooser-alpha]');
2964
+ this.#alphaCursorElem = this.$('.muigui-color-chooser-alpha-cursor');
2965
+
2966
+ const handleSatLevelChange = (e) => {
2967
+ const s = clamp$1(e.nx, 0, 1);
2968
+ const v = clamp$1(e.ny, 0, 1);
2969
+ this.#hsva[1] = s;
2970
+ this.#hsva[2] = (1 - v);
2971
+ this.#skipHueUpdate = true;
2972
+ this.#skipAlphaUpdate = true;
2973
+ const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva));
2974
+ if (valid) {
2975
+ setter.setValue(newV);
2976
+ }
2977
+ };
2978
+
2979
+ const handleHueChange = (e) => {
2980
+ const h = clamp$1(e.nx, 0, 1);
2981
+ this.#hsva[0] = h;
2982
+ this.#skipSatLevelUpdate = true;
2983
+ this.#skipAlphaUpdate = true;
2984
+ const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva));
2985
+ if (valid) {
2986
+ setter.setValue(newV);
2987
+ }
2988
+ };
2989
+
2990
+ const handleAlphaChange = (e) => {
2991
+ const a = clamp$1(e.nx, 0, 1);
2992
+ this.#hsva[3] = a;
2993
+ this.#skipHueUpdate = true;
2994
+ this.#skipSatLevelUpdate = true;
2995
+ const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva));
2996
+ if (valid) {
2997
+ setter.setValue(newV);
2998
+ }
2999
+ };
3000
+
3001
+ addTouchEvents(this.#satLevelElem, {
3002
+ onDown: handleSatLevelChange,
3003
+ onMove: handleSatLevelChange,
3004
+ });
3005
+ addTouchEvents(this.#hueUIElem, {
3006
+ onDown: handleHueChange,
3007
+ onMove: handleHueChange,
3008
+ });
3009
+ addTouchEvents(this.#alphaUIElem, {
3010
+ onDown: handleAlphaChange,
3011
+ onMove: handleAlphaChange,
3012
+ });
3013
+ this.setOptions(options);
3014
+ }
3015
+ updateDisplay(newV) {
3016
+ if (!this.#hsva) {
3017
+ this.#hsva = this.#convertHexToInternal(this.#to(newV));
3018
+ }
3019
+ {
3020
+ const [h, s, v, a = 1] = this.#convertHexToInternal(this.#to(newV));
3021
+ // Don't copy the hue if it was un-computable.
3022
+ if (!this.#skipHueUpdate) {
3023
+ this.#hsva[0] = s > 0.001 && v > 0.001 ? h : this.#hsva[0];
3024
+ }
3025
+ if (!this.#skipSatLevelUpdate) {
3026
+ this.#hsva[1] = s;
3027
+ this.#hsva[2] = v;
3028
+ }
3029
+ if (!this.#skipAlphaUpdate) {
3030
+ this.#hsva[3] = a;
3031
+ }
3032
+ }
3033
+ {
3034
+ const [h, s, v, a] = this.#hsva;
3035
+ const [hue, sat, lum] = rgbaFloatToHsla01(hsva01ToRGBAFloat(this.#hsva));
3036
+
3037
+ if (!this.#skipHueUpdate) {
3038
+ this.#hueCursorElem.setAttribute('transform', `translate(${h * 64}, 0)`);
3039
+ }
3040
+ this.#hueElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} 0% 100% / ${a})`);
3041
+ this.#hueElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} 100% 50% / ${a})`);
3042
+ if (!this.#skipAlphaUpdate) {
3043
+ this.#alphaCursorElem.setAttribute('transform', `translate(${a * 64}, 0)`);
3044
+ }
3045
+ this.#alphaElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 0)`);
3046
+ this.#alphaElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 1)`);
3047
+
3048
+ if (!this.#skipSatLevelUpdate) {
3049
+ this.#circleElem.setAttribute('cx', `${s * 64}`);
3050
+ this.#circleElem.setAttribute('cy', `${(1 - v) * 48}`);
3051
+ }
3052
+ }
3053
+ this.#skipHueUpdate = false;
3054
+ this.#skipSatLevelUpdate = false;
3055
+ this.#skipAlphaUpdate = false;
3056
+ }
3057
+ setOptions(options) {
3058
+ copyExistingProperties(this.#options, options);
3059
+ const {converters: {to, from}, alpha} = this.#options;
3060
+ this.#alphaUIElem.style.display = alpha ? '' : 'none';
3061
+ this.#convertInternalToHex = alpha
3062
+ ? v => floatRGBAToHex(hsva01ToRGBAFloat(v))
3063
+ : v => floatRGBToHex(hsv01ToRGBFloat(v));
3064
+ this.#convertHexToInternal = alpha
3065
+ ? v => rgbaFloatToHSVA01(hexToFloatRGBA(v))
3066
+ : v => rgbFloatToHSV01(hexToFloatRGB(v));
3067
+ this.#to = to;
3068
+ this.#from = from;
3069
+ return this;
3070
+ }
3071
+ }
3072
+
3073
+ /*
3074
+
3075
+ holder = new TabHolder
3076
+ tab = holder.add(new Tab("name"))
3077
+ tab.add(...)
3078
+
3079
+
3080
+ pc = new PopdownController
3081
+ top = pc.add(new Row())
3082
+ top.add(new Button());
3083
+ values = topRow.add(new Div())
3084
+ bottom = pc.add(new Row());
3085
+
3086
+
3087
+
3088
+ pc = new PopdownController
3089
+ pc.addTop
3090
+ pc.addTop
3091
+
3092
+ pc.addBottom
3093
+
3094
+
3095
+ */
3096
+
3097
+ class PopDownController extends ValueController {
3098
+ #top;
3099
+ #valuesView;
3100
+ #checkboxElem;
3101
+ #bottom;
3102
+ #options = {
3103
+ open: false,
3104
+ };
3105
+
3106
+ constructor(object, property, options = {}) {
3107
+ super(object, property, 'muigui-pop-down-controller');
3108
+ /*
3109
+ [ValueView
3110
+ [[B][values]] upper row
3111
+ [[ visual ]] lower row
3112
+ ]
3113
+ */
3114
+ this.#top = this.add(new ElementView('div', 'muigui-pop-down-top'));
3115
+ // this.#top.add(new CheckboxView(makeSetter(this.#options, 'open')));
3116
+ const checkboxElem = this.#top.addElem(createElem('input', {
3117
+ type: 'checkbox',
3118
+ onChange: () => {
3119
+ this.#options.open = checkboxElem.checked;
3120
+ this.updateDisplay();
3121
+ },
3122
+ }));
3123
+ this.#checkboxElem = checkboxElem;
3124
+ this.#valuesView = this.#top.add(new ElementView('div', 'muigui-pop-down-values'));
3125
+ const container = new ElementView('div', 'muigui-pop-down-bottom muigui-open-container');
3126
+ this.#bottom = new ElementView('div');
3127
+ container.add(this.#bottom);
3128
+ this.add(container);
3129
+ this.setOptions(options);
3130
+ }
3131
+ setKnobColor(bgCssColor/*, fgCssColor*/) {
3132
+ if (this.#checkboxElem) {
3133
+ this.#checkboxElem.style = `
3134
+ --range-color: ${bgCssColor};
3135
+ --value-bg-color: ${bgCssColor};
3136
+ `;
3137
+ }
3138
+ }
3139
+ updateDisplay() {
3140
+ super.updateDisplay();
3141
+ const {open} = this.#options;
3142
+ this.domElement.children[1].classList.toggle('muigui-open', open);
3143
+ this.domElement.children[1].classList.toggle('muigui-closed', !open);
3144
+ }
3145
+ setOptions(options) {
3146
+ copyExistingProperties(this.#options, options);
3147
+ super.setOptions(options);
3148
+ this.updateDisplay();
3149
+ }
3150
+ addTop(view) {
3151
+ return this.#valuesView.add(view);
3152
+ }
3153
+ addBottom(view) {
3154
+ return this.#bottom.add(view);
3155
+ }
3156
+ }
3157
+
3158
+ /* eslint-disable no-underscore-dangle */
3159
+
3160
+ class ColorChooser extends PopDownController {
3161
+ #colorView;
3162
+ #textView;
3163
+ #to;
3164
+
3165
+ constructor(object, property, options = {}) {
3166
+ super(object, property, 'muigui-color-chooser');
3167
+ const format = options.format || guessFormat(this.getValue());
3168
+ const {color, text} = colorFormatConverters[format];
3169
+ this.#to = color.to;
3170
+ this.#textView = new TextView(this, {converters: text, alpha: hasAlpha(format)});
3171
+ this.#colorView = new ColorChooserView(this, {converters: color, alpha: hasAlpha(format)});
3172
+ this.addTop(this.#textView);
3173
+ this.addBottom(this.#colorView);
3174
+ // WTF! FIX!
3175
+ this.___setKnobHelper = true;
3176
+ this.updateDisplay();
3177
+ }
3178
+ #setKnobHelper() {
3179
+ if (this.#to) {
3180
+ const hex6Or8 = this.#to(this.getValue());
3181
+ const alpha = hex6Or8.length === 9 ? hex6Or8.substring(7, 9) : 'FF';
3182
+ const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8));
3183
+ hsl[2] = (hsl[2] + 50) % 100;
3184
+ const hex = uint8RGBToHex(hslToRgbUint8(hsl));
3185
+ this.setKnobColor(`${hex6Or8.substring(0, 7)}${alpha}`, hex);
3186
+ }
3187
+ }
3188
+ updateDisplay() {
3189
+ super.updateDisplay();
3190
+ if (this.___setKnobHelper) {
3191
+ this.#setKnobHelper();
3192
+ }
3193
+ }
3194
+ setOptions(options) {
3195
+ super.setOptions(options);
3196
+ return this;
3197
+ }
3198
+ }
3199
+
3200
+ function showCSS(ob) {
3201
+ if (ob.prototype.css) {
3202
+ showCSS(ob.prototype);
3203
+ }
3204
+ }
3205
+
3206
+ class Layout extends View {
3207
+ static css = 'bar';
3208
+ constructor(tag, className) {
3209
+ super(createElem(tag, {className}));
3210
+
3211
+ showCSS(this);
3212
+ }
3213
+ }
3214
+
3215
+ /*
3216
+ class ValueController ?? {
3217
+ const row = this.add(new Row());
3218
+ const label = row.add(new Label());
3219
+ const div = row.add(new Div());
3220
+ const row = div.add(new Row());
3221
+ }
3222
+ */
3223
+
3224
+ /*
3225
+ class MyCustomThing extends ValueController {
3226
+ constructor(object, property, options) {
3227
+ const topRow = this.add(new Row());
3228
+ const bottomRow = this.add(new Row());
3229
+ topRow.add(new NumberView());
3230
+ topRow.add(new NumberView());
3231
+ topRow.add(new NumberView());
3232
+ topRow.add(new NumberView());
3233
+ bottomRow.add(new DirectionView());
3234
+ bottomRow.add(new DirectionView());
3235
+ bottomRow.add(new DirectionView());
3236
+ bottomRow.add(new DirectionView());
3237
+ }
3238
+ }
3239
+ new Grid([
3240
+ [new
3241
+ ]
3242
+ */
3243
+
3244
+ class Column extends Layout {
3245
+ constructor() {
3246
+ super('div', 'muigui-row');
3247
+ }
3248
+ }
3249
+
3250
+ class Frame extends Layout {
3251
+ static css = 'foo';
3252
+ constructor() {
3253
+ super('div', 'muigui-frame');
3254
+ }
3255
+ static get foo() {
3256
+ return 'boo';
3257
+ }
3258
+ }
3259
+
3260
+ class Grid extends Layout {
3261
+ constructor() {
3262
+ super('div', 'muigui-grid');
3263
+ }
3264
+ }
3265
+
3266
+ class Row extends Layout {
3267
+ constructor() {
3268
+ super('div', 'muigui-row');
3269
+ }
3270
+ }
3271
+
3272
+ class GUIFolder extends Folder {
3273
+ add(object, property, ...args) {
3274
+ const controller = object instanceof Controller
3275
+ ? object
3276
+ : createController(object, property, ...args);
3277
+ return this.addController(controller);
3278
+ }
3279
+ addCanvas(name) {
3280
+ return this.addController(new Canvas(name));
3281
+ }
3282
+ addColor(object, property, options = {}) {
3283
+ const value = object[property];
3284
+ if (hasAlpha(options.format || guessFormat(value))) {
3285
+ return this.addController(new ColorChooser(object, property, options));
3286
+ } else {
3287
+ return this.addController(new Color(object, property, options));
3288
+ }
3289
+ }
3290
+ addDivider() {
3291
+ return this.addController(new Divider());
3292
+ }
3293
+ addFolder(name) {
3294
+ return this.addController(new GUIFolder(name));
3295
+ }
3296
+ addLabel(text) {
3297
+ return this.addController(new Label(text));
3298
+ }
3299
+ addButton(name, fn) {
3300
+ const o = {fn};
3301
+ return this.add(o, 'fn').name(name);
3302
+ }
3303
+ }
3304
+
3305
+ class MuiguiElement extends HTMLElement {
3306
+ constructor() {
3307
+ super();
3308
+ this.shadow = this.attachShadow({mode: 'open'});
3309
+ }
3310
+ }
3311
+
3312
+ customElements.define('muigui-element', MuiguiElement);
3313
+
3314
+ const baseStyleSheet = new CSSStyleSheet();
3315
+ //baseStyleSheet.replaceSync(css.default);
3316
+ const userStyleSheet = new CSSStyleSheet();
3317
+
3318
+ function makeStyleSheetUpdater(styleSheet) {
3319
+ let newCss;
3320
+ let newCssPromise;
3321
+
3322
+ function updateStyle() {
3323
+ if (newCss && !newCssPromise) {
3324
+ const s = newCss;
3325
+ newCss = undefined;
3326
+ newCssPromise = styleSheet.replace(s).then(() => {
3327
+ newCssPromise = undefined;
3328
+ updateStyle();
3329
+ });
3330
+ }
3331
+ }
3332
+
3333
+ return function updateStyleSheet(css) {
3334
+ newCss = css;
3335
+ updateStyle();
3336
+ };
3337
+ }
3338
+
3339
+ const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet);
3340
+ const updateUserStyle = makeStyleSheetUpdater(userStyleSheet);
3341
+
3342
+ function getTheme(name) {
3343
+ const { include, css: cssStr } = css.themes[name];
3344
+ return `${include.map(m => css[m]).join('\n')} : css.default}\n${cssStr || ''}`;
3345
+ }
3346
+
3347
+ class GUI extends GUIFolder {
3348
+ static converters = converters;
3349
+ static mapRange = mapRange;
3350
+ static makeRangeConverters = makeRangeConverters;
3351
+ static makeRangeOptions = makeRangeOptions;
3352
+ static makeMinMaxPair = makeMinMaxPair;
3353
+ #localStyleSheet = new CSSStyleSheet();
3354
+
3355
+ constructor(options = {}) {
3356
+ super('Controls', 'muigui-root');
3357
+ if (options instanceof HTMLElement) {
3358
+ options = {parent: options};
3359
+ }
3360
+ const {
3361
+ autoPlace = true,
3362
+ width,
3363
+ title = 'Controls',
3364
+ } = options;
3365
+ let {
3366
+ parent,
3367
+ } = options;
3368
+
3369
+ if (width) {
3370
+ this.domElement.style.width = /^\d+$/.test(width) ? `${width}px` : width;
3371
+ }
3372
+ if (parent === undefined && autoPlace) {
3373
+ parent = document.body;
3374
+ this.domElement.classList.add('muigui-auto-place');
3375
+ }
3376
+ if (parent) {
3377
+ const muiguiElement = createElem('muigui-element');
3378
+ muiguiElement.shadowRoot.adoptedStyleSheets = [this.#localStyleSheet, baseStyleSheet, userStyleSheet];
3379
+ muiguiElement.shadow.appendChild(this.domElement);
3380
+ parent.appendChild(muiguiElement);
3381
+ }
3382
+ if (title) {
3383
+ this.title(title);
3384
+ }
3385
+ this.#localStyleSheet.replaceSync(css.default);
3386
+ this.domElement.classList.add('muigui', 'muigui-colors');
3387
+ }
3388
+ setStyle(css) {
3389
+ this.#localStyleSheet.replace(css);
3390
+ }
3391
+ static setBaseStyles(css) {
3392
+ updateBaseStyle(css);
3393
+ }
3394
+ static getBaseStyleSheet() {
3395
+ return baseStyleSheet;
3396
+ }
3397
+ static setUserStyles(css) {
3398
+ updateUserStyle(css);
3399
+ }
3400
+ static getUserStyleSheet() {
3401
+ return userStyleSheet;
3402
+ }
3403
+ setTheme(name) {
3404
+ this.setStyle(getTheme(name));
3405
+ }
3406
+ static setTheme(name) {
3407
+ GUI.setBaseStyles(getTheme(name));
3408
+ }
3409
+ }
3410
+
3411
+ function noop() {
3412
+ }
3413
+
3414
+ const keyDirections = {
3415
+ ArrowLeft: [-1, 0],
3416
+ ArrowRight: [1, 0],
3417
+ ArrowUp: [0, -1],
3418
+ ArrowDown: [0, 1],
3419
+ };
3420
+
3421
+ // This probably needs to be global
3422
+ function addKeyboardEvents(elem, {onDown = noop, onUp = noop}) {
3423
+ const keyDown = function (event) {
3424
+ const mult = event.shiftKey ? 10 : 1;
3425
+ const [dx, dy] = (keyDirections[event.key] || [0, 0]).map(v => v * mult);
3426
+ const fn = event.type === 'keydown' ? onDown : onUp;
3427
+ fn({
3428
+ type: event.type.substring(3),
3429
+ dx,
3430
+ dy,
3431
+ event,
3432
+ });
3433
+ };
3434
+
3435
+ elem.addEventListener('keydown', keyDown);
3436
+ elem.addEventListener('keyup', keyDown);
3437
+
3438
+ return function () {
3439
+ elem.removeEventListener('keydown', keyDown);
3440
+ elem.removeEventListener('keyup', keyDown);
3441
+ };
3442
+ }
3443
+
3444
+ function assert(truthy, msg = '') {
3445
+ if (!truthy) {
3446
+ throw new Error(msg);
3447
+ }
3448
+ }
3449
+
3450
+ function getEllipsePointForAngle(cx, cy, rx, ry, phi, theta) {
3451
+ const m = Math.abs(rx) * Math.cos(theta);
3452
+ const n = Math.abs(ry) * Math.sin(theta);
3453
+
3454
+ return [
3455
+ cx + Math.cos(phi) * m - Math.sin(phi) * n,
3456
+ cy + Math.sin(phi) * m + Math.cos(phi) * n,
3457
+ ];
3458
+ }
3459
+
3460
+ function getEndpointParameters(cx, cy, rx, ry, phi, theta, dTheta) {
3461
+ const [x1, y1] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta);
3462
+ const [x2, y2] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta + dTheta);
3463
+
3464
+ const fa = Math.abs(dTheta) > Math.PI ? 1 : 0;
3465
+ const fs = dTheta > 0 ? 1 : 0;
3466
+
3467
+ return { x1, y1, x2, y2, fa, fs };
3468
+ }
3469
+
3470
+ function arc(cx, cy, r, start, end) {
3471
+ assert(Math.abs(start - end) <= Math.PI * 2);
3472
+ assert(start >= -Math.PI && start <= Math.PI * 2);
3473
+ assert(start <= end);
3474
+ assert(end >= -Math.PI && end <= Math.PI * 4);
3475
+
3476
+ const { x1, y1, x2, y2, fa, fs } = getEndpointParameters(cx, cy, r, r, 0, start, end - start);
3477
+ return Math.abs(Math.abs(start - end) - Math.PI * 2) > Number.EPSILON
3478
+ ? `M${cx} ${cy} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2} L${cx} ${cy}`
3479
+ : `M${x1} ${y1} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2}`;
3480
+ }
3481
+
3482
+ const svg$2 = `
3483
+ <svg tabindex="0" viewBox="-32 -32 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
3484
+ <!--<circle id="muigui-outline" cx="0" cy="0" r="28.871" class="muigui-direction-circle"/>-->
3485
+ <path id="muigui-range" class="muigui-direction-range" />
3486
+ <g id="muigui-arrow">
3487
+ <g transform="translate(-32, -32)">
3488
+ <path d="M31.029,33.883c-1.058,-0.007 -1.916,-0.868 -1.916,-1.928c0,-1.065 0.864,-1.929 1.929,-1.929c0.204,0 0.401,0.032 0.586,0.091l14.729,-0l0,-2.585l12.166,4.468l-12.166,4.468l0,-2.585l-15.315,0l-0.013,0Z" class="muigui-direction-arrow"/>
3489
+ </g>
3490
+ </g>
3491
+ </svg>
3492
+ `;
3493
+
3494
+ const twoPiMod = v => euclideanModulo$1(v + Math.PI, Math.PI * 2) - Math.PI;
3495
+
3496
+ class DirectionView extends EditView {
3497
+ #arrowElem;
3498
+ #rangeElem;
3499
+ #lastV;
3500
+ #wrap;
3501
+ #options = {
3502
+ step: 1,
3503
+ min: -180,
3504
+ max: 180,
3505
+
3506
+ /*
3507
+ --------
3508
+ / -π/2 \
3509
+ / | \
3510
+ |<- -π * |
3511
+ | * 0 ->| zero is down the positive X axis
3512
+ |<- +π * |
3513
+ \ | /
3514
+ \ π/2 /
3515
+ --------
3516
+ */
3517
+ dirMin: -Math.PI,
3518
+ dirMax: Math.PI,
3519
+ //dirMin: Math.PI * 0.5,
3520
+ //dirMax: Math.PI * 2.5,
3521
+ //dirMin: -Math.PI * 0.75, // test 10:30 to 7:30
3522
+ //dirMax: Math.PI * 0.75,
3523
+ //dirMin: Math.PI * 0.75, // test 7:30 to 10:30
3524
+ //dirMax: -Math.PI * 0.75,
3525
+ //dirMin: -Math.PI * 0.75, // test 10:30 to 1:30
3526
+ //dirMax: -Math.PI * 0.25,
3527
+ //dirMin: Math.PI * 0.25, // test 4:30 to 7:30
3528
+ //dirMax: Math.PI * 0.75,
3529
+ //dirMin: Math.PI * 0.75, // test 4:30 to 7:30
3530
+ //dirMax: Math.PI * 0.25,
3531
+ wrap: undefined,
3532
+ converters: identity,
3533
+ };
3534
+
3535
+ constructor(setter, options = {}) {
3536
+ const wheelHelper = createWheelHelper();
3537
+ super(createElem('div', {
3538
+ className: 'muigui-direction muigui-no-scroll',
3539
+ innerHTML: svg$2,
3540
+ onWheel: e => {
3541
+ e.preventDefault();
3542
+ const {min, max, step} = this.#options;
3543
+ const delta = wheelHelper(e, step);
3544
+ let tempV = this.#lastV + delta;
3545
+ if (this.#wrap) {
3546
+ tempV = euclideanModulo$1(tempV - min, max - min) + min;
3547
+ }
3548
+ const newV = clamp$1(stepify(tempV, v => v, step), min, max);
3549
+ setter.setValue(newV);
3550
+ },
3551
+ }));
3552
+ const handleTouch = (e) => {
3553
+ const {min, max, step, dirMin, dirMax} = this.#options;
3554
+ const nx = e.nx * 2 - 1;
3555
+ const ny = e.ny * 2 - 1;
3556
+ const a = Math.atan2(ny, nx);
3557
+
3558
+ const center = (dirMin + dirMax) / 2;
3559
+
3560
+ const centeredAngle = twoPiMod(a - center);
3561
+ const centeredStart = twoPiMod(dirMin - center);
3562
+ const diff = dirMax - dirMin;
3563
+
3564
+ const n = clamp$1((centeredAngle - centeredStart) / (diff), 0, 1);
3565
+ const newV = stepify(min + (max - min) * n, v => v, step);
3566
+ setter.setValue(newV);
3567
+ };
3568
+ addTouchEvents(this.domElement, {
3569
+ onDown: handleTouch,
3570
+ onMove: handleTouch,
3571
+ });
3572
+ addKeyboardEvents(this.domElement, {
3573
+ onDown: (e) => {
3574
+ const {min, max, step} = this.#options;
3575
+ const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max);
3576
+ setter.setValue(newV);
3577
+ },
3578
+ });
3579
+ this.#arrowElem = this.$('#muigui-arrow');
3580
+ this.#rangeElem = this.$('#muigui-range');
3581
+ this.setOptions(options);
3582
+ }
3583
+ updateDisplay(v) {
3584
+ this.#lastV = v;
3585
+ const {min, max} = this.#options;
3586
+ const n = (v - min) / (max - min);
3587
+ const angle = lerp$1(this.#options.dirMin, this.#options.dirMax, n);
3588
+ this.#arrowElem.style.transform = `rotate(${angle}rad)`;
3589
+ }
3590
+ setOptions(options) {
3591
+ copyExistingProperties(this.#options, options);
3592
+ const {dirMin, dirMax, wrap} = this.#options;
3593
+ this.#wrap = wrap !== undefined
3594
+ ? wrap
3595
+ : Math.abs(dirMin - dirMax) >= Math.PI * 2 - Number.EPSILON;
3596
+ const [min, max] = dirMin < dirMax ? [dirMin, dirMax] : [dirMax , dirMin];
3597
+ this.#rangeElem.setAttribute('d', arc(0, 0, 28.87, min, max));
3598
+ }
3599
+ }
3600
+
3601
+ // deg2rad
3602
+ // where is 0
3603
+ // range (0, 360), (-180, +180), (0,0) Really this is a range
3604
+
3605
+ class Direction extends PopDownController {
3606
+ #options;
3607
+ constructor(object, property, options) {
3608
+ super(object, property, 'muigui-direction');
3609
+ this.#options = options; // FIX
3610
+ this.addTop(new NumberView(this,
3611
+ identity));
3612
+ this.addBottom(new DirectionView(this, options));
3613
+ this.updateDisplay();
3614
+ }
3615
+ }
3616
+
3617
+ class RadioGridView extends EditView {
3618
+ #values;
3619
+
3620
+ constructor(setter, keyValues, cols = 3) {
3621
+ const values = [];
3622
+ const name = makeId();
3623
+ super(createElem('div', {}, keyValues.map(([key, value], ndx) => {
3624
+ values.push(value);
3625
+ return createElem('label', {}, [
3626
+ createElem('input', {
3627
+ type: 'radio',
3628
+ name,
3629
+ value: ndx,
3630
+ onChange: function () {
3631
+ if (this.checked) {
3632
+ setter.setFinalValue(that.#values[this.value]);
3633
+ }
3634
+ },
3635
+ }),
3636
+ createElem('button', {
3637
+ type: 'button',
3638
+ textContent: key,
3639
+ onClick: function () {
3640
+ this.previousElementSibling.click();
3641
+ },
3642
+ }),
3643
+ ]);
3644
+ })));
3645
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
3646
+ const that = this;
3647
+ this.#values = values;
3648
+ this.cols(cols);
3649
+ }
3650
+ updateDisplay(v) {
3651
+ const ndx = this.#values.indexOf(v);
3652
+ for (let i = 0; i < this.domElement.children.length; ++i) {
3653
+ this.domElement.children[i].children[0].checked = i === ndx;
3654
+ }
3655
+ }
3656
+ cols(cols) {
3657
+ this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
3658
+ }
3659
+ }
3660
+
3661
+ class RadioGrid extends ValueController {
3662
+ constructor(object, property, options) {
3663
+ super(object, property, 'muigui-radio-grid');
3664
+ const valueIsNumber = typeof this.getValue() === 'number';
3665
+ const {
3666
+ keyValues: keyValuesInput,
3667
+ cols = 3,
3668
+ } = options;
3669
+ const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber);
3670
+ this.add(new RadioGridView(this, keyValues, cols));
3671
+ this.updateDisplay();
3672
+ }
3673
+ }
3674
+
3675
+ function onResize(elem, callback) {
3676
+ new ResizeObserver(() => {
3677
+ callback({rect: elem.getBoundingClientRect(), elem});
3678
+ }).observe(elem);
3679
+ }
3680
+
3681
+ function onResizeSVGNoScale(elem, hAnchor, vAnchor, callback) {
3682
+ onResize(elem, ({rect}) => {
3683
+ const {width, height} = rect;
3684
+ elem.setAttribute('viewBox', `-${width * hAnchor} -${height * vAnchor} ${width} ${height}`);
3685
+ callback({elem, rect});
3686
+ });
3687
+ }
3688
+
3689
+ function onResizeCanvas(elem, callback) {
3690
+ onResize(elem, ({rect}) => {
3691
+ const {width, height} = rect;
3692
+ elem.width = width;
3693
+ elem.height = height;
3694
+ callback({elem, rect});
3695
+ });
3696
+ }
3697
+
3698
+ const svg$1 = `
3699
+ <svg tabindex="0" viewBox="-32 -32 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
3700
+ <g id="muigui-orientation">
3701
+ <g id="muigui-origin">
3702
+ <g transform="translate(0, 4)">
3703
+ <path id="muigui-ticks" class="muigui-ticks"/>
3704
+ <path id="muigui-thicks" class="muigui-thicks"/>
3705
+ </g>
3706
+ <g transform="translate(0, 14)">
3707
+ <g id="muigui-number-orientation">
3708
+ <g id="muigui-numbers" transform="translate(0, -3)" class="muigui-svg-text"/>
3709
+ </g>
3710
+ </g>
3711
+ </g>
3712
+ <linearGradient id="muigui-bg-to-transparent">
3713
+ <stop stop-color="var(--value-bg-color)" offset="0%"/>
3714
+ <stop stop-color="var(--value-bg-color)" stop-opacity="0" offset="100%"/>
3715
+ </linearGradient>
3716
+ <linearGradient id="muigui-transparent-to-bg">
3717
+ <stop stop-color="var(--value-bg-color)" stop-opacity="0" offset="0%"/>
3718
+ <stop stop-color="var(--value-bg-color)" offset="100%"/>
3719
+ </linearGradient>
3720
+ <!--<circle cx="0" cy="2" r="2" class="muigui-mark"/>-->
3721
+ <!--<rect x="-1" y="0" width="2" height="10" class="muigui-mark"/>-->
3722
+ <path d="M0 4L-2 0L2 0" class="muigui-mark"/>
3723
+ </g>
3724
+ <rect id="muigui-left-grad" x="0" y="0" width="20" height="20" fill="url(#muigui-bg-to-transparent)"/>
3725
+ <rect id="muigui-right-grad" x="48" y="0" width="20" height="20" fill="url(#muigui-transparent-to-bg)"/>
3726
+ </svg>
3727
+ `;
3728
+
3729
+ function createSVGTicks(start, end, step, min, max, height) {
3730
+ const p = [];
3731
+ if (start < min) {
3732
+ start += stepify(min - start, v => v, step);
3733
+ }
3734
+ end = Math.min(end, max);
3735
+ for (let i = start; i <= end; i += step) {
3736
+ p.push(`M${i} 0 l0 ${height}`);
3737
+ }
3738
+ return p.join(' ');
3739
+ }
3740
+
3741
+ function createSVGNumbers(start, end, unitSize, unit, minusSize, min, max, labelFn) {
3742
+ const texts = [];
3743
+ if (start < min) {
3744
+ start += stepify(min - start, v => v, unitSize);
3745
+ }
3746
+ end = Math.min(end, max);
3747
+ const digits = Math.max(0, -Math.log10(unit));
3748
+ const f = v => labelFn(v.toFixed(digits));
3749
+ for (let i = start; i <= end; i += unitSize) {
3750
+ texts.push(`<text text-anchor="middle" dominant-baseline="hanging" x="${i >= 0 ? i : (i - minusSize / 2) }" y="0">${f(i / unitSize * unit)}</text>`);
3751
+ }
3752
+ return texts.join('\n');
3753
+ }
3754
+
3755
+ function computeSizeOfMinus(elem) {
3756
+ const oldHTML = elem.innerHTML;
3757
+ elem.innerHTML = '<text>- </text>';
3758
+ const text = elem.querySelector('text');
3759
+ const size = text.getComputedTextLength();
3760
+ elem.innerHTML = oldHTML;
3761
+ return size;
3762
+ }
3763
+
3764
+ class SliderView extends EditView {
3765
+ #svgElem;
3766
+ #originElem;
3767
+ #ticksElem;
3768
+ #thicksElem;
3769
+ #numbersElem;
3770
+ #leftGradElem;
3771
+ #rightGradElem;
3772
+ #width;
3773
+ #height;
3774
+ #lastV;
3775
+ #minusSize;
3776
+ #options = {
3777
+ min: -100,
3778
+ max: 100,
3779
+ step: 1,
3780
+ unit: 10,
3781
+ unitSize: 10,
3782
+ ticksPerUnit: 5,
3783
+ labelFn: v => v,
3784
+ tickHeight: 1,
3785
+ limits: true,
3786
+ thicksColor: undefined,
3787
+ orientation: undefined,
3788
+ };
3789
+
3790
+ constructor(setter, options) {
3791
+ const wheelHelper = createWheelHelper();
3792
+ super(createElem('div', {
3793
+ innerHTML: svg$1,
3794
+ className: 'muigui-no-v-scroll',
3795
+ onWheel: e => {
3796
+ e.preventDefault();
3797
+ const {min, max, step} = this.#options;
3798
+ const delta = wheelHelper(e, step);
3799
+ const newV = clamp$1(stepify(this.#lastV + delta, v => v, step), min, max);
3800
+ setter.setValue(newV);
3801
+ },
3802
+ }));
3803
+ this.#svgElem = this.$('svg');
3804
+ this.#originElem = this.$('#muigui-origin');
3805
+ this.#ticksElem = this.$('#muigui-ticks');
3806
+ this.#thicksElem = this.$('#muigui-thicks');
3807
+ this.#numbersElem = this.$('#muigui-numbers');
3808
+ this.#leftGradElem = this.$('#muigui-left-grad');
3809
+ this.#rightGradElem = this.$('#muigui-right-grad');
3810
+ this.setOptions(options);
3811
+ let startV;
3812
+ addTouchEvents(this.domElement, {
3813
+ onDown: () => {
3814
+ startV = this.#lastV;
3815
+ },
3816
+ onMove: (e) => {
3817
+ const {min, max, unitSize, unit, step} = this.#options;
3818
+ const newV = clamp$1(stepify(startV - e.dx / unitSize * unit, v => v, step), min, max);
3819
+ setter.setValue(newV);
3820
+ },
3821
+ });
3822
+ addKeyboardEvents(this.domElement, {
3823
+ onDown: (e) => {
3824
+ const {min, max, step} = this.#options;
3825
+ const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max);
3826
+ setter.setValue(newV);
3827
+ },
3828
+ });
3829
+ onResizeSVGNoScale(this.#svgElem, 0.5, 0, ({rect: {width}}) => {
3830
+ this.#leftGradElem.setAttribute('x', -width / 2);
3831
+ this.#rightGradElem.setAttribute('x', width / 2 - 20);
3832
+ this.#minusSize = computeSizeOfMinus(this.#numbersElem);
3833
+ this.#width = width;
3834
+ this.#updateSlider();
3835
+ });
3836
+ }
3837
+ // |--------V--------|
3838
+ // . . | . . . | . . . |
3839
+ //
3840
+ #updateSlider() {
3841
+ // There's no size if ResizeObserver has not fired yet.
3842
+ if (!this.#width || this.#lastV === undefined) {
3843
+ return;
3844
+ }
3845
+ const {
3846
+ labelFn,
3847
+ limits,
3848
+ min,
3849
+ max,
3850
+ orientation,
3851
+ tickHeight,
3852
+ ticksPerUnit,
3853
+ unit,
3854
+ unitSize,
3855
+ thicksColor,
3856
+ } = this.#options;
3857
+ const unitsAcross = Math.ceil(this.#width / unitSize);
3858
+ const center = this.#lastV;
3859
+ const centerUnitSpace = center / unit;
3860
+ const startUnitSpace = Math.round(centerUnitSpace - unitsAcross);
3861
+ const endUnitSpace = startUnitSpace + unitsAcross * 2;
3862
+ const start = startUnitSpace * unitSize;
3863
+ const end = endUnitSpace * unitSize;
3864
+ const minUnitSpace = limits ? min * unitSize / unit : start;
3865
+ const maxUnitSpace = limits ? max * unitSize / unit : end;
3866
+ const height = labelFn(1) === '' ? 10 : 5;
3867
+ if (ticksPerUnit > 1) {
3868
+ this.#ticksElem.setAttribute('d', createSVGTicks(start, end, unitSize / ticksPerUnit, minUnitSpace, maxUnitSpace, height * tickHeight));
3869
+ }
3870
+ this.#thicksElem.style.stroke = thicksColor; //setAttribute('stroke', thicksColor);
3871
+ this.#thicksElem.setAttribute('d', createSVGTicks(start, end, unitSize, minUnitSpace, maxUnitSpace, height));
3872
+ this.#numbersElem.innerHTML = createSVGNumbers(start, end, unitSize, unit, this.#minusSize, minUnitSpace, maxUnitSpace, labelFn);
3873
+ this.#originElem.setAttribute('transform', `translate(${-this.#lastV * unitSize / unit} 0)`);
3874
+ this.#svgElem.classList.toggle('muigui-slider-up', orientation === 'up');
3875
+ }
3876
+ updateDisplay(v) {
3877
+ this.#lastV = v;
3878
+ this.#updateSlider();
3879
+ }
3880
+ setOptions(options) {
3881
+ copyExistingProperties(this.#options, options);
3882
+ return this;
3883
+ }
3884
+ }
3885
+
3886
+ class Slider extends ValueController {
3887
+ constructor(object, property, options = {}) {
3888
+ super(object, property, 'muigui-slider');
3889
+ this.add(new SliderView(this, options));
3890
+ this.add(new NumberView(this, options));
3891
+ this.updateDisplay();
3892
+ }
3893
+ }
3894
+
3895
+ const svg = `
3896
+ <svg tabindex="0" viewBox="-32 -32 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
3897
+ <path d="m-3200,0L3200,0M0,-3200L0,3200" class="muigui-vec2-axis"/>
3898
+ <path id="muigui-arrow" d="" class="muigui-vec2-line"/>
3899
+ <g id="muigui-circle" transform="translate(0, 0)">
3900
+ <circle r="3" class="muigui-vec2-axis"/>
3901
+ </g>
3902
+ </svg>
3903
+ `;
3904
+
3905
+ class Vec2View extends EditView {
3906
+ #svgElem;
3907
+ #arrowElem;
3908
+ #circleElem;
3909
+ #lastV = [];
3910
+
3911
+ constructor(setter) {
3912
+ super(createElem('div', {
3913
+ innerHTML: svg,
3914
+ className: 'muigui-no-scroll',
3915
+ }));
3916
+ const onTouch = (e) => {
3917
+ const {width, height} = this.#svgElem.getBoundingClientRect();
3918
+ const nx = e.nx * 2 - 1;
3919
+ const ny = e.ny * 2 - 1;
3920
+ setter.setValue([nx * width * 0.5, ny * height * 0.5]);
3921
+ };
3922
+ addTouchEvents(this.domElement, {
3923
+ onDown: onTouch,
3924
+ onMove: onTouch,
3925
+ });
3926
+ this.#svgElem = this.$('svg');
3927
+ this.#arrowElem = this.$('#muigui-arrow');
3928
+ this.#circleElem = this.$('#muigui-circle');
3929
+ onResizeSVGNoScale(this.#svgElem, 0.5, 0.5, () => this.#updateDisplayImpl);
3930
+ }
3931
+ #updateDisplayImpl() {
3932
+ const [x, y] = this.#lastV;
3933
+ this.#arrowElem.setAttribute('d', `M0,0L${x},${y}`);
3934
+ this.#circleElem.setAttribute('transform', `translate(${x}, ${y})`);
3935
+ }
3936
+ updateDisplay(v) {
3937
+ this.#lastV[0] = v[0];
3938
+ this.#lastV[1] = v[1];
3939
+ this.#updateDisplayImpl();
3940
+ }
3941
+ }
3942
+
3943
+ // TODO: zoom with wheel and pinch?
3944
+ // TODO: grid?
3945
+ // // options
3946
+ // scale:
3947
+ // range: number (both x and y + /)
3948
+ // range: array (min, max)
3949
+ // xRange:
3950
+ // deg/rad/turn
3951
+
3952
+ class Vec2 extends PopDownController {
3953
+ constructor(object, property) {
3954
+ super(object, property, 'muigui-vec2');
3955
+
3956
+ const makeSetter = (ndx) => {
3957
+ return {
3958
+ setValue: (v) => {
3959
+ const newV = this.getValue();
3960
+ newV[ndx] = v;
3961
+ this.setValue(newV);
3962
+ },
3963
+ setFinalValue: (v) => {
3964
+ const newV = this.getValue();
3965
+ newV[ndx] = v;
3966
+ this.setFinalValue(newV);
3967
+ },
3968
+ };
3969
+ };
3970
+
3971
+ this.addTop(new NumberView(makeSetter(0), {
3972
+ converters: {
3973
+ to: v => v[0],
3974
+ from: strToNumber.from,
3975
+ },
3976
+ }));
3977
+ this.addTop(new NumberView(makeSetter(1), {
3978
+ converters: {
3979
+ to: v => v[1],
3980
+ from: strToNumber.from,
3981
+ },
3982
+ }));
3983
+ this.addBottom(new Vec2View(this));
3984
+ this.updateDisplay();
3985
+ }
3986
+ }
3987
+
3988
+ const darkColors = {
3989
+ main: '#ddd',
3990
+ };
3991
+ const lightColors = {
3992
+ main: '#333',
3993
+ };
3994
+
3995
+ const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
3996
+
3997
+ let colors;
3998
+ let isDarkMode;
3999
+
4000
+ function update() {
4001
+ isDarkMode = darkMatcher.matches;
4002
+ colors = isDarkMode ? darkColors : lightColors;
4003
+ }
4004
+ darkMatcher.addEventListener('change', update);
4005
+ update();
4006
+
4007
+ function graph(canvas, data, {
4008
+ min = -1,
4009
+ max = 1,
4010
+ interval = 16,
4011
+ color,
4012
+ }) {
4013
+ const ctx = canvas.getContext('2d');
4014
+
4015
+ function render() {
4016
+ const {width, height} = canvas;
4017
+ ctx.clearRect(0, 0, width, height);
4018
+ ctx.beginPath();
4019
+ const range = max - min;
4020
+ for (let i = 0; i < data.length; ++i) {
4021
+ const x = i * width / data.length;
4022
+ const y = (data[i] - min) * height / range;
4023
+ ctx.lineTo(x, y);
4024
+ }
4025
+ ctx.strokeStyle = color || colors.main;
4026
+ ctx.stroke();
4027
+ }
4028
+ setInterval(render, interval);
4029
+ }
4030
+
4031
+ function monitor(label, object, property, {interval = 200} = {}) {
4032
+ setInterval(() => {
4033
+ label.text(JSON.stringify(object[property], null, 2));
4034
+ }, interval);
4035
+ }
4036
+
4037
+ const helpers = {
4038
+ graph,
4039
+ monitor,
4040
+ };
4041
+
4042
+ export { ColorChooser, Direction, RadioGrid, Range, Select, Slider, TextNumber, Vec2, GUI as default, helpers };
4043
+ //# sourceMappingURL=muigui.module.js.map