muigui 0.0.1 → 0.0.3

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 (72) hide show
  1. package/README.md +72 -7
  2. package/package.json +10 -6
  3. package/src/controllers/Button.js +34 -0
  4. package/src/controllers/Canvas.js +17 -0
  5. package/src/controllers/Checkbox.js +11 -0
  6. package/src/controllers/Color.js +31 -0
  7. package/src/controllers/ColorChooser.js +12 -0
  8. package/src/controllers/Container.js +58 -0
  9. package/src/controllers/Controller.js +138 -0
  10. package/src/controllers/Direction.js +23 -0
  11. package/src/controllers/Divider.js +9 -0
  12. package/src/controllers/Folder.js +37 -0
  13. package/src/controllers/Label.js +14 -0
  14. package/src/controllers/LabelController.js +32 -0
  15. package/src/controllers/PopDownController.js +84 -0
  16. package/src/controllers/RadioGrid.js +17 -0
  17. package/src/controllers/Range.js +11 -0
  18. package/src/controllers/Select.js +14 -0
  19. package/src/controllers/Slider.js +12 -0
  20. package/src/controllers/TabHolder.js +36 -0
  21. package/src/controllers/Text.js +10 -0
  22. package/src/controllers/TextNumber.js +18 -0
  23. package/src/controllers/ValueController.js +107 -0
  24. package/src/controllers/Vec2.js +50 -0
  25. package/src/controllers/create-controller.js +49 -0
  26. package/src/layout/Column.js +7 -0
  27. package/src/layout/Frame.js +11 -0
  28. package/src/layout/Grid.js +7 -0
  29. package/src/layout/Layout.js +47 -0
  30. package/src/layout/Row.js +7 -0
  31. package/src/libs/assert.js +5 -0
  32. package/src/libs/color-utils.js +406 -0
  33. package/src/libs/conversions.js +14 -0
  34. package/src/libs/css-utils.js +3 -0
  35. package/src/libs/elem.js +8 -3
  36. package/src/libs/emitter.js +68 -0
  37. package/src/libs/iterable-array.js +57 -0
  38. package/src/libs/key-values.js +25 -0
  39. package/src/libs/keyboard.js +32 -0
  40. package/src/libs/resize-helpers.js +22 -0
  41. package/src/libs/svg.js +33 -0
  42. package/src/libs/taskrunner.js +56 -0
  43. package/src/libs/touch.js +50 -0
  44. package/src/libs/utils.js +38 -2
  45. package/src/libs/wheel.js +10 -0
  46. package/src/muigui.js +79 -19
  47. package/src/styles/muigui.css.js +641 -0
  48. package/src/umd.js +3 -0
  49. package/src/views/CheckboxView.js +21 -0
  50. package/src/views/ColorChooserView.js +124 -0
  51. package/src/views/ColorView.js +50 -0
  52. package/src/views/DirectionView.js +127 -0
  53. package/src/views/EditView.js +100 -0
  54. package/src/views/ElementView.js +8 -0
  55. package/src/views/GridView.js +15 -0
  56. package/src/views/NumberView.js +67 -0
  57. package/src/views/RadioGridView.js +46 -0
  58. package/src/views/RangeView.js +73 -0
  59. package/src/views/SelectView.js +23 -0
  60. package/src/views/SliderView.js +194 -0
  61. package/src/views/TextView.js +49 -0
  62. package/src/views/ValueView.js +11 -0
  63. package/src/views/Vec2View.js +51 -0
  64. package/src/views/View.js +56 -0
  65. package/src/widgets/checkbox.js +0 -0
  66. package/src/widgets/divider.js +0 -0
  67. package/src/widgets/menu.js +0 -0
  68. package/src/widgets/radio.js +0 -0
  69. package/src/widgets/select.js +0 -1
  70. package/src/widgets/slider.js +0 -41
  71. package/src/widgets/text.js +0 -0
  72. package/src/widgets/widget.js +0 -51
@@ -0,0 +1,124 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import { addTouchEvents } from '../libs/touch.js';
3
+ import { clamp } from '../libs/utils.js';
4
+ import EditView from './EditView.js';
5
+ import {
6
+ hexToUint8RGB,
7
+ hexToFloatRGB,
8
+ hslToRgbUint8,
9
+ hsv01ToRGBFloat,
10
+ rgbFloatToHSV01,
11
+ rgbUint8ToHsl,
12
+ floatRGBToHex,
13
+ uint8RGBToHex,
14
+ } from '../libs/color-utils.js';
15
+
16
+ const svg = `
17
+
18
+ <svg 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;">
19
+ <linearGradient id="muigui-color-chooser-light-dark" x1="0" x2="0" y1="0" y2="1">
20
+ <stop stop-color="rgba(0,0,0,0)" offset="0%"/>
21
+ <stop stop-color="#000" offset="100%"/>
22
+ </linearGradient>
23
+ <linearGradient id="muigui-color-chooser-hue">
24
+ <stop stop-color="hsl(60, 0%, 100%)" offset="0%"/>
25
+ <stop stop-color="hsl(60, 100%, 50%)" offset="100%"/>
26
+ </linearGradient>
27
+
28
+ <rect width="64" height="48" fill="url(#muigui-color-chooser-hue)"/>
29
+ <rect width="64" height="48" fill="url(#muigui-color-chooser-light-dark)"/>
30
+ <circle r="4" class="muigui-color-chooser-circle"/>
31
+ </svg>
32
+ <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;">
33
+ <linearGradient id="muigui-color-chooser-hues" x1="0" x2="1" y1="0" y2="0">
34
+ <stop stop-color="hsl(0,100%,50%)" offset="0%"/>
35
+ <stop stop-color="hsl(60,100%,50%)" offset="16.666%"/>
36
+ <stop stop-color="hsl(120,100%,50%)" offset="33.333%"/>
37
+ <stop stop-color="hsl(180,100%,50%)" offset="50%"/>
38
+ <stop stop-color="hsl(240,100%,50%)" offset="66.666%"/>
39
+ <stop stop-color="hsl(300,100%,50%)" offset="83.333%"/>
40
+ <stop stop-color="hsl(360,100%,50%)" offset="100%"/>
41
+ </linearGradient>
42
+ <rect y="1" width="64" height="4" fill="url('#muigui-color-chooser-hues')"/>
43
+ <g class="muigui-color-chooser-cursor">
44
+ <rect x="-3" width="6" height="6" />
45
+ </g>
46
+ </svg>
47
+ `;
48
+
49
+ export default class ColorChooserView extends EditView {
50
+ #satLevelElem;
51
+ #hueUIElem;
52
+ #circleElem;
53
+ #hueElem;
54
+ #hueCursorElem;
55
+ #hsv;
56
+ #skipHueUpdate;
57
+ #skipSatLevelUpdate;
58
+
59
+ constructor(setter) {
60
+ super(createElem('div', {
61
+ innerHTML: svg,
62
+ }));
63
+ this.#satLevelElem = this.domElement.children[0];
64
+ this.#hueUIElem = this.domElement.children[1];
65
+ this.#circleElem = this.$('.muigui-color-chooser-circle');
66
+ this.#hueElem = this.$('#muigui-color-chooser-hue');
67
+ this.#hueCursorElem = this.$('.muigui-color-chooser-cursor');
68
+
69
+ const handleSatLevelChange = (e) => {
70
+ const s = clamp(e.nx, 0, 1);
71
+ const v = clamp(e.ny, 0, 1);
72
+ this.#hsv[1] = s;
73
+ this.#hsv[2] = (1 - v);
74
+ this.#skipHueUpdate = true;
75
+ setter.setValue(floatRGBToHex(hsv01ToRGBFloat(this.#hsv)));
76
+ };
77
+
78
+ const handleHueChange = (e) => {
79
+ const h = clamp(e.nx, 0, 1);
80
+ this.#hsv[0] = h;
81
+ this.#skipSatLevelUpdate = true;
82
+ setter.setValue(floatRGBToHex(hsv01ToRGBFloat(this.#hsv)));
83
+ };
84
+
85
+ addTouchEvents(this.#satLevelElem, {
86
+ onDown: handleSatLevelChange,
87
+ onMove: handleSatLevelChange,
88
+ });
89
+ addTouchEvents(this.#hueUIElem, {
90
+ onDown: handleHueChange,
91
+ onMove: handleHueChange,
92
+ });
93
+ }
94
+ updateDisplay(newV) {
95
+ if (!this.#hsv) {
96
+ this.#hsv = rgbFloatToHSV01(hexToFloatRGB(newV));
97
+ }
98
+ {
99
+ const [h, s, v] = rgbFloatToHSV01(hexToFloatRGB(newV));
100
+ // Don't copy the hue if it was un-computable.
101
+ if (!this.#skipHueUpdate) {
102
+ this.#hsv[0] = s > 0.001 && v > 0.001 ? h : this.#hsv[0];
103
+ }
104
+ if (!this.#skipSatLevelUpdate) {
105
+ this.#hsv[1] = s;
106
+ this.#hsv[2] = v;
107
+ }
108
+ }
109
+ {
110
+ const [h, s, v] = this.#hsv;
111
+ if (!this.#skipHueUpdate) {
112
+ this.#hueCursorElem.setAttribute('transform', `translate(${h * 64}, 0)`);
113
+ this.#hueElem.children[0].setAttribute('stop-color', `hsl(${h * 360}, 0%, 100%)`);
114
+ this.#hueElem.children[1].setAttribute('stop-color', `hsl(${h * 360}, 100%, 50%)`);
115
+ }
116
+ if (!this.#skipSatLevelUpdate) {
117
+ this.#circleElem.setAttribute('cx', `${s * 64}`);
118
+ this.#circleElem.setAttribute('cy', `${(1 - v) * 48}`);
119
+ }
120
+ }
121
+ this.#skipHueUpdate = false;
122
+ this.#skipSatLevelUpdate = false;
123
+ }
124
+ }
@@ -0,0 +1,50 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import { identity } from '../libs/conversions.js';
3
+ import EditView from './EditView.js';
4
+ import { copyExistingProperties } from '../libs/utils.js';
5
+
6
+ export default class ColorView extends EditView {
7
+ #to;
8
+ #from;
9
+ #colorElem;
10
+ #skipUpdate;
11
+ #options = {
12
+ converters: identity,
13
+ };
14
+
15
+ constructor(setter, options) {
16
+ const colorElem = createElem('input', {
17
+ type: 'color',
18
+ onInput: () => {
19
+ const [valid, newV] = this.#from(colorElem.value);
20
+ if (valid) {
21
+ this.#skipUpdate = true;
22
+ setter.setValue(newV);
23
+ }
24
+ },
25
+ onChange: () => {
26
+ const [valid, newV] = this.#from(colorElem.value);
27
+ if (valid) {
28
+ this.#skipUpdate = true;
29
+ setter.setFinalValue(newV);
30
+ }
31
+ },
32
+ });
33
+ super(createElem('div', {}, [colorElem]));
34
+ this.setOptions(options);
35
+ this.#colorElem = colorElem;
36
+ }
37
+ updateDisplay(v) {
38
+ if (!this.#skipUpdate) {
39
+ this.#colorElem.value = this.#to(v);
40
+ }
41
+ this.#skipUpdate = false;
42
+ }
43
+ setOptions(options) {
44
+ copyExistingProperties(this.#options, options);
45
+ const {converters: {to, from}} = this.#options;
46
+ this.#to = to;
47
+ this.#from = from;
48
+ return this;
49
+ }
50
+ }
@@ -0,0 +1,127 @@
1
+ import { identity } from '../libs/conversions.js';
2
+ import { createElem } from '../libs/elem.js';
3
+ import { addKeyboardEvents } from '../libs/keyboard.js';
4
+ import { arc } from '../libs/svg.js';
5
+ import { addTouchEvents } from '../libs/touch.js';
6
+ import { createWheelHelper } from '../libs/wheel.js';
7
+ import { clamp, copyExistingProperties, euclideanModulo, lerp, stepify } from '../libs/utils.js';
8
+ import EditView from './EditView.js';
9
+
10
+ const svg = `
11
+ <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;">
12
+ <!--<circle id="muigui-outline" cx="0" cy="0" r="28.871" class="muigui-direction-circle"/>-->
13
+ <path id="muigui-range" class="muigui-direction-range" />
14
+ <g id="muigui-arrow">
15
+ <g transform="translate(-32, -32)">
16
+ <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"/>
17
+ </g>
18
+ </g>
19
+ </svg>
20
+ `;
21
+
22
+ const twoPiMod = v => euclideanModulo(v + Math.PI, Math.PI * 2) - Math.PI;
23
+
24
+ export default class DirectionView extends EditView {
25
+ #arrowElem;
26
+ #rangeElem;
27
+ #lastV;
28
+ #wrap;
29
+ #options = {
30
+ step: 1,
31
+ min: -180,
32
+ max: 180,
33
+
34
+ /*
35
+ --------
36
+ / -π/2 \
37
+ / | \
38
+ |<- -π * |
39
+ | * 0 ->| zero is down the positive X axis
40
+ |<- +π * |
41
+ \ | /
42
+ \ π/2 /
43
+ --------
44
+ */
45
+ dirMin: -Math.PI,
46
+ dirMax: Math.PI,
47
+ //dirMin: Math.PI * 0.5,
48
+ //dirMax: Math.PI * 2.5,
49
+ //dirMin: -Math.PI * 0.75, // test 10:30 to 7:30
50
+ //dirMax: Math.PI * 0.75,
51
+ //dirMin: Math.PI * 0.75, // test 7:30 to 10:30
52
+ //dirMax: -Math.PI * 0.75,
53
+ //dirMin: -Math.PI * 0.75, // test 10:30 to 1:30
54
+ //dirMax: -Math.PI * 0.25,
55
+ //dirMin: Math.PI * 0.25, // test 4:30 to 7:30
56
+ //dirMax: Math.PI * 0.75,
57
+ //dirMin: Math.PI * 0.75, // test 4:30 to 7:30
58
+ //dirMax: Math.PI * 0.25,
59
+ wrap: undefined,
60
+ converters: identity,
61
+ };
62
+
63
+ constructor(setter, options = {}) {
64
+ const wheelHelper = createWheelHelper();
65
+ super(createElem('div', {
66
+ className: 'muigui-direction',
67
+ innerHTML: svg,
68
+ onWheel: e => {
69
+ e.preventDefault();
70
+ const {min, max, step} = this.#options;
71
+ const delta = wheelHelper(e, step);
72
+ let tempV = this.#lastV + delta;
73
+ if (this.#wrap) {
74
+ tempV = euclideanModulo(tempV - min, max - min) + min;
75
+ }
76
+ const newV = clamp(stepify(tempV, v => v, step), min, max);
77
+ setter.setValue(newV);
78
+ },
79
+ }));
80
+ const handleTouch = (e) => {
81
+ const {min, max, step, dirMin, dirMax} = this.#options;
82
+ const nx = e.nx * 2 - 1;
83
+ const ny = e.ny * 2 - 1;
84
+ const a = Math.atan2(ny, nx);
85
+
86
+ const center = (dirMin + dirMax) / 2;
87
+
88
+ const centeredAngle = twoPiMod(a - center);
89
+ const centeredStart = twoPiMod(dirMin - center);
90
+ const diff = dirMax - dirMin;
91
+
92
+ const n = clamp((centeredAngle - centeredStart) / (diff), 0, 1);
93
+ const newV = stepify(min + (max - min) * n, v => v, step);
94
+ setter.setValue(newV);
95
+ };
96
+ addTouchEvents(this.domElement, {
97
+ onDown: handleTouch,
98
+ onMove: handleTouch,
99
+ });
100
+ addKeyboardEvents(this.domElement, {
101
+ onDown: (e) => {
102
+ const {min, max, step} = this.#options;
103
+ const newV = clamp(stepify(this.#lastV + e.dx * step, v => v, step), min, max);
104
+ setter.setValue(newV);
105
+ },
106
+ });
107
+ this.#arrowElem = this.$('#muigui-arrow');
108
+ this.#rangeElem = this.$('#muigui-range');
109
+ this.setOptions(options);
110
+ }
111
+ updateDisplay(v) {
112
+ this.#lastV = v;
113
+ const {min, max} = this.#options;
114
+ const n = (v - min) / (max - min);
115
+ const angle = lerp(this.#options.dirMin, this.#options.dirMax, n);
116
+ this.#arrowElem.style.transform = `rotate(${angle}rad)`;
117
+ }
118
+ setOptions(options) {
119
+ copyExistingProperties(this.#options, options);
120
+ const {dirMin, dirMax, wrap} = this.#options;
121
+ this.#wrap = wrap !== undefined
122
+ ? wrap
123
+ : Math.abs(dirMin - dirMax) >= Math.PI * 2 - Number.EPSILON;
124
+ const [min, max] = dirMin < dirMax ? [dirMin, dirMax] : [dirMax , dirMin];
125
+ this.#rangeElem.setAttribute('d', arc(0, 0, 28.87, min, max));
126
+ }
127
+ }
@@ -0,0 +1,100 @@
1
+ import { isTypedArray } from '../libs/utils.js';
2
+ import View from './View.js';
3
+
4
+ function arraysEqual(a, b) {
5
+ if (a.length !== b.length) {
6
+ return false;
7
+ }
8
+ for (let i = 0; i < a.length; ++i) {
9
+ if (a[i] !== b[i]) {
10
+ return false;
11
+ }
12
+ }
13
+ return true;
14
+ }
15
+
16
+ function copyArrayElementsFromTo(src, dst) {
17
+ dst.length = src.length;
18
+ for (let i = 0; i < src.length; ++i) {
19
+ dst[i] = src[i];
20
+ }
21
+ }
22
+
23
+ export default class EditView extends View {
24
+ #oldV;
25
+ #updateCheck;
26
+
27
+ #checkArrayNeedsUpdate(newV) {
28
+ // It's an array, we need to compare all elements
29
+ // Example, vec2, [r,g,b], ...
30
+ const needUpdate = !arraysEqual(newV, this.#oldV);
31
+ if (needUpdate) {
32
+ copyArrayElementsFromTo(newV, this.#oldV);
33
+ }
34
+ return needUpdate;
35
+ }
36
+
37
+ #checkTypedArrayNeedsUpdate() {
38
+ let once = true;
39
+ return function checkTypedArrayNeedsUpdateImpl(newV) {
40
+ // It's a typedarray, we need to compare all elements
41
+ // Example: Float32Array([r, g, b])
42
+ let needUpdate = once;
43
+ once = false;
44
+ if (!needUpdate) {
45
+ needUpdate = !arraysEqual(newV, this.#oldV);
46
+ }
47
+ return needUpdate;
48
+ };
49
+ }
50
+
51
+ #checkObjectNeedsUpdate(newV) {
52
+ let needUpdate = false;
53
+ for (const key in newV) {
54
+ if (newV[key] !== this.#oldV[key]) {
55
+ needUpdate = true;
56
+ this.#oldV[key] = newV[key];
57
+ }
58
+ }
59
+ return needUpdate;
60
+ }
61
+
62
+ #checkValueNeedsUpdate(newV) {
63
+ const needUpdate = newV !== this.#oldV;
64
+ this.#oldV = newV;
65
+ return needUpdate;
66
+ }
67
+
68
+ #getUpdateCheckForType(newV) {
69
+ if (Array.isArray(newV)) {
70
+ this.#oldV = [];
71
+ return this.#checkArrayNeedsUpdate.bind(this);
72
+ } else if (isTypedArray(newV)) {
73
+ this.#oldV = new newV.constructor(newV);
74
+ return this.#checkTypedArrayNeedsUpdate(this);
75
+ } else if (typeof newV === 'object') {
76
+ this.#oldV = {};
77
+ return this.#checkObjectNeedsUpdate.bind(this);
78
+ } else {
79
+ return this.#checkValueNeedsUpdate.bind(this);
80
+ }
81
+ }
82
+
83
+ // The point of this is updating DOM elements
84
+ // is slow but if we've called `listen` then
85
+ // every frame we're going to try to update
86
+ // things with the current value so if nothing
87
+ // has changed then skip it.
88
+ updateDisplayIfNeeded(newV, ignoreCache) {
89
+ this.#updateCheck = this.#updateCheck || this.#getUpdateCheckForType(newV);
90
+ // Note: We call #updateCheck first because it updates
91
+ // the cache
92
+ if (this.#updateCheck(newV) || ignoreCache) {
93
+ this.updateDisplay(newV);
94
+ }
95
+ }
96
+ setOptions(/*options*/) {
97
+ // override this
98
+ return this;
99
+ }
100
+ }
@@ -0,0 +1,8 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import View from './View.js';
3
+
4
+ export default class ElementView extends View {
5
+ constructor(tag, className) {
6
+ super(createElem(tag, {className}));
7
+ }
8
+ }
@@ -0,0 +1,15 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import View from './View.js';
3
+
4
+ export default class GridView extends View {
5
+ // FIX: should this be 'options'?
6
+ constructor(cols) {
7
+ super(createElem('div', {
8
+ className: 'muigui-grid',
9
+ }));
10
+ this.cols(cols);
11
+ }
12
+ cols(cols) {
13
+ this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
14
+ }
15
+ }
@@ -0,0 +1,67 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import { strToNumber } from '../libs/conversions.js';
3
+ import { createWheelHelper } from '../libs/wheel.js';
4
+ import { clamp, copyExistingProperties, stepify } from '../libs/utils.js';
5
+ import EditView from './EditView.js';
6
+
7
+ export default class NumberView extends EditView {
8
+ #to;
9
+ #from;
10
+ #step;
11
+ #skipUpdate;
12
+ #options = {
13
+ step: 0.01,
14
+ converters: strToNumber,
15
+ min: Number.NEGATIVE_INFINITY,
16
+ max: Number.POSITIVE_INFINITY,
17
+ };
18
+
19
+ constructor(setter, options) {
20
+ const setValue = setter.setValue.bind(setter);
21
+ const setFinalValue = setter.setFinalValue.bind(setter);
22
+ const wheelHelper = createWheelHelper();
23
+ super(createElem('input', {
24
+ type: 'number',
25
+ onInput: () => this.#handleInput(setValue, true),
26
+ onChange: () => this.#handleInput(setFinalValue, false),
27
+ onWheel: e => {
28
+ e.preventDefault();
29
+ const {min, max, step} = this.#options;
30
+ const delta = wheelHelper(e, step);
31
+ const v = parseFloat(this.domElement.value);
32
+ const newV = clamp(stepify(v + delta, v => v, step), min, max);
33
+ setter.setValue(newV);
34
+ },
35
+ }));
36
+ this.setOptions(options);
37
+ }
38
+ #handleInput(setFn, skipUpdate) {
39
+ const v = parseFloat(this.domElement.value);
40
+ const [valid, newV] = this.#from(v);
41
+ let inRange;
42
+ if (valid && !Number.isNaN(v)) {
43
+ const {min, max} = this.#options;
44
+ inRange = newV >= min && newV <= max;
45
+ this.#skipUpdate = skipUpdate;
46
+ setFn(clamp(newV, min, max));
47
+ }
48
+ this.domElement.classList.toggle('muigui-invalid-value', !valid || !inRange);
49
+ }
50
+ updateDisplay(v) {
51
+ if (!this.#skipUpdate) {
52
+ this.domElement.value = stepify(v, this.#to, this.#step);
53
+ }
54
+ this.#skipUpdate = false;
55
+ }
56
+ setOptions(options) {
57
+ copyExistingProperties(this.#options, options);
58
+ const {
59
+ step,
60
+ converters: {to, from},
61
+ } = this.#options;
62
+ this.#to = to;
63
+ this.#from = from;
64
+ this.#step = step;
65
+ return this;
66
+ }
67
+ }
@@ -0,0 +1,46 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import { makeId } from '../libs/ids.js';
3
+ import EditView from './EditView.js';
4
+
5
+ export default class RadioGridView extends EditView {
6
+ #values;
7
+
8
+ constructor(setter, keyValues, cols = 3) {
9
+ const values = [];
10
+ const name = makeId();
11
+ super(createElem('div', {}, keyValues.map(([key, value], ndx) => {
12
+ values.push(value);
13
+ return createElem('label', {}, [
14
+ createElem('input', {
15
+ type: 'radio',
16
+ name,
17
+ value: ndx,
18
+ onChange: function() {
19
+ if (this.checked) {
20
+ setter.setFinalValue(that.#values[this.value]);
21
+ }
22
+ },
23
+ }),
24
+ createElem('button', {
25
+ type: 'button',
26
+ textContent: key,
27
+ onClick: function() {
28
+ this.previousElementSibling.click();
29
+ },
30
+ }),
31
+ ]);
32
+ })));
33
+ const that = this;
34
+ this.#values = values;
35
+ this.cols(cols);
36
+ }
37
+ updateDisplay(v) {
38
+ const ndx = this.#values.indexOf(v);
39
+ for (let i = 0; i < this.domElement.children.length; ++i) {
40
+ this.domElement.children[i].children[0].checked = i === ndx;
41
+ }
42
+ }
43
+ cols(cols) {
44
+ this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
45
+ }
46
+ }
@@ -0,0 +1,73 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import { identity } from '../libs/conversions.js';
3
+ import { clamp, copyExistingProperties, stepify } from '../libs/utils.js';
4
+ import { createWheelHelper } from '../libs/wheel.js';
5
+ import EditView from './EditView.js';
6
+
7
+ export default class RangeView extends EditView {
8
+ #to;
9
+ #from;
10
+ #step;
11
+ #skipUpdate;
12
+ #options = {
13
+ step: 0.01,
14
+ min: 0,
15
+ max: 1,
16
+ converters: identity,
17
+ };
18
+
19
+ constructor(setter, options) {
20
+ const wheelHelper = createWheelHelper();
21
+ super(createElem('input', {
22
+ type: 'range',
23
+ onInput: () => {
24
+ this.#skipUpdate = true;
25
+ const [valid, v] = this.#from(parseFloat(this.domElement.value));
26
+ if (valid) {
27
+ setter.setValue(v);
28
+ }
29
+ },
30
+ onChange: () => {
31
+ this.#skipUpdate = true;
32
+ const [valid, v] = this.#from(parseFloat(this.domElement.value));
33
+ if (valid) {
34
+ setter.setFinalValue(v);
35
+ }
36
+ },
37
+ onWheel: e => {
38
+ e.preventDefault();
39
+ const [valid, v] = this.#from(parseFloat(this.domElement.value));
40
+ if (!valid) {
41
+ return;
42
+ }
43
+ const {min, max, step} = this.#options;
44
+ const delta = wheelHelper(e, step);
45
+ const newV = clamp(stepify(v + delta, v => v, step), min, max);
46
+ setter.setValue(newV);
47
+ },
48
+ }));
49
+ this.setOptions(options);
50
+ }
51
+ updateDisplay(v) {
52
+ if (!this.#skipUpdate) {
53
+ this.domElement.value = stepify(v, this.#to, this.#step);
54
+ }
55
+ this.#skipUpdate = false;
56
+ }
57
+ setOptions(options) {
58
+ copyExistingProperties(this.#options, options);
59
+ const {
60
+ step,
61
+ min,
62
+ max,
63
+ converters: {to, from},
64
+ } = this.#options;
65
+ this.#to = to;
66
+ this.#from = from;
67
+ this.#step = step;
68
+ this.domElement.step = step;
69
+ this.domElement.min = min;
70
+ this.domElement.max = max;
71
+ return this;
72
+ }
73
+ }
@@ -0,0 +1,23 @@
1
+ import { createElem } from '../libs/elem.js';
2
+ import EditView from './EditView.js';
3
+
4
+ export default class SelectView extends EditView {
5
+ #values;
6
+
7
+ constructor(setter, keyValues) {
8
+ const values = [];
9
+ super(createElem('select', {
10
+ onChange: () => {
11
+ setter.setFinalValue(this.#values[this.domElement.selectedIndex]);
12
+ },
13
+ }, keyValues.map(([key, value]) => {
14
+ values.push(value);
15
+ return createElem('option', {textContent: key});
16
+ })));
17
+ this.#values = values;
18
+ }
19
+ updateDisplay(v) {
20
+ const ndx = this.#values.indexOf(v);
21
+ this.domElement.selectedIndex = ndx;
22
+ }
23
+ }