├── .gitignore ├── LICENSE ├── README.md ├── dist ├── perfect-gui.mjs └── perfect-gui.umd.js ├── package-lock.json ├── package.json ├── src ├── addons │ └── three.js ├── components │ └── Slider.js ├── index.js └── styles │ ├── _button.css.js │ ├── _color.css.js │ ├── _folder.css.js │ ├── _image.css.js │ ├── _list.css.js │ ├── _slider.css.js │ ├── _switch.css.js │ ├── _vector2.css.js │ └── styles.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | Thumbs.db 4 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thibaut Foussard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perfect GUI 2 | A nice, simple and (probably not) perfect GUI for JavaScript. 3 | 4 | ## API 5 | [Documentation](https://thibka.github.io/perfect-gui/public/) 6 | 7 | 8 | ## Features 9 | - image buttons 10 | - multiple panels 11 | - easy positioning 12 | - draggable panels 13 | - two-dimensional vector visualization 14 | 15 | 16 | 17 | ## Install 18 | 19 | ### With NPM 20 | 21 | ```bash 22 | npm i perfect-gui 23 | ``` 24 | 25 | ### Import from a CDN 26 | 27 | ```javascript 28 | 35 | ``` 36 | 37 | ## Hello world 38 | 39 | ```javascript 40 | import GUI from 'perfect-gui'; 41 | 42 | const gui = new GUI(); 43 | 44 | gui.button('Click me', callback); 45 | ``` 46 | 47 | ## Options 48 | ```javascript 49 | const gui = new GUI({ 50 | name: 'My GUI', 51 | // Name of the panel. 52 | // Default is null. 53 | 54 | container: '#container', 55 | // Element containing the GUI 56 | // Default is document.body 57 | 58 | width: 250, 59 | // Width of the panel (in pixels). 60 | // Default is 290. 61 | 62 | maxHeight: 500, 63 | // Maximum height beyond which scrolling is necessary. 64 | // Default is the smallest value between the height of the window and the height of the container. 65 | 66 | closed: false, 67 | // Defines whether the panel should be closed by default. 68 | // Default is false. 69 | 70 | position: 'bottom right', 71 | // Defines where to place the panel on screen. 72 | // Accepted values are 'top', 'bottom', 'left' and 'right' in no particular order ('bottom right' = 'right bottom'). 73 | // If multiple instances have the same position, they will be stacked horizontally. 74 | // Default is 'top right'. 75 | 76 | draggable: false, 77 | // Defines if the panel can be manually moved across the screen. 78 | // Default is false. 79 | 80 | autoRepositioning: true, 81 | // If set to true, the panel position will be reset when the screen is resized. 82 | // If a panel has been dragged, it won't be be affected. 83 | // Default is true. 84 | 85 | color: '#bada55', 86 | // Default is #2e2e2e 87 | 88 | onUpdate: () => { 89 | // Callback function triggered each time this GUI instance is updated. 90 | } 91 | }); 92 | ``` 93 | 94 | ## [Methods](https://thibka.github.io/perfect-gui/public/) 95 | 96 | * [button](https://thibka.github.io/perfect-gui/public/#button) 97 | * [slider](https://thibka.github.io/perfect-gui/public/#slider) 98 | * [toggle](https://thibka.github.io/perfect-gui/public/#toggle) 99 | * [list](https://thibka.github.io/perfect-gui/public/#list) 100 | * [image](https://thibka.github.io/perfect-gui/public/#image) 101 | * [color](https://thibka.github.io/perfect-gui/public/#color) 102 | * [vector2](https://thibka.github.io/perfect-gui/public/#vector2) 103 | * [folder](https://thibka.github.io/perfect-gui/public/#folder) 104 | * [toggleClose](https://thibka.github.io/perfect-gui/public) -------------------------------------------------------------------------------- /dist/perfect-gui.mjs: -------------------------------------------------------------------------------- 1 | class R { 2 | constructor(e, t = {}, i) { 3 | if (this.parent = e, this.propReferences = [], typeof t != "object") 4 | throw Error(`[GUI] slider() first parameter must be an object. Received: ${typeof t}.`); 5 | let a = typeof t.name == "string" && t.name || " "; 6 | this.isObject = !1; 7 | let s = null; 8 | this.obj = t.obj, this.prop = t.prop; 9 | let o = typeof t.value == "number" ? t.value : null; 10 | if (this.min = t.min ?? 0, this.max = t.max ?? 1, this.step = t.step || (this.max - this.min) / 100, this.decimals = this.parent._countDecimals(this.step), this.callback = typeof i == "function" ? i : null, o !== null) 11 | (this.prop != null || this.obj != null) && console.warn('[GUI] slider() "obj" and "prop" parameters are ignored when a "value" parameter is used.'); 12 | else if (this.prop != null && this.obj != null) { 13 | if (typeof this.prop != "string") 14 | throw Error(`[GUI] slider() "prop" parameter must be an string. Received: ${typeof this.prop}.`); 15 | if (typeof this.obj != "object") 16 | throw Error(`[GUI] slider() "obj" parameter must be an object. Received: ${typeof this.obj}.`); 17 | a == " " && (a = this.prop), s = this.propReferences.push(this.obj[this.prop]) - 1, this.isObject = !0; 18 | } else 19 | (this.prop != null && this.obj == null || this.prop == null && this.obj != null) && console.warn('[GUI] slider() "obj" and "prop" parameters must be used together.'), o = (this.max - this.min) / 2; 20 | const r = typeof t.tooltip == "string" ? t.tooltip : t.tooltip === !0 ? a : null; 21 | this.imageContainer = null; 22 | const n = document.createElement("div"); 23 | n.className = "p-gui__slider", r && n.setAttribute("title", r); 24 | const c = document.createElement("div"); 25 | c.className = "p-gui__slider-name", c.textContent = a, n.append(c), this.ctrlDiv = document.createElement("div"), this.ctrlDiv.className = "p-gui__slider-ctrl", this.ctrlDiv.setAttribute("type", "range"), this.ctrlDiv.setAttribute("min", this.min), this.ctrlDiv.setAttribute("max", this.max), n.append(this.ctrlDiv); 26 | const l = document.createElement("div"); 27 | return l.className = "p-gui__slider-bar", this.ctrlDiv.append(l), this.handle = document.createElement("div"), this.handle.className = "p-gui__slider-handle", this.ctrlDiv.append(this.handle), this.filling = document.createElement("div"), this.filling.className = "p-gui__slider-filling", l.append(this.filling), this.valueInput = document.createElement("input"), this.valueInput.className = "p-gui__slider-value", this.valueInput.value = this.isObject ? this.obj[this.prop] : o, n.append(this.valueInput), setTimeout(() => { 28 | const d = this.handle.offsetWidth; 29 | this.handle.position = this._mapLinear(this.valueInput.value, this.min, this.max, d / 2, 88 - d / 2), this.handle.position = Math.min(this.handle.position, 88 - d / 2), this.handle.position = Math.max(this.handle.position, d / 2), this.handle.style.transform = `translate(-50%, -50%) translateX(${this.handle.position}px)`, this.filling.style.width = `${this.handle.position}px`; 30 | }, 0), this.valueInput.addEventListener("change", () => { 31 | this._updateHandlePositionFromValue(), this._triggerCallbacks(); 32 | }), this.ctrlDiv.addEventListener("pointerdown", (d) => { 33 | this.ctrlDiv.pointerDown = !0, this.ctrlDiv.prevPosition = d.clientX, this._updateHandlePositionFromPointer(d, !0); 34 | }), window.addEventListener("pointerup", (d) => { 35 | this.ctrlDiv.pointerDown = !1; 36 | }), window.addEventListener("pointermove", (d) => { 37 | this.ctrlDiv.pointerDown && (this.ctrlDiv.pointerDelta = d.clientX - this.ctrlDiv.prevPosition, this._updateHandlePositionFromPointer(d)); 38 | }), this.isObject && Object.defineProperty(this.obj, this.prop, { 39 | set: (d) => { 40 | this.propReferences[s] = d, this.valueInput.value = d, this._updateHandlePositionFromValue(), this.callback && this.callback(parseFloat(this.valueInput.value)); 41 | }, 42 | get: () => this.propReferences[s] 43 | }), n; 44 | } 45 | _updateHandlePositionFromPointer(e, t = !1) { 46 | const i = this.ctrlDiv.offsetWidth, a = this.handle.offsetWidth, s = e.clientX - this.ctrlDiv.prevPosition, o = parseFloat(this.valueInput.value); 47 | let r; 48 | t ? r = e.offsetX : r = this.handle.position + s, r = Math.max(a / 2, Math.min(r, i - a / 2)); 49 | let n = this.min + (this.max - this.min) * (r - a / 2) / (i - a); 50 | n > o ? n = this._quantizeFloor(n, this.step) : n = this._quantizeCeil(n, this.step), n = parseFloat(n.toFixed(9)); 51 | const c = parseFloat((o + this.step).toFixed(9)), l = parseFloat((o - this.step).toFixed(9)); 52 | (n >= c || n <= l) && (n = n.toFixed(this.decimals), this.valueInput.value = n, this.ctrlDiv.prevPosition = e.clientX, this.handle.style.transform = `translate(-50%, -50%) translateX(${r}px)`, this.handle.position = r, this.filling.style.width = this.handle.position + "px", this._triggerCallbacks()); 53 | } 54 | _updateHandlePositionFromValue() { 55 | const e = this.ctrlDiv.offsetWidth, t = this.handle.offsetWidth; 56 | let i = this._mapLinear(this.valueInput.value, this.min, this.max, t / 2, e - t / 2); 57 | i = Math.max(t / 2, Math.min(i, e - t / 2)), this.handle.style.transform = `translate(-50%, -50%) translateX(${i}px)`, this.handle.position = i, this.filling.style.width = this.handle.position + "px"; 58 | } 59 | _triggerCallbacks() { 60 | this.isObject ? this.obj[this.prop] = parseFloat(this.valueInput.value) : this.callback && this.callback(parseFloat(this.valueInput.value)), this.parent.onUpdate ? this.parent.onUpdate() : this.parent.isFolder && this.parent.firstParent.onUpdate && this.parent.firstParent.onUpdate(); 61 | } 62 | _mapLinear(e, t, i, a, s) { 63 | return a + (e - t) * (s - a) / (i - t); 64 | } 65 | _quantize(e, t) { 66 | return t * Math.round(e / t); 67 | } 68 | _quantizeCeil(e, t) { 69 | return t * Math.ceil(e / t); 70 | } 71 | _quantizeFloor(e, t) { 72 | return t * Math.floor(e / t); 73 | } 74 | } 75 | const P = ( 76 | /* css */ 77 | ` 78 | .p-gui__button { 79 | background: var(--color-accent); 80 | text-align: center; 81 | color: white; 82 | border: none; 83 | border: 1px solid transparent; 84 | box-sizing: border-box; 85 | transition: var(--transition) background, var(--transition) border-color; 86 | } 87 | 88 | .p-gui__button:hover { 89 | background: var(--color-accent-hover); 90 | color: var(--color-text-light); 91 | border-color: rgba(255, 255, 255, 0.2); 92 | } 93 | 94 | .p-gui__folder .p-gui__button { 95 | margin-inline: 0; 96 | } 97 | ` 98 | ), O = ( 99 | /* css */ 100 | ` 101 | .p-gui__slider { 102 | position: relative; 103 | min-height: 14px; 104 | display: flex; 105 | align-items: center; 106 | justify-content: space-between; 107 | gap: 10px; 108 | color: var(--color-text-dark); 109 | transition: color var(--transition); 110 | } 111 | 112 | .p-gui__slider:hover { 113 | color: var(--color-text-light); 114 | } 115 | 116 | .p-gui__slider-name { 117 | width: 50%; 118 | text-overflow: ellipsis; 119 | overflow: hidden; 120 | } 121 | 122 | .p-gui__slider-ctrl { 123 | -webkit-appearance: none; 124 | padding: 0; 125 | font: inherit; 126 | outline: none; 127 | box-sizing: border-box; 128 | cursor: pointer; 129 | position: relative; 130 | right: 0; 131 | height: 14px; 132 | margin: 0 0 0 auto; 133 | width: 37%; 134 | } 135 | 136 | .p-gui__slider-bar { 137 | position: absolute; 138 | top: 50%; 139 | left: 0; 140 | height: 2px; 141 | background: rgba(255, 255, 255, .2); 142 | width: 100%; 143 | transform: translateY(-50%); 144 | } 145 | 146 | .p-gui__slider-filling { 147 | position: absolute; 148 | top: -25%; 149 | left: 0; 150 | height: 150%; 151 | background: var(--color-accent); 152 | width: 0; 153 | } 154 | 155 | .p-gui__slider:hover .p-gui__slider-filling { 156 | background: var(--color-accent-hover); 157 | } 158 | 159 | .p-gui__slider-handle { 160 | width: 15px; 161 | height: 8px; 162 | position: absolute; 163 | top: 50%; 164 | left: 0; 165 | border-radius: 2px; 166 | transform: translate(-50%, -50%); 167 | pointer-events: none; 168 | background: var(--color-text-dark); 169 | box-shadow: 0 0 2px rgba(0, 0, 0, .5); 170 | } 171 | 172 | .p-gui__slider:hover .p-gui__slider-handle { 173 | background: var(--color-text-light); 174 | } 175 | 176 | .p-gui__slider-value { 177 | display: inline-block; 178 | right: 7px; 179 | width: 13%; 180 | border: none; 181 | color: white; 182 | border-radius: 2px; 183 | background: rgba(255, 255, 255, 0.1); 184 | padding: 2px 4px; 185 | color: inherit; 186 | } 187 | 188 | .p-gui__slider-value:focus { 189 | outline: none; 190 | } 191 | ` 192 | ), $ = ( 193 | /* css */ 194 | ` 195 | .p-gui__list { 196 | cursor: default; 197 | color: var(--color-text-dark); 198 | transition: var(--transition) color; 199 | } 200 | 201 | .p-gui__list:hover { 202 | color: var(--color-text-light); 203 | } 204 | 205 | .p-gui__list-dropdown { 206 | background: rgba(255, 255, 255,.05); 207 | color: white; 208 | padding: 0 12px 0 5px; 209 | top: 0px; 210 | } 211 | 212 | .p-gui__list-dropdown { 213 | position: absolute; 214 | right: 5px; 215 | top: 0; 216 | bottom: 0; 217 | margin: auto; 218 | height: 21px; 219 | cursor: pointer; 220 | border-radius: 3px; 221 | border: 1px solid var(--color-border-2); 222 | outline: none; 223 | } 224 | 225 | .p-gui__list-dropdown:hover { 226 | background: rgba(255, 255, 255, .1); 227 | } 228 | ` 229 | ), D = ( 230 | /* css */ 231 | ` 232 | .p-gui__switch { 233 | background: rgba(255, 255, 255, .05); 234 | color: var(--color-text-dark); 235 | transition: var(--transition) background, var(--transition) color; 236 | } 237 | 238 | .p-gui__switch:hover { 239 | background: rgba(255, 255, 255, .1); 240 | color: var(--color-text-light); 241 | } 242 | 243 | .p-gui__folder .p-gui__switch { 244 | margin-inline: 0; 245 | } 246 | 247 | .p-gui__switch-checkbox { 248 | width: 5px; 249 | height: 5px; 250 | background-color: rgba(0, 0, 0, .5); 251 | border: 1px solid grey; 252 | position: absolute; 253 | top: 0; 254 | right: 10px; 255 | bottom: 0; 256 | margin: auto; 257 | border-radius: 50%; 258 | pointer-events: none; 259 | } 260 | 261 | .p-gui__switch-checkbox--active { 262 | background-color: #00ff89; 263 | box-shadow: 0 0 7px #00ff89; 264 | } 265 | ` 266 | ), F = ( 267 | /* css */ 268 | ` 269 | .p-gui__color { 270 | cursor: default; 271 | color: var(--color-text-dark); 272 | transition: var(--transition) color; 273 | } 274 | 275 | .p-gui__color:hover { 276 | color: var(--color-text-light); 277 | } 278 | 279 | .p-gui__color-picker { 280 | position: absolute; 281 | right: 5px; 282 | top: 0; 283 | bottom: 0; 284 | margin: auto; 285 | height: 21px; 286 | cursor: pointer; 287 | border-radius: 3px; 288 | border: 1px solid var(--color-border-2); 289 | outline: none; 290 | -webkit-appearance: none; 291 | padding: 0; 292 | background-color: transparent; 293 | border: 1px solid #222222; 294 | overflow: hidden; 295 | } 296 | 297 | .p-gui__color-picker::-webkit-color-swatch-wrapper { 298 | padding: 0; 299 | } 300 | .p-gui__color-picker::-webkit-color-swatch { 301 | border: none; 302 | } 303 | ` 304 | ), L = ( 305 | /* css */ 306 | ` 307 | .p-gui__vector2 { 308 | background: transparent; 309 | aspect-ratio: 1; 310 | padding-bottom: 0; 311 | color: var(--color-text-dark); 312 | } 313 | 314 | .p-gui__vector2:hover { 315 | color: var(--color-text-light); 316 | } 317 | 318 | .p-gui__vector2-area { 319 | position: relative; 320 | background: rgba(0, 0, 0, .3); 321 | margin-top: 8px; 322 | width: 100%; 323 | height: calc(100% - 28px); 324 | touch-action: none; 325 | } 326 | 327 | .p-gui__vector2-line { 328 | position: absolute; 329 | background: white; 330 | opacity: .3; 331 | pointer-events: none; 332 | } 333 | 334 | .p-gui__vector2-line-x { 335 | width: 100%; 336 | height: 1px; 337 | left: 0; 338 | top: 50%; 339 | transform: translateY(-50%); 340 | } 341 | 342 | .p-gui__vector2-line-y { 343 | width: 1px; 344 | height: 100%; 345 | top: 0; 346 | left: 50%; 347 | transform: translateX(-50%); 348 | } 349 | 350 | .p-gui__vector2-dot { 351 | position: absolute; 352 | top: 0; 353 | left: 0; 354 | width: 8px; 355 | height: 8px; 356 | border-radius: 50%; 357 | background: #d5d5d5; 358 | border: 2px solid #ff9999; 359 | transform: translate(-50%, -50%); 360 | pointer-events: none; 361 | } 362 | 363 | .p-gui__vector-value { 364 | display: inline-block; 365 | right: 7px; 366 | position: absolute; 367 | } 368 | ` 369 | ), H = ( 370 | /* css */ 371 | ` 372 | .p-gui__image-container { 373 | width: 100%; 374 | padding: 3px; 375 | display: flex; 376 | justify-content: flex-start; 377 | flex-wrap: wrap; 378 | box-sizing: border-box; 379 | } 380 | 381 | .p-gui__image { 382 | background-size: cover; 383 | cursor: pointer; 384 | position: relative; 385 | margin: 1px 2.5px 19px 2.5px; 386 | border-radius: var(--main-border-radius); 387 | flex: 0 0 calc(33.333% - 5px); 388 | height: 90px; 389 | background-position: center; 390 | color: var(--color-text-dark); 391 | transition: var(--transition) color; 392 | } 393 | 394 | .p-gui__image:hover { 395 | color: var(--color-text-light); 396 | } 397 | 398 | .p-gui__image::after { 399 | position: absolute; 400 | top: 0; 401 | left: 0; 402 | width: 100%; 403 | height: 100%; 404 | content: ''; 405 | border: 1px solid transparent; 406 | box-sizing: border-box; 407 | border-radius: var(--main-border-radius); 408 | transition: var(--transition) border-color; 409 | } 410 | .p-gui__image--selected::after { 411 | border-color: #06FF89; 412 | } 413 | 414 | .p-gui__image-text { 415 | position: absolute; 416 | bottom: -15px; 417 | text-shadow: 0 -1px 0 #111; 418 | white-space: nowrap; 419 | width: 100%; 420 | overflow: hidden; 421 | text-overflow: ellipsis; 422 | 423 | } 424 | ` 425 | ), N = ( 426 | /* css */ 427 | ` 428 | .p-gui__folder { 429 | width: 100%; 430 | position: relative; 431 | background: #434343; 432 | overflow: auto; 433 | margin-bottom: 2px; 434 | display: flex; 435 | flex-wrap: wrap; 436 | border: 1px solid grey; 437 | padding: 0 3px 0 3px; 438 | border-radius: var(--main-border-radius); 439 | box-sizing: border-box; 440 | } 441 | 442 | .p-gui__folder--first { 443 | margin-top: 0; 444 | } 445 | 446 | .p-gui__folder--closed { 447 | height: 32px; 448 | overflow: hidden; 449 | } 450 | 451 | .p-gui__folder-header { 452 | padding: 10px 5px; 453 | background-color: rgba(0, 0, 0, .5); 454 | color: white; 455 | cursor: pointer; 456 | width: 100%; 457 | margin: 0 -2px 2px -3px; 458 | } 459 | 460 | .p-gui__folder-header:hover { 461 | background-color: rgba(0, 0, 0, .75); 462 | } 463 | 464 | .p-gui__folder-arrow { 465 | width: 8px; 466 | height: 8px; 467 | display: inline-block; 468 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAHlBMVEUAAAD///////////////////////////////////8kfJuVAAAACXRSTlMA9Z1fCdMo1yxEJnA0AAAAK0lEQVQI12PABlRgjKkJUMZMYRhjpgqMAZSEMICSaIzpDWiKhdENhEhgAgATSg5jyWnYewAAAABJRU5ErkJggg==); 469 | background-size: contain; 470 | margin-right: 5px; 471 | transform: rotate(90deg) 472 | } 473 | 474 | .p-gui__folder--closed .p-gui__folder-arrow { 475 | transform: rotate(0deg); 476 | } 477 | ` 478 | ); 479 | function M(U) { 480 | return ( 481 | /* css */ 482 | ` 483 | .p-gui { 484 | --main-border-radius: 5px; 485 | --color-bg: #121212; 486 | --color-border: #484848; 487 | --color-border-2: rgba(255,255,255,.1); 488 | --color-text-light: #ffffff; 489 | --color-text-dark: #bbbbbb; 490 | --color-accent: #1681ca; 491 | --color-accent-hover: #218fda; 492 | --transition: .1s linear; 493 | 494 | position: ${U}; 495 | top: 0; 496 | left: 0; 497 | transform: translate3d(0,0,0); 498 | padding-top: 21px; 499 | padding-inline: 3px; 500 | background: var(--color-bg); 501 | display: block; 502 | justify-content: center; 503 | flex-wrap: wrap; 504 | font-family: "Arial Rounded MT Bold", Arial, sans-serif; 505 | width: 290px; 506 | overflow: auto; 507 | box-shadow: 0 0 2px black; 508 | box-sizing: border-box; 509 | z-index: 99999; 510 | user-select: none; 511 | border-bottom-right-radius: 3px; 512 | border-bottom-left-radius: 3px; 513 | cursor: auto; 514 | border-radius: var(--main-border-radius); 515 | border: 1px solid var(--color-border); 516 | line-height: normal; 517 | transition: var(--transition) opacity; 518 | } 519 | 520 | .p-gui:hover { 521 | opacity: 1!important; 522 | } 523 | 524 | .p-gui * { 525 | font-size: 11px; 526 | } 527 | 528 | .p-gui::-webkit-scrollbar, 529 | .p-gui *::-webkit-scrollbar { 530 | width: 10px; 531 | } 532 | 533 | .p-gui::-webkit-scrollbar-track, 534 | .p-gui *::-webkit-scrollbar-track { 535 | background: #2f2f2f; 536 | border-radius: 3px; 537 | } 538 | 539 | .p-gui::-webkit-scrollbar-thumb, 540 | .p-gui *::-webkit-scrollbar-thumb { 541 | background: #757576; 542 | border-radius: 10px; 543 | box-sizing: border-box; 544 | border: 1px solid #2f2f2f; 545 | } 546 | 547 | .p-gui--collapsed { 548 | height: 0; 549 | padding: 21px 10px 0 10px; 550 | overflow: hidden; 551 | } 552 | 553 | .p-gui__header { 554 | position: absolute; 555 | top: 0; 556 | left: 0; 557 | width: 100%; 558 | height: 20px; 559 | background-color: rgba(0, 0, 0, .8); 560 | cursor: grab; 561 | color: grey; 562 | font-size: 10px; 563 | line-height: 20px; 564 | padding-left: 12px; 565 | box-sizing: border-box; 566 | touch-action: none; 567 | } 568 | 569 | .p-gui__header-close { 570 | width: 20px; 571 | height: 20px; 572 | position: absolute; 573 | top: 0; 574 | right: 5px; 575 | cursor: pointer; 576 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABFJREFUCNdjIAb8//8BjIkAAOrOBd3TR0jRAAAAAElFTkSuQmCC); 577 | background-size: 50% 50%; 578 | background-position: center; 579 | background-repeat: no-repeat; 580 | } 581 | 582 | .p-gui--collapsed .p-gui__header-close { 583 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABVJREFUCNdjYEhgIIj///8AwsSoBQD43QydY5mb0QAAAABJRU5ErkJggg==); 584 | } 585 | 586 | .p-gui__slider, 587 | .p-gui__button, 588 | .p-gui__switch, 589 | .p-gui__list, 590 | .p-gui__vector2, 591 | .p-gui__color { 592 | width: 100%; 593 | padding: 7px; 594 | cursor: pointer; 595 | position: relative; 596 | box-sizing: border-box; 597 | margin-block: 3px; 598 | border: 1px solid var(--color-border-2); 599 | border-radius: var(--main-border-radius); 600 | transition: var(--transition) border-color; 601 | } 602 | 603 | .p-gui__slider:hover, 604 | .p-gui__button:hover, 605 | .p-gui__switch:hover, 606 | .p-gui__list:hover, 607 | .p-gui__vector2:hover, 608 | .p-gui__color:hover { 609 | border-color: rgba(255,255,255,.2); 610 | } 611 | 612 | ${P} 613 | 614 | ${H} 615 | 616 | ${$} 617 | 618 | ${D} 619 | 620 | ${O} 621 | 622 | ${F} 623 | 624 | ${L} 625 | 626 | ${N} 627 | ` 628 | ); 629 | } 630 | class C { 631 | constructor(e = {}) { 632 | if (this.firstParent = this, e.container ? (this.container = typeof e.container == "string" ? document.querySelector(e.container) : e.container, this.position_type = "absolute") : (this.container = document.body, this.position_type = "fixed"), this.propReferences = [], this.folders = [], e.isFolder) { 633 | this._folderConstructor(e.folderOptions); 634 | return; 635 | } 636 | typeof e.onUpdate == "function" && (this.onUpdate = e.onUpdate), this.name = e != null && typeof e.name == "string" ? e.name : "", this.backgroundColor = e.color || null, this.opacity = e.opacity || 1, this.container == document.body ? this.maxHeight = window.innerHeight : this.maxHeight = Math.min(this.container.clientHeight, window.innerHeight), e.maxHeight && (this.initMaxHeight = e.maxHeight, this.maxHeight = Math.min(this.initMaxHeight, this.maxHeight)), this.screenCorner = this._parseScreenCorner(e.position), window.perfectGUI || (window.perfectGUI = {}), window.perfectGUI.instanceCounter == null ? window.perfectGUI.instanceCounter = 0 : window.perfectGUI.instanceCounter++, this.instanceId = window.perfectGUI.instanceCounter, this.wrapperWidth = e.width || 290, this.stylesheet = document.createElement("style"), this.stylesheet.setAttribute("type", "text/css"), this.stylesheet.setAttribute("id", "lm-gui-stylesheet"), document.head.append(this.stylesheet), this.instanceId == 0 && this._addStyles(`${M(this.position_type)}`), this._styleInstance(), this._addWrapper(), this.wrapper.setAttribute("data-corner-x", this.screenCorner.x), this.wrapper.setAttribute("data-corner-y", this.screenCorner.y), e.autoRepositioning != !1 && window.addEventListener("resize", this._handleResize.bind(this)), this._handleResize(), this.hasBeenDragged = !1, e.draggable == !0 && this._makeDraggable(), this.closed = !1, e.closed && this.toggleClose(); 637 | } 638 | _styleInstance() { 639 | let e = this._getScrollbarWidth(this.container); 640 | if (this.screenCorner.x == "left" ? this.xOffset = 0 : this.xOffset = this.container.clientWidth - this.wrapperWidth - e, this.instanceId > 0) { 641 | let t = this.container.querySelectorAll(".p-gui"); 642 | for (let i = 0; i < t.length; i++) 643 | this.screenCorner.y == t[i].dataset.cornerY && (this.screenCorner.x == "left" && t[i].dataset.cornerX == "left" ? this.xOffset += t[i].offsetWidth : this.screenCorner.x == "right" && t[i].dataset.cornerX == "right" && (this.xOffset -= t[i].offsetWidth)); 644 | } 645 | this.yOffset = 0, this.position = { 646 | prevX: this.xOffset, 647 | prevY: this.yOffset, 648 | x: this.xOffset, 649 | y: this.yOffset 650 | }, this._addStyles(`#p-gui-${this.instanceId} { 651 | width: ${this.wrapperWidth}px; 652 | max-height: ${this.maxHeight}px; 653 | transform: translate3d(${this.xOffset}px,${this.yOffset}px,0); 654 | ${this.screenCorner.y == "top" ? "" : "top: auto; bottom: 0;"} 655 | ${this.backgroundColor ? "background: " + this.backgroundColor + ";" : ""} 656 | opacity: ${this.opacity}; 657 | }`); 658 | } 659 | _folderConstructor(e) { 660 | this.wrapper = e.wrapper, this.isFolder = !0, this.parent = e.parent, this.firstParent = e.firstParent; 661 | } 662 | _parseScreenCorner(e) { 663 | let t = { x: "right", y: "top" }; 664 | return e == null || (typeof e != "string" && console.error("[perfect-gui] Position must be a string."), e.includes("left") && (t.x = "left"), e.includes("bottom") && (t.y = "bottom")), t; 665 | } 666 | _getScrollbarWidth(e) { 667 | return e === document.body ? window.innerWidth - document.documentElement.clientWidth : e.offsetWidth - e.clientWidth; 668 | } 669 | _handleResize() { 670 | if (this.container == document.body ? this.maxHeight = window.innerHeight : this.maxHeight = Math.min(this.container.clientHeight, window.innerHeight), this.initMaxHeight && (this.maxHeight = Math.min(this.initMaxHeight, this.maxHeight)), this.wrapper.style.maxHeight = this.maxHeight + "px", this.hasBeenDragged) 671 | return; 672 | let e = this._getScrollbarWidth(this.container); 673 | if (this.xOffset = this.screenCorner.x == "left" ? 0 : this.container.clientWidth - this.wrapperWidth - e, this.instanceId > 0) { 674 | let t = this.container.querySelectorAll(`.p-gui:not(#${this.wrapper.id}):not([data-dragged])`); 675 | for (let i = 0; i < t.length && !(parseInt(t[i].id.replace("p-gui-", "")) > this.instanceId); i++) 676 | this.screenCorner.y == t[i].dataset.cornerY && (this.screenCorner.x == "left" && t[i].dataset.cornerX == "left" ? this.xOffset += t[i].offsetWidth : this.screenCorner.x == "right" && t[i].dataset.cornerX == "right" && (this.xOffset -= t[i].offsetWidth)); 677 | } 678 | this.position = { prevX: this.xOffset, prevY: this.yOffset, x: this.xOffset, y: this.yOffset }, this.wrapper.style.transform = `translate3d(${this.position.x}px, ${this.position.y}px, 0)`; 679 | } 680 | _addStyles(e) { 681 | this.stylesheet.innerHTML += e; 682 | } 683 | _addWrapper() { 684 | this.wrapper = document.createElement("div"), this.wrapper.id = "p-gui-" + this.instanceId, this.wrapper.className = "p-gui", this.container.append(this.wrapper), this.header = document.createElement("div"), this.header.className = "p-gui__header", this.header.textContent = this.name, this.header.style = `${this.backgroundColor ? "border-color: " + this.backgroundColor + ";" : ""}`, this.wrapper.append(this.header); 685 | const e = document.createElement("div"); 686 | e.className = "p-gui__header-close", e.addEventListener("click", this.toggleClose.bind(this)), this.header.append(e); 687 | } 688 | button(e, t) { 689 | let i = ""; 690 | typeof e != "string" ? typeof e == "object" && (e != null && e.hasOwnProperty("name")) ? i = e.name == "" ? " " : e.name : i = " " : i = e == "" ? " " : e; 691 | const a = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? i : null; 692 | this.imageContainer = null; 693 | const s = document.createElement("div"); 694 | s.className = "p-gui__button", s.textContent = i, a && s.setAttribute("title", a), s.addEventListener("click", () => { 695 | t && t(), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 696 | }), this.wrapper.append(s), typeof e.color == "string" && (s.style.setProperty("--color-accent", e.color), s.style.setProperty("--color-accent-hover", e.hoverColor || e.color)); 697 | } 698 | image(e = {}, t) { 699 | if (typeof e != "object") 700 | throw Error(`[GUI] image() first parameter must be an object. Received: ${typeof e}.`); 701 | let i; 702 | if (typeof e.path == "string") 703 | i = e.path; 704 | else 705 | throw typeof e.path == null ? Error("[GUI] image() path must be provided.") : Error("[GUI] image() path must be a string."); 706 | let a = i.replace(/^.*[\\\/]/, ""), s; 707 | e.name == null ? s = a : s = typeof e.name == "string" && e.name || " "; 708 | const o = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? s : null, r = e.selected === !0, n = e.selectionBorder !== !1; 709 | let c = ""; 710 | e.width && (typeof e.width == "number" && (e.width += "px"), c += `flex: 0 0 calc(${e.width} - 5px); `), e.height && (typeof e.height == "number" && (e.height += "px"), c += `height: ${e.height}; `), this.imageContainer || (this.imageContainer = document.createElement("div"), this.imageContainer.className = "p-gui__image-container", this.wrapper.append(this.imageContainer)); 711 | const l = document.createElement("div"); 712 | l.className = "p-gui__image", l.style = "background-image: url(" + i + "); " + c, o && l.setAttribute("title", o), this.imageContainer.append(l), r && n && l.classList.add("p-gui__image--selected"); 713 | const d = document.createElement("div"); 714 | return d.className = "p-gui__image-text", d.textContent = s, l.append(d), l.addEventListener("click", () => { 715 | let p = l.parentElement.querySelectorAll(".p-gui__image--selected"); 716 | for (let h = 0; h < p.length; h++) 717 | p[h].classList.remove("p-gui__image--selected"); 718 | n && l.classList.add("p-gui__image--selected"), typeof t == "function" && t({ path: i, text: s }), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 719 | }), l; 720 | } 721 | slider(e = {}, t) { 722 | const i = new R(this, e, t); 723 | this.wrapper.append(i); 724 | } 725 | toggle(e = {}, t) { 726 | if (typeof e != "object") 727 | throw Error(`[GUI] toggle() first parameter must be an object. Received: ${typeof e}.`); 728 | let i = typeof e.name == "string" && e.name || " ", a = !1, s = null, o = e.obj, r = e.prop, n = typeof e.value == "boolean" ? e.value : null; 729 | if (n !== null) 730 | (r != null || o != null) && console.warn('[GUI] toggle() "obj" and "prop" parameters are ignored when a "value" parameter is used.'); 731 | else if (r != null && o != null) { 732 | if (typeof r != "string") 733 | throw Error(`[GUI] toggle() "prop" parameter must be an string. Received: ${typeof r}.`); 734 | if (typeof o != "object") 735 | throw Error(`[GUI] toggle() "obj" parameter must be an object. Received: ${typeof o}.`); 736 | i == " " && (i = r), s = this.propReferences.push(o[r]) - 1, a = !0; 737 | } else 738 | (r != null && o == null || r == null && o == null) && console.warn('[GUI] toggle() "obj" and "prop" parameters must be used together.'); 739 | const c = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? i : null; 740 | this.imageContainer = null; 741 | const l = document.createElement("div"); 742 | l.textContent = i, l.className = "p-gui__switch", c && l.setAttribute("title", c), this.wrapper.append(l), l.addEventListener("click", (h) => { 743 | const u = h.target.childNodes[1]; 744 | let f = !0; 745 | u.classList.contains("p-gui__switch-checkbox--active") && (f = !1), u.classList.toggle("p-gui__switch-checkbox--active"), a ? o[r] = f : typeof t == "function" && t(f), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 746 | }); 747 | let d = (() => a ? o[r] ? " p-gui__switch-checkbox--active" : "" : n ? " p-gui__switch-checkbox--active" : "")(); 748 | const p = document.createElement("div"); 749 | p.className = "p-gui__switch-checkbox" + d, l.append(p), a && Object.defineProperty(o, r, { 750 | set: (h) => { 751 | this.propReferences[s] = h, h ? p.classList.add("p-gui__switch-checkbox--active") : p.classList.remove("p-gui__switch-checkbox--active"), typeof t == "function" && t(h); 752 | }, 753 | get: () => this.propReferences[s] 754 | }); 755 | } 756 | list(e = {}, t) { 757 | if (typeof e != "object") 758 | throw Error(`[GUI] list() first parameter must be an object. Received: ${typeof e}.`); 759 | let i = typeof e.name == "string" ? e.name : " ", a = !1, s = null, o = e.obj, r = e.prop, n = Array.isArray(e.values) ? e.values : null, c, l = typeof n[0] != "string"; 760 | const d = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? i : null; 761 | if (t = typeof t == "function" ? t : null, e.value !== void 0 || e.value === void 0 && o === void 0 && r === void 0) 762 | (r != null || o != null) && console.warn('[GUI] list() "obj" and "prop" parameters are ignored when a "value" parameter is used.'), c = (() => { 763 | if (!n) 764 | return null; 765 | if (typeof e.value == "string") 766 | return n.indexOf(e.value); 767 | if (typeof e.value == "number") 768 | return e.value; 769 | })(); 770 | else if (r != null && o != null) { 771 | if (typeof r != "string") 772 | throw Error(`[GUI] list() "prop" parameter must be an string. Received: ${typeof r}.`); 773 | if (typeof o != "object") 774 | throw Error(`[GUI] list() "obj" parameter must be an object. Received: ${typeof o}.`); 775 | c = (() => { 776 | if (!n) 777 | return null; 778 | if (typeof o[r] == "string") 779 | return l ? n.find((u) => u.value === o[r]).value : n.indexOf(o[r]); 780 | if (typeof o[r] == "number") 781 | return l ? n.find((u) => u.value === o[r]).value : o[r]; 782 | })(), s = this.propReferences.push(o[r]) - 1, a = !0; 783 | } else 784 | (r != null && o == null || r == null && o == null) && console.warn('[GUI] list() "obj" and "prop" parameters must be used together.'); 785 | this.imageContainer = null; 786 | let p = document.createElement("div"); 787 | p.className = "p-gui__list", p.textContent = i, d && p.setAttribute("title", d), this.wrapper.append(p); 788 | let h = document.createElement("select"); 789 | p.append(h), h.className = "p-gui__list-dropdown", h.addEventListener("change", (u) => { 790 | a ? o[r] = u.target.value : t && t(u.target.value), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 791 | }), n && n.forEach((u, f) => { 792 | const g = l ? u.name : u, m = l ? u.value : u; 793 | let x = document.createElement("option"); 794 | x.setAttribute("value", m), x.textContent = g, h.append(x), (!l && c == f || l && c == m) && x.setAttribute("selected", ""); 795 | }), a && Object.defineProperty(o, r, { 796 | set: (u) => { 797 | let f, g, m; 798 | l ? (m = n.find((v) => v.value == u), g = (m == null ? void 0 : m.value) || n[0].value, f = n.indexOf(m)) : (typeof u == "string" && (f = n.indexOf(u), g = u), typeof u == "number" && (f = u, g = n[u])), this.propReferences[s] = l ? g : u; 799 | const x = h.querySelector("[selected]"); 800 | x && x.removeAttribute("selected"), h.querySelectorAll("option")[f].setAttribute("selected", ""), typeof t == "function" && t(l ? m : g, f); 801 | }, 802 | get: () => this.propReferences[s] 803 | }); 804 | } 805 | color(e = {}, t) { 806 | if (typeof e != "object") 807 | throw Error(`[GUI] color() first parameter must be an object. Received: ${typeof e}.`); 808 | let i = typeof e.name == "string" && e.name || " ", a = !1, s = null, o = e.obj, r = e.prop, n; 809 | const c = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? i : null; 810 | if (typeof e.value == "string" && (e.value.length != 7 || e.value[0] != "#" ? console.error(`[GUI] color() 'value' parameter must be an hexadecimal string in the format "#ffffff". Received: "${e.value}".`) : n = e.value), n || (n = "#000000"), e.value !== void 0) 811 | (r != null || o != null) && console.warn('[GUI] color() "obj" and "prop" parameters are ignored when a "value" parameter is used.'); 812 | else if (r != null && o != null) { 813 | if (typeof r != "string") 814 | throw Error(`[GUI] color() "prop" parameter must be an string. Received: ${typeof r}.`); 815 | if (typeof o != "object") 816 | throw Error(`[GUI] color() "obj" parameter must be an object. Received: ${typeof o}.`); 817 | i == " " && (i = r), s = this.propReferences.push(o[r]) - 1, a = !0; 818 | } else 819 | (r != null && o == null || r == null && o == null) && console.warn('[GUI] color() "obj" and "prop" parameters must be used together.'); 820 | this.imageContainer = null; 821 | const l = document.createElement("div"); 822 | l.className = "p-gui__color", l.textContent = i, c && l.setAttribute("title", c), this.wrapper.append(l); 823 | const d = document.createElement("input"); 824 | d.className = "p-gui__color-picker", d.setAttribute("type", "color"), d.value = n, l.append(d), typeof t == "function" && d.addEventListener("input", () => { 825 | a ? o[r] = d.value : typeof t == "function" && t(d.value), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 826 | }), a && Object.defineProperty(o, r, { 827 | set: (p) => { 828 | this.propReferences[s] = p, d.value = p, typeof t == "function" && t(p); 829 | }, 830 | get: () => this.propReferences[s] 831 | }); 832 | } 833 | vector2(e = {}, t) { 834 | if (typeof e != "object") 835 | throw Error(`[GUI] vector2() first parameter must be an object. Received: ${typeof e}.`); 836 | let i = typeof e.name == "string" && e.name || " "; 837 | const a = e.x.min ?? 0, s = e.x.max ?? 1, o = e.y.min ?? 0, r = e.y.max ?? 1, n = e.x.step || (s - a) / 100, c = e.y.step || (r - o) / 100, l = this._countDecimals(n), d = this._countDecimals(c), p = e.x.obj, h = e.x.prop, u = this.propReferences.push(p[h]) - 1, f = e.y.obj, g = e.y.prop, m = this.propReferences.push(f[g]) - 1, x = typeof e.tooltip == "string" ? e.tooltip : e.tooltip === !0 ? i : null; 838 | t = typeof t == "function" ? t : null, this.imageContainer = null; 839 | const v = document.createElement("div"); 840 | v.className = "p-gui__vector2", v.textContent = i, x && v.setAttribute("title", x), this.wrapper.append(v); 841 | const y = document.createElement("div"); 842 | y.className = "p-gui__vector-value", y.textContent = p[h] + ", " + f[g], v.append(y); 843 | const b = document.createElement("div"); 844 | b.className = "p-gui__vector2-area", v.append(b), b.addEventListener("click", (_) => { 845 | const k = parseFloat(this._mapLinear(_.offsetX, 0, b.clientWidth, a, s)), E = parseFloat(this._mapLinear(_.offsetY, 0, b.clientHeight, r, o)); 846 | p[h] = k.toFixed(l), f[g] = E.toFixed(d), t && t(p[h], p[g]), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 847 | }); 848 | let A = !1; 849 | b.addEventListener("pointerdown", (_) => { 850 | A = !0; 851 | }), b.addEventListener("pointerup", () => { 852 | A = !1; 853 | }), b.addEventListener("pointermove", (_) => { 854 | if (A) { 855 | const k = parseFloat(this._mapLinear(_.offsetX, 0, b.clientWidth, a, s)), E = parseFloat(this._mapLinear(_.offsetY, 0, b.clientHeight, r, o)); 856 | p[h] = k.toFixed(l), f[g] = E.toFixed(d), t && t(p[h], p[g]), this.onUpdate ? this.onUpdate() : this.isFolder && this.firstParent.onUpdate && this.firstParent.onUpdate(); 857 | } 858 | }); 859 | const j = document.createElement("div"); 860 | j.className = "p-gui__vector2-line p-gui__vector2-line-x", b.append(j); 861 | const I = document.createElement("div"); 862 | I.className = "p-gui__vector2-line p-gui__vector2-line-y", b.append(I); 863 | const w = document.createElement("div"); 864 | w.className = "p-gui__vector2-dot", b.append(w), w.style.left = this._mapLinear(p[h], a, s, 0, b.clientWidth) + "px", w.style.top = this._mapLinear(f[g], o, r, b.clientHeight, 0) + "px", Object.defineProperty(p, h, { 865 | set: (_) => { 866 | this.propReferences[u] = _, w.style.left = this._mapLinear(_, a, s, 0, b.clientWidth) + "px", y.textContent = String(_) + ", " + f[g]; 867 | }, 868 | get: () => this.propReferences[u] 869 | }), Object.defineProperty(f, g, { 870 | set: (_) => { 871 | this.propReferences[m] = _, w.style.top = this._mapLinear(_, o, r, b.clientHeight, 0) + "px", y.textContent = p[h] + ", " + String(_); 872 | }, 873 | get: () => this.propReferences[m] 874 | }); 875 | } 876 | folder(e = {}) { 877 | let t = typeof e.closed == "boolean" ? e.closed : !1, i = e.name || "", a = e.color || null, s = e.maxHeight || null; 878 | this.imageContainer = null; 879 | let o = "p-gui__folder"; 880 | this.folders.length == 0 && (o += " p-gui__folder--first"), t && (o += " p-gui__folder--closed"); 881 | let r = a ? `background-color: ${a};` : ""; 882 | r += s ? `max-height: ${s}px;` : ""; 883 | const n = document.createElement("div"); 884 | n.className = o, n.style = r, this.wrapper.append(n); 885 | const c = document.createElement("div"); 886 | c.innerHTML = `${i}`, c.className = "p-gui__folder-header", n.append(c), c.addEventListener("click", () => { 887 | n.classList.toggle("p-gui__folder--closed"); 888 | }); 889 | let l = new C({ isFolder: !0, folderOptions: { 890 | wrapper: n, 891 | parent: this, 892 | firstParent: this.firstParent 893 | } }); 894 | return this.folders.push(l), l; 895 | } 896 | _makeDraggable() { 897 | var e = this; 898 | this.header.addEventListener("pointerdown", t), this.header.addEventListener("pointerup", a); 899 | function t(s) { 900 | s.preventDefault(), e.position.initX = e.position.x, e.position.initY = e.position.y, e.position.prevX = s.clientX, e.position.prevY = s.clientY, document.addEventListener("pointermove", i); 901 | } 902 | function i(s) { 903 | s.preventDefault(), e.hasBeenDragged || (e.hasBeenDragged = !0, e.wrapper.setAttribute("data-dragged", "true")), e.position.x = e.position.initX + s.clientX - e.position.prevX, e.position.y = e.position.initY + s.clientY - e.position.prevY, e.wrapper.style.transform = "translate3d(" + e.position.x + "px," + e.position.y + "px,0)"; 904 | } 905 | function a(s) { 906 | document.removeEventListener("pointermove", i); 907 | } 908 | } 909 | toggleClose() { 910 | this.closed = !this.closed, this.closed ? (this.previousInnerScroll = this.wrapper.scrollTop, this.wrapper.scrollTo(0, 0)) : this.wrapper.scrollTo(0, this.previousInnerScroll), this.wrapper.classList.toggle("p-gui--collapsed"); 911 | } 912 | kill() { 913 | this.wrapper.remove(); 914 | } 915 | _mapLinear(e, t, i, a, s) { 916 | return a + (e - t) * (s - a) / (i - t); 917 | } 918 | _countDecimals(e) { 919 | const t = e.toString(), i = t.indexOf("."); 920 | return i === -1 ? 0 : t.length - i - 1; 921 | } 922 | } 923 | export { 924 | C as default 925 | }; 926 | -------------------------------------------------------------------------------- /dist/perfect-gui.umd.js: -------------------------------------------------------------------------------- 1 | (function(y,A){typeof exports=="object"&&typeof module<"u"?module.exports=A():typeof define=="function"&&define.amd?define(A):(y=typeof globalThis<"u"?globalThis:y||self,y["Perfect GUI"]=A())})(this,function(){"use strict";class y{constructor(e,t={},i){if(this.parent=e,this.propReferences=[],typeof t!="object")throw Error(`[GUI] slider() first parameter must be an object. Received: ${typeof t}.`);let a=typeof t.name=="string"&&t.name||" ";this.isObject=!1;let s=null;this.obj=t.obj,this.prop=t.prop;let o=typeof t.value=="number"?t.value:null;if(this.min=t.min??0,this.max=t.max??1,this.step=t.step||(this.max-this.min)/100,this.decimals=this.parent._countDecimals(this.step),this.callback=typeof i=="function"?i:null,o!==null)(this.prop!=null||this.obj!=null)&&console.warn('[GUI] slider() "obj" and "prop" parameters are ignored when a "value" parameter is used.');else if(this.prop!=null&&this.obj!=null){if(typeof this.prop!="string")throw Error(`[GUI] slider() "prop" parameter must be an string. Received: ${typeof this.prop}.`);if(typeof this.obj!="object")throw Error(`[GUI] slider() "obj" parameter must be an object. Received: ${typeof this.obj}.`);a==" "&&(a=this.prop),s=this.propReferences.push(this.obj[this.prop])-1,this.isObject=!0}else(this.prop!=null&&this.obj==null||this.prop==null&&this.obj!=null)&&console.warn('[GUI] slider() "obj" and "prop" parameters must be used together.'),o=(this.max-this.min)/2;const r=typeof t.tooltip=="string"?t.tooltip:t.tooltip===!0?a:null;this.imageContainer=null;const n=document.createElement("div");n.className="p-gui__slider",r&&n.setAttribute("title",r);const c=document.createElement("div");c.className="p-gui__slider-name",c.textContent=a,n.append(c),this.ctrlDiv=document.createElement("div"),this.ctrlDiv.className="p-gui__slider-ctrl",this.ctrlDiv.setAttribute("type","range"),this.ctrlDiv.setAttribute("min",this.min),this.ctrlDiv.setAttribute("max",this.max),n.append(this.ctrlDiv);const l=document.createElement("div");return l.className="p-gui__slider-bar",this.ctrlDiv.append(l),this.handle=document.createElement("div"),this.handle.className="p-gui__slider-handle",this.ctrlDiv.append(this.handle),this.filling=document.createElement("div"),this.filling.className="p-gui__slider-filling",l.append(this.filling),this.valueInput=document.createElement("input"),this.valueInput.className="p-gui__slider-value",this.valueInput.value=this.isObject?this.obj[this.prop]:o,n.append(this.valueInput),setTimeout(()=>{const d=this.handle.offsetWidth;this.handle.position=this._mapLinear(this.valueInput.value,this.min,this.max,d/2,88-d/2),this.handle.position=Math.min(this.handle.position,88-d/2),this.handle.position=Math.max(this.handle.position,d/2),this.handle.style.transform=`translate(-50%, -50%) translateX(${this.handle.position}px)`,this.filling.style.width=`${this.handle.position}px`},0),this.valueInput.addEventListener("change",()=>{this._updateHandlePositionFromValue(),this._triggerCallbacks()}),this.ctrlDiv.addEventListener("pointerdown",d=>{this.ctrlDiv.pointerDown=!0,this.ctrlDiv.prevPosition=d.clientX,this._updateHandlePositionFromPointer(d,!0)}),window.addEventListener("pointerup",d=>{this.ctrlDiv.pointerDown=!1}),window.addEventListener("pointermove",d=>{this.ctrlDiv.pointerDown&&(this.ctrlDiv.pointerDelta=d.clientX-this.ctrlDiv.prevPosition,this._updateHandlePositionFromPointer(d))}),this.isObject&&Object.defineProperty(this.obj,this.prop,{set:d=>{this.propReferences[s]=d,this.valueInput.value=d,this._updateHandlePositionFromValue(),this.callback&&this.callback(parseFloat(this.valueInput.value))},get:()=>this.propReferences[s]}),n}_updateHandlePositionFromPointer(e,t=!1){const i=this.ctrlDiv.offsetWidth,a=this.handle.offsetWidth,s=e.clientX-this.ctrlDiv.prevPosition,o=parseFloat(this.valueInput.value);let r;t?r=e.offsetX:r=this.handle.position+s,r=Math.max(a/2,Math.min(r,i-a/2));let n=this.min+(this.max-this.min)*(r-a/2)/(i-a);n>o?n=this._quantizeFloor(n,this.step):n=this._quantizeCeil(n,this.step),n=parseFloat(n.toFixed(9));const c=parseFloat((o+this.step).toFixed(9)),l=parseFloat((o-this.step).toFixed(9));(n>=c||n<=l)&&(n=n.toFixed(this.decimals),this.valueInput.value=n,this.ctrlDiv.prevPosition=e.clientX,this.handle.style.transform=`translate(-50%, -50%) translateX(${r}px)`,this.handle.position=r,this.filling.style.width=this.handle.position+"px",this._triggerCallbacks())}_updateHandlePositionFromValue(){const e=this.ctrlDiv.offsetWidth,t=this.handle.offsetWidth;let i=this._mapLinear(this.valueInput.value,this.min,this.max,t/2,e-t/2);i=Math.max(t/2,Math.min(i,e-t/2)),this.handle.style.transform=`translate(-50%, -50%) translateX(${i}px)`,this.handle.position=i,this.filling.style.width=this.handle.position+"px"}_triggerCallbacks(){this.isObject?this.obj[this.prop]=parseFloat(this.valueInput.value):this.callback&&this.callback(parseFloat(this.valueInput.value)),this.parent.onUpdate?this.parent.onUpdate():this.parent.isFolder&&this.parent.firstParent.onUpdate&&this.parent.firstParent.onUpdate()}_mapLinear(e,t,i,a,s){return a+(e-t)*(s-a)/(i-t)}_quantize(e,t){return t*Math.round(e/t)}_quantizeCeil(e,t){return t*Math.ceil(e/t)}_quantizeFloor(e,t){return t*Math.floor(e/t)}}const A=` 2 | .p-gui__button { 3 | background: var(--color-accent); 4 | text-align: center; 5 | color: white; 6 | border: none; 7 | border: 1px solid transparent; 8 | box-sizing: border-box; 9 | transition: var(--transition) background, var(--transition) border-color; 10 | } 11 | 12 | .p-gui__button:hover { 13 | background: var(--color-accent-hover); 14 | color: var(--color-text-light); 15 | border-color: rgba(255, 255, 255, 0.2); 16 | } 17 | 18 | .p-gui__folder .p-gui__button { 19 | margin-inline: 0; 20 | } 21 | `,O=` 22 | .p-gui__slider { 23 | position: relative; 24 | min-height: 14px; 25 | display: flex; 26 | align-items: center; 27 | justify-content: space-between; 28 | gap: 10px; 29 | color: var(--color-text-dark); 30 | transition: color var(--transition); 31 | } 32 | 33 | .p-gui__slider:hover { 34 | color: var(--color-text-light); 35 | } 36 | 37 | .p-gui__slider-name { 38 | width: 50%; 39 | text-overflow: ellipsis; 40 | overflow: hidden; 41 | } 42 | 43 | .p-gui__slider-ctrl { 44 | -webkit-appearance: none; 45 | padding: 0; 46 | font: inherit; 47 | outline: none; 48 | box-sizing: border-box; 49 | cursor: pointer; 50 | position: relative; 51 | right: 0; 52 | height: 14px; 53 | margin: 0 0 0 auto; 54 | width: 37%; 55 | } 56 | 57 | .p-gui__slider-bar { 58 | position: absolute; 59 | top: 50%; 60 | left: 0; 61 | height: 2px; 62 | background: rgba(255, 255, 255, .2); 63 | width: 100%; 64 | transform: translateY(-50%); 65 | } 66 | 67 | .p-gui__slider-filling { 68 | position: absolute; 69 | top: -25%; 70 | left: 0; 71 | height: 150%; 72 | background: var(--color-accent); 73 | width: 0; 74 | } 75 | 76 | .p-gui__slider:hover .p-gui__slider-filling { 77 | background: var(--color-accent-hover); 78 | } 79 | 80 | .p-gui__slider-handle { 81 | width: 15px; 82 | height: 8px; 83 | position: absolute; 84 | top: 50%; 85 | left: 0; 86 | border-radius: 2px; 87 | transform: translate(-50%, -50%); 88 | pointer-events: none; 89 | background: var(--color-text-dark); 90 | box-shadow: 0 0 2px rgba(0, 0, 0, .5); 91 | } 92 | 93 | .p-gui__slider:hover .p-gui__slider-handle { 94 | background: var(--color-text-light); 95 | } 96 | 97 | .p-gui__slider-value { 98 | display: inline-block; 99 | right: 7px; 100 | width: 13%; 101 | border: none; 102 | color: white; 103 | border-radius: 2px; 104 | background: rgba(255, 255, 255, 0.1); 105 | padding: 2px 4px; 106 | color: inherit; 107 | } 108 | 109 | .p-gui__slider-value:focus { 110 | outline: none; 111 | } 112 | `,$=` 113 | .p-gui__list { 114 | cursor: default; 115 | color: var(--color-text-dark); 116 | transition: var(--transition) color; 117 | } 118 | 119 | .p-gui__list:hover { 120 | color: var(--color-text-light); 121 | } 122 | 123 | .p-gui__list-dropdown { 124 | background: rgba(255, 255, 255,.05); 125 | color: white; 126 | padding: 0 12px 0 5px; 127 | top: 0px; 128 | } 129 | 130 | .p-gui__list-dropdown { 131 | position: absolute; 132 | right: 5px; 133 | top: 0; 134 | bottom: 0; 135 | margin: auto; 136 | height: 21px; 137 | cursor: pointer; 138 | border-radius: 3px; 139 | border: 1px solid var(--color-border-2); 140 | outline: none; 141 | } 142 | 143 | .p-gui__list-dropdown:hover { 144 | background: rgba(255, 255, 255, .1); 145 | } 146 | `,D=` 147 | .p-gui__switch { 148 | background: rgba(255, 255, 255, .05); 149 | color: var(--color-text-dark); 150 | transition: var(--transition) background, var(--transition) color; 151 | } 152 | 153 | .p-gui__switch:hover { 154 | background: rgba(255, 255, 255, .1); 155 | color: var(--color-text-light); 156 | } 157 | 158 | .p-gui__folder .p-gui__switch { 159 | margin-inline: 0; 160 | } 161 | 162 | .p-gui__switch-checkbox { 163 | width: 5px; 164 | height: 5px; 165 | background-color: rgba(0, 0, 0, .5); 166 | border: 1px solid grey; 167 | position: absolute; 168 | top: 0; 169 | right: 10px; 170 | bottom: 0; 171 | margin: auto; 172 | border-radius: 50%; 173 | pointer-events: none; 174 | } 175 | 176 | .p-gui__switch-checkbox--active { 177 | background-color: #00ff89; 178 | box-shadow: 0 0 7px #00ff89; 179 | } 180 | `,F=` 181 | .p-gui__color { 182 | cursor: default; 183 | color: var(--color-text-dark); 184 | transition: var(--transition) color; 185 | } 186 | 187 | .p-gui__color:hover { 188 | color: var(--color-text-light); 189 | } 190 | 191 | .p-gui__color-picker { 192 | position: absolute; 193 | right: 5px; 194 | top: 0; 195 | bottom: 0; 196 | margin: auto; 197 | height: 21px; 198 | cursor: pointer; 199 | border-radius: 3px; 200 | border: 1px solid var(--color-border-2); 201 | outline: none; 202 | -webkit-appearance: none; 203 | padding: 0; 204 | background-color: transparent; 205 | border: 1px solid #222222; 206 | overflow: hidden; 207 | } 208 | 209 | .p-gui__color-picker::-webkit-color-swatch-wrapper { 210 | padding: 0; 211 | } 212 | .p-gui__color-picker::-webkit-color-swatch { 213 | border: none; 214 | } 215 | `,L=` 216 | .p-gui__vector2 { 217 | background: transparent; 218 | aspect-ratio: 1; 219 | padding-bottom: 0; 220 | color: var(--color-text-dark); 221 | } 222 | 223 | .p-gui__vector2:hover { 224 | color: var(--color-text-light); 225 | } 226 | 227 | .p-gui__vector2-area { 228 | position: relative; 229 | background: rgba(0, 0, 0, .3); 230 | margin-top: 8px; 231 | width: 100%; 232 | height: calc(100% - 28px); 233 | touch-action: none; 234 | } 235 | 236 | .p-gui__vector2-line { 237 | position: absolute; 238 | background: white; 239 | opacity: .3; 240 | pointer-events: none; 241 | } 242 | 243 | .p-gui__vector2-line-x { 244 | width: 100%; 245 | height: 1px; 246 | left: 0; 247 | top: 50%; 248 | transform: translateY(-50%); 249 | } 250 | 251 | .p-gui__vector2-line-y { 252 | width: 1px; 253 | height: 100%; 254 | top: 0; 255 | left: 50%; 256 | transform: translateX(-50%); 257 | } 258 | 259 | .p-gui__vector2-dot { 260 | position: absolute; 261 | top: 0; 262 | left: 0; 263 | width: 8px; 264 | height: 8px; 265 | border-radius: 50%; 266 | background: #d5d5d5; 267 | border: 2px solid #ff9999; 268 | transform: translate(-50%, -50%); 269 | pointer-events: none; 270 | } 271 | 272 | .p-gui__vector-value { 273 | display: inline-block; 274 | right: 7px; 275 | position: absolute; 276 | } 277 | `,H=` 278 | .p-gui__image-container { 279 | width: 100%; 280 | padding: 3px; 281 | display: flex; 282 | justify-content: flex-start; 283 | flex-wrap: wrap; 284 | box-sizing: border-box; 285 | } 286 | 287 | .p-gui__image { 288 | background-size: cover; 289 | cursor: pointer; 290 | position: relative; 291 | margin: 1px 2.5px 19px 2.5px; 292 | border-radius: var(--main-border-radius); 293 | flex: 0 0 calc(33.333% - 5px); 294 | height: 90px; 295 | background-position: center; 296 | color: var(--color-text-dark); 297 | transition: var(--transition) color; 298 | } 299 | 300 | .p-gui__image:hover { 301 | color: var(--color-text-light); 302 | } 303 | 304 | .p-gui__image::after { 305 | position: absolute; 306 | top: 0; 307 | left: 0; 308 | width: 100%; 309 | height: 100%; 310 | content: ''; 311 | border: 1px solid transparent; 312 | box-sizing: border-box; 313 | border-radius: var(--main-border-radius); 314 | transition: var(--transition) border-color; 315 | } 316 | .p-gui__image--selected::after { 317 | border-color: #06FF89; 318 | } 319 | 320 | .p-gui__image-text { 321 | position: absolute; 322 | bottom: -15px; 323 | text-shadow: 0 -1px 0 #111; 324 | white-space: nowrap; 325 | width: 100%; 326 | overflow: hidden; 327 | text-overflow: ellipsis; 328 | 329 | } 330 | `,N=` 331 | .p-gui__folder { 332 | width: 100%; 333 | position: relative; 334 | background: #434343; 335 | overflow: auto; 336 | margin-bottom: 2px; 337 | display: flex; 338 | flex-wrap: wrap; 339 | border: 1px solid grey; 340 | padding: 0 3px 0 3px; 341 | border-radius: var(--main-border-radius); 342 | box-sizing: border-box; 343 | } 344 | 345 | .p-gui__folder--first { 346 | margin-top: 0; 347 | } 348 | 349 | .p-gui__folder--closed { 350 | height: 32px; 351 | overflow: hidden; 352 | } 353 | 354 | .p-gui__folder-header { 355 | padding: 10px 5px; 356 | background-color: rgba(0, 0, 0, .5); 357 | color: white; 358 | cursor: pointer; 359 | width: 100%; 360 | margin: 0 -2px 2px -3px; 361 | } 362 | 363 | .p-gui__folder-header:hover { 364 | background-color: rgba(0, 0, 0, .75); 365 | } 366 | 367 | .p-gui__folder-arrow { 368 | width: 8px; 369 | height: 8px; 370 | display: inline-block; 371 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAHlBMVEUAAAD///////////////////////////////////8kfJuVAAAACXRSTlMA9Z1fCdMo1yxEJnA0AAAAK0lEQVQI12PABlRgjKkJUMZMYRhjpgqMAZSEMICSaIzpDWiKhdENhEhgAgATSg5jyWnYewAAAABJRU5ErkJggg==); 372 | background-size: contain; 373 | margin-right: 5px; 374 | transform: rotate(90deg) 375 | } 376 | 377 | .p-gui__folder--closed .p-gui__folder-arrow { 378 | transform: rotate(0deg); 379 | } 380 | `;function M(C){return` 381 | .p-gui { 382 | --main-border-radius: 5px; 383 | --color-bg: #121212; 384 | --color-border: #484848; 385 | --color-border-2: rgba(255,255,255,.1); 386 | --color-text-light: #ffffff; 387 | --color-text-dark: #bbbbbb; 388 | --color-accent: #1681ca; 389 | --color-accent-hover: #218fda; 390 | --transition: .1s linear; 391 | 392 | position: ${C}; 393 | top: 0; 394 | left: 0; 395 | transform: translate3d(0,0,0); 396 | padding-top: 21px; 397 | padding-inline: 3px; 398 | background: var(--color-bg); 399 | display: block; 400 | justify-content: center; 401 | flex-wrap: wrap; 402 | font-family: "Arial Rounded MT Bold", Arial, sans-serif; 403 | width: 290px; 404 | overflow: auto; 405 | box-shadow: 0 0 2px black; 406 | box-sizing: border-box; 407 | z-index: 99999; 408 | user-select: none; 409 | border-bottom-right-radius: 3px; 410 | border-bottom-left-radius: 3px; 411 | cursor: auto; 412 | border-radius: var(--main-border-radius); 413 | border: 1px solid var(--color-border); 414 | line-height: normal; 415 | transition: var(--transition) opacity; 416 | } 417 | 418 | .p-gui:hover { 419 | opacity: 1!important; 420 | } 421 | 422 | .p-gui * { 423 | font-size: 11px; 424 | } 425 | 426 | .p-gui::-webkit-scrollbar, 427 | .p-gui *::-webkit-scrollbar { 428 | width: 10px; 429 | } 430 | 431 | .p-gui::-webkit-scrollbar-track, 432 | .p-gui *::-webkit-scrollbar-track { 433 | background: #2f2f2f; 434 | border-radius: 3px; 435 | } 436 | 437 | .p-gui::-webkit-scrollbar-thumb, 438 | .p-gui *::-webkit-scrollbar-thumb { 439 | background: #757576; 440 | border-radius: 10px; 441 | box-sizing: border-box; 442 | border: 1px solid #2f2f2f; 443 | } 444 | 445 | .p-gui--collapsed { 446 | height: 0; 447 | padding: 21px 10px 0 10px; 448 | overflow: hidden; 449 | } 450 | 451 | .p-gui__header { 452 | position: absolute; 453 | top: 0; 454 | left: 0; 455 | width: 100%; 456 | height: 20px; 457 | background-color: rgba(0, 0, 0, .8); 458 | cursor: grab; 459 | color: grey; 460 | font-size: 10px; 461 | line-height: 20px; 462 | padding-left: 12px; 463 | box-sizing: border-box; 464 | touch-action: none; 465 | } 466 | 467 | .p-gui__header-close { 468 | width: 20px; 469 | height: 20px; 470 | position: absolute; 471 | top: 0; 472 | right: 5px; 473 | cursor: pointer; 474 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABFJREFUCNdjIAb8//8BjIkAAOrOBd3TR0jRAAAAAElFTkSuQmCC); 475 | background-size: 50% 50%; 476 | background-position: center; 477 | background-repeat: no-repeat; 478 | } 479 | 480 | .p-gui--collapsed .p-gui__header-close { 481 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABVJREFUCNdjYEhgIIj///8AwsSoBQD43QydY5mb0QAAAABJRU5ErkJggg==); 482 | } 483 | 484 | .p-gui__slider, 485 | .p-gui__button, 486 | .p-gui__switch, 487 | .p-gui__list, 488 | .p-gui__vector2, 489 | .p-gui__color { 490 | width: 100%; 491 | padding: 7px; 492 | cursor: pointer; 493 | position: relative; 494 | box-sizing: border-box; 495 | margin-block: 3px; 496 | border: 1px solid var(--color-border-2); 497 | border-radius: var(--main-border-radius); 498 | transition: var(--transition) border-color; 499 | } 500 | 501 | .p-gui__slider:hover, 502 | .p-gui__button:hover, 503 | .p-gui__switch:hover, 504 | .p-gui__list:hover, 505 | .p-gui__vector2:hover, 506 | .p-gui__color:hover { 507 | border-color: rgba(255,255,255,.2); 508 | } 509 | 510 | ${A} 511 | 512 | ${H} 513 | 514 | ${$} 515 | 516 | ${D} 517 | 518 | ${O} 519 | 520 | ${F} 521 | 522 | ${L} 523 | 524 | ${N} 525 | `}class E{constructor(e={}){if(this.firstParent=this,e.container?(this.container=typeof e.container=="string"?document.querySelector(e.container):e.container,this.position_type="absolute"):(this.container=document.body,this.position_type="fixed"),this.propReferences=[],this.folders=[],e.isFolder){this._folderConstructor(e.folderOptions);return}typeof e.onUpdate=="function"&&(this.onUpdate=e.onUpdate),this.name=e!=null&&typeof e.name=="string"?e.name:"",this.backgroundColor=e.color||null,this.opacity=e.opacity||1,this.container==document.body?this.maxHeight=window.innerHeight:this.maxHeight=Math.min(this.container.clientHeight,window.innerHeight),e.maxHeight&&(this.initMaxHeight=e.maxHeight,this.maxHeight=Math.min(this.initMaxHeight,this.maxHeight)),this.screenCorner=this._parseScreenCorner(e.position),window.perfectGUI||(window.perfectGUI={}),window.perfectGUI.instanceCounter==null?window.perfectGUI.instanceCounter=0:window.perfectGUI.instanceCounter++,this.instanceId=window.perfectGUI.instanceCounter,this.wrapperWidth=e.width||290,this.stylesheet=document.createElement("style"),this.stylesheet.setAttribute("type","text/css"),this.stylesheet.setAttribute("id","lm-gui-stylesheet"),document.head.append(this.stylesheet),this.instanceId==0&&this._addStyles(`${M(this.position_type)}`),this._styleInstance(),this._addWrapper(),this.wrapper.setAttribute("data-corner-x",this.screenCorner.x),this.wrapper.setAttribute("data-corner-y",this.screenCorner.y),e.autoRepositioning!=!1&&window.addEventListener("resize",this._handleResize.bind(this)),this._handleResize(),this.hasBeenDragged=!1,e.draggable==!0&&this._makeDraggable(),this.closed=!1,e.closed&&this.toggleClose()}_styleInstance(){let e=this._getScrollbarWidth(this.container);if(this.screenCorner.x=="left"?this.xOffset=0:this.xOffset=this.container.clientWidth-this.wrapperWidth-e,this.instanceId>0){let t=this.container.querySelectorAll(".p-gui");for(let i=0;i0){let t=this.container.querySelectorAll(`.p-gui:not(#${this.wrapper.id}):not([data-dragged])`);for(let i=0;ithis.instanceId);i++)this.screenCorner.y==t[i].dataset.cornerY&&(this.screenCorner.x=="left"&&t[i].dataset.cornerX=="left"?this.xOffset+=t[i].offsetWidth:this.screenCorner.x=="right"&&t[i].dataset.cornerX=="right"&&(this.xOffset-=t[i].offsetWidth))}this.position={prevX:this.xOffset,prevY:this.yOffset,x:this.xOffset,y:this.yOffset},this.wrapper.style.transform=`translate3d(${this.position.x}px, ${this.position.y}px, 0)`}_addStyles(e){this.stylesheet.innerHTML+=e}_addWrapper(){this.wrapper=document.createElement("div"),this.wrapper.id="p-gui-"+this.instanceId,this.wrapper.className="p-gui",this.container.append(this.wrapper),this.header=document.createElement("div"),this.header.className="p-gui__header",this.header.textContent=this.name,this.header.style=`${this.backgroundColor?"border-color: "+this.backgroundColor+";":""}`,this.wrapper.append(this.header);const e=document.createElement("div");e.className="p-gui__header-close",e.addEventListener("click",this.toggleClose.bind(this)),this.header.append(e)}button(e,t){let i="";typeof e!="string"?typeof e=="object"&&(e!=null&&e.hasOwnProperty("name"))?i=e.name==""?" ":e.name:i=" ":i=e==""?" ":e;const a=typeof e.tooltip=="string"?e.tooltip:e.tooltip===!0?i:null;this.imageContainer=null;const s=document.createElement("div");s.className="p-gui__button",s.textContent=i,a&&s.setAttribute("title",a),s.addEventListener("click",()=>{t&&t(),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()}),this.wrapper.append(s),typeof e.color=="string"&&(s.style.setProperty("--color-accent",e.color),s.style.setProperty("--color-accent-hover",e.hoverColor||e.color))}image(e={},t){if(typeof e!="object")throw Error(`[GUI] image() first parameter must be an object. Received: ${typeof e}.`);let i;if(typeof e.path=="string")i=e.path;else throw typeof e.path==null?Error("[GUI] image() path must be provided."):Error("[GUI] image() path must be a string.");let a=i.replace(/^.*[\\\/]/,""),s;e.name==null?s=a:s=typeof e.name=="string"&&e.name||" ";const o=typeof e.tooltip=="string"?e.tooltip:e.tooltip===!0?s:null,r=e.selected===!0,n=e.selectionBorder!==!1;let c="";e.width&&(typeof e.width=="number"&&(e.width+="px"),c+=`flex: 0 0 calc(${e.width} - 5px); `),e.height&&(typeof e.height=="number"&&(e.height+="px"),c+=`height: ${e.height}; `),this.imageContainer||(this.imageContainer=document.createElement("div"),this.imageContainer.className="p-gui__image-container",this.wrapper.append(this.imageContainer));const l=document.createElement("div");l.className="p-gui__image",l.style="background-image: url("+i+"); "+c,o&&l.setAttribute("title",o),this.imageContainer.append(l),r&&n&&l.classList.add("p-gui__image--selected");const d=document.createElement("div");return d.className="p-gui__image-text",d.textContent=s,l.append(d),l.addEventListener("click",()=>{let p=l.parentElement.querySelectorAll(".p-gui__image--selected");for(let h=0;h{const u=h.target.childNodes[1];let f=!0;u.classList.contains("p-gui__switch-checkbox--active")&&(f=!1),u.classList.toggle("p-gui__switch-checkbox--active"),a?o[r]=f:typeof t=="function"&&t(f),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()});let d=(()=>a?o[r]?" p-gui__switch-checkbox--active":"":n?" p-gui__switch-checkbox--active":"")();const p=document.createElement("div");p.className="p-gui__switch-checkbox"+d,l.append(p),a&&Object.defineProperty(o,r,{set:h=>{this.propReferences[s]=h,h?p.classList.add("p-gui__switch-checkbox--active"):p.classList.remove("p-gui__switch-checkbox--active"),typeof t=="function"&&t(h)},get:()=>this.propReferences[s]})}list(e={},t){if(typeof e!="object")throw Error(`[GUI] list() first parameter must be an object. Received: ${typeof e}.`);let i=typeof e.name=="string"?e.name:" ",a=!1,s=null,o=e.obj,r=e.prop,n=Array.isArray(e.values)?e.values:null,c,l=typeof n[0]!="string";const d=typeof e.tooltip=="string"?e.tooltip:e.tooltip===!0?i:null;if(t=typeof t=="function"?t:null,e.value!==void 0||e.value===void 0&&o===void 0&&r===void 0)(r!=null||o!=null)&&console.warn('[GUI] list() "obj" and "prop" parameters are ignored when a "value" parameter is used.'),c=(()=>{if(!n)return null;if(typeof e.value=="string")return n.indexOf(e.value);if(typeof e.value=="number")return e.value})();else if(r!=null&&o!=null){if(typeof r!="string")throw Error(`[GUI] list() "prop" parameter must be an string. Received: ${typeof r}.`);if(typeof o!="object")throw Error(`[GUI] list() "obj" parameter must be an object. Received: ${typeof o}.`);c=(()=>{if(!n)return null;if(typeof o[r]=="string")return l?n.find(u=>u.value===o[r]).value:n.indexOf(o[r]);if(typeof o[r]=="number")return l?n.find(u=>u.value===o[r]).value:o[r]})(),s=this.propReferences.push(o[r])-1,a=!0}else(r!=null&&o==null||r==null&&o==null)&&console.warn('[GUI] list() "obj" and "prop" parameters must be used together.');this.imageContainer=null;let p=document.createElement("div");p.className="p-gui__list",p.textContent=i,d&&p.setAttribute("title",d),this.wrapper.append(p);let h=document.createElement("select");p.append(h),h.className="p-gui__list-dropdown",h.addEventListener("change",u=>{a?o[r]=u.target.value:t&&t(u.target.value),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()}),n&&n.forEach((u,f)=>{const g=l?u.name:u,m=l?u.value:u;let x=document.createElement("option");x.setAttribute("value",m),x.textContent=g,h.append(x),(!l&&c==f||l&&c==m)&&x.setAttribute("selected","")}),a&&Object.defineProperty(o,r,{set:u=>{let f,g,m;l?(m=n.find(v=>v.value==u),g=(m==null?void 0:m.value)||n[0].value,f=n.indexOf(m)):(typeof u=="string"&&(f=n.indexOf(u),g=u),typeof u=="number"&&(f=u,g=n[u])),this.propReferences[s]=l?g:u;const x=h.querySelector("[selected]");x&&x.removeAttribute("selected"),h.querySelectorAll("option")[f].setAttribute("selected",""),typeof t=="function"&&t(l?m:g,f)},get:()=>this.propReferences[s]})}color(e={},t){if(typeof e!="object")throw Error(`[GUI] color() first parameter must be an object. Received: ${typeof e}.`);let i=typeof e.name=="string"&&e.name||" ",a=!1,s=null,o=e.obj,r=e.prop,n;const c=typeof e.tooltip=="string"?e.tooltip:e.tooltip===!0?i:null;if(typeof e.value=="string"&&(e.value.length!=7||e.value[0]!="#"?console.error(`[GUI] color() 'value' parameter must be an hexadecimal string in the format "#ffffff". Received: "${e.value}".`):n=e.value),n||(n="#000000"),e.value!==void 0)(r!=null||o!=null)&&console.warn('[GUI] color() "obj" and "prop" parameters are ignored when a "value" parameter is used.');else if(r!=null&&o!=null){if(typeof r!="string")throw Error(`[GUI] color() "prop" parameter must be an string. Received: ${typeof r}.`);if(typeof o!="object")throw Error(`[GUI] color() "obj" parameter must be an object. Received: ${typeof o}.`);i==" "&&(i=r),s=this.propReferences.push(o[r])-1,a=!0}else(r!=null&&o==null||r==null&&o==null)&&console.warn('[GUI] color() "obj" and "prop" parameters must be used together.');this.imageContainer=null;const l=document.createElement("div");l.className="p-gui__color",l.textContent=i,c&&l.setAttribute("title",c),this.wrapper.append(l);const d=document.createElement("input");d.className="p-gui__color-picker",d.setAttribute("type","color"),d.value=n,l.append(d),typeof t=="function"&&d.addEventListener("input",()=>{a?o[r]=d.value:typeof t=="function"&&t(d.value),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()}),a&&Object.defineProperty(o,r,{set:p=>{this.propReferences[s]=p,d.value=p,typeof t=="function"&&t(p)},get:()=>this.propReferences[s]})}vector2(e={},t){if(typeof e!="object")throw Error(`[GUI] vector2() first parameter must be an object. Received: ${typeof e}.`);let i=typeof e.name=="string"&&e.name||" ";const a=e.x.min??0,s=e.x.max??1,o=e.y.min??0,r=e.y.max??1,n=e.x.step||(s-a)/100,c=e.y.step||(r-o)/100,l=this._countDecimals(n),d=this._countDecimals(c),p=e.x.obj,h=e.x.prop,u=this.propReferences.push(p[h])-1,f=e.y.obj,g=e.y.prop,m=this.propReferences.push(f[g])-1,x=typeof e.tooltip=="string"?e.tooltip:e.tooltip===!0?i:null;t=typeof t=="function"?t:null,this.imageContainer=null;const v=document.createElement("div");v.className="p-gui__vector2",v.textContent=i,x&&v.setAttribute("title",x),this.wrapper.append(v);const k=document.createElement("div");k.className="p-gui__vector-value",k.textContent=p[h]+", "+f[g],v.append(k);const b=document.createElement("div");b.className="p-gui__vector2-area",v.append(b),b.addEventListener("click",_=>{const j=parseFloat(this._mapLinear(_.offsetX,0,b.clientWidth,a,s)),I=parseFloat(this._mapLinear(_.offsetY,0,b.clientHeight,r,o));p[h]=j.toFixed(l),f[g]=I.toFixed(d),t&&t(p[h],p[g]),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()});let U=!1;b.addEventListener("pointerdown",_=>{U=!0}),b.addEventListener("pointerup",()=>{U=!1}),b.addEventListener("pointermove",_=>{if(U){const j=parseFloat(this._mapLinear(_.offsetX,0,b.clientWidth,a,s)),I=parseFloat(this._mapLinear(_.offsetY,0,b.clientHeight,r,o));p[h]=j.toFixed(l),f[g]=I.toFixed(d),t&&t(p[h],p[g]),this.onUpdate?this.onUpdate():this.isFolder&&this.firstParent.onUpdate&&this.firstParent.onUpdate()}});const R=document.createElement("div");R.className="p-gui__vector2-line p-gui__vector2-line-x",b.append(R);const P=document.createElement("div");P.className="p-gui__vector2-line p-gui__vector2-line-y",b.append(P);const w=document.createElement("div");w.className="p-gui__vector2-dot",b.append(w),w.style.left=this._mapLinear(p[h],a,s,0,b.clientWidth)+"px",w.style.top=this._mapLinear(f[g],o,r,b.clientHeight,0)+"px",Object.defineProperty(p,h,{set:_=>{this.propReferences[u]=_,w.style.left=this._mapLinear(_,a,s,0,b.clientWidth)+"px",k.textContent=String(_)+", "+f[g]},get:()=>this.propReferences[u]}),Object.defineProperty(f,g,{set:_=>{this.propReferences[m]=_,w.style.top=this._mapLinear(_,o,r,b.clientHeight,0)+"px",k.textContent=p[h]+", "+String(_)},get:()=>this.propReferences[m]})}folder(e={}){let t=typeof e.closed=="boolean"?e.closed:!1,i=e.name||"",a=e.color||null,s=e.maxHeight||null;this.imageContainer=null;let o="p-gui__folder";this.folders.length==0&&(o+=" p-gui__folder--first"),t&&(o+=" p-gui__folder--closed");let r=a?`background-color: ${a};`:"";r+=s?`max-height: ${s}px;`:"";const n=document.createElement("div");n.className=o,n.style=r,this.wrapper.append(n);const c=document.createElement("div");c.innerHTML=`${i}`,c.className="p-gui__folder-header",n.append(c),c.addEventListener("click",()=>{n.classList.toggle("p-gui__folder--closed")});let l=new E({isFolder:!0,folderOptions:{wrapper:n,parent:this,firstParent:this.firstParent}});return this.folders.push(l),l}_makeDraggable(){var e=this;this.header.addEventListener("pointerdown",t),this.header.addEventListener("pointerup",a);function t(s){s.preventDefault(),e.position.initX=e.position.x,e.position.initY=e.position.y,e.position.prevX=s.clientX,e.position.prevY=s.clientY,document.addEventListener("pointermove",i)}function i(s){s.preventDefault(),e.hasBeenDragged||(e.hasBeenDragged=!0,e.wrapper.setAttribute("data-dragged","true")),e.position.x=e.position.initX+s.clientX-e.position.prevX,e.position.y=e.position.initY+s.clientY-e.position.prevY,e.wrapper.style.transform="translate3d("+e.position.x+"px,"+e.position.y+"px,0)"}function a(s){document.removeEventListener("pointermove",i)}}toggleClose(){this.closed=!this.closed,this.closed?(this.previousInnerScroll=this.wrapper.scrollTop,this.wrapper.scrollTo(0,0)):this.wrapper.scrollTo(0,this.previousInnerScroll),this.wrapper.classList.toggle("p-gui--collapsed")}kill(){this.wrapper.remove()}_mapLinear(e,t,i,a,s){return a+(e-t)*(s-a)/(i-t)}_countDecimals(e){const t=e.toString(),i=t.indexOf(".");return i===-1?0:t.length-i-1}}return E}); 533 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfect-gui", 3 | "version": "4.9.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "perfect-gui", 9 | "version": "4.9.3", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "vite": "^4.5.0" 13 | } 14 | }, 15 | "node_modules/@esbuild/android-arm": { 16 | "version": "0.18.20", 17 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", 18 | "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", 19 | "cpu": [ 20 | "arm" 21 | ], 22 | "dev": true, 23 | "optional": true, 24 | "os": [ 25 | "android" 26 | ], 27 | "engines": { 28 | "node": ">=12" 29 | } 30 | }, 31 | "node_modules/@esbuild/android-arm64": { 32 | "version": "0.18.20", 33 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", 34 | "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", 35 | "cpu": [ 36 | "arm64" 37 | ], 38 | "dev": true, 39 | "optional": true, 40 | "os": [ 41 | "android" 42 | ], 43 | "engines": { 44 | "node": ">=12" 45 | } 46 | }, 47 | "node_modules/@esbuild/android-x64": { 48 | "version": "0.18.20", 49 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", 50 | "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", 51 | "cpu": [ 52 | "x64" 53 | ], 54 | "dev": true, 55 | "optional": true, 56 | "os": [ 57 | "android" 58 | ], 59 | "engines": { 60 | "node": ">=12" 61 | } 62 | }, 63 | "node_modules/@esbuild/darwin-arm64": { 64 | "version": "0.18.20", 65 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", 66 | "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", 67 | "cpu": [ 68 | "arm64" 69 | ], 70 | "dev": true, 71 | "optional": true, 72 | "os": [ 73 | "darwin" 74 | ], 75 | "engines": { 76 | "node": ">=12" 77 | } 78 | }, 79 | "node_modules/@esbuild/darwin-x64": { 80 | "version": "0.18.20", 81 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", 82 | "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", 83 | "cpu": [ 84 | "x64" 85 | ], 86 | "dev": true, 87 | "optional": true, 88 | "os": [ 89 | "darwin" 90 | ], 91 | "engines": { 92 | "node": ">=12" 93 | } 94 | }, 95 | "node_modules/@esbuild/freebsd-arm64": { 96 | "version": "0.18.20", 97 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", 98 | "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", 99 | "cpu": [ 100 | "arm64" 101 | ], 102 | "dev": true, 103 | "optional": true, 104 | "os": [ 105 | "freebsd" 106 | ], 107 | "engines": { 108 | "node": ">=12" 109 | } 110 | }, 111 | "node_modules/@esbuild/freebsd-x64": { 112 | "version": "0.18.20", 113 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", 114 | "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", 115 | "cpu": [ 116 | "x64" 117 | ], 118 | "dev": true, 119 | "optional": true, 120 | "os": [ 121 | "freebsd" 122 | ], 123 | "engines": { 124 | "node": ">=12" 125 | } 126 | }, 127 | "node_modules/@esbuild/linux-arm": { 128 | "version": "0.18.20", 129 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", 130 | "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", 131 | "cpu": [ 132 | "arm" 133 | ], 134 | "dev": true, 135 | "optional": true, 136 | "os": [ 137 | "linux" 138 | ], 139 | "engines": { 140 | "node": ">=12" 141 | } 142 | }, 143 | "node_modules/@esbuild/linux-arm64": { 144 | "version": "0.18.20", 145 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", 146 | "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", 147 | "cpu": [ 148 | "arm64" 149 | ], 150 | "dev": true, 151 | "optional": true, 152 | "os": [ 153 | "linux" 154 | ], 155 | "engines": { 156 | "node": ">=12" 157 | } 158 | }, 159 | "node_modules/@esbuild/linux-ia32": { 160 | "version": "0.18.20", 161 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", 162 | "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", 163 | "cpu": [ 164 | "ia32" 165 | ], 166 | "dev": true, 167 | "optional": true, 168 | "os": [ 169 | "linux" 170 | ], 171 | "engines": { 172 | "node": ">=12" 173 | } 174 | }, 175 | "node_modules/@esbuild/linux-loong64": { 176 | "version": "0.18.20", 177 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", 178 | "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", 179 | "cpu": [ 180 | "loong64" 181 | ], 182 | "dev": true, 183 | "optional": true, 184 | "os": [ 185 | "linux" 186 | ], 187 | "engines": { 188 | "node": ">=12" 189 | } 190 | }, 191 | "node_modules/@esbuild/linux-mips64el": { 192 | "version": "0.18.20", 193 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", 194 | "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", 195 | "cpu": [ 196 | "mips64el" 197 | ], 198 | "dev": true, 199 | "optional": true, 200 | "os": [ 201 | "linux" 202 | ], 203 | "engines": { 204 | "node": ">=12" 205 | } 206 | }, 207 | "node_modules/@esbuild/linux-ppc64": { 208 | "version": "0.18.20", 209 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", 210 | "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", 211 | "cpu": [ 212 | "ppc64" 213 | ], 214 | "dev": true, 215 | "optional": true, 216 | "os": [ 217 | "linux" 218 | ], 219 | "engines": { 220 | "node": ">=12" 221 | } 222 | }, 223 | "node_modules/@esbuild/linux-riscv64": { 224 | "version": "0.18.20", 225 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", 226 | "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", 227 | "cpu": [ 228 | "riscv64" 229 | ], 230 | "dev": true, 231 | "optional": true, 232 | "os": [ 233 | "linux" 234 | ], 235 | "engines": { 236 | "node": ">=12" 237 | } 238 | }, 239 | "node_modules/@esbuild/linux-s390x": { 240 | "version": "0.18.20", 241 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", 242 | "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", 243 | "cpu": [ 244 | "s390x" 245 | ], 246 | "dev": true, 247 | "optional": true, 248 | "os": [ 249 | "linux" 250 | ], 251 | "engines": { 252 | "node": ">=12" 253 | } 254 | }, 255 | "node_modules/@esbuild/linux-x64": { 256 | "version": "0.18.20", 257 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", 258 | "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", 259 | "cpu": [ 260 | "x64" 261 | ], 262 | "dev": true, 263 | "optional": true, 264 | "os": [ 265 | "linux" 266 | ], 267 | "engines": { 268 | "node": ">=12" 269 | } 270 | }, 271 | "node_modules/@esbuild/netbsd-x64": { 272 | "version": "0.18.20", 273 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", 274 | "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", 275 | "cpu": [ 276 | "x64" 277 | ], 278 | "dev": true, 279 | "optional": true, 280 | "os": [ 281 | "netbsd" 282 | ], 283 | "engines": { 284 | "node": ">=12" 285 | } 286 | }, 287 | "node_modules/@esbuild/openbsd-x64": { 288 | "version": "0.18.20", 289 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", 290 | "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", 291 | "cpu": [ 292 | "x64" 293 | ], 294 | "dev": true, 295 | "optional": true, 296 | "os": [ 297 | "openbsd" 298 | ], 299 | "engines": { 300 | "node": ">=12" 301 | } 302 | }, 303 | "node_modules/@esbuild/sunos-x64": { 304 | "version": "0.18.20", 305 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", 306 | "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", 307 | "cpu": [ 308 | "x64" 309 | ], 310 | "dev": true, 311 | "optional": true, 312 | "os": [ 313 | "sunos" 314 | ], 315 | "engines": { 316 | "node": ">=12" 317 | } 318 | }, 319 | "node_modules/@esbuild/win32-arm64": { 320 | "version": "0.18.20", 321 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", 322 | "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", 323 | "cpu": [ 324 | "arm64" 325 | ], 326 | "dev": true, 327 | "optional": true, 328 | "os": [ 329 | "win32" 330 | ], 331 | "engines": { 332 | "node": ">=12" 333 | } 334 | }, 335 | "node_modules/@esbuild/win32-ia32": { 336 | "version": "0.18.20", 337 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", 338 | "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", 339 | "cpu": [ 340 | "ia32" 341 | ], 342 | "dev": true, 343 | "optional": true, 344 | "os": [ 345 | "win32" 346 | ], 347 | "engines": { 348 | "node": ">=12" 349 | } 350 | }, 351 | "node_modules/@esbuild/win32-x64": { 352 | "version": "0.18.20", 353 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", 354 | "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", 355 | "cpu": [ 356 | "x64" 357 | ], 358 | "dev": true, 359 | "optional": true, 360 | "os": [ 361 | "win32" 362 | ], 363 | "engines": { 364 | "node": ">=12" 365 | } 366 | }, 367 | "node_modules/esbuild": { 368 | "version": "0.18.20", 369 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", 370 | "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", 371 | "dev": true, 372 | "hasInstallScript": true, 373 | "bin": { 374 | "esbuild": "bin/esbuild" 375 | }, 376 | "engines": { 377 | "node": ">=12" 378 | }, 379 | "optionalDependencies": { 380 | "@esbuild/android-arm": "0.18.20", 381 | "@esbuild/android-arm64": "0.18.20", 382 | "@esbuild/android-x64": "0.18.20", 383 | "@esbuild/darwin-arm64": "0.18.20", 384 | "@esbuild/darwin-x64": "0.18.20", 385 | "@esbuild/freebsd-arm64": "0.18.20", 386 | "@esbuild/freebsd-x64": "0.18.20", 387 | "@esbuild/linux-arm": "0.18.20", 388 | "@esbuild/linux-arm64": "0.18.20", 389 | "@esbuild/linux-ia32": "0.18.20", 390 | "@esbuild/linux-loong64": "0.18.20", 391 | "@esbuild/linux-mips64el": "0.18.20", 392 | "@esbuild/linux-ppc64": "0.18.20", 393 | "@esbuild/linux-riscv64": "0.18.20", 394 | "@esbuild/linux-s390x": "0.18.20", 395 | "@esbuild/linux-x64": "0.18.20", 396 | "@esbuild/netbsd-x64": "0.18.20", 397 | "@esbuild/openbsd-x64": "0.18.20", 398 | "@esbuild/sunos-x64": "0.18.20", 399 | "@esbuild/win32-arm64": "0.18.20", 400 | "@esbuild/win32-ia32": "0.18.20", 401 | "@esbuild/win32-x64": "0.18.20" 402 | } 403 | }, 404 | "node_modules/fsevents": { 405 | "version": "2.3.3", 406 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 407 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 408 | "dev": true, 409 | "hasInstallScript": true, 410 | "optional": true, 411 | "os": [ 412 | "darwin" 413 | ], 414 | "engines": { 415 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 416 | } 417 | }, 418 | "node_modules/nanoid": { 419 | "version": "3.3.6", 420 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 421 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 422 | "dev": true, 423 | "funding": [ 424 | { 425 | "type": "github", 426 | "url": "https://github.com/sponsors/ai" 427 | } 428 | ], 429 | "bin": { 430 | "nanoid": "bin/nanoid.cjs" 431 | }, 432 | "engines": { 433 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 434 | } 435 | }, 436 | "node_modules/picocolors": { 437 | "version": "1.0.0", 438 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 439 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 440 | "dev": true 441 | }, 442 | "node_modules/postcss": { 443 | "version": "8.4.31", 444 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 445 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 446 | "dev": true, 447 | "funding": [ 448 | { 449 | "type": "opencollective", 450 | "url": "https://opencollective.com/postcss/" 451 | }, 452 | { 453 | "type": "tidelift", 454 | "url": "https://tidelift.com/funding/github/npm/postcss" 455 | }, 456 | { 457 | "type": "github", 458 | "url": "https://github.com/sponsors/ai" 459 | } 460 | ], 461 | "dependencies": { 462 | "nanoid": "^3.3.6", 463 | "picocolors": "^1.0.0", 464 | "source-map-js": "^1.0.2" 465 | }, 466 | "engines": { 467 | "node": "^10 || ^12 || >=14" 468 | } 469 | }, 470 | "node_modules/rollup": { 471 | "version": "3.29.4", 472 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", 473 | "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", 474 | "dev": true, 475 | "bin": { 476 | "rollup": "dist/bin/rollup" 477 | }, 478 | "engines": { 479 | "node": ">=14.18.0", 480 | "npm": ">=8.0.0" 481 | }, 482 | "optionalDependencies": { 483 | "fsevents": "~2.3.2" 484 | } 485 | }, 486 | "node_modules/source-map-js": { 487 | "version": "1.0.2", 488 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 489 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 490 | "dev": true, 491 | "engines": { 492 | "node": ">=0.10.0" 493 | } 494 | }, 495 | "node_modules/vite": { 496 | "version": "4.5.0", 497 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", 498 | "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", 499 | "dev": true, 500 | "dependencies": { 501 | "esbuild": "^0.18.10", 502 | "postcss": "^8.4.27", 503 | "rollup": "^3.27.1" 504 | }, 505 | "bin": { 506 | "vite": "bin/vite.js" 507 | }, 508 | "engines": { 509 | "node": "^14.18.0 || >=16.0.0" 510 | }, 511 | "funding": { 512 | "url": "https://github.com/vitejs/vite?sponsor=1" 513 | }, 514 | "optionalDependencies": { 515 | "fsevents": "~2.3.2" 516 | }, 517 | "peerDependencies": { 518 | "@types/node": ">= 14", 519 | "less": "*", 520 | "lightningcss": "^1.21.0", 521 | "sass": "*", 522 | "stylus": "*", 523 | "sugarss": "*", 524 | "terser": "^5.4.0" 525 | }, 526 | "peerDependenciesMeta": { 527 | "@types/node": { 528 | "optional": true 529 | }, 530 | "less": { 531 | "optional": true 532 | }, 533 | "lightningcss": { 534 | "optional": true 535 | }, 536 | "sass": { 537 | "optional": true 538 | }, 539 | "stylus": { 540 | "optional": true 541 | }, 542 | "sugarss": { 543 | "optional": true 544 | }, 545 | "terser": { 546 | "optional": true 547 | } 548 | } 549 | } 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfect-gui", 3 | "version": "4.11.9", 4 | "description": "GUI for JavaScript", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "vite build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/thibka/perfect-gui.git" 12 | }, 13 | "keywords": [ 14 | "gui" 15 | ], 16 | "author": "thibka", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/thibka/perfect-gui/issues" 20 | }, 21 | "homepage": "https://thibka.github.io/perfect-gui/public/", 22 | "devDependencies": { 23 | "vite": "^4.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/addons/three.js: -------------------------------------------------------------------------------- 1 | import GUI from '../index.js'; 2 | 3 | /* 4 | GUI.prototype.materialFolder = materialFolder; 5 | gui.materialFolder({name: 'material', obj: material}) 6 | */ 7 | export function materialFolder(params = {}) { 8 | const material = params.obj; 9 | 10 | if (material.isMeshStandardMaterial) { 11 | console.error('[GUI materialFolder]', 'materialFolder() only works with meshStandardMaterial'); 12 | } 13 | 14 | const folder = this.folder({ name: params.name }); 15 | 16 | folder.slider({ obj: material, prop: 'roughness' }); 17 | folder.slider({ obj: material, prop: 'metalness' }); 18 | folder.color({ name: 'color', obj: material, value: '#'+material.color.getHexString() }, val => { 19 | material.color.set(val) 20 | }); 21 | folder.slider({ obj: material, prop: 'envMapIntensity' }); 22 | return folder; 23 | } 24 | 25 | export function threejsColor() { 26 | 27 | } -------------------------------------------------------------------------------- /src/components/Slider.js: -------------------------------------------------------------------------------- 1 | export default class Slider { 2 | constructor(parent, params = {}, callback) { 3 | this.parent = parent; 4 | this.propReferences = []; 5 | 6 | if (typeof params != 'object') { 7 | throw Error(`[GUI] slider() first parameter must be an object. Received: ${typeof params}.`); 8 | } 9 | 10 | let name = typeof params.name == 'string' ? params.name || ' ' : ' '; 11 | this.isObject = false; 12 | let propReferenceIndex = null; 13 | this.obj = params.obj; 14 | this.prop = params.prop; 15 | let value = typeof params.value == 'number' ? params.value : null; 16 | this.min = params.min ?? 0; 17 | this.max = params.max ?? 1; 18 | this.step = params.step || (this.max - this.min) / 100; 19 | this.decimals = this.parent._countDecimals(this.step); 20 | this.callback = typeof callback == 'function' ? callback : null; 21 | 22 | // callback mode 23 | if ( value !== null ) { 24 | if (this.prop != undefined || this.obj != undefined) { 25 | console.warn(`[GUI] slider() "obj" and "prop" parameters are ignored when a "value" parameter is used.`); 26 | } 27 | } 28 | // object-binding 29 | else if (this.prop != undefined && this.obj != undefined) { 30 | if (typeof this.prop != 'string') { 31 | throw Error(`[GUI] slider() "prop" parameter must be an string. Received: ${typeof this.prop}.`); 32 | } 33 | if (typeof this.obj != 'object') { 34 | throw Error(`[GUI] slider() "obj" parameter must be an object. Received: ${typeof this.obj}.`); 35 | } 36 | 37 | if (name == ' ') { 38 | name = this.prop; 39 | } 40 | 41 | propReferenceIndex = this.propReferences.push(this.obj[this.prop]) - 1; 42 | this.isObject = true; 43 | } 44 | else { 45 | if ((this.prop != undefined && this.obj == undefined) || (this.prop == undefined && this.obj != undefined)) { 46 | console.warn(`[GUI] slider() "obj" and "prop" parameters must be used together.`); 47 | } 48 | 49 | value = (this.max - this.min) / 2; 50 | } 51 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 52 | 53 | this.imageContainer = null; 54 | 55 | const container = document.createElement('div'); 56 | container.className = 'p-gui__slider'; 57 | 58 | if (tooltip) { 59 | container.setAttribute('title', tooltip); 60 | } 61 | 62 | const slider_name = document.createElement('div'); 63 | slider_name.className = 'p-gui__slider-name'; 64 | slider_name.textContent = name; 65 | container.append(slider_name); 66 | 67 | this.ctrlDiv = document.createElement('div'); 68 | this.ctrlDiv.className = 'p-gui__slider-ctrl'; 69 | this.ctrlDiv.setAttribute('type', 'range'); 70 | this.ctrlDiv.setAttribute('min', this.min); 71 | this.ctrlDiv.setAttribute('max', this.max); 72 | container.append(this.ctrlDiv); 73 | 74 | const slider_bar = document.createElement('div'); 75 | slider_bar.className = 'p-gui__slider-bar'; 76 | this.ctrlDiv.append(slider_bar); 77 | 78 | this.handle = document.createElement('div'); 79 | this.handle.className = 'p-gui__slider-handle'; 80 | this.ctrlDiv.append(this.handle); 81 | 82 | this.filling = document.createElement('div'); 83 | this.filling.className = 'p-gui__slider-filling'; 84 | slider_bar.append(this.filling); 85 | 86 | this.valueInput = document.createElement('input'); 87 | this.valueInput.className = 'p-gui__slider-value'; 88 | this.valueInput.value = this.isObject ? this.obj[this.prop] : value; 89 | container.append(this.valueInput); 90 | 91 | // init position 92 | setTimeout(() => { 93 | const handleWidth = this.handle.offsetWidth; 94 | this.handle.position = this._mapLinear(this.valueInput.value, this.min, this.max, handleWidth / 2, 88 - handleWidth / 2); 95 | this.handle.position = Math.min(this.handle.position, 88 - handleWidth / 2); 96 | this.handle.position = Math.max(this.handle.position, handleWidth / 2); 97 | this.handle.style.transform = `translate(-50%, -50%) translateX(${this.handle.position}px)`; 98 | this.filling.style.width = `${this.handle.position}px`; 99 | }, 0); // wait for render 100 | 101 | this.valueInput.addEventListener('change', () => { 102 | this._updateHandlePositionFromValue(); 103 | this._triggerCallbacks(); 104 | }) 105 | 106 | this.ctrlDiv.addEventListener('pointerdown', (evt) => { 107 | this.ctrlDiv.pointerDown = true; 108 | this.ctrlDiv.prevPosition = evt.clientX; 109 | this._updateHandlePositionFromPointer(evt, true); 110 | }); 111 | 112 | window.addEventListener('pointerup', (evt) => { 113 | this.ctrlDiv.pointerDown = false; 114 | }); 115 | 116 | window.addEventListener('pointermove', (evt) => { 117 | if (this.ctrlDiv.pointerDown) { 118 | this.ctrlDiv.pointerDelta = evt.clientX - this.ctrlDiv.prevPosition; 119 | this._updateHandlePositionFromPointer(evt); 120 | } 121 | }); 122 | 123 | if ( this.isObject ) { 124 | Object.defineProperty( this.obj, this.prop, { 125 | set: val => { 126 | this.propReferences[propReferenceIndex] = val; 127 | this.valueInput.value = val; 128 | 129 | this._updateHandlePositionFromValue(); 130 | 131 | if (this.callback) { 132 | this.callback(parseFloat(this.valueInput.value)); 133 | } 134 | }, 135 | get: () => { 136 | return this.propReferences[propReferenceIndex]; 137 | } 138 | }); 139 | } 140 | 141 | return container; 142 | } 143 | 144 | _updateHandlePositionFromPointer(evt, firstDown = false) { 145 | const sliderWidth = this.ctrlDiv.offsetWidth; 146 | const handleWidth = this.handle.offsetWidth; 147 | const pointerDelta = evt.clientX - this.ctrlDiv.prevPosition; 148 | const currentValue = parseFloat(this.valueInput.value); 149 | let handlePosition; 150 | 151 | if (firstDown) { 152 | handlePosition = evt.offsetX; 153 | } else { 154 | handlePosition = this.handle.position + pointerDelta; 155 | } 156 | 157 | handlePosition = Math.max(handleWidth / 2, Math.min(handlePosition, sliderWidth - handleWidth / 2)); 158 | 159 | let newValue = this.min + (this.max - this.min) * (handlePosition - handleWidth / 2) / (sliderWidth - handleWidth); 160 | if ( newValue > currentValue ) { 161 | newValue = this._quantizeFloor(newValue, this.step); 162 | } else { 163 | newValue = this._quantizeCeil(newValue, this.step); 164 | } 165 | 166 | // toFixed(9) avoids weird javascript infinite decimals 167 | newValue = parseFloat(newValue.toFixed(9)); 168 | const nextValue = parseFloat((currentValue + this.step).toFixed(9)); 169 | const prevValue = parseFloat((currentValue - this.step).toFixed(9)); 170 | 171 | if (newValue >= nextValue || newValue <= prevValue) { 172 | newValue = newValue.toFixed(this.decimals); 173 | 174 | this.valueInput.value = newValue; 175 | 176 | this.ctrlDiv.prevPosition = evt.clientX; 177 | 178 | this.handle.style.transform = `translate(-50%, -50%) translateX(${handlePosition}px)`; 179 | this.handle.position = handlePosition; 180 | 181 | this.filling.style.width = this.handle.position + 'px'; 182 | 183 | this._triggerCallbacks(); 184 | } 185 | } 186 | 187 | _updateHandlePositionFromValue() { 188 | const sliderWidth = this.ctrlDiv.offsetWidth; 189 | const handleWidth = this.handle.offsetWidth; 190 | let handlePosition = this._mapLinear(this.valueInput.value, this.min, this.max, handleWidth / 2, sliderWidth - handleWidth / 2); 191 | 192 | handlePosition = Math.max(handleWidth / 2, Math.min(handlePosition, sliderWidth - handleWidth / 2)); 193 | 194 | this.handle.style.transform = `translate(-50%, -50%) translateX(${handlePosition}px)`; 195 | this.handle.position = handlePosition; 196 | 197 | this.filling.style.width = this.handle.position + 'px'; 198 | } 199 | 200 | _triggerCallbacks() { 201 | if ( this.isObject ) { 202 | this.obj[this.prop] = parseFloat(this.valueInput.value); 203 | } 204 | else { 205 | if (this.callback) { 206 | this.callback(parseFloat(this.valueInput.value)); 207 | } 208 | } 209 | 210 | if (this.parent.onUpdate) { 211 | this.parent.onUpdate(); 212 | } else if (this.parent.isFolder && this.parent.firstParent.onUpdate) { 213 | this.parent.firstParent.onUpdate(); 214 | } 215 | } 216 | 217 | _mapLinear( x, a1, a2, b1, b2 ) { 218 | return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 ); 219 | } 220 | 221 | _quantize(x, step) { 222 | return step * Math.round(x / step); 223 | } 224 | 225 | _quantizeCeil(x, step) { 226 | return step * Math.ceil(x / step); 227 | } 228 | 229 | _quantizeFloor(x, step) { 230 | return step * Math.floor(x / step); 231 | } 232 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Slider from './components/Slider'; 2 | import styles from './styles/styles'; 3 | 4 | export default class GUI { 5 | constructor(options = {}) { 6 | this.firstParent = this; 7 | 8 | if ( options.container ) { 9 | this.container = typeof options.container == "string" ? document.querySelector(options.container) : options.container; 10 | this.position_type = 'absolute'; 11 | } else { 12 | this.container = document.body; 13 | this.position_type = 'fixed'; 14 | } 15 | 16 | this.propReferences = []; 17 | this.folders = []; 18 | 19 | if ( options.isFolder ) { 20 | this._folderConstructor(options.folderOptions); 21 | return; 22 | } 23 | 24 | if ( typeof options.onUpdate == 'function' ) { 25 | this.onUpdate = options.onUpdate; 26 | } 27 | 28 | this.name = (options != undefined && typeof options.name == "string") ? options.name : ''; 29 | 30 | this.backgroundColor = options.color || null; 31 | this.opacity = options.opacity || 1; 32 | 33 | if (this.container == document.body) { 34 | this.maxHeight = window.innerHeight; 35 | } else { 36 | this.maxHeight = Math.min(this.container.clientHeight, window.innerHeight) 37 | } 38 | if ( options.maxHeight ) { 39 | this.initMaxHeight = options.maxHeight; 40 | this.maxHeight = Math.min(this.initMaxHeight, this.maxHeight); 41 | } 42 | 43 | this.screenCorner = this._parseScreenCorner(options.position); 44 | 45 | if (!window.perfectGUI) { 46 | window.perfectGUI = {}; 47 | } 48 | if ( window.perfectGUI.instanceCounter == undefined ) { 49 | window.perfectGUI.instanceCounter = 0; 50 | } else { 51 | window.perfectGUI.instanceCounter++; 52 | } 53 | this.instanceId = window.perfectGUI.instanceCounter; 54 | 55 | this.wrapperWidth = options.width || 290; 56 | this.stylesheet = document.createElement('style'); 57 | this.stylesheet.setAttribute('type', 'text/css'); 58 | this.stylesheet.setAttribute('id', 'lm-gui-stylesheet'); 59 | document.head.append(this.stylesheet); 60 | 61 | // Common styles 62 | if (this.instanceId == 0) { 63 | this._addStyles(`${styles(this.position_type)}`); 64 | } 65 | 66 | // Instance specific styles 67 | this._styleInstance(); 68 | 69 | this._addWrapper(); 70 | this.wrapper.setAttribute('data-corner-x', this.screenCorner.x); 71 | this.wrapper.setAttribute('data-corner-y', this.screenCorner.y); 72 | 73 | if (options.autoRepositioning != false) { 74 | window.addEventListener('resize', this._handleResize.bind(this)); 75 | } 76 | this._handleResize(); 77 | 78 | this.hasBeenDragged = false; 79 | if (options.draggable == true) this._makeDraggable(); 80 | 81 | this.closed = false; 82 | if (options.closed) this.toggleClose(); 83 | } 84 | 85 | _styleInstance() { 86 | let scrollbar_width = this._getScrollbarWidth(this.container); 87 | if (this.screenCorner.x == 'left') { 88 | this.xOffset = 0; 89 | } else { 90 | this.xOffset = this.container.clientWidth - this.wrapperWidth - scrollbar_width; 91 | } 92 | 93 | if (this.instanceId > 0) { 94 | let existingDomInstances = this.container.querySelectorAll('.p-gui'); 95 | for (let i = 0; i < existingDomInstances.length; i++) { 96 | if (this.screenCorner.y == existingDomInstances[i].dataset.cornerY) { 97 | if (this.screenCorner.x == 'left' && existingDomInstances[i].dataset.cornerX == 'left') { 98 | this.xOffset += existingDomInstances[i].offsetWidth; 99 | } 100 | else if (this.screenCorner.x == 'right' && existingDomInstances[i].dataset.cornerX == 'right') { 101 | this.xOffset -= existingDomInstances[i].offsetWidth; 102 | } 103 | } 104 | } 105 | } 106 | this.yOffset = 0; 107 | this.position = { 108 | prevX: this.xOffset, 109 | prevY: this.yOffset, 110 | x: this.xOffset, 111 | y: this.yOffset 112 | }; 113 | 114 | this._addStyles(`#p-gui-${this.instanceId} { 115 | width: ${this.wrapperWidth}px; 116 | max-height: ${this.maxHeight}px; 117 | transform: translate3d(${this.xOffset}px,${this.yOffset}px,0); 118 | ${ this.screenCorner.y == 'top' ? '' : 'top: auto; bottom: 0;' } 119 | ${ this.backgroundColor ? 'background: ' + this.backgroundColor + ';' : '' } 120 | opacity: ${this.opacity}; 121 | }`); 122 | } 123 | 124 | _folderConstructor(folderOptions) { 125 | this.wrapper = folderOptions.wrapper; 126 | this.isFolder = true; 127 | this.parent = folderOptions.parent; 128 | this.firstParent = folderOptions.firstParent; 129 | } 130 | 131 | _parseScreenCorner(position) { 132 | let parsedPosition = {x: 'right', y: 'top'}; 133 | 134 | if (position == undefined) return parsedPosition; 135 | else if (typeof position != 'string') console.error('[perfect-gui] Position must be a string.'); 136 | 137 | if (position.includes('left')) parsedPosition.x = 'left'; 138 | if (position.includes('bottom')) parsedPosition.y = 'bottom'; 139 | 140 | return parsedPosition; 141 | } 142 | 143 | _getScrollbarWidth(element) { 144 | if (element === document.body) { 145 | return window.innerWidth - document.documentElement.clientWidth; 146 | } else { 147 | return element.offsetWidth - element.clientWidth; 148 | } 149 | } 150 | 151 | _handleResize() { 152 | if (this.container == document.body) { 153 | this.maxHeight = window.innerHeight; 154 | } else { 155 | this.maxHeight = Math.min(this.container.clientHeight, window.innerHeight) 156 | } 157 | if (this.initMaxHeight) { 158 | this.maxHeight = Math.min(this.initMaxHeight, this.maxHeight); 159 | } 160 | this.wrapper.style.maxHeight = this.maxHeight + 'px'; 161 | 162 | if (this.hasBeenDragged) { 163 | return; 164 | } 165 | 166 | let scrollbar_width = this._getScrollbarWidth(this.container); 167 | this.xOffset = this.screenCorner.x == 'left' ? 0 : this.container.clientWidth - this.wrapperWidth - scrollbar_width; 168 | if (this.instanceId > 0) { 169 | let existingDomInstances = this.container.querySelectorAll(`.p-gui:not(#${this.wrapper.id}):not([data-dragged])`); 170 | for (let i = 0; i < existingDomInstances.length; i++) { 171 | let instanceId = parseInt(existingDomInstances[i].id.replace('p-gui-', '')); 172 | if (instanceId > this.instanceId) break; 173 | if (this.screenCorner.y == existingDomInstances[i].dataset.cornerY) { 174 | if (this.screenCorner.x == 'left' && existingDomInstances[i].dataset.cornerX == 'left') { 175 | this.xOffset += existingDomInstances[i].offsetWidth; 176 | } 177 | else if (this.screenCorner.x == 'right' && existingDomInstances[i].dataset.cornerX == 'right') { 178 | this.xOffset -= existingDomInstances[i].offsetWidth; 179 | } 180 | } 181 | } 182 | } 183 | this.position = {prevX:this.xOffset, prevY:this.yOffset, x:this.xOffset, y:this.yOffset}; 184 | this.wrapper.style.transform = `translate3d(${this.position.x}px, ${this.position.y}px, 0)`; 185 | } 186 | 187 | _addStyles(styles) { 188 | this.stylesheet.innerHTML += styles; 189 | } 190 | 191 | _addWrapper() { 192 | this.wrapper = document.createElement('div'); 193 | this.wrapper.id = 'p-gui-'+this.instanceId; 194 | this.wrapper.className = 'p-gui'; 195 | this.wrapper.setAttribute('data-lenis-prevent', ''); 196 | this.container.append(this.wrapper); 197 | 198 | this.header = document.createElement('div'); 199 | this.header.className = 'p-gui__header'; 200 | this.header.textContent = this.name; 201 | this.header.style = `${ this.backgroundColor ? 'border-color: ' + this.backgroundColor + ';' : ''}`; 202 | this.wrapper.append(this.header); 203 | 204 | const close_btn = document.createElement('div'); 205 | close_btn.className = 'p-gui__header-close'; 206 | close_btn.addEventListener('click', this.toggleClose.bind(this)); 207 | this.header.append(close_btn); 208 | } 209 | 210 | button(options, callback) { 211 | let name = ''; 212 | if (typeof options != 'string') { 213 | if (typeof options == 'object' && options?.hasOwnProperty('name')) { 214 | name = options.name == '' ? ' ' : options.name; 215 | } else { 216 | name = ' '; 217 | } 218 | } else { 219 | name = options == '' ? ' ' : options; 220 | } 221 | 222 | const tooltip = (typeof options.tooltip === 'string') ? options.tooltip : (options.tooltip === true ? name : null); 223 | 224 | this.imageContainer = null; 225 | 226 | const el = document.createElement('div'); 227 | el.className = 'p-gui__button'; 228 | el.textContent = name; 229 | if ( tooltip ) { 230 | el.setAttribute('title', tooltip); 231 | } 232 | el.addEventListener('click', () => { 233 | if (callback) { 234 | callback(); 235 | } 236 | 237 | if (this.onUpdate) { 238 | this.onUpdate(); 239 | } else if (this.isFolder && this.firstParent.onUpdate) { 240 | this.firstParent.onUpdate(); 241 | } 242 | }); 243 | this.wrapper.append(el); 244 | 245 | if (typeof options.color == 'string') { 246 | el.style.setProperty('--color-accent', options.color); 247 | el.style.setProperty('--color-accent-hover', options.hoverColor || options.color); 248 | } 249 | } 250 | 251 | image(params = {}, callback) { 252 | if (typeof params != 'object') { 253 | throw Error(`[GUI] image() first parameter must be an object. Received: ${typeof params}.`); 254 | } 255 | 256 | let path; 257 | if (typeof params.path == 'string') { 258 | path = params.path; 259 | } else { 260 | if (typeof params.path == undefined) { 261 | throw Error(`[GUI] image() path must be provided.`); 262 | } else { 263 | throw Error(`[GUI] image() path must be a string.`); 264 | } 265 | } 266 | let filename = path.replace(/^.*[\\\/]/, ''); 267 | let name; 268 | if (params.name == undefined) { 269 | name = filename; 270 | } else { 271 | name = typeof params.name == 'string' ? params.name || ' ' : ' '; 272 | } 273 | 274 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 275 | 276 | const selected = params.selected === true; 277 | const selectionBorder = params.selectionBorder !== false; 278 | 279 | // width & height options 280 | let inline_styles = ''; 281 | if (params.width) { 282 | if (typeof params.width == 'number') { 283 | params.width += 'px'; 284 | } 285 | inline_styles += `flex: 0 0 calc(${params.width} - 5px); `; 286 | } 287 | 288 | if (params.height) { 289 | if (typeof params.height == 'number') { 290 | params.height += 'px'; 291 | } 292 | inline_styles += `height: ${params.height}; `; 293 | } 294 | 295 | if (!this.imageContainer) { 296 | this.imageContainer = document.createElement('div'); 297 | this.imageContainer.className = 'p-gui__image-container'; 298 | this.wrapper.append(this.imageContainer); 299 | } 300 | 301 | // Image button 302 | const image = document.createElement('div'); 303 | image.className = 'p-gui__image'; 304 | image.style = 'background-image: url(' + path + '); ' + inline_styles; 305 | if ( tooltip ) { 306 | image.setAttribute('title', tooltip); 307 | } 308 | this.imageContainer.append(image); 309 | 310 | if (selected && selectionBorder) { 311 | image.classList.add('p-gui__image--selected'); 312 | } 313 | 314 | // Text inside image 315 | const text = document.createElement('div'); 316 | text.className = 'p-gui__image-text'; 317 | text.textContent = name; 318 | image.append(text); 319 | 320 | image.addEventListener('click', () => { 321 | let selected_items = image.parentElement.querySelectorAll('.p-gui__image--selected'); 322 | for (let i = 0; i < selected_items.length; i++) { 323 | selected_items[i].classList.remove('p-gui__image--selected'); 324 | } 325 | if (selectionBorder) { 326 | image.classList.add('p-gui__image--selected'); 327 | } 328 | if (typeof callback == 'function') { 329 | callback({ path, text: name }); 330 | } 331 | if (this.onUpdate) { 332 | this.onUpdate(); 333 | } else if (this.isFolder && this.firstParent.onUpdate) { 334 | this.firstParent.onUpdate(); 335 | } 336 | }); 337 | 338 | return image; 339 | } 340 | 341 | slider (params = {}, callback) { 342 | const el = new Slider(this, params, callback); 343 | this.wrapper.append(el); 344 | } 345 | 346 | toggle(params = {}, callback) { 347 | if (typeof params != 'object') { 348 | throw Error(`[GUI] toggle() first parameter must be an object. Received: ${typeof params}.`); 349 | } 350 | 351 | let name = typeof params.name == 'string' ? params.name || ' ' : ' '; 352 | let isObject = false; 353 | let propReferenceIndex = null; 354 | let obj = params.obj; 355 | let prop = params.prop; 356 | let value = typeof params.value === 'boolean' ? params.value : null; 357 | 358 | // callback mode 359 | if ( value !== null ) { 360 | if (prop != undefined || obj != undefined) { 361 | console.warn(`[GUI] toggle() "obj" and "prop" parameters are ignored when a "value" parameter is used.`); 362 | } 363 | } 364 | 365 | // object-binding 366 | else if (prop != undefined && obj != undefined) { 367 | if (typeof prop != 'string') { 368 | throw Error(`[GUI] toggle() "prop" parameter must be an string. Received: ${typeof prop}.`); 369 | } 370 | if (typeof obj != 'object') { 371 | throw Error(`[GUI] toggle() "obj" parameter must be an object. Received: ${typeof obj}.`); 372 | } 373 | 374 | if (name == ' ') { 375 | name = prop; 376 | } 377 | 378 | propReferenceIndex = this.propReferences.push(obj[prop]) - 1; 379 | isObject = true; 380 | } 381 | else { 382 | if ((prop != undefined && obj == undefined) || (prop == undefined && obj == undefined)) { 383 | console.warn(`[GUI] toggle() "obj" and "prop" parameters must be used together.`); 384 | } 385 | } 386 | 387 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 388 | 389 | this.imageContainer = null; 390 | 391 | const container = document.createElement('div'); 392 | container.textContent = name; 393 | container.className = 'p-gui__switch'; 394 | if ( tooltip ) { 395 | container.setAttribute('title', tooltip); 396 | } 397 | this.wrapper.append(container); 398 | 399 | container.addEventListener('click', (ev) => { 400 | const checkbox = ev.target.childNodes[1]; 401 | 402 | let value = true; 403 | 404 | if (checkbox.classList.contains('p-gui__switch-checkbox--active')) { 405 | value = false; 406 | } 407 | 408 | checkbox.classList.toggle('p-gui__switch-checkbox--active'); 409 | 410 | if ( isObject ) { 411 | obj[prop] = value; 412 | } 413 | 414 | else { 415 | if (typeof callback == 'function') { 416 | callback(value); 417 | } 418 | } 419 | 420 | if (this.onUpdate) { 421 | this.onUpdate(); 422 | } else if (this.isFolder && this.firstParent.onUpdate) { 423 | this.firstParent.onUpdate(); 424 | } 425 | }); 426 | 427 | let activeClass = (() => { 428 | if (!isObject) { 429 | return value ? ' p-gui__switch-checkbox--active' : ''; 430 | } else { 431 | return obj[prop] ? ' p-gui__switch-checkbox--active' : ''; 432 | } 433 | })(); 434 | 435 | const checkbox = document.createElement('div'); 436 | checkbox.className = 'p-gui__switch-checkbox' + activeClass; 437 | container.append(checkbox); 438 | 439 | if ( isObject ) { 440 | Object.defineProperty( obj, prop, { 441 | set: val => { 442 | this.propReferences[propReferenceIndex] = val; 443 | 444 | if (val) { 445 | checkbox.classList.add('p-gui__switch-checkbox--active'); 446 | } else { 447 | checkbox.classList.remove('p-gui__switch-checkbox--active'); 448 | } 449 | 450 | if (typeof callback == 'function') { 451 | callback(val); 452 | } 453 | }, 454 | get: () => { 455 | return this.propReferences[propReferenceIndex]; 456 | } 457 | }); 458 | } 459 | } 460 | 461 | list(params = {}, callback) { 462 | if (typeof params != 'object') { 463 | throw Error(`[GUI] list() first parameter must be an object. Received: ${typeof params}.`); 464 | } 465 | 466 | let name = typeof params.name == 'string' ? params.name : ' '; 467 | let isObject = false; 468 | let propReferenceIndex = null; 469 | let obj = params.obj; 470 | let prop = params.prop; 471 | let values = Array.isArray(params.values) ? params.values : null; 472 | let value; 473 | let objectValues = typeof values[0] == 'string' ? false : true; 474 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 475 | 476 | callback = typeof callback == 'function' ? callback : null; 477 | 478 | // callback mode 479 | if ( params.value !== undefined || 480 | (params.value === undefined && obj === undefined && prop === undefined)) { 481 | if (prop != undefined || obj != undefined) { 482 | console.warn(`[GUI] list() "obj" and "prop" parameters are ignored when a "value" parameter is used.`); 483 | } 484 | 485 | value = (() => { 486 | if (!values) { 487 | return null; 488 | } 489 | if (typeof params.value == 'string') { 490 | return values.indexOf(params.value); 491 | } 492 | if (typeof params.value == 'number') { 493 | return params.value; 494 | } 495 | })(); 496 | } 497 | 498 | // object-binding mode 499 | else if (prop != undefined && obj != undefined) { 500 | if (typeof prop != 'string') { 501 | throw Error(`[GUI] list() "prop" parameter must be an string. Received: ${typeof prop}.`); 502 | } 503 | if (typeof obj != 'object') { 504 | throw Error(`[GUI] list() "obj" parameter must be an object. Received: ${typeof obj}.`); 505 | } 506 | 507 | value = (() => { 508 | if (!values) { 509 | return null; 510 | } 511 | if (typeof obj[prop] == 'string') { 512 | if ( !objectValues ) { // values is an array of strings 513 | return values.indexOf(obj[prop]); 514 | } 515 | else { // values is an array of objects 516 | return values.find(item => item.value === obj[prop]).value; 517 | } 518 | } 519 | if (typeof obj[prop] == 'number') { 520 | if ( !objectValues ) { // values is an array of strings 521 | return obj[prop]; 522 | } 523 | else { // values is an array of objects 524 | return values.find(item => item.value === obj[prop]).value; 525 | } 526 | } 527 | })(); 528 | 529 | propReferenceIndex = this.propReferences.push(obj[prop]) - 1; 530 | isObject = true; 531 | } 532 | 533 | else { 534 | if ((prop != undefined && obj == undefined) || (prop == undefined && obj == undefined)) { 535 | console.warn(`[GUI] list() "obj" and "prop" parameters must be used together.`); 536 | } 537 | } 538 | 539 | this.imageContainer = null; 540 | 541 | let container = document.createElement('div'); 542 | container.className = 'p-gui__list'; 543 | container.textContent = name; 544 | if (tooltip) { 545 | container.setAttribute('title', tooltip); 546 | } 547 | this.wrapper.append(container); 548 | 549 | let select = document.createElement('select'); 550 | container.append(select); 551 | select.className = 'p-gui__list-dropdown'; 552 | select.addEventListener('change', (ev) => { 553 | if ( isObject ) { 554 | obj[prop] = ev.target.value; 555 | } 556 | 557 | else if (callback) { 558 | callback(ev.target.value); 559 | } 560 | 561 | if (this.onUpdate) { 562 | this.onUpdate(); 563 | } else if (this.isFolder && this.firstParent.onUpdate) { 564 | this.firstParent.onUpdate(); 565 | } 566 | }); 567 | 568 | if (values) 569 | { 570 | values.forEach((item, index) => 571 | { 572 | const optionName = objectValues ? item.name : item; 573 | const optionValue = objectValues ? item.value : item; 574 | let option = document.createElement('option'); 575 | option.setAttribute('value', optionValue); 576 | option.textContent = optionName; 577 | select.append(option); 578 | 579 | if (!objectValues && value == index || objectValues && value == optionValue) { 580 | option.setAttribute('selected', ''); 581 | } 582 | }); 583 | } 584 | 585 | if ( isObject ) { 586 | Object.defineProperty( obj, prop, { 587 | set: val => { 588 | let newIndex, newValue, newObj; 589 | if (objectValues) { 590 | newObj = values.find(item => { 591 | return item.value == val; 592 | }); 593 | newValue = newObj?.value || values[0].value; 594 | newIndex = values.indexOf(newObj); 595 | } else { 596 | if (typeof val == 'string') { 597 | newIndex = values.indexOf(val); 598 | newValue = val; 599 | } 600 | if (typeof val == 'number') { 601 | newIndex = val; 602 | newValue = values[val]; 603 | } 604 | } 605 | 606 | this.propReferences[propReferenceIndex] = objectValues ? newValue : val; 607 | 608 | const previousSelection = select.querySelector('[selected]'); 609 | if ( previousSelection ) { 610 | previousSelection.removeAttribute('selected') 611 | } 612 | select.querySelectorAll('option')[newIndex].setAttribute('selected', ''); 613 | 614 | if (typeof callback == 'function') { 615 | if (objectValues) { 616 | callback(newObj, newIndex); 617 | } else { 618 | callback(newValue, newIndex); 619 | } 620 | } 621 | }, 622 | get: () => { 623 | return this.propReferences[propReferenceIndex]; 624 | } 625 | }); 626 | } 627 | } 628 | 629 | color(params = {}, callback) { 630 | if (typeof params != 'object') { 631 | throw Error(`[GUI] color() first parameter must be an object. Received: ${typeof params}.`); 632 | } 633 | 634 | let name = typeof params.name == 'string' ? params.name || ' ' : ' '; 635 | 636 | let isObject = false; 637 | let propReferenceIndex = null; 638 | let obj = params.obj; 639 | let prop = params.prop; 640 | let value; 641 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 642 | 643 | if (typeof params.value == 'string') { 644 | if (params.value.length != 7 || params.value[0] != '#') { 645 | console.error(`[GUI] color() 'value' parameter must be an hexadecimal string in the format "#ffffff". Received: "${params.value}".`) 646 | } 647 | else { 648 | value = params.value; 649 | } 650 | } 651 | if (!value) value = '#000000'; 652 | 653 | // callback mode 654 | if ( params.value !== undefined ) { 655 | if (prop != undefined || obj != undefined) { 656 | console.warn(`[GUI] color() "obj" and "prop" parameters are ignored when a "value" parameter is used.`); 657 | } 658 | } 659 | 660 | // object-binding 661 | else if (prop != undefined && obj != undefined) { 662 | if (typeof prop != 'string') { 663 | throw Error(`[GUI] color() "prop" parameter must be an string. Received: ${typeof prop}.`); 664 | } 665 | if (typeof obj != 'object') { 666 | throw Error(`[GUI] color() "obj" parameter must be an object. Received: ${typeof obj}.`); 667 | } 668 | 669 | if (name == ' ') { 670 | name = prop; 671 | } 672 | 673 | propReferenceIndex = this.propReferences.push(obj[prop]) - 1; 674 | isObject = true; 675 | } 676 | else { 677 | if ((prop != undefined && obj == undefined) || (prop == undefined && obj == undefined)) { 678 | console.warn(`[GUI] color() "obj" and "prop" parameters must be used together.`); 679 | } 680 | } 681 | 682 | this.imageContainer = null; 683 | 684 | const container = document.createElement('div'); 685 | container.className = 'p-gui__color'; 686 | container.textContent = name; 687 | if ( tooltip ) { 688 | container.setAttribute('title', tooltip); 689 | } 690 | this.wrapper.append(container); 691 | 692 | const colorpicker = document.createElement('input'); 693 | colorpicker.className = 'p-gui__color-picker'; 694 | colorpicker.setAttribute('type', 'color'); 695 | colorpicker.value = value; 696 | container.append(colorpicker); 697 | 698 | if (typeof callback == 'function') { 699 | colorpicker.addEventListener('input', () => { 700 | if ( isObject ) { 701 | obj[prop] = colorpicker.value; 702 | } 703 | 704 | else if (typeof callback == 'function') { 705 | callback(colorpicker.value); 706 | } 707 | 708 | if (this.onUpdate) { 709 | this.onUpdate(); 710 | } else if (this.isFolder && this.firstParent.onUpdate) { 711 | this.firstParent.onUpdate(); 712 | } 713 | }); 714 | } 715 | 716 | if ( isObject ) { 717 | Object.defineProperty( obj, prop, { 718 | set: val => { 719 | this.propReferences[propReferenceIndex] = val; 720 | 721 | colorpicker.value = val; 722 | 723 | if (typeof callback == 'function') { 724 | callback(val); 725 | } 726 | }, 727 | get: () => { 728 | return this.propReferences[propReferenceIndex]; 729 | } 730 | }); 731 | } 732 | } 733 | 734 | vector2( params = {}, callback) { 735 | if (typeof params != 'object') { 736 | throw Error(`[GUI] vector2() first parameter must be an object. Received: ${typeof params}.`); 737 | } 738 | 739 | let name = typeof params.name == 'string' ? params.name || ' ' : ' '; 740 | 741 | const minX = params.x.min ?? 0; 742 | const maxX = params.x.max ?? 1; 743 | const minY = params.y.min ?? 0; 744 | const maxY = params.y.max ?? 1; 745 | const stepX = params.x.step || (maxX - minX) / 100; 746 | const stepY = params.y.step || (maxY - minY) / 100; 747 | const decimalsX = this._countDecimals(stepX); 748 | const decimalsY = this._countDecimals(stepY); 749 | 750 | const objectX = params.x.obj; 751 | const propX = params.x.prop; 752 | const propXReferenceIndex = this.propReferences.push(objectX[propX]) - 1; 753 | 754 | const objectY = params.y.obj; 755 | const propY = params.y.prop; 756 | const propYReferenceIndex = this.propReferences.push(objectY[propY]) - 1; 757 | 758 | const tooltip = (typeof params.tooltip === 'string') ? params.tooltip : (params.tooltip === true ? name : null); 759 | 760 | callback = typeof callback == 'function' ? callback : null; 761 | 762 | this.imageContainer = null; 763 | 764 | const container = document.createElement('div'); 765 | container.className = 'p-gui__vector2'; 766 | container.textContent = name; 767 | if ( tooltip ) { 768 | container.setAttribute('title', tooltip); 769 | } 770 | this.wrapper.append(container); 771 | 772 | const vector_value = document.createElement('div'); 773 | vector_value.className = 'p-gui__vector-value'; 774 | vector_value.textContent = objectX[propX] + ', ' + objectY[propY]; 775 | container.append(vector_value); 776 | 777 | const area = document.createElement('div'); 778 | area.className = 'p-gui__vector2-area'; 779 | container.append(area); 780 | area.addEventListener('click', evt => { 781 | const newX = parseFloat(this._mapLinear(evt.offsetX, 0, area.clientWidth, minX, maxX)); 782 | const newY = parseFloat(this._mapLinear(evt.offsetY, 0, area.clientHeight, maxY, minY)); 783 | objectX[propX] = newX.toFixed(decimalsX); 784 | objectY[propY] = newY.toFixed(decimalsY); 785 | 786 | if (callback) { 787 | callback(objectX[propX], objectX[propY]); 788 | } 789 | 790 | if (this.onUpdate) { 791 | this.onUpdate(); 792 | } else if (this.isFolder && this.firstParent.onUpdate) { 793 | this.firstParent.onUpdate(); 794 | } 795 | }); 796 | 797 | let pointer_is_down = false; 798 | area.addEventListener('pointerdown', (evt) => { 799 | pointer_is_down = true; 800 | }); 801 | area.addEventListener('pointerup', () => { 802 | pointer_is_down = false; 803 | }); 804 | area.addEventListener('pointermove', (evt) => { 805 | if (pointer_is_down) { 806 | const newX = parseFloat(this._mapLinear(evt.offsetX, 0, area.clientWidth, minX, maxX)); 807 | const newY = parseFloat(this._mapLinear(evt.offsetY, 0, area.clientHeight, maxY, minY)); 808 | objectX[propX] = newX.toFixed(decimalsX); 809 | objectY[propY] = newY.toFixed(decimalsY); 810 | 811 | if (callback) { 812 | callback(objectX[propX], objectX[propY]); 813 | } 814 | 815 | if (this.onUpdate) { 816 | this.onUpdate(); 817 | } else if (this.isFolder && this.firstParent.onUpdate) { 818 | this.firstParent.onUpdate(); 819 | } 820 | } 821 | }); 822 | 823 | const line_x = document.createElement('div'); 824 | line_x.className = 'p-gui__vector2-line p-gui__vector2-line-x'; 825 | area.append(line_x); 826 | 827 | const line_y = document.createElement('div'); 828 | line_y.className = 'p-gui__vector2-line p-gui__vector2-line-y'; 829 | area.append(line_y); 830 | 831 | const dot = document.createElement('div'); 832 | dot.className = 'p-gui__vector2-dot'; 833 | area.append(dot); 834 | 835 | dot.style.left = this._mapLinear(objectX[propX], minX, maxX, 0, area.clientWidth) + 'px'; 836 | dot.style.top = this._mapLinear(objectY[propY], minY, maxY, area.clientHeight, 0) + 'px'; 837 | 838 | Object.defineProperty( objectX, propX, { 839 | set: val => { 840 | this.propReferences[propXReferenceIndex] = val; 841 | dot.style.left = this._mapLinear(val, minX, maxX, 0, area.clientWidth) + 'px'; 842 | vector_value.textContent = String( val ) + ', ' + objectY[propY]; 843 | }, 844 | get: () => { 845 | return this.propReferences[propXReferenceIndex]; 846 | } 847 | }); 848 | 849 | Object.defineProperty( objectY, propY, { 850 | set: val => { 851 | this.propReferences[propYReferenceIndex] = val; 852 | dot.style.top = this._mapLinear(val, minY, maxY, area.clientHeight, 0) + 'px'; 853 | vector_value.textContent = objectX[propX] + ', ' + String( val ); 854 | }, 855 | get: () => { 856 | return this.propReferences[propYReferenceIndex]; 857 | } 858 | }); 859 | } 860 | 861 | folder(options = {}) { 862 | let closed = typeof options.closed == 'boolean' ? options.closed : false; 863 | let name = options.name || ''; 864 | let color = options.color || null; 865 | let maxHeight = options.maxHeight || null; 866 | 867 | this.imageContainer = null; 868 | 869 | let className = 'p-gui__folder'; 870 | 871 | if (this.folders.length == 0) { 872 | className += ' p-gui__folder--first'; 873 | } 874 | 875 | if (closed) { 876 | className += ' p-gui__folder--closed'; 877 | } 878 | 879 | let container_style = color ? `background-color: ${color};` : ''; 880 | container_style += maxHeight ? `max-height: ${maxHeight}px;` : ''; 881 | 882 | const container = document.createElement('div'); 883 | container.className = className; 884 | container.style = container_style; 885 | this.wrapper.append(container); 886 | 887 | const folderHeader = document.createElement('div'); 888 | folderHeader.innerHTML = `${name}`; 889 | folderHeader.className = 'p-gui__folder-header'; 890 | container.append(folderHeader); 891 | folderHeader.addEventListener('click', () => { 892 | container.classList.toggle('p-gui__folder--closed'); 893 | }); 894 | 895 | let folder = new GUI({isFolder: true, folderOptions: { 896 | wrapper: container, 897 | parent: this, 898 | firstParent: this.firstParent 899 | }}); 900 | this.folders.push(folder); 901 | return folder; 902 | } 903 | 904 | _makeDraggable() { 905 | var that = this; 906 | this.header.addEventListener('pointerdown', dragMouseDown); 907 | this.header.addEventListener('pointerup', dragMouseUp); 908 | 909 | function dragMouseDown(ev) { 910 | ev.preventDefault(); 911 | 912 | that.position.initX = that.position.x; 913 | that.position.initY = that.position.y; 914 | 915 | that.position.prevX = ev.clientX; 916 | that.position.prevY = ev.clientY; 917 | 918 | document.addEventListener('pointermove', dragElement); 919 | } 920 | 921 | function dragElement(ev) { 922 | ev.preventDefault(); 923 | if (!that.hasBeenDragged) { 924 | that.hasBeenDragged = true; 925 | that.wrapper.setAttribute('data-dragged', 'true') 926 | } 927 | 928 | that.position.x = that.position.initX + ev.clientX - that.position.prevX; 929 | that.position.y = that.position.initY + ev.clientY - that.position.prevY; 930 | 931 | that.wrapper.style.transform = "translate3d("+that.position.x + "px,"+that.position.y + "px,0)"; 932 | } 933 | 934 | function dragMouseUp(ev) { 935 | document.removeEventListener('pointermove', dragElement); 936 | } 937 | } 938 | 939 | toggleClose() { 940 | this.closed = !this.closed; 941 | 942 | if (this.closed) { 943 | this.previousInnerScroll = this.wrapper.scrollTop; 944 | this.wrapper.scrollTo(0,0); 945 | } else { 946 | this.wrapper.scrollTo(0,this.previousInnerScroll); 947 | } 948 | 949 | this.wrapper.classList.toggle('p-gui--collapsed'); 950 | } 951 | 952 | kill() { 953 | this.wrapper.remove(); 954 | } 955 | 956 | _mapLinear( x, a1, a2, b1, b2 ) { 957 | return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 ); 958 | } 959 | 960 | _countDecimals(num) { 961 | // Convert the number to a string 962 | const numStr = num.toString(); 963 | 964 | // Find the position of the decimal point 965 | const decimalIndex = numStr.indexOf('.'); 966 | 967 | // If there is no decimal point, return 0 968 | if (decimalIndex === -1) { 969 | return 0; 970 | } 971 | 972 | // Calculate the number of digits after the decimal point 973 | const decimalPlaces = numStr.length - decimalIndex - 1; 974 | 975 | return decimalPlaces; 976 | } 977 | } -------------------------------------------------------------------------------- /src/styles/_button.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__button { 3 | background: var(--color-accent); 4 | text-align: center; 5 | color: white; 6 | border: none; 7 | border: 1px solid transparent; 8 | box-sizing: border-box; 9 | transition: var(--transition) background, var(--transition) border-color; 10 | } 11 | 12 | .p-gui__button:hover { 13 | background: var(--color-accent-hover); 14 | color: var(--color-text-light); 15 | border-color: rgba(255, 255, 255, 0.2); 16 | } 17 | 18 | .p-gui__folder .p-gui__button { 19 | margin-inline: 0; 20 | } 21 | `; -------------------------------------------------------------------------------- /src/styles/_color.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__color { 3 | cursor: default; 4 | color: var(--color-text-dark); 5 | transition: var(--transition) color; 6 | } 7 | 8 | .p-gui__color:hover { 9 | color: var(--color-text-light); 10 | } 11 | 12 | .p-gui__color-picker { 13 | position: absolute; 14 | right: 5px; 15 | top: 0; 16 | bottom: 0; 17 | margin: auto; 18 | height: 21px; 19 | cursor: pointer; 20 | border-radius: 3px; 21 | border: 1px solid var(--color-border-2); 22 | outline: none; 23 | -webkit-appearance: none; 24 | padding: 0; 25 | background-color: transparent; 26 | border: 1px solid #222222; 27 | overflow: hidden; 28 | } 29 | 30 | .p-gui__color-picker::-webkit-color-swatch-wrapper { 31 | padding: 0; 32 | } 33 | .p-gui__color-picker::-webkit-color-swatch { 34 | border: none; 35 | } 36 | `; -------------------------------------------------------------------------------- /src/styles/_folder.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__folder { 3 | width: 100%; 4 | position: relative; 5 | background: #434343; 6 | overflow: auto; 7 | margin-bottom: 2px; 8 | display: flex; 9 | flex-wrap: wrap; 10 | border: 1px solid grey; 11 | padding: 0 3px 0 3px; 12 | border-radius: var(--main-border-radius); 13 | box-sizing: border-box; 14 | } 15 | 16 | .p-gui__folder--first { 17 | margin-top: 0; 18 | } 19 | 20 | .p-gui__folder--closed { 21 | height: 32px; 22 | overflow: hidden; 23 | } 24 | 25 | .p-gui__folder-header { 26 | padding: 10px 5px; 27 | background-color: rgba(0, 0, 0, .5); 28 | color: white; 29 | cursor: pointer; 30 | width: 100%; 31 | margin: 0 -2px 2px -3px; 32 | } 33 | 34 | .p-gui__folder-header:hover { 35 | background-color: rgba(0, 0, 0, .75); 36 | } 37 | 38 | .p-gui__folder-arrow { 39 | width: 8px; 40 | height: 8px; 41 | display: inline-block; 42 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAHlBMVEUAAAD///////////////////////////////////8kfJuVAAAACXRSTlMA9Z1fCdMo1yxEJnA0AAAAK0lEQVQI12PABlRgjKkJUMZMYRhjpgqMAZSEMICSaIzpDWiKhdENhEhgAgATSg5jyWnYewAAAABJRU5ErkJggg==); 43 | background-size: contain; 44 | margin-right: 5px; 45 | transform: rotate(90deg) 46 | } 47 | 48 | .p-gui__folder--closed .p-gui__folder-arrow { 49 | transform: rotate(0deg); 50 | } 51 | `; -------------------------------------------------------------------------------- /src/styles/_image.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__image-container { 3 | width: 100%; 4 | padding: 3px; 5 | display: flex; 6 | justify-content: flex-start; 7 | flex-wrap: wrap; 8 | box-sizing: border-box; 9 | } 10 | 11 | .p-gui__image { 12 | background-size: cover; 13 | cursor: pointer; 14 | position: relative; 15 | margin: 1px 2.5px 19px 2.5px; 16 | border-radius: var(--main-border-radius); 17 | flex: 0 0 calc(33.333% - 5px); 18 | height: 90px; 19 | background-position: center; 20 | color: var(--color-text-dark); 21 | transition: var(--transition) color; 22 | } 23 | 24 | .p-gui__image:hover { 25 | color: var(--color-text-light); 26 | } 27 | 28 | .p-gui__image::after { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 100%; 33 | height: 100%; 34 | content: ''; 35 | border: 1px solid transparent; 36 | box-sizing: border-box; 37 | border-radius: var(--main-border-radius); 38 | transition: var(--transition) border-color; 39 | } 40 | .p-gui__image--selected::after { 41 | border-color: #06FF89; 42 | } 43 | 44 | .p-gui__image-text { 45 | position: absolute; 46 | bottom: -15px; 47 | text-shadow: 0 -1px 0 #111; 48 | white-space: nowrap; 49 | width: 100%; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | 53 | } 54 | `; -------------------------------------------------------------------------------- /src/styles/_list.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__list { 3 | cursor: default; 4 | color: var(--color-text-dark); 5 | transition: var(--transition) color; 6 | } 7 | 8 | .p-gui__list:hover { 9 | color: var(--color-text-light); 10 | } 11 | 12 | .p-gui__list-dropdown { 13 | background: rgba(255, 255, 255,.05); 14 | color: white; 15 | padding: 0 12px 0 5px; 16 | top: 0px; 17 | } 18 | 19 | .p-gui__list-dropdown { 20 | position: absolute; 21 | right: 5px; 22 | top: 0; 23 | bottom: 0; 24 | margin: auto; 25 | height: 21px; 26 | cursor: pointer; 27 | border-radius: 3px; 28 | border: 1px solid var(--color-border-2); 29 | outline: none; 30 | } 31 | 32 | .p-gui__list-dropdown:hover { 33 | background: rgba(255, 255, 255, .1); 34 | } 35 | `; -------------------------------------------------------------------------------- /src/styles/_slider.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__slider { 3 | position: relative; 4 | min-height: 14px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | gap: 10px; 9 | color: var(--color-text-dark); 10 | transition: color var(--transition); 11 | } 12 | 13 | .p-gui__slider:hover { 14 | color: var(--color-text-light); 15 | } 16 | 17 | .p-gui__slider-name { 18 | width: 50%; 19 | text-overflow: ellipsis; 20 | overflow: hidden; 21 | } 22 | 23 | .p-gui__slider-ctrl { 24 | -webkit-appearance: none; 25 | padding: 0; 26 | font: inherit; 27 | outline: none; 28 | box-sizing: border-box; 29 | cursor: pointer; 30 | position: relative; 31 | right: 0; 32 | height: 14px; 33 | margin: 0 0 0 auto; 34 | width: 37%; 35 | } 36 | 37 | .p-gui__slider-bar { 38 | position: absolute; 39 | top: 50%; 40 | left: 0; 41 | height: 2px; 42 | background: rgba(255, 255, 255, .2); 43 | width: 100%; 44 | transform: translateY(-50%); 45 | } 46 | 47 | .p-gui__slider-filling { 48 | position: absolute; 49 | top: -25%; 50 | left: 0; 51 | height: 150%; 52 | background: var(--color-accent); 53 | width: 0; 54 | } 55 | 56 | .p-gui__slider:hover .p-gui__slider-filling { 57 | background: var(--color-accent-hover); 58 | } 59 | 60 | .p-gui__slider-handle { 61 | width: 15px; 62 | height: 8px; 63 | position: absolute; 64 | top: 50%; 65 | left: 0; 66 | border-radius: 2px; 67 | transform: translate(-50%, -50%); 68 | pointer-events: none; 69 | background: var(--color-text-dark); 70 | box-shadow: 0 0 2px rgba(0, 0, 0, .5); 71 | } 72 | 73 | .p-gui__slider:hover .p-gui__slider-handle { 74 | background: var(--color-text-light); 75 | } 76 | 77 | .p-gui__slider-value { 78 | display: inline-block; 79 | right: 7px; 80 | width: 13%; 81 | border: none; 82 | color: white; 83 | border-radius: 2px; 84 | background: rgba(255, 255, 255, 0.1); 85 | padding: 2px 4px; 86 | color: inherit; 87 | } 88 | 89 | .p-gui__slider-value:focus { 90 | outline: none; 91 | } 92 | `; -------------------------------------------------------------------------------- /src/styles/_switch.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__switch { 3 | background: rgba(255, 255, 255, .05); 4 | color: var(--color-text-dark); 5 | transition: var(--transition) background, var(--transition) color; 6 | } 7 | 8 | .p-gui__switch:hover { 9 | background: rgba(255, 255, 255, .1); 10 | color: var(--color-text-light); 11 | } 12 | 13 | .p-gui__folder .p-gui__switch { 14 | margin-inline: 0; 15 | } 16 | 17 | .p-gui__switch-checkbox { 18 | width: 5px; 19 | height: 5px; 20 | background-color: rgba(0, 0, 0, .5); 21 | border: 1px solid grey; 22 | position: absolute; 23 | top: 0; 24 | right: 10px; 25 | bottom: 0; 26 | margin: auto; 27 | border-radius: 50%; 28 | pointer-events: none; 29 | } 30 | 31 | .p-gui__switch-checkbox--active { 32 | background-color: #00ff89; 33 | box-shadow: 0 0 7px #00ff89; 34 | } 35 | `; -------------------------------------------------------------------------------- /src/styles/_vector2.css.js: -------------------------------------------------------------------------------- 1 | export default /* css */ ` 2 | .p-gui__vector2 { 3 | background: transparent; 4 | aspect-ratio: 1; 5 | padding-bottom: 0; 6 | color: var(--color-text-dark); 7 | } 8 | 9 | .p-gui__vector2:hover { 10 | color: var(--color-text-light); 11 | } 12 | 13 | .p-gui__vector2-area { 14 | position: relative; 15 | background: rgba(0, 0, 0, .3); 16 | margin-top: 8px; 17 | width: 100%; 18 | height: calc(100% - 28px); 19 | touch-action: none; 20 | } 21 | 22 | .p-gui__vector2-line { 23 | position: absolute; 24 | background: white; 25 | opacity: .3; 26 | pointer-events: none; 27 | } 28 | 29 | .p-gui__vector2-line-x { 30 | width: 100%; 31 | height: 1px; 32 | left: 0; 33 | top: 50%; 34 | transform: translateY(-50%); 35 | } 36 | 37 | .p-gui__vector2-line-y { 38 | width: 1px; 39 | height: 100%; 40 | top: 0; 41 | left: 50%; 42 | transform: translateX(-50%); 43 | } 44 | 45 | .p-gui__vector2-dot { 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 8px; 50 | height: 8px; 51 | border-radius: 50%; 52 | background: #d5d5d5; 53 | border: 2px solid #ff9999; 54 | transform: translate(-50%, -50%); 55 | pointer-events: none; 56 | } 57 | 58 | .p-gui__vector-value { 59 | display: inline-block; 60 | right: 7px; 61 | position: absolute; 62 | } 63 | `; -------------------------------------------------------------------------------- /src/styles/styles.js: -------------------------------------------------------------------------------- 1 | import _button from "./_button.css.js" 2 | import _slider from "./_slider.css.js" 3 | import _list from "./_list.css.js" 4 | import _switch from "./_switch.css.js" 5 | import _color from "./_color.css.js" 6 | import _vector2 from "./_vector2.css.js" 7 | import _image from "./_image.css.js" 8 | import _folder from "./_folder.css.js" 9 | 10 | /** 11 | * JS instead of CSS to avoid 12 | * depending on a css loader 13 | */ 14 | 15 | export default function( position_type ) { 16 | return /* css */` 17 | .p-gui { 18 | --main-border-radius: 5px; 19 | --color-bg: #121212; 20 | --color-border: #484848; 21 | --color-border-2: rgba(255,255,255,.1); 22 | --color-text-light: #ffffff; 23 | --color-text-dark: #bbbbbb; 24 | --color-accent: #1681ca; 25 | --color-accent-hover: #218fda; 26 | --transition: .1s linear; 27 | 28 | position: ${ position_type }; 29 | top: 0; 30 | left: 0; 31 | transform: translate3d(0,0,0); 32 | padding-top: 21px; 33 | padding-inline: 3px; 34 | background: var(--color-bg); 35 | display: block; 36 | justify-content: center; 37 | flex-wrap: wrap; 38 | font-family: "Arial Rounded MT Bold", Arial, sans-serif; 39 | width: 290px; 40 | overflow: auto; 41 | box-shadow: 0 0 2px black; 42 | box-sizing: border-box; 43 | z-index: 99999; 44 | user-select: none; 45 | border-bottom-right-radius: 3px; 46 | border-bottom-left-radius: 3px; 47 | cursor: auto; 48 | border-radius: var(--main-border-radius); 49 | border: 1px solid var(--color-border); 50 | line-height: normal; 51 | transition: var(--transition) opacity; 52 | } 53 | 54 | .p-gui:hover { 55 | opacity: 1!important; 56 | } 57 | 58 | .p-gui * { 59 | font-size: 11px; 60 | } 61 | 62 | .p-gui::-webkit-scrollbar, 63 | .p-gui *::-webkit-scrollbar { 64 | width: 10px; 65 | } 66 | 67 | .p-gui::-webkit-scrollbar-track, 68 | .p-gui *::-webkit-scrollbar-track { 69 | background: #2f2f2f; 70 | border-radius: 3px; 71 | } 72 | 73 | .p-gui::-webkit-scrollbar-thumb, 74 | .p-gui *::-webkit-scrollbar-thumb { 75 | background: #757576; 76 | border-radius: 10px; 77 | box-sizing: border-box; 78 | border: 1px solid #2f2f2f; 79 | } 80 | 81 | .p-gui--collapsed { 82 | height: 0; 83 | padding: 21px 10px 0 10px; 84 | overflow: hidden; 85 | } 86 | 87 | .p-gui__header { 88 | position: absolute; 89 | top: 0; 90 | left: 0; 91 | width: 100%; 92 | height: 20px; 93 | background-color: rgba(0, 0, 0, .8); 94 | cursor: grab; 95 | color: grey; 96 | font-size: 10px; 97 | line-height: 20px; 98 | padding-left: 12px; 99 | box-sizing: border-box; 100 | touch-action: none; 101 | } 102 | 103 | .p-gui__header-close { 104 | width: 20px; 105 | height: 20px; 106 | position: absolute; 107 | top: 0; 108 | right: 5px; 109 | cursor: pointer; 110 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABFJREFUCNdjIAb8//8BjIkAAOrOBd3TR0jRAAAAAElFTkSuQmCC); 111 | background-size: 50% 50%; 112 | background-position: center; 113 | background-repeat: no-repeat; 114 | } 115 | 116 | .p-gui--collapsed .p-gui__header-close { 117 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAABVJREFUCNdjYEhgIIj///8AwsSoBQD43QydY5mb0QAAAABJRU5ErkJggg==); 118 | } 119 | 120 | .p-gui__slider, 121 | .p-gui__button, 122 | .p-gui__switch, 123 | .p-gui__list, 124 | .p-gui__vector2, 125 | .p-gui__color { 126 | width: 100%; 127 | padding: 7px; 128 | cursor: pointer; 129 | position: relative; 130 | box-sizing: border-box; 131 | margin-block: 3px; 132 | border: 1px solid var(--color-border-2); 133 | border-radius: var(--main-border-radius); 134 | transition: var(--transition) border-color; 135 | } 136 | 137 | .p-gui__slider:hover, 138 | .p-gui__button:hover, 139 | .p-gui__switch:hover, 140 | .p-gui__list:hover, 141 | .p-gui__vector2:hover, 142 | .p-gui__color:hover { 143 | border-color: rgba(255,255,255,.2); 144 | } 145 | 146 | ${ _button } 147 | 148 | ${ _image } 149 | 150 | ${ _list } 151 | 152 | ${ _switch } 153 | 154 | ${ _slider } 155 | 156 | ${ _color } 157 | 158 | ${ _vector2 } 159 | 160 | ${ _folder } 161 | `}; -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, "src/index.js"), 8 | name: "Perfect GUI" 9 | }, 10 | minify: true 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------