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.
- package/README.md +72 -7
- package/package.json +10 -6
- package/src/controllers/Button.js +34 -0
- package/src/controllers/Canvas.js +17 -0
- package/src/controllers/Checkbox.js +11 -0
- package/src/controllers/Color.js +31 -0
- package/src/controllers/ColorChooser.js +12 -0
- package/src/controllers/Container.js +58 -0
- package/src/controllers/Controller.js +138 -0
- package/src/controllers/Direction.js +23 -0
- package/src/controllers/Divider.js +9 -0
- package/src/controllers/Folder.js +37 -0
- package/src/controllers/Label.js +14 -0
- package/src/controllers/LabelController.js +32 -0
- package/src/controllers/PopDownController.js +84 -0
- package/src/controllers/RadioGrid.js +17 -0
- package/src/controllers/Range.js +11 -0
- package/src/controllers/Select.js +14 -0
- package/src/controllers/Slider.js +12 -0
- package/src/controllers/TabHolder.js +36 -0
- package/src/controllers/Text.js +10 -0
- package/src/controllers/TextNumber.js +18 -0
- package/src/controllers/ValueController.js +107 -0
- package/src/controllers/Vec2.js +50 -0
- package/src/controllers/create-controller.js +49 -0
- package/src/layout/Column.js +7 -0
- package/src/layout/Frame.js +11 -0
- package/src/layout/Grid.js +7 -0
- package/src/layout/Layout.js +47 -0
- package/src/layout/Row.js +7 -0
- package/src/libs/assert.js +5 -0
- package/src/libs/color-utils.js +406 -0
- package/src/libs/conversions.js +14 -0
- package/src/libs/css-utils.js +3 -0
- package/src/libs/elem.js +8 -3
- package/src/libs/emitter.js +68 -0
- package/src/libs/iterable-array.js +57 -0
- package/src/libs/key-values.js +25 -0
- package/src/libs/keyboard.js +32 -0
- package/src/libs/resize-helpers.js +22 -0
- package/src/libs/svg.js +33 -0
- package/src/libs/taskrunner.js +56 -0
- package/src/libs/touch.js +50 -0
- package/src/libs/utils.js +38 -2
- package/src/libs/wheel.js +10 -0
- package/src/muigui.js +79 -19
- package/src/styles/muigui.css.js +641 -0
- package/src/umd.js +3 -0
- package/src/views/CheckboxView.js +21 -0
- package/src/views/ColorChooserView.js +124 -0
- package/src/views/ColorView.js +50 -0
- package/src/views/DirectionView.js +127 -0
- package/src/views/EditView.js +100 -0
- package/src/views/ElementView.js +8 -0
- package/src/views/GridView.js +15 -0
- package/src/views/NumberView.js +67 -0
- package/src/views/RadioGridView.js +46 -0
- package/src/views/RangeView.js +73 -0
- package/src/views/SelectView.js +23 -0
- package/src/views/SliderView.js +194 -0
- package/src/views/TextView.js +49 -0
- package/src/views/ValueView.js +11 -0
- package/src/views/Vec2View.js +51 -0
- package/src/views/View.js +56 -0
- package/src/widgets/checkbox.js +0 -0
- package/src/widgets/divider.js +0 -0
- package/src/widgets/menu.js +0 -0
- package/src/widgets/radio.js +0 -0
- package/src/widgets/select.js +0 -1
- package/src/widgets/slider.js +0 -41
- package/src/widgets/text.js +0 -0
- 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,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
|
+
}
|