muigui 0.0.6 → 0.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muigui",
3
- "version": "0.0.6",
3
+ "version": "0.0.10",
4
4
  "description": "A Simple GUI",
5
5
  "main": "muigui.js",
6
6
  "module": "src/muigui.js",
@@ -26,6 +26,12 @@ export default class Container extends Controller {
26
26
  }
27
27
  return this;
28
28
  }
29
+ updateDisplay() {
30
+ for (const controller of this.#controllers) {
31
+ controller.updateDisplay();
32
+ }
33
+ return this;
34
+ }
29
35
  remove(controller) {
30
36
  const ndx = this.#controllers.indexOf(controller);
31
37
  if (ndx >= 0) {
@@ -113,6 +113,9 @@ export default class Controller extends View {
113
113
  }
114
114
  }
115
115
  }
116
+ updateDisplay() {
117
+ // placeholder. override
118
+ }
116
119
  getColors() {
117
120
  const toCamelCase = s => s.replace(/-([a-z])/g, (m, m1) => m1.toUpperCase());
118
121
  const keys = [
@@ -1,6 +1,5 @@
1
1
  import ElementView from '../views/ElementView.js';
2
2
  import ValueController from './ValueController.js';
3
- import CheckboxView from '../views/CheckboxView.js';
4
3
  import { copyExistingProperties } from '../libs/utils.js';
5
4
  import { createElem } from '../libs/elem.js';
6
5
  /*
@@ -27,17 +26,6 @@ pc.addBottom
27
26
 
28
27
  */
29
28
 
30
- function makeSetter(object, property) {
31
- return {
32
- setValue(v) {
33
- object[property] = v;
34
- },
35
- setFinalValue(v) {
36
- this.setValue(v);
37
- },
38
- };
39
- }
40
-
41
29
  export default class PopDownController extends ValueController {
42
30
  #top;
43
31
  #valuesView;
@@ -34,31 +34,39 @@ export default class ValueController extends LabelController {
34
34
  return view;
35
35
  }
36
36
  #setValueImpl(v, ignoreCache) {
37
+ let isDifferent = false;
37
38
  if (typeof v === 'object') {
38
39
  const dst = this.#object[this.#property];
39
40
  // don't replace objects, just their values.
40
- if (Array.isArray(v)) {
41
+ if (Array.isArray(v) || isTypedArray(v)) {
41
42
  for (let i = 0; i < v.length; ++i) {
43
+ isDifferent ||= dst[i] !== v[i];
42
44
  dst[i] = v[i];
43
45
  }
44
- } else if (isTypedArray(v)) {
45
- dst.set(v);
46
46
  } else {
47
+ for (const key of Object.keys(v)) {
48
+ isDifferent ||= dst[key] !== v[key];
49
+ }
47
50
  Object.assign(dst, v);
48
51
  }
49
52
  } else {
53
+ isDifferent = this.#object[this.#property] !== v;
50
54
  this.#object[this.#property] = v;
51
55
  }
52
56
  this.updateDisplay(ignoreCache);
53
- this.emitChange(this.getValue(), this.#object, this.#property);
54
- return this;
57
+ if (isDifferent) {
58
+ this.emitChange(this.getValue(), this.#object, this.#property);
59
+ }
60
+ return isDifferent;
55
61
  }
56
62
  setValue(v) {
57
63
  this.#setValueImpl(v);
58
64
  }
59
65
  setFinalValue(v) {
60
- this.#setValueImpl(v, true);
61
- this.emitFinalChange(this.getValue(), this.#object, this.#property);
66
+ const isDifferent = this.#setValueImpl(v, true);
67
+ if (isDifferent) {
68
+ this.emitFinalChange(this.getValue(), this.#object, this.#property);
69
+ }
62
70
  return this;
63
71
  }
64
72
  updateDisplay(ignoreCache) {
@@ -1,4 +1,3 @@
1
- import GridView from '../views/GridView.js';
2
1
  import NumberView from '../views/NumberView.js';
3
2
  import Vec2View from '../views/Vec2View.js';
4
3
  import PopDownController from './PopDownController.js';
@@ -3,7 +3,6 @@ import View from '../views/View.js';
3
3
 
4
4
  function showCSS(ob) {
5
5
  if (ob.prototype.css) {
6
- console.log(ob.prototype.css);
7
6
  showCSS(ob.prototype);
8
7
  }
9
8
  }
@@ -34,12 +34,14 @@ const cssRGBToHex = v => {
34
34
  const m = cssRGBRegex.exec(v);
35
35
  return uint8RGBToHex([m[1], m[2], m[3]].map(v => parseInt(v)));
36
36
  };
37
+ const cssRGBARegex = /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/;
37
38
 
38
39
  const hexToCssHSL = v => {
39
40
  const hsl = rgbUint8ToHsl(hexToUint8RGB(v)).map(v => f0(v));
40
41
  return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
41
42
  };
42
43
  const cssHSLRegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)\s*$/;
44
+ const cssHSLARegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)\s*$/;
43
45
 
44
46
  const hex3DigitTo6Digit = v => `${v[0]}${v[0]}${v[1]}${v[1]}${v[2]}${v[2]}`;
45
47
  const cssHSLToHex = v => {
@@ -121,13 +123,20 @@ export function rgbFloatToHSV01([r, g, b]) {
121
123
  window.hsv01ToRGBFloat = hsv01ToRGBFloat;
122
124
  window.rgbFloatToHSV01 = rgbFloatToHSV01;
123
125
 
126
+ // Yea, meh!
127
+ export const hasAlpha = format => format.endsWith('a') || format.startsWith('hex8');
128
+
124
129
  const cssStringFormats = [
125
130
  { re: /^#(?:[0-9a-f]){6}$/i, format: 'hex6' },
126
131
  { re: /^(?:[0-9a-f]){6}$/i, format: 'hex6-no-hash' },
132
+ { re: /^#(?:[0-9a-f]){8}$/i, format: 'hex8' },
133
+ { re: /^(?:[0-9a-f]){8}$/i, format: 'hex8-no-hash' },
127
134
  { re: /^#(?:[0-9a-f]){3}$/i, format: 'hex3' },
128
135
  { re: /^(?:[0-9a-f]){3}$/i, format: 'hex3-no-hash' },
129
136
  { re: cssRGBRegex, format: 'css-rgb' },
130
137
  { re: cssHSLRegex, format: 'css-hsl' },
138
+ { re: cssRGBARegex, format: 'css-rgba' },
139
+ { re: cssHSLARegex, format: 'css-hsla' },
131
140
  ];
132
141
 
133
142
  function guessStringColorFormat(v) {
@@ -142,7 +151,8 @@ function guessStringColorFormat(v) {
142
151
  export function guessFormat(v) {
143
152
  switch (typeof v) {
144
153
  case 'number':
145
- return 'uint32-rgb';
154
+ 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"}');
155
+ return v <= 0xFFFFFF ? 'uint32-rgb' : 'uint32-rgba';
146
156
  case 'string': {
147
157
  const formatInfo = guessStringColorFormat(v.trim());
148
158
  if (formatInfo) {
@@ -154,18 +164,28 @@ export function guessFormat(v) {
154
164
  if (v instanceof Uint8Array || v instanceof Uint8ClampedArray) {
155
165
  if (v.length === 3) {
156
166
  return 'uint8-rgb';
167
+ } else if (v.length === 4) {
168
+ return 'uint8-rgba';
157
169
  }
158
170
  } else if (v instanceof Float32Array) {
159
171
  if (v.length === 3) {
160
172
  return 'float-rgb';
173
+ } else if (v.length === 4) {
174
+ return 'float-rgba';
161
175
  }
162
176
  } else if (Array.isArray(v)) {
163
177
  if (v.length === 3) {
164
178
  return 'float-rgb';
179
+ } else if (v.length === 4) {
180
+ return 'float-rgba';
165
181
  }
166
182
  } else {
167
183
  if ('r' in v && 'g' in v && 'b' in v) {
168
- return 'object-rgb';
184
+ if ('a' in v) {
185
+ return 'object-rgba';
186
+ } else {
187
+ return 'object-rgb';
188
+ }
169
189
  }
170
190
  }
171
191
  }
package/src/libs/touch.js CHANGED
@@ -29,9 +29,9 @@ export function addTouchEvents(elem, {onDown = noop, onMove = noop, onUp = noop}
29
29
  elem.releasePointerCapture(event.pointerId);
30
30
  elem.removeEventListener('pointermove', pointerMove);
31
31
  elem.removeEventListener('pointerup', pointerUp);
32
-
32
+
33
33
  document.body.style.backgroundColor = '';
34
-
34
+
35
35
  onUp('up');
36
36
  };
37
37
 
package/src/libs/utils.js CHANGED
@@ -72,3 +72,34 @@ export const makeRangeOptions = ({from, to, step}) => {
72
72
  };
73
73
  };
74
74
 
75
+ // TODO: remove an use one in conversions. Move makeRangeConverters there?
76
+ export const identity = {
77
+ to: v => v,
78
+ from: v => [true, v],
79
+ };
80
+ export function makeMinMaxPair(gui, properties, minPropName, maxPropName, options) {
81
+ const { converters: { from } = identity } = options;
82
+ const { min, max } = options;
83
+ const guiMinRange = options.minRange || 0;
84
+ const valueMinRange = from(guiMinRange)[1];
85
+ const minGui = gui
86
+ .add(properties, minPropName, {
87
+ ...options,
88
+ min,
89
+ max: max - guiMinRange,
90
+ })
91
+ .onChange(v => {
92
+ maxGui.setValue(Math.min(max, Math.max(v + valueMinRange, properties[maxPropName])));
93
+ });
94
+ const maxGui = gui
95
+ .add(properties, maxPropName, {
96
+ ...options,
97
+ min: min + guiMinRange,
98
+ max,
99
+ })
100
+ .onChange(v => {
101
+ minGui.setValue(Math.max(min, Math.min(v - valueMinRange, properties[minPropName])));
102
+ });
103
+ return [ minGui, maxGui ];
104
+ }
105
+
package/src/muigui.js CHANGED
@@ -5,16 +5,22 @@ import {
5
5
  mapRange,
6
6
  makeRangeConverters,
7
7
  makeRangeOptions,
8
+ makeMinMaxPair,
8
9
  } from './libs/utils.js';
9
10
  import {
10
11
  converters
11
12
  } from './libs/conversions.js';
13
+ import {
14
+ hasAlpha,
15
+ guessFormat,
16
+ } from './libs/color-utils.js';
12
17
  import Canvas from './controllers/Canvas.js';
13
18
  import Color from './controllers/Color.js';
14
19
  import Divider from './controllers/Divider.js';
15
20
  import Folder from './controllers/Folder.js';
16
21
  import Label from './controllers/Label.js';
17
22
  import Controller from './controllers/Controller.js';
23
+ import ColorChooser from './controllers/ColorChooser.js';
18
24
 
19
25
  import Column from './layout/Column.js';
20
26
  import Frame from './layout/Frame.js';
@@ -28,9 +34,6 @@ export {
28
34
  Row,
29
35
  };
30
36
 
31
- let stylesInjected = false;
32
- const styleElem = createElem('style');
33
-
34
37
  export class GUIFolder extends Folder {
35
38
  add(object, property, ...args) {
36
39
  const controller = object instanceof Controller
@@ -41,8 +44,13 @@ export class GUIFolder extends Folder {
41
44
  addCanvas(name) {
42
45
  return this.addController(new Canvas(name));
43
46
  }
44
- addColor(object, property, ...args) {
45
- return this.addController(new Color(object, property, ...args));
47
+ addColor(object, property, options = {}) {
48
+ const value = object[property];
49
+ if (hasAlpha(options.format || guessFormat(value))) {
50
+ return this.addController(new ColorChooser(object, property, options));
51
+ } else {
52
+ return this.addController(new Color(object, property, options));
53
+ }
46
54
  }
47
55
  addDivider() {
48
56
  return this.addController(new Divider());
@@ -55,11 +63,50 @@ export class GUIFolder extends Folder {
55
63
  }
56
64
  }
57
65
 
66
+ class MuiguiElement extends HTMLElement {
67
+ constructor() {
68
+ super();
69
+ this.shadow = this.attachShadow({mode: 'open'});
70
+ }
71
+ }
72
+
73
+ customElements.define('muigui-element', MuiguiElement);
74
+
75
+ const baseStyleSheet = new CSSStyleSheet();
76
+ baseStyleSheet.replaceSync(css.default);
77
+ const userStyleSheet = new CSSStyleSheet();
78
+
79
+ function makeStyleSheetUpdater(styleSheet) {
80
+ let newCss;
81
+ let newCssPromise;
82
+
83
+ function updateStyle() {
84
+ if (newCss && !newCssPromise) {
85
+ const s = newCss;
86
+ newCss = undefined;
87
+ newCssPromise = styleSheet.replace(s).then(() => {
88
+ newCssPromise = undefined;
89
+ updateStyle();
90
+ });
91
+ }
92
+ }
93
+
94
+ return function updateStyleSheet(css) {
95
+ newCss = css;
96
+ updateStyle();
97
+ };
98
+ }
99
+
100
+ const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet);
101
+ const updateUserStyle = makeStyleSheetUpdater(userStyleSheet);
102
+
58
103
  export class GUI extends GUIFolder {
59
104
  static converters = converters;
60
105
  static mapRange = mapRange;
61
106
  static makeRangeConverters = makeRangeConverters;
62
107
  static makeRangeOptions = makeRangeOptions;
108
+ static makeMinMaxPair = makeMinMaxPair;
109
+ #localStyleSheet = new CSSStyleSheet();
63
110
 
64
111
  constructor(options = {}) {
65
112
  super('Controls', 'muigui-root');
@@ -70,16 +117,11 @@ export class GUI extends GUIFolder {
70
117
  autoPlace = true,
71
118
  width,
72
119
  title = 'Controls',
73
- injectStyles = true,
74
120
  } = options;
75
121
  let {
76
122
  parent,
77
123
  } = options;
78
- if (injectStyles && !stylesInjected) {
79
- stylesInjected = true;
80
- (document.head || document.documentElement).appendChild(styleElem);
81
- styleElem.textContent = css;
82
- }
124
+
83
125
  if (width) {
84
126
  this.domElement.style.width = /^\d+$/.test(width) ? `${width}px` : width;
85
127
  }
@@ -88,13 +130,34 @@ export class GUI extends GUIFolder {
88
130
  this.domElement.classList.add('muigui-auto-place');
89
131
  }
90
132
  if (parent) {
91
- parent.appendChild(this.domElement);
133
+ const muiguiElement = createElem('muigui-element');
134
+ muiguiElement.shadowRoot.adoptedStyleSheets = [baseStyleSheet, userStyleSheet, this.#localStyleSheet];
135
+ muiguiElement.shadow.appendChild(this.domElement);
136
+ parent.appendChild(muiguiElement);
92
137
  }
93
138
  if (title) {
94
139
  this.title(title);
95
140
  }
96
141
  this.domElement.classList.add('muigui', 'muigui-colors');
97
142
  }
143
+ setStyle(css) {
144
+ this.#localStyleSheet.replace(css);
145
+ }
146
+ static setBaseStyles(css) {
147
+ updateBaseStyle(css);
148
+ }
149
+ static getBaseStyleSheet() {
150
+ return baseStyleSheet;
151
+ }
152
+ static setUserStyles(css) {
153
+ updateUserStyle(css);
154
+ }
155
+ static getUserStyleSheet() {
156
+ return userStyleSheet;
157
+ }
158
+ static setTheme(name) {
159
+ GUI.setBaseStyles(`${css.default}\n${css.themes[name] || ''}`);
160
+ }
98
161
  }
99
162
 
100
163
  export default GUI;
@@ -1,7 +1,9 @@
1
- export default `
2
- .muigui-colors {
1
+ export default {
2
+ default: `
3
+ .muigui {
3
4
  --bg-color: #ddd;
4
5
  --color: #222;
6
+ --contrast-color: #eee;
5
7
  --value-color: #145 ;
6
8
  --value-bg-color: #eeee;
7
9
  --disabled-color: #999;
@@ -24,9 +26,10 @@ export default `
24
26
  }
25
27
 
26
28
  @media (prefers-color-scheme: dark) {
27
- .muigui-colors {
29
+ .muigui {
28
30
  --bg-color: #222222;
29
31
  --color: #dddddd;
32
+ --contrast-color: #000;
30
33
  --value-color: #43e5f7;
31
34
  --value-bg-color: #444444;
32
35
  --disabled-color: #666666;
@@ -678,5 +681,65 @@ export default `
678
681
  z-index: 100001;
679
682
  }
680
683
 
681
- `;
684
+ `,
685
+ themes: {
686
+ default: '',
687
+ float: `
688
+ :root {
689
+ color-scheme: light dark,
690
+ }
691
+
692
+ .muigui {
693
+ --width: 400px;
694
+ --bg-color: initial;
695
+ --label-width: 25%;
696
+ --number-width: 20%;
697
+ }
698
+
699
+ input,
700
+ .muigui-label-controller>label {
701
+ text-shadow:
702
+ -1px -1px 0 var(--contrast-color),
703
+ 1px -1px 0 var(--contrast-color),
704
+ -1px 1px 0 var(--contrast-color),
705
+ 1px 1px 0 var(--contrast-color);
706
+ }
682
707
 
708
+ .muigui-controller > label:nth-child(1) {
709
+ place-content: center end;
710
+ margin-right: 1em;
711
+ }
712
+
713
+ .muigui-value > :nth-child(2) {
714
+ margin-left: 1em;
715
+ }
716
+
717
+ .muigui-root>*:nth-child(1) {
718
+ display: none;
719
+ }
720
+
721
+ .muigui-range input[type=range]::-webkit-slider-thumb {
722
+ border-radius: 1em;
723
+ }
724
+
725
+ .muigui-range input[type=range]::-webkit-slider-runnable-track {
726
+ -webkit-appearance: initial;
727
+ appearance: none;
728
+ border: 1px solid rgba(0, 0, 0, 0.25);
729
+ height: 2px;
730
+ }
731
+
732
+ .muigui-colors {
733
+ --value-color: var(--color );
734
+ --value-bg-color: rgba(0, 0, 0, 0.1);
735
+ --disabled-color: #cccccc;
736
+ --menu-bg-color: rgba(0, 0, 0, 0.1);
737
+ --menu-sep-color: #bbbbbb;
738
+ --hover-bg-color: rgba(0, 0, 0, 0);
739
+ --invalid-color: #FF0000;
740
+ --selected-color: rgba(0, 0, 0, 0.3);
741
+ --range-color: rgba(0, 0, 0, 0.125);
742
+ }
743
+ `,
744
+ },
745
+ };
@@ -2,6 +2,7 @@ import { createElem } from '../libs/elem.js';
2
2
  import EditView from './EditView.js';
3
3
 
4
4
  export default class CheckboxView extends EditView {
5
+ #checkboxElem;
5
6
  constructor(setter, id) {
6
7
  const checkboxElem = createElem('input', {
7
8
  type: 'checkbox',
@@ -14,8 +15,9 @@ export default class CheckboxView extends EditView {
14
15
  },
15
16
  });
16
17
  super(createElem('label', {}, [checkboxElem]));
18
+ this.#checkboxElem = checkboxElem;
17
19
  }
18
20
  updateDisplay(v) {
19
- this.domElement.checked = v;
21
+ this.#checkboxElem.checked = v;
20
22
  }
21
23
  }
@@ -3,14 +3,10 @@ import { addTouchEvents } from '../libs/touch.js';
3
3
  import { clamp } from '../libs/utils.js';
4
4
  import EditView from './EditView.js';
5
5
  import {
6
- hexToUint8RGB,
7
6
  hexToFloatRGB,
8
- hslToRgbUint8,
9
7
  hsv01ToRGBFloat,
10
8
  rgbFloatToHSV01,
11
- rgbUint8ToHsl,
12
9
  floatRGBToHex,
13
- uint8RGBToHex,
14
10
  } from '../libs/color-utils.js';
15
11
 
16
12
  const svg = `
@@ -22,16 +22,22 @@ export default class RangeView extends EditView {
22
22
  type: 'range',
23
23
  onInput: () => {
24
24
  this.#skipUpdate = true;
25
- const [valid, v] = this.#from(parseFloat(this.domElement.value));
25
+ const {min, max, step} = this.#options;
26
+ const v = parseFloat(this.domElement.value);
27
+ const newV = clamp(stepify(v, v => v, step), min, max);
28
+ const [valid, validV] = this.#from(newV);
26
29
  if (valid) {
27
- setter.setValue(v);
30
+ setter.setValue(validV);
28
31
  }
29
32
  },
30
33
  onChange: () => {
31
34
  this.#skipUpdate = true;
32
- const [valid, v] = this.#from(parseFloat(this.domElement.value));
35
+ const {min, max, step} = this.#options;
36
+ const v = parseFloat(this.domElement.value);
37
+ const newV = clamp(stepify(v, v => v, step), min, max);
38
+ const [valid, validV] = this.#from(newV);
33
39
  if (valid) {
34
- setter.setFinalValue(v);
40
+ setter.setFinalValue(validV);
35
41
  }
36
42
  },
37
43
  onWheel: e => {