From 4a373b57c5b41be9d84e3698f83333ea4e149ad6 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Fri, 24 Nov 2023 12:18:17 -0800 Subject: [PATCH] publish on github --- .github/workflows/build-and-deploy.yml | 34 + .github/workflows/test.yml | 24 + .gitignore | 7 + build/prep-for-deploy.js | 9 + dist/0.x/muigui.js | 3831 ------------------------ dist/0.x/muigui.min.js | 2 - dist/0.x/muigui.module.js | 3814 ----------------------- dist/0.x/muigui.module.min.js | 2 - package-lock.json | 14 +- package.json | 10 +- 10 files changed, 87 insertions(+), 7660 deletions(-) create mode 100644 .github/workflows/build-and-deploy.yml create mode 100644 .github/workflows/test.yml create mode 100644 build/prep-for-deploy.js delete mode 100644 dist/0.x/muigui.js delete mode 100644 dist/0.x/muigui.min.js delete mode 100644 dist/0.x/muigui.module.js delete mode 100644 dist/0.x/muigui.module.min.js diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..660edaf --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,34 @@ +name: Build and Deploy +on: + push: + tags: + - v* +permissions: + contents: write +jobs: + build-and-deploy: + runs-on: ubuntu-latest + environment: deploy + steps: + - name: Checkout ๐Ÿ›Ž๏ธ + uses: actions/checkout@v3 + + - name: Use Node.js ๐Ÿ˜‚ + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install and Build ๐Ÿ”ง + run: | + npm ci + npm run build-ci + + - name: Deploy ๐Ÿš€ + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: . + + - name: Publish to NPM ๐Ÿ“– + uses: JS-DevTools/npm-publish@v2 + with: + token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b20f8ad --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Test +on: + push: + branches: + - main + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout ๐Ÿ”๐ŸŸ๐Ÿฅค + uses: actions/checkout@v3 + with: + persist-credentials: false + + - name: Use Node.js ๐Ÿ˜‚ + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Test ๐Ÿงช + run: | + npm ci + npm run check-ci diff --git a/.gitignore b/.gitignore index c15b84f..bcf9cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# -- clip-for-deploy-start -- + +/docs +/dist + +# -- clip-for-deploy-end -- + *.pyc .DS_Store node_modules diff --git a/build/prep-for-deploy.js b/build/prep-for-deploy.js new file mode 100644 index 0000000..8bc69c9 --- /dev/null +++ b/build/prep-for-deploy.js @@ -0,0 +1,9 @@ +import fs from 'fs'; +import path from 'path'; +import * as url from 'url'; +const dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +const ignoreFilename = path.join(dirname, '..', '..', '.gitignore'); +const ignore = fs.readFileSync(ignoreFilename, {encoding: 'utf8'}); +const newIgnore = ignore.replace(/# -- clip-for-deploy-start --[\s\S]*?# -- clip-for-deploy-end --/, ''); +fs.writeFileSync(ignoreFilename, newIgnore); diff --git a/dist/0.x/muigui.js b/dist/0.x/muigui.js deleted file mode 100644 index 3e3bb6f..0000000 --- a/dist/0.x/muigui.js +++ /dev/null @@ -1,3831 +0,0 @@ -/* muigui@0.0.10, license MIT */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.GUI = factory()); -})(this, (function () { 'use strict'; - - var css = { - default: ` -.muigui { - --bg-color: #ddd; - --color: #222; - --contrast-color: #eee; - --value-color: #145 ; - --value-bg-color: #eeee; - --disabled-color: #999; - --menu-bg-color: #f8f8f8; - --menu-sep-color: #bbb; - --hover-bg-color: #999; - --focus-color: #68C; - --range-color: #888888; - --invalid-color: #FF0000; - --selected-color: rgb(255, 255, 255, 0.9); - - --button-bg-color: var(--value-bg-color); - - --range-left-color: var(--value-color); - --range-right-color: var(--value-bg-color); - --range-right-hover-color: var(--hover-bg-color); - - color: var(--color); - background-color: var(--bg-color); -} - -@media (prefers-color-scheme: dark) { - .muigui { - --bg-color: #222222; - --color: #dddddd; - --contrast-color: #000; - --value-color: #43e5f7; - --value-bg-color: #444444; - --disabled-color: #666666; - --menu-bg-color: #080808; - --menu-sep-color: #444444; - --hover-bg-color: #666666; - --focus-color: #88AAFF; - --range-color: #888888; - --invalid-color: #FF6666; - --selected-color: rgba(255, 255, 255, 0.3); - - --button-bg-color: var(--value-bg-color); - - --range-left-color: var(--value-color); - --range-right-color: var(--value-bg-color); - --range-right-hover-color: var(--hover-bg-color); - - color: var(--color); - background-color: var(--bg-color); - } -} - -.muigui { - --width: 250px; - --label-width: 45%; - --number-width: 40%; - - - --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; - --font-size: 11px; - --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace; - --font-size-mono: 11px; - - --line-height: 1.7em; - --border-radius: 0px; - - width: var(--width); - font-family: var(--font-family); - font-size: var(--font-size); - box-sizing: border-box; - line-height: 100%; -} -.muigui * { - box-sizing: inherit; -} - -.muigui-no-scroll { - touch-action: none; -} -.muigui-no-h-scroll { - touch-action: pan-y; -} -.muigui-no-v-scroll { - touch-action: pan-x; -} - -.muigui-invalid-value { - background-color: red !important; - color: white !important; -} - -.muigui-grid { - display: grid; -} -.muigui-rows { - display: flex; - flex-direction: column; - - min-height: 20px; - border: 2px solid red; -} -.muigui-columns { - display: flex; - flex-direction: row; - - height: 20px; - border: 2px solid green; -} -.muigui-rows>*, -.muigui-columns>* { - flex: 1 1 auto; - align-items: stretch; - min-height: 0; - min-width: 0; -} - -.muigui-row { - border: 2px solid yellow; - min-height: 10px -} -.muigui-column { - border: 2px solid lightgreen; -} - -/* -------- */ - -.muigui-show { /* */ } -.muigui-hide { - display: none !important; -} -.muigui-disabled { - pointer-events: none; - --color: var(--disabled-color) !important; - --value-color: var(--disabled-color) !important; - --range-left-color: var(--disabled-color) !important; -} - -.muigui canvas, -.muigui svg { - display: block; - border-radius: var(--border-radius); -} -.muigui canvas { - background-color: var(--value-bg-color); -} - -.muigui-controller { - min-width: 0; - min-height: var(--line-height); -} -.muigui-root, -.muigui-menu { - display: flex; - flex-direction: column; - position: relative; - user-select: none; - height: fit-content; - margin: 0; - padding-bottom: 0.1em; - border-radius: var(--border-radius); -} -.muigui-menu { - border-bottom: 1px solid var(--menu-sep-color); -} - -.muigui-root>button:nth-child(1), -.muigui-menu>button:nth-child(1) { - border-top: 1px solid var(--menu-sep-color); - border-bottom: 1px solid var(--menu-sep-color); - position: relative; - text-align: left; - color: var(--color); - background-color: var(--menu-bg-color); - min-height: var(--line-height); - padding-top: 0.2em; - padding-bottom: 0.2em; - cursor: pointer; - border-radius: var(--border-radius); -} -.muigui-root>div:nth-child(2), -.muigui-menu>div:nth-child(2) { - flex: 1 1 auto; -} - -.muigui-controller { - margin-left: 0.2em; - margin-right: 0.2em; -} -.muigui-root.muigui-controller, -.muigui-menu.muigui-controller { - margin-left: 0; - margin-right: 0; -} -.muigui-controller>*:nth-child(1) { - flex: 1 0 var(--label-width); - min-width: 0; - white-space: pre; -} -.muigui-controller>label:nth-child(1) { - place-content: center start; - display: inline-grid; - overflow: hidden; -} -.muigui-controller>*:nth-child(2) { - flex: 1 1 75%; - min-width: 0; -} - -/* ----------------------------------------- - a label controller is [[label][value]] -*/ - -.muigui-label-controller { - display: flex; - margin: 0.4em 0 0.4em 0; - word-wrap: initial; - align-items: stretch; -} - -.muigui-value { - display: flex; - align-items: stretch; -} -.muigui-value>* { - flex: 1 1 auto; - min-width: 0; -} -.muigui-value>*:nth-child(1) { - flex: 1 1 calc(100% - var(--number-width)); -} -.muigui-value>*:nth-child(2) { - flex: 1 1 var(--number-width); - margin-left: 0.2em; -} - -/* fix! */ -.muigui-open>button>label::before, -.muigui-closed>button>label::before { - width: 1.25em; - height: var(--line-height); - display: inline-grid; - place-content: center start; - pointer-events: none; -} -.muigui-open>button>label::before { - content: "โ“ง"; /*"โ–ผ";*/ -} -.muigui-closed>button>label::before { - content: "โจ"; /*"โ–ถ";*/ -} -.muigui-open>*:nth-child(2) { - transition: max-height 0.2s ease-out, - opacity 0.5s ease-out; - max-height: 100vh; - overflow: auto; - opacity: 1; -} - -.muigui-closed>*:nth-child(2) { - transition: max-height 0.2s ease-out, - opacity 1s; - max-height: 0; - opacity: 0; - overflow: hidden; -} - -/* ---- popdown ---- */ - -.muigui-pop-down-top { - display: flex; -} -/* fix? */ -.muigui-value>*:nth-child(1).muigui-pop-down-top { - flex: 0; -} -.muigui-pop-down-bottom { - -} - -.muigui-pop-down-values { - min-width: 0; - display: flex; -} -.muigui-pop-down-values>* { - flex: 1 1 auto; - min-width: 0; -} - -.muigui-value.muigui-pop-down-controller { - flex-direction: column; -} - -.muigui-pop-down-top input[type=checkbox] { - -webkit-appearance: none; - appearance: none; - width: auto; - color: var(--value-color); - background-color: var(--value-bg-color); - cursor: pointer; - - display: grid; - place-content: center; - margin: 0; - font: inherit; - color: currentColor; - width: 1.7em; - height: 1.7em; - transform: translateY(-0.075em); -} - -.muigui-pop-down-top input[type=checkbox]::before { - content: "+"; - display: grid; - place-content: center; - border-radius: calc(var(--border-radius) + 2px); - border-left: 1px solid rgba(255,255,255,0.3); - border-top: 1px solid rgba(255,255,255,0.3); - border-bottom: 1px solid rgba(0,0,0,0.2); - border-right: 1px solid rgba(0,0,0,0.2); - background-color: var(--range-color); - color: var(--value-bg-color); - width: calc(var(--line-height) - 4px); - height: calc(var(--line-height) - 4px); -} - -.muigui-pop-down-top input[type=checkbox]:checked::before { - content: "๏ผธ"; -} - - -/* ---- select ---- */ - -.muigui select, -.muigui option, -.muigui input, -.muigui button { - color: var(--value-color); - background-color: var(--value-bg-color); - font-family: var(--font-family); - font-size: var(--font-size); - border: none; - margin: 0; - border-radius: var(--border-radius); -} -.muigui select { - appearance: none; - margin: 0; - margin-left: 0; /*?*/ - overflow: hidden; /* Safari */ -} - -.muigui select:focus, -.muigui input:focus, -.muigui button:focus { - outline: 1px solid var(--focus-color); -} - -.muigui select:hover, -.muigui option:hover, -.muigui input:hover, -.muigui button:hover { - background-color: var(--hover-bg-color); -} - -/* ------ [ label ] ------ */ - -.muigui-label { - border-top: 1px solid var(--menu-sep-color); - border-bottom: 1px solid var(--menu-sep-color); - padding-top: 0.4em; - padding-bottom: 0.3em; - place-content: center start; - background-color: var(--menu-bg-color); - white-space: pre; - border-radius: var(--border-radius); -} - -/* ------ [ divider] ------ */ - -.muigui-divider { - min-height: 6px; - border-top: 2px solid var(--menu-sep-color); - margin-top: 6px; -} - -/* ------ [ button ] ------ */ - -.muigui-button { - display: grid; - -} -.muigui-button button { - border: none; - color: var(--value-color); - background-color: var(--button-bg-color); - cursor: pointer; - place-content: center center; -} - -/* ------ [ color ] ------ */ - -.muigui-color>div { - overflow: hidden; - position: relative; - margin-left: 0; - margin-right: 0; /* why? */ - max-width: var(--line-height); - border-radius: var(--border-radius); -} - -.muigui-color>div:focus-within { - outline: 1px solid var(--focus-color); -} - -.muigui-color input[type=color] { - border: none; - padding: 0; - background: inherit; - cursor: pointer; - position: absolute; - width: 200%; - left: -10px; - top: -10px; - height: 200%; -} -.muigui-disabled canvas, -.muigui-disabled svg, -.muigui-disabled img, -.muigui-disabled .muigui-color input[type=color] { - opacity: 0.2; -} - -/* ------ [ checkbox ] ------ */ - -.muigui-checkbox>label:nth-child(2) { - display: grid; - place-content: center start; - margin: 0; -} - -.muigui-checkbox input[type=checkbox] { - -webkit-appearance: none; - appearance: none; - width: auto; - color: var(--value-color); - background-color: var(--value-bg-color); - cursor: pointer; - - display: grid; - place-content: center; - margin: 0; - font: inherit; - color: currentColor; - width: 1.7em; - height: 1.7em; - transform: translateY(-0.075em); -} - -.muigui-checkbox input[type=checkbox]::before { - content: ""; - color: var(--value-color); - display: grid; - place-content: center; -} - -.muigui-checkbox input[type=checkbox]:checked::before { - content: "โœ”"; -} - -.muigui input[type=number]::-webkit-inner-spin-button, -.muigui input[type=number]::-webkit-outer-spin-button { - -webkit-appearance: none; - appearance: none; - margin: 0; -} -.muigui input[type=number] { - -moz-appearance: textfield; -} - -/* ------ [ radio grid ] ------ */ - -.muigui-radio-grid>div { - display: grid; - gap: 2px; -} - -.muigui-radio-grid input { - appearance: none; - display: none; -} - -.muigui-radio-grid button { - color: var(--color); - width: 100%; - text-align: left; -} - -.muigui-radio-grid input:checked + button { - color: var(--value-color); - background-color: var(--selected-color); -} - -/* ------ [ color-chooser ] ------ */ - -.muigui-color-chooser-cursor { - stroke-width: 1px; - stroke: white; - fill: none; -} -.muigui-color-chooser-circle { - stroke-width: 1px; - stroke: white; - fill: none; -} - - -/* ------ [ vec2 ] ------ */ - -.muigui-vec2 svg { - background-color: var(--value-bg-color); -} - -.muigui-vec2-axis { - stroke: 1px; - stroke: var(--focus-color); -} - -.muigui-vec2-line { - stroke-width: 1px; - stroke: var(--value-color); - fill: var(--value-color); -} - -/* ------ [ direction ] ------ */ - -.muigui-direction svg { - background-color: rgba(0,0,0,0.2); -} - -.muigui-direction:focus-within svg { - outline: none; -} -.muigui-direction-range { - fill: var(--value-bg-color); -} -.muigui-direction svg:focus { - outline: none; -} -.muigui-direction svg:focus .muigui-direction-range { - stroke-width: 0.5px; - stroke: var(--focus-color); -} - -.muigui-direction-arrow { - fill: var(--value-color); -} - -/* ------ [ slider ] ------ */ - -.muigui-slider>div { - display: flex; - align-items: stretch; - height: var(--line-height); -} -.muigui-slider svg { - flex: 1 1 auto; -} -.muigui-slider .muigui-slider-up #muigui-orientation { - transform: scale(1, -1) translateY(-100%); -} - -.muigui-slider .muigui-slider-up #muigui-number-orientation { - transform: scale(1,-1); -} - -.muigui-ticks { - stroke: var(--range-color); -} -.muigui-thicks { - stroke: var(--color); - stroke-width: 2px; -} -.muigui-svg-text { - fill: var(--color); - font-size: 7px; -} -.muigui-mark { - fill: var(--value-color); -} - -/* ------ [ range ] ------ */ - - -.muigui-range input[type=range] { - -webkit-appearance: none; - appearance: none; - background-color: transparent; -} - -.muigui-range input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - border-radius: calc(var(--border-radius) + 2px); - border-left: 1px solid rgba(255,255,255,0.3); - border-top: 1px solid rgba(255,255,255,0.3); - border-bottom: 1px solid rgba(0,0,0,0.2); - border-right: 1px solid rgba(0,0,0,0.2); - background-color: var(--range-color); - margin-top: calc((var(--line-height) - 2px) / -2); - width: calc(var(--line-height) - 2px); - height: calc(var(--line-height) - 2px); -} - -.muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - appearance: none; - border: 1px solid var(--menu-sep-color); - height: 2px; -} - - -/* dat.gui style - doesn't work on Safari iOS */ - -/* -.muigui-range input[type=range] { - cursor: ew-resize; - overflow: hidden; -} - -.muigui-range input[type=range] { - -webkit-appearance: none; - appearance: none; - background-color: var(--range-right-color); - margin: 0; -} -.muigui-range input[type=range]:hover { - background-color: var(--range-right-hover-color); -} - -.muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - appearance: none; - height: max-content; - color: var(--range-left-color); - margin-top: -1px; -} - -.muigui-range input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 0px; - height: max-content; - box-shadow: -1000px 0 0 1000px var(--range-left-color); -} -*/ - -/* FF */ -/* -.muigui-range input[type=range]::-moz-slider-progress { - background-color: var(--range-left-color); -} -.muigui-range input[type=range]::-moz-slider-thumb { - height: max-content; - width: 0; - border: none; - box-shadow: -1000px 0 0 1000px var(--range-left-color); - box-sizing: border-box; -} -*/ - -.muigui-checkered-background { - background-color: #404040; - background-image: - linear-gradient(45deg, #808080 25%, transparent 25%), - linear-gradient(-45deg, #808080 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #808080 75%), - linear-gradient(-45deg, transparent 75%, #808080 75%); - background-size: 16px 16px; - background-position: 0 0, 0 8px, 8px -8px, -8px 0px; -} - -/* ---------------------------------------------------------- */ - -/* needs to be at bottom to take precedence */ -.muigui-auto-place { - max-height: 100%; - position: fixed; - top: 0; - right: 15px; - z-index: 100001; -} - -`, - themes: { - default: '', - float: ` - :root { - color-scheme: light dark, - } - - .muigui { - --width: 400px; - --bg-color: initial; - --label-width: 25%; - --number-width: 20%; - } - - input, - .muigui-label-controller>label { - text-shadow: - -1px -1px 0 var(--contrast-color), - 1px -1px 0 var(--contrast-color), - -1px 1px 0 var(--contrast-color), - 1px 1px 0 var(--contrast-color); - } - - .muigui-controller > label:nth-child(1) { - place-content: center end; - margin-right: 1em; - } - - .muigui-value > :nth-child(2) { - margin-left: 1em; - } - - .muigui-root>*:nth-child(1) { - display: none; - } - - .muigui-range input[type=range]::-webkit-slider-thumb { - border-radius: 1em; - } - - .muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: initial; - appearance: none; - border: 1px solid rgba(0, 0, 0, 0.25); - height: 2px; - } - - .muigui-colors { - --value-color: var(--color ); - --value-bg-color: rgba(0, 0, 0, 0.1); - --disabled-color: #cccccc; - --menu-bg-color: rgba(0, 0, 0, 0.1); - --menu-sep-color: #bbbbbb; - --hover-bg-color: rgba(0, 0, 0, 0); - --invalid-color: #FF0000; - --selected-color: rgba(0, 0, 0, 0.3); - --range-color: rgba(0, 0, 0, 0.125); - } -`, - }, - }; - - function setElemProps(elem, attrs, children) { - for (const [key, value] of Object.entries(attrs)) { - if (typeof value === 'function' && key.startsWith('on')) { - const eventName = key.substring(2).toLowerCase(); - elem.addEventListener(eventName, value, {passive: false}); - } else if (typeof value === 'object') { - for (const [k, v] of Object.entries(value)) { - elem[key][k] = v; - } - } else if (elem[key] === undefined) { - elem.setAttribute(key, value); - } else { - elem[key] = value; - } - } - for (const child of children) { - elem.appendChild(child); - } - return elem; - } - - function createElem(tag, attrs = {}, children = []) { - const elem = document.createElement(tag); - setElemProps(elem, attrs, children); - return elem; - } - - function addElem(tag, parent, attrs = {}, children = []) { - const elem = createElem(tag, attrs, children); - parent.appendChild(elem); - return elem; - } - - let nextId = 0; - function getNewId() { - return `muigui-id-${nextId++}`; - } - - function removeArrayElem(array, value) { - const ndx = array.indexOf(value); - if (ndx) { - array.splice(ndx, 1); - } - return array; - } - - /** - * Converts an camelCase or snake_case id to "camel case" or "snake case" - * @param {string} id - */ - const underscoreRE = /_/g; - const upperLowerRE = /([A-Z])([a-z])/g; - function idToLabel(id) { - return id.replace(underscoreRE, ' ') - .replace(upperLowerRE, (m, m1, m2) => `${m1.toLowerCase()} ${m2}`); - } - - function clamp$1(v, min, max) { - return Math.max(min, Math.min(max, v)); - } - - const isTypedArray = typeof SharedArrayBuffer !== 'undefined' - ? function isArrayBufferOrSharedArrayBuffer(a) { - return a && a.buffer && (a.buffer instanceof ArrayBuffer || a.buffer instanceof SharedArrayBuffer); - } - : function isArrayBuffer(a) { - return a && a.buffer && a.buffer instanceof ArrayBuffer; - }; - - const isArrayOrTypedArray = v => Array.isArray(v) || isTypedArray(v); - - // Yea, I know this should be `Math.round(v / step) * step - // but try step = 0.1, newV = 19.95 - // - // I get - // Math.round(19.95 / 0.1) * 0.1 - // 19.900000000000002 - // vs - // Math.round(19.95 / 0.1) / (1 / 0.1) - // 19.9 - // - const stepify = (v, from, step) => Math.round(from(v) / step) / (1 / step); - - const euclideanModulo$1 = (v, n) => ((v % n) + n) % n; - const lerp$1 = (a, b, t) => a + (b - a) * t; - function copyExistingProperties(dst, src) { - for (const key in src) { - if (key in dst) { - dst[key] = src[key]; - } - } - return dst; - } - - const mapRange = (v, inMin, inMax, outMin, outMax) => (v - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; - - const makeRangeConverters = ({from, to}) => { - return { - to: v => mapRange(v, ...from, ...to), - from: v => [true, mapRange(v, ...to, ...from)], - }; - }; - - const makeRangeOptions = ({from, to, step}) => { - return { - min: to[0], - max: to[1], - ...(step && {step}), - converters: makeRangeConverters({from, to}), - }; - }; - - // TODO: remove an use one in conversions. Move makeRangeConverters there? - const identity$1 = { - to: v => v, - from: v => [true, v], - }; - function makeMinMaxPair(gui, properties, minPropName, maxPropName, options) { - const { converters: { from } = identity$1 } = options; - const { min, max } = options; - const guiMinRange = options.minRange || 0; - const valueMinRange = from(guiMinRange)[1]; - const minGui = gui - .add(properties, minPropName, { - ...options, - min, - max: max - guiMinRange, - }) - .onChange(v => { - maxGui.setValue(Math.min(max, Math.max(v + valueMinRange, properties[maxPropName]))); - }); - const maxGui = gui - .add(properties, maxPropName, { - ...options, - min: min + guiMinRange, - max, - }) - .onChange(v => { - minGui.setValue(Math.max(min, Math.min(v - valueMinRange, properties[minPropName]))); - }); - return [ minGui, maxGui ]; - } - - class View { - domElement; - #childDestElem; - #views = []; - constructor(elem) { - this.domElement = elem; - this.#childDestElem = elem; - } - addElem(elem) { - this.#childDestElem.appendChild(elem); - return elem; - } - removeElem(elem) { - this.#childDestElem.removeChild(elem); - return elem; - } - pushSubElem(elem) { - this.#childDestElem.appendChild(elem); - this.#childDestElem = elem; - } - popSubElem() { - this.#childDestElem = this.#childDestElem.parentElement; - } - add(view) { - this.#views.push(view); - this.addElem(view.domElement); - return view; - } - remove(view) { - this.removeElem(view.domElement); - removeArrayElem(this.#views, view); - return view; - } - pushSubView(view) { - this.pushSubElem(view.domElement); - } - popSubView() { - this.popSubElem(); - } - setOptions(options) { - for (const view of this.#views) { - view.setOptions(options); - } - } - updateDisplayIfNeeded(newV, ignoreCache) { - for (const view of this.#views) { - view.updateDisplayIfNeeded(newV, ignoreCache); - } - return this; - } - $(selector) { - return this.domElement.querySelector(selector); - } - } - - class Controller extends View { - #changeFns; - #finishChangeFns; - #parent; - - constructor(className) { - super(createElem('div', {className: 'muigui-controller'})); - this.#changeFns = []; - this.#finishChangeFns = []; - // we need the specialization to come last so it takes precedence. - if (className) { - this.domElement.classList.add(className); - } - } - get parent() { - return this.#parent; - } - setParent(parent) { - this.#parent = parent; - this.enable(!this.disabled()); - } - show(show = true) { - this.domElement.classList.toggle('muigui-hide', !show); - this.domElement.classList.toggle('muigui-show', show); - return this; - } - hide() { - return this.show(false); - } - disabled() { - return !!this.domElement.closest('.muigui-disabled'); - } - - enable(enable = true) { - this.domElement.classList.toggle('muigui-disabled', !enable); - - // If disabled we need to set the attribute 'disabled=true' to all - // input/select/button/textarea's below - // - // If enabled we need to set the attribute 'disabled=false' to all below - // until we hit a disabled controller. - // - // ATM the problem is we can find the input/select/button/textarea elements - // but we can't easily find which controller they belong do. - // But we don't need to? We can just check up if it or parent has - // '.muigui-disabled' - ['input', 'button', 'select', 'textarea'].forEach(tag => { - this.domElement.querySelectorAll(tag).forEach(elem => { - const disabled = !!elem.closest('.muigui-disabled'); - elem.disabled = disabled; - }); - }); - - return this; - } - disable(disable = true) { - return this.enable(!disable); - } - onChange(fn) { - this.removeChange(fn); - this.#changeFns.push(fn); - return this; - } - removeChange(fn) { - removeArrayElem(this.#changeFns, fn); - return this; - } - onFinishChange(fn) { - this.removeFinishChange(fn); - this.#finishChangeFns.push(fn); - return this; - } - removeFinishChange(fn) { - removeArrayElem(this.#finishChangeFns, fn); - return this; - } - #callListeners(fns, newV) { - for (const fn of fns) { - fn.call(this, newV); - } - } - emitChange(value, object, property) { - this.#callListeners(this.#changeFns, value); - if (this.#parent) { - if (object === undefined) { - this.#parent.emitChange(value); - } else { - this.#parent.emitChange({ - object, - property, - value, - controller: this, - }); - } - } - } - emitFinalChange(value, object, property) { - this.#callListeners(this.#finishChangeFns, value); - if (this.#parent) { - if (object === undefined) { - this.#parent.emitChange(value); - } else { - this.#parent.emitFinalChange({ - object, - property, - value, - controller: this, - }); - } - } - } - updateDisplay() { - // placeholder. override - } - getColors() { - const toCamelCase = s => s.replace(/-([a-z])/g, (m, m1) => m1.toUpperCase()); - const keys = [ - 'color', - 'bg-color', - 'value-color', - 'value-bg-color', - 'hover-bg-color', - 'menu-bg-color', - 'menu-sep-color', - 'disabled-color', - ]; - const div = createElem('div'); - this.domElement.appendChild(div); - const colors = Object.fromEntries(keys.map(key => { - div.style.color = `var(--${key})`; - const s = getComputedStyle(div); - return [toCamelCase(key), s.color]; - })); - div.remove(); - return colors; - } - } - - class Button extends Controller { - #object; - #property; - #buttonElem; - #options = { - name: '', - }; - - constructor(object, property, options = {}) { - super('muigui-button', ''); - this.#object = object; - this.#property = property; - - this.#buttonElem = this.addElem( - createElem('button', { - type: 'button', - onClick: () => { - this.#object[this.#property](this); - }, - })); - this.setOptions({name: property, ...options}); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {name} = this.#options; - this.#buttonElem.textContent = name; - } - } - - function arraysEqual(a, b) { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - return true; - } - - function copyArrayElementsFromTo(src, dst) { - dst.length = src.length; - for (let i = 0; i < src.length; ++i) { - dst[i] = src[i]; - } - } - - class EditView extends View { - #oldV; - #updateCheck; - - #checkArrayNeedsUpdate(newV) { - // It's an array, we need to compare all elements - // Example, vec2, [r,g,b], ... - const needUpdate = !arraysEqual(newV, this.#oldV); - if (needUpdate) { - copyArrayElementsFromTo(newV, this.#oldV); - } - return needUpdate; - } - - #checkTypedArrayNeedsUpdate() { - let once = true; - return function checkTypedArrayNeedsUpdateImpl(newV) { - // It's a typedarray, we need to compare all elements - // Example: Float32Array([r, g, b]) - let needUpdate = once; - once = false; - if (!needUpdate) { - needUpdate = !arraysEqual(newV, this.#oldV); - } - return needUpdate; - }; - } - - #checkObjectNeedsUpdate(newV) { - let needUpdate = false; - for (const key in newV) { - if (newV[key] !== this.#oldV[key]) { - needUpdate = true; - this.#oldV[key] = newV[key]; - } - } - return needUpdate; - } - - #checkValueNeedsUpdate(newV) { - const needUpdate = newV !== this.#oldV; - this.#oldV = newV; - return needUpdate; - } - - #getUpdateCheckForType(newV) { - if (Array.isArray(newV)) { - this.#oldV = []; - return this.#checkArrayNeedsUpdate.bind(this); - } else if (isTypedArray(newV)) { - this.#oldV = new newV.constructor(newV); - return this.#checkTypedArrayNeedsUpdate(this); - } else if (typeof newV === 'object') { - this.#oldV = {}; - return this.#checkObjectNeedsUpdate.bind(this); - } else { - return this.#checkValueNeedsUpdate.bind(this); - } - } - - // The point of this is updating DOM elements - // is slow but if we've called `listen` then - // every frame we're going to try to update - // things with the current value so if nothing - // has changed then skip it. - updateDisplayIfNeeded(newV, ignoreCache) { - this.#updateCheck = this.#updateCheck || this.#getUpdateCheckForType(newV); - // Note: We call #updateCheck first because it updates - // the cache - if (this.#updateCheck(newV) || ignoreCache) { - this.updateDisplay(newV); - } - } - setOptions(/*options*/) { - // override this - return this; - } - } - - class CheckboxView extends EditView { - #checkboxElem; - constructor(setter, id) { - const checkboxElem = createElem('input', { - type: 'checkbox', - id, - onInput: () => { - setter.setValue(checkboxElem.checked); - }, - onChange: () => { - setter.setFinalValue(checkboxElem.checked); - }, - }); - super(createElem('label', {}, [checkboxElem])); - this.#checkboxElem = checkboxElem; - } - updateDisplay(v) { - this.#checkboxElem.checked = v; - } - } - - const tasks = []; - const tasksToRemove = new Set(); - - let requestId; - let processing; - - function removeTasks() { - if (!tasksToRemove.size) { - return; - } - - if (processing) { - queueProcessing(); - return; - } - - tasksToRemove.forEach(task => { - removeArrayElem(tasks, task); - }); - tasksToRemove.clear(); - } - - function processTasks() { - requestId = undefined; - processing = true; - for (const task of tasks) { - if (!tasksToRemove.has(task)) { - task(); - } - } - processing = false; - removeTasks(); - queueProcessing(); - } - - function queueProcessing() { - if (!requestId && tasks.length) { - requestId = requestAnimationFrame(processTasks); - } - } - - function addTask(fn) { - tasks.push(fn); - queueProcessing(); - } - - function removeTask(fn) { - tasksToRemove.set(fn); - - const ndx = tasks.indexOf(fn); - if (ndx >= 0) { - tasks.splice(ndx, 1); - } - } - - let id = 0; - - function makeId() { - return `muigui-${++id}`; - } - - class ValueView extends View { - constructor(className = '') { - super(createElem('div', {className: 'muigui-value'})); - if (className) { - this.domElement.classList.add(className); - } - } - } - - class LabelController extends Controller { - #id; - #nameElem; - - constructor(className = '', name = '') { - super('muigui-label-controller'); - this.#id = makeId(); - this.#nameElem = createElem('label', {for: this.#id}); - this.domElement.appendChild(this.#nameElem); - this.pushSubView(new ValueView(className)); - this.name(name); - } - get id() { - return this.#id; - } - name(name) { - if (this.#nameElem.title === this.#nameElem.textContent) { - this.#nameElem.title = name; - } - this.#nameElem.textContent = name; - return this; - } - tooltip(tip) { - this.#nameElem.title = tip; - } - } - - class ValueController extends LabelController { - #object; - #property; - #initialValue; - #listening; - #views; - #updateFn; - - constructor(object, property, className = '') { - super(className, property); - this.#object = object; - this.#property = property; - this.#initialValue = this.getValue(); - this.#listening = false; - this.#views = []; - } - get initialValue() { - return this.#initialValue; - } - get object() { - return this.#object; - } - get property() { - return this.#property; - } - add(view) { - this.#views.push(view); - super.add(view); - this.updateDisplay(); - return view; - } - #setValueImpl(v, ignoreCache) { - let isDifferent = false; - if (typeof v === 'object') { - const dst = this.#object[this.#property]; - // don't replace objects, just their values. - if (Array.isArray(v) || isTypedArray(v)) { - for (let i = 0; i < v.length; ++i) { - isDifferent ||= dst[i] !== v[i]; - dst[i] = v[i]; - } - } else { - for (const key of Object.keys(v)) { - isDifferent ||= dst[key] !== v[key]; - } - Object.assign(dst, v); - } - } else { - isDifferent = this.#object[this.#property] !== v; - this.#object[this.#property] = v; - } - this.updateDisplay(ignoreCache); - if (isDifferent) { - this.emitChange(this.getValue(), this.#object, this.#property); - } - return isDifferent; - } - setValue(v) { - this.#setValueImpl(v); - } - setFinalValue(v) { - const isDifferent = this.#setValueImpl(v, true); - if (isDifferent) { - this.emitFinalChange(this.getValue(), this.#object, this.#property); - } - return this; - } - updateDisplay(ignoreCache) { - const newV = this.getValue(); - for (const view of this.#views) { - view.updateDisplayIfNeeded(newV, ignoreCache); - } - return this; - } - setOptions(options) { - for (const view of this.#views) { - view.setOptions(options); - } - this.updateDisplay(); - return this; - } - getValue() { - return this.#object[this.#property]; - } - value(v) { - this.setValue(v); - return this; - } - reset() { - this.setValue(this.#initialValue); - return this; - } - listen(listen = true) { - if (!this.#updateFn) { - this.#updateFn = this.updateDisplay.bind(this); - } - if (listen) { - if (!this.#listening) { - this.#listening = true; - addTask(this.#updateFn); - } - } else { - if (this.#listening) { - this.#listening = false; - removeTask(this.#updateFn); - } - } - return this; - } - } - - class Checkbox extends ValueController { - constructor(object, property) { - super(object, property, 'muigui-checkbox'); - const id = this.id; - this.add(new CheckboxView(this, id)); - this.updateDisplay(); - } - } - - const identity = { - to: v => v, - from: v => [true, v], - }; - - // from: from string to value - // to: from value to string - const strToNumber = { - to: v => v.toString(), - from: v => { - const newV = parseFloat(v); - return [!Number.isNaN(newV), newV]; - }, - }; - - const converters = { - radToDeg: makeRangeConverters({to: [0, 180], from: [0, Math.PI]}), - }; - - function createWheelHelper() { - let wheelAccum = 0; - return function(e, step, wheelScale = 5) { - wheelAccum -= e.deltaY * step / wheelScale; - const wheelSteps = Math.floor(Math.abs(wheelAccum) / step) * Math.sign(wheelAccum); - const delta = wheelSteps * step; - wheelAccum -= delta; - return delta; - }; - } - - class NumberView extends EditView { - #to; - #from; - #step; - #skipUpdate; - #options = { - step: 0.01, - converters: strToNumber, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - }; - - constructor(setter, options) { - const setValue = setter.setValue.bind(setter); - const setFinalValue = setter.setFinalValue.bind(setter); - const wheelHelper = createWheelHelper(); - super(createElem('input', { - type: 'number', - onInput: () => this.#handleInput(setValue, true), - onChange: () => this.#handleInput(setFinalValue, false), - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.setOptions(options); - } - #handleInput(setFn, skipUpdate) { - const v = parseFloat(this.domElement.value); - const [valid, newV] = this.#from(v); - let inRange; - if (valid && !Number.isNaN(v)) { - const {min, max} = this.#options; - inRange = newV >= min && newV <= max; - this.#skipUpdate = skipUpdate; - setFn(clamp$1(newV, min, max)); - } - this.domElement.classList.toggle('muigui-invalid-value', !valid || !inRange); - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = stepify(v, this.#to, this.#step); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - step, - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - this.#step = step; - return this; - } - } - - // Wanted to name this `Number` but it conflicts with - // JavaScript `Number`. It most likely wouldn't be - // an issue? But users might `import {Number} ...` and - // things would break. - class TextNumber extends ValueController { - #textView; - #step; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-checkbox'); - this.#textView = this.add(new NumberView(this, options)); - this.updateDisplay(); - } - } - - class SelectView extends EditView { - #values; - - constructor(setter, keyValues) { - const values = []; - super(createElem('select', { - onChange: () => { - setter.setFinalValue(this.#values[this.domElement.selectedIndex]); - }, - }, keyValues.map(([key, value]) => { - values.push(value); - return createElem('option', {textContent: key}); - }))); - this.#values = values; - } - updateDisplay(v) { - const ndx = this.#values.indexOf(v); - this.domElement.selectedIndex = ndx; - } - } - - // 4 cases - // (a) keyValues is array of arrays, each sub array is key value - // (b) keyValues is array and value is number then keys = array contents, value = index - // (c) keyValues is array and value is not number, key = array contents, value = array contents - // (d) keyValues is object then key->value - function convertToKeyValues(keyValues, valueIsNumber) { - if (Array.isArray(keyValues)) { - if (Array.isArray(keyValues[0])) { - // (a) keyValues is array of arrays, each sub array is key value - return keyValues; - } else { - if (valueIsNumber) { - // (b) keyValues is array and value is number then keys = array contents, value = index - return keyValues.map((v, ndx) => [v, ndx]); - } else { - // (c) keyValues is array and value is not number, key = array contents, value = array contents - return keyValues.map(v => [v, v]); - } - } - } else { - // (d) - return [...Object.entries(keyValues)]; - } - } - - class Select extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-select'); - const valueIsNumber = typeof this.getValue() === 'number'; - const {keyValues: keyValuesInput} = options; - const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); - this.add(new SelectView(this, keyValues)); - this.updateDisplay(); - } - } - - class RangeView extends EditView { - #to; - #from; - #step; - #skipUpdate; - #options = { - step: 0.01, - min: 0, - max: 1, - converters: identity, - }; - - constructor(setter, options) { - const wheelHelper = createWheelHelper(); - super(createElem('input', { - type: 'range', - onInput: () => { - this.#skipUpdate = true; - const {min, max, step} = this.#options; - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v, v => v, step), min, max); - const [valid, validV] = this.#from(newV); - if (valid) { - setter.setValue(validV); - } - }, - onChange: () => { - this.#skipUpdate = true; - const {min, max, step} = this.#options; - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v, v => v, step), min, max); - const [valid, validV] = this.#from(newV); - if (valid) { - setter.setFinalValue(validV); - } - }, - onWheel: e => { - e.preventDefault(); - const [valid, v] = this.#from(parseFloat(this.domElement.value)); - if (!valid) { - return; - } - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const newV = clamp$1(stepify(v + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.setOptions(options); - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = stepify(v, this.#to, this.#step); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - step, - min, - max, - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - this.#step = step; - this.domElement.step = step; - this.domElement.min = min; - this.domElement.max = max; - return this; - } - } - - class Range extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-range'); - this.add(new RangeView(this, options)); - this.add(new NumberView(this, options)); - } - } - - class TextView extends EditView { - #to; - #from; - #skipUpdate; - #options = { - converters: identity, - }; - - constructor(setter, options) { - const setValue = setter.setValue.bind(setter); - const setFinalValue = setter.setFinalValue.bind(setter); - super(createElem('input', { - type: 'text', - onInput: () => this.#handleInput(setValue, true), - onChange: () => this.#handleInput(setFinalValue, false), - })); - this.setOptions(options); - } - #handleInput(setFn, skipUpdate) { - const [valid, newV] = this.#from(this.domElement.value); - if (valid) { - this.#skipUpdate = skipUpdate; - setFn(newV); - } - this.domElement.style.color = valid ? '' : 'var(--invalid-color)'; - - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = this.#to(v); - this.domElement.style.color = ''; - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - return this; - } - } - - class Text extends ValueController { - constructor(object, property) { - super(object, property, 'muigui-checkbox'); - this.add(new TextView(this)); - this.updateDisplay(); - } - } - - // const isConversion = o => typeof o.to === 'function' && typeof o.from === 'function'; - - /** - * possible inputs - * add(o, p, min: number, max: number) - * add(o, p, min: number, max: number, step: number) - * add(o, p, array: [value]) - * add(o, p, array: [[key, value]]) - * - * @param {*} object - * @param {string} property - * @param {...any} args - * @returns {Controller} - */ - function createController(object, property, ...args) { - const [arg1] = args; - if (Array.isArray(arg1)) { - return new Select(object, property, {keyValues: arg1}); - } - - const t = typeof object[property]; - switch (t) { - case 'number': - if (typeof args[0] === 'number' && typeof args[1] === 'number') { - const min = args[0]; - const max = args[1]; - const step = args[2]; - return new Range(object, property, {min, max, ...(step && {step})}); - } - return args.length === 0 - ? new TextNumber(object, property, ...args) - : new Range(object, property, ...args); - case 'boolean': - return new Checkbox(object, property, ...args); - case 'function': - return new Button(object, property, ...args); - case 'string': - return new Text(object, property, ...args); - case 'undefined': - throw new Error(`no property named ${property}`); - default: - throw new Error(`unhandled type ${t} for property ${property}`); - } - } - - const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); - const lerp = (a, b, t) => a + (b - a) * t; - const fract = v => v >= 0 ? v % 1 : 1 - (v % 1); - - const f0 = v => +v.toFixed(0); // converts to string (eg 1.2 => "1"), then converts back to number (eg, "1.200" => 1.2) - const f3 = v => +v.toFixed(3); // converts to string (eg 1.2 => "1.200"), then converts back to number (eg, "1.200" => 1.2) - - const hexToUint32RGB = v => (parseInt(v.substring(1, 3), 16) << 16) | - (parseInt(v.substring(3, 5), 16) << 8 ) | - (parseInt(v.substring(5, 7), 16) ); - const uint32RGBToHex = v => `#${(Math.round(v)).toString(16).padStart(6, '0')}`; - const hexToUint32RGBA = v => (parseInt(v.substring(1, 3), 16) * 2 ** 24) + - (parseInt(v.substring(3, 5), 16) * 2 ** 16) + - (parseInt(v.substring(5, 7), 16) * 2 ** 8) + - (parseInt(v.substring(7, 9), 16) ); - const uint32RGBAToHex = v => `#${(Math.round(v)).toString(16).padStart(8, '0')}`; - - const hexToUint8RGB = v => [ - parseInt(v.substring(1, 3), 16), - parseInt(v.substring(3, 5), 16), - parseInt(v.substring(5, 7), 16), - ]; - const uint8RGBToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`; - - const hexToUint8RGBA = v => [ - parseInt(v.substring(1, 3), 16), - parseInt(v.substring(3, 5), 16), - parseInt(v.substring(5, 7), 16), - parseInt(v.substring(7, 9), 16), - ]; - const uint8RGBAToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`; - - const hexToFloatRGB = v => hexToUint8RGB(v).map(v => f3(v / 255)); - const floatRGBToHex = v => uint8RGBToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255)))); - - const hexToFloatRGBA = v => hexToUint8RGBA(v).map(v => f3(v / 255)); - const floatRGBAToHex = v => uint8RGBAToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255)))); - - const scaleAndClamp = v => clamp(Math.round(v * 255), 0, 255).toString(16).padStart(2, '0'); - - const hexToObjectRGB = v => ({ - r: parseInt(v.substring(1, 3), 16) / 255, - g: parseInt(v.substring(3, 5), 16) / 255, - b: parseInt(v.substring(5, 7), 16) / 255, - }); - const objectRGBToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}`; - const hexToObjectRGBA = v => ({ - r: parseInt(v.substring(1, 3), 16) / 255, - g: parseInt(v.substring(3, 5), 16) / 255, - b: parseInt(v.substring(5, 7), 16) / 255, - a: parseInt(v.substring(7, 9), 16) / 255, - }); - const objectRGBAToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}${scaleAndClamp(v.a)}`; - - const hexToCssRGB = v => `rgb(${hexToUint8RGB(v).join(', ')})`; - const cssRGBRegex = /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/; - const cssRGBToHex = v => { - const m = cssRGBRegex.exec(v); - return uint8RGBToHex([m[1], m[2], m[3]].map(v => parseInt(v))); - }; - const hexToCssRGBA = v => `rgba(${hexToUint8RGBA(v).map((v, i) => i === 3 ? v / 255 : v).join(', ')})`; - const cssRGBARegex = /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+\.\d+|\d+)\s*\)\s*$/; - const cssRGBAToHex = v => { - const m = cssRGBARegex.exec(v); - return uint8RGBAToHex([m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? (parseFloat(v) * 255 | 0) : parseInt(v))); - }; - - const hexToCssHSL = v => { - const hsl = rgbUint8ToHsl(hexToUint8RGB(v)).map(v => f0(v)); - return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`; - }; - const hexToCssHSLA = v => { - const hsla = rgbaUint8ToHsla(hexToUint8RGBA(v)).map((v, i) => i === 3 ? f3(v) : f0(v)); - return `hsl(${hsla[0]} ${hsla[1]}% ${hsla[2]}% / ${hsla[3]})`; - }; - const cssHSLRegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\)\s*$/; - const cssHSLARegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\/\s*(\d+\.\d+|\d+)\s*\)\s*$/; - - const hex3DigitTo6Digit = v => `${v[0]}${v[0]}${v[1]}${v[1]}${v[2]}${v[2]}`; - const cssHSLToHex = v => { - const m = cssHSLRegex.exec(v); - const rgb = hslToRgbUint8([m[1], m[2], m[3]].map(v => parseFloat(v))); - return uint8RGBToHex(rgb); - }; - const cssHSLAToHex = v => { - const m = cssHSLARegex.exec(v); - const rgba = hslaToRgbaUint8([m[1], m[2], m[3], m[4]].map(v => parseFloat(v))); - return uint8RGBAToHex(rgba); - }; - - const euclideanModulo = (v, n) => ((v % n) + n) % n; - - function hslToRgbUint8([h, s, l]) { - h = euclideanModulo(h, 360); - s = clamp(s / 100, 0, 1); - l = clamp(l / 100, 0, 1); - - const a = s * Math.min(l, 1 - l); - - function f(n) { - const k = (n + h / 30) % 12; - return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); - } - - return [f(0), f(8), f(4)].map(v => Math.round(v * 255)); - } - - function hslaToRgbaUint8([h, s, l, a]) { - const rgb = hslToRgbUint8([h, s, l]); - return [...rgb, a * 255 | 0]; - } - - function rgbFloatToHsl01([r, g, b]) { - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (min + max) * 0.5; - const d = max - min; - let h = 0; - let s = 0; - - if (d !== 0) { - s = (l === 0 || l === 1) - ? 0 - : (max - l) / Math.min(l, 1 - l); - - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; - } - } - - return [h / 6, s, l]; - } - - function rgbaFloatToHsla01([r, g, b, a]) { - const hsl = rgbFloatToHsl01([r, g, b]); - return [...hsl, a]; - } - - const rgbUint8ToHsl = (rgb) => { - const [h, s, l] = rgbFloatToHsl01(rgb.map(v => v / 255)); - return [h * 360, s * 100, l * 100]; - }; - - const rgbaUint8ToHsla = (rgba) => { - const [h, s, l, a] = rgbaFloatToHsla01(rgba.map(v => v / 255)); - return [h * 360, s * 100, l * 100, a]; - }; - - function hsv01ToRGBFloat([hue, sat, val]) { - sat = clamp(sat, 0, 1); - val = clamp(val, 0, 1); - return [hue, hue + 2 / 3, hue + 1 / 3].map( - v => lerp(1, clamp(Math.abs(fract(v) * 6 - 3.0) - 1, 0, 1), sat) * val - ); - } - - function hsva01ToRGBAFloat([hue, sat, val, alpha]) { - const rgb = hsv01ToRGBFloat([hue, sat, val]); - return [...rgb, alpha]; - } - - const round3 = v => Math.round(v * 1000) / 1000; - - function rgbFloatToHSV01([r, g, b]) { - const p = b > g - ? [b, g, -1, 2 / 3] - : [g, b, 0, -1 / 3]; - const q = p[0] > r - ? [p[0], p[1], p[3], r] - : [r, p[1], p[2], p[0]]; - const d = q[0] - Math.min(q[3], q[1]); - return [ - Math.abs(q[2] + (q[3] - q[1]) / (6 * d + Number.EPSILON)), - d / (q[0] + Number.EPSILON), - q[0], - ].map(round3); - } - - function rgbaFloatToHSVA01([r, g, b, a]) { - const hsv = rgbFloatToHSV01([r, g, b]); - return [...hsv, a]; - } - - // window.hsv01ToRGBFloat = hsv01ToRGBFloat; - // window.rgbFloatToHSV01 = rgbFloatToHSV01; - - // Yea, meh! - const hasAlpha = format => format.endsWith('a') || format.startsWith('hex8'); - - const cssStringFormats = [ - { re: /^#(?:[0-9a-f]){6}$/i, format: 'hex6' }, - { re: /^(?:[0-9a-f]){6}$/i, format: 'hex6-no-hash' }, - { re: /^#(?:[0-9a-f]){8}$/i, format: 'hex8' }, - { re: /^(?:[0-9a-f]){8}$/i, format: 'hex8-no-hash' }, - { re: /^#(?:[0-9a-f]){3}$/i, format: 'hex3' }, - { re: /^(?:[0-9a-f]){3}$/i, format: 'hex3-no-hash' }, - { re: cssRGBRegex, format: 'css-rgb' }, - { re: cssHSLRegex, format: 'css-hsl' }, - { re: cssRGBARegex, format: 'css-rgba' }, - { re: cssHSLARegex, format: 'css-hsla' }, - ]; - - function guessStringColorFormat(v) { - for (const formatInfo of cssStringFormats) { - if (formatInfo.re.test(v)) { - return formatInfo; - } - } - return undefined; - } - - function guessFormat(v) { - switch (typeof v) { - case 'number': - console.warn('can not reliably guess format based on a number. You should pass in a format like {format: "uint32-rgb"} or {format: "uint32-rgb"}'); - return v <= 0xFFFFFF ? 'uint32-rgb' : 'uint32-rgba'; - case 'string': { - const formatInfo = guessStringColorFormat(v.trim()); - if (formatInfo) { - return formatInfo.format; - } - break; - } - case 'object': - if (v instanceof Uint8Array || v instanceof Uint8ClampedArray) { - if (v.length === 3) { - return 'uint8-rgb'; - } else if (v.length === 4) { - return 'uint8-rgba'; - } - } else if (v instanceof Float32Array) { - if (v.length === 3) { - return 'float-rgb'; - } else if (v.length === 4) { - return 'float-rgba'; - } - } else if (Array.isArray(v)) { - if (v.length === 3) { - return 'float-rgb'; - } else if (v.length === 4) { - return 'float-rgba'; - } - } else { - if ('r' in v && 'g' in v && 'b' in v) { - if ('a' in v) { - return 'object-rgba'; - } else { - return 'object-rgb'; - } - } - } - } - throw new Error(`unknown color format: ${v}`); - } - - function fixHex6(v) { - return v.trim(v); - //const formatInfo = guessStringColorFormat(v.trim()); - //const fix = formatInfo ? formatInfo.fix : v => v; - //return fix(v.trim()); - } - - function fixHex8(v) { - return v.trim(v); - //const formatInfo = guessStringColorFormat(v.trim()); - //const fix = formatInfo ? formatInfo.fix : v => v; - //return fix(v.trim()); - } - - function hex6ToHex3(hex6) { - return (hex6[1] === hex6[2] && - hex6[3] === hex6[4] && - hex6[5] === hex6[6]) - ? `#${hex6[1]}${hex6[3]}${hex6[5]}` - : hex6; - } - - const hex3RE = /^(#|)([0-9a-f]{3})$/i; - function hex3ToHex6(hex3) { - const m = hex3RE.exec(hex3); - if (m) { - const [, , m2] = m; - return `#${hex3DigitTo6Digit(m2)}`; - } - return hex3; - } - - function fixHex3(v) { - return hex6ToHex3(fixHex6(v)); - } - - const strToRGBObject = (s) => { - try { - const json = s.replace(/([a-z])/g, '"$1"'); - const rgb = JSON.parse(json); - if (Number.isNaN(rgb.r) || Number.isNaN(rgb.g) || Number.isNaN(rgb.b)) { - throw new Error('not {r, g, b}'); - } - return [true, rgb]; - } catch (e) { - return [false]; - } - }; - - const strToRGBAObject = (s) => { - try { - const json = s.replace(/([a-z])/g, '"$1"'); - const rgba = JSON.parse(json); - if (Number.isNaN(rgba.r) || Number.isNaN(rgba.g) || Number.isNaN(rgba.b) || Number.isNaN(rgba.a)) { - throw new Error('not {r, g, b, a}'); - } - return [true, rgba]; - } catch (e) { - return [false]; - } - }; - - const strToCssRGB = s => { - const m = cssRGBRegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, `rgb(${v.join(', ')})`]; - }; - - const strToCssRGBA = s => { - const m = cssRGBARegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? parseFloat(v) : parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, `rgba(${v.join(', ')})`]; - }; - - const strToCssHSL = s => { - const m = cssHSLRegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseFloat(v)); - const outOfRange = v.find(v => Number.isNaN(v)); - return [!outOfRange, `hsl(${v[0]}, ${v[1]}%, ${v[2]}%)`]; - }; - - const strToCssHSLA = s => { - const m = cssHSLARegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map(v => parseFloat(v)); - const outOfRange = v.find(v => Number.isNaN(v)); - return [!outOfRange, `hsl(${v[0]} ${v[1]}% ${v[2]}% / ${v[3]})`]; - }; - - const rgbObjectToStr = rgb => { - return `{r:${f3(rgb.r)}, g:${f3(rgb.g)}, b:${f3(rgb.b)}}`; - }; - const rgbaObjectToStr = rgba => { - return `{r:${f3(rgba.r)}, g:${f3(rgba.g)}, b:${f3(rgba.b)}}, a:${f3(rgba.a)}}`; - }; - - const strTo3IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/; - const strTo3Ints = s => { - const m = strTo3IntsRE.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, v]; - }; - - const strTo4IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/; - const strTo4Ints = s => { - const m = strTo4IntsRE.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, v]; - }; - - const strTo3Floats = s => { - const numbers = s.split(',').map(s => s.trim()); - const v = numbers.map(v => parseFloat(v)); - if (v.length !== 3) { - return [false]; - } - // Note: using isNaN not Number.isNaN - const badNdx = numbers.findIndex(v => isNaN(v)); - return [badNdx < 0, v.map(v => f3(v))]; - }; - - const strTo4Floats = s => { - const numbers = s.split(',').map(s => s.trim()); - const v = numbers.map(v => parseFloat(v)); - if (v.length !== 4) { - return [false]; - } - // Note: using isNaN not Number.isNaN - const badNdx = numbers.findIndex(v => isNaN(v)); - return [badNdx < 0, v.map(v => f3(v))]; - }; - - const strToUint32RGBRegex = /^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i; - const strToUint32RGB = s => { - const m = strToUint32RGBRegex.exec(s); - if (!m) { - return [false]; - } - return [true, parseInt(m[1], 16)]; - }; - - const strToUint32RGBARegex = /^\s*(?:0x){0,1}([0-9a-z]{1,8})\s*$/i; - const strToUint32RGBA = s => { - const m = strToUint32RGBARegex.exec(s); - if (!m) { - return [false]; - } - return [true, parseInt(m[1], 16)]; - }; - - const hex6RE = /^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i; - const hexNoHash6RE = /^\s*[a-f0-9]{6}\s*$/i; - const hex8RE = /^\s*#[a-f0-9]{8}\s*$/i; - const hexNoHash8RE = /^\s*[a-f0-9]{8}\s*$/i; - - // For each format converter - // - // fromHex/toHex convert from/to '#RRGGBB' - // - // fromHex converts from the string '#RRBBGG' to the format - // (eg: for uint32-rgb, '#123456' becomes 0x123456) - // - // toHex converts from the format to '#RRGGBB' - // (eg: for uint8-rgb, [16, 33, 50] becomes '#102132') - // - // - // fromStr/toStr convert from/to what's in the input[type=text] element - // - // toStr converts from the format to its string representation - // (eg, for object-rgb, {r: 1, g: 0.5, b:0} becomes "{r: 1, g: 0.5, b:0}") - // ^object ^string - // - // fromStr converts its string representation to its format - // (eg, for object-rgb) "{r: 1, g: 0.5, b:0}" becomes {r: 1, g: 0.5, b:0}) - // ^string ^object - // fromString returns an array which is [valid, v] - // where valid is true if the string was a valid and v is the converted - // format if v is true. - // - // Note: toStr should convert to "ideal" form (whatever that is). - // (eg, for css-rgb - // "{ r: 0.10000, g: 001, b: 0}" becomes "{r: 0.1, g: 1, b: 0}" - // notice that css-rgb is a string to a string - // ) - const colorFormatConverters = { - 'hex6': { - color: { - from: v => [true, v], - to: fixHex6, - }, - text: { - from: v => [hex6RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex8': { - color: { - from: v => [true, v], - to: fixHex8, - }, - text: { - from: v => [hex8RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex3': { - color: { - from: v => [true, fixHex3(v)], - to: hex3ToHex6, - }, - text: { - from: v => [hex6RE.test(v), hex6ToHex3(v.trim())], - to: v => v, - }, - }, - 'hex6-no-hash': { - color: { - from: v => [true, v.substring(1)], - to: v => `#${fixHex6(v)}`, - }, - text: { - from: v => [hexNoHash6RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex8-no-hash': { - color: { - from: v => [true, v.substring(1)], - to: v => `#${fixHex8(v)}`, - }, - text: { - from: v => [hexNoHash8RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex3-no-hash': { - color: { - from: v => [true, fixHex3(v).substring(1)], - to: hex3ToHex6, - }, - text: { - from: v => [hexNoHash6RE.test(v), hex6ToHex3(v.trim())], - to: v => v, - }, - }, - 'uint32-rgb': { - color: { - from: v => [true, hexToUint32RGB(v)], - to: uint32RGBToHex, - }, - text: { - from: v => strToUint32RGB(v), - to: v => `0x${v.toString(16).padStart(6, '0')}`, - }, - }, - 'uint32-rgba': { - color: { - from: v => [true, hexToUint32RGBA(v)], - to: uint32RGBAToHex, - }, - text: { - from: v => strToUint32RGBA(v), - to: v => `0x${v.toString(16).padStart(8, '0')}`, - }, - }, - 'uint8-rgb': { - color: { - from: v => [true, hexToUint8RGB(v)], - to: uint8RGBToHex, - }, - text: { - from: strTo3Ints, - to: v => v.join(', '), - }, - }, - 'uint8-rgba': { - color: { - from: v => [true, hexToUint8RGBA(v)], - to: uint8RGBAToHex, - }, - text: { - from: strTo4Ints, - to: v => v.join(', '), - }, - }, - 'float-rgb': { - color: { - from: v => [true, hexToFloatRGB(v)], - to: floatRGBToHex, - }, - text: { - from: strTo3Floats, - // need Array.from because map of Float32Array makes a Float32Array - to: v => Array.from(v).map(v => f3(v)).join(', '), - }, - }, - 'float-rgba': { - color: { - from: v => [true, hexToFloatRGBA(v)], - to: floatRGBAToHex, - }, - text: { - from: strTo4Floats, - // need Array.from because map of Float32Array makes a Float32Array - to: v => Array.from(v).map(v => f3(v)).join(', '), - }, - }, - 'object-rgb': { - color: { - from: v => [true, hexToObjectRGB(v)], - to: objectRGBToHex, - }, - text: { - from: strToRGBObject, - to: rgbObjectToStr, - }, - }, - 'object-rgba': { - color: { - from: v => [true, hexToObjectRGBA(v)], - to: objectRGBAToHex, - }, - text: { - from: strToRGBAObject, - to: rgbaObjectToStr, - }, - }, - 'css-rgb': { - color: { - from: v => [true, hexToCssRGB(v)], - to: cssRGBToHex, - }, - text: { - from: strToCssRGB, - to: v => strToCssRGB(v)[1], - }, - }, - 'css-rgba': { - color: { - from: v => [true, hexToCssRGBA(v)], - to: cssRGBAToHex, - }, - text: { - from: strToCssRGBA, - to: v => strToCssRGBA(v)[1], - }, - }, - 'css-hsl': { - color: { - from: v => [true, hexToCssHSL(v)], - to: cssHSLToHex, - }, - text: { - from: strToCssHSL, - to: v => strToCssHSL(v)[1], - }, - }, - 'css-hsla': { - color: { - from: v => [true, hexToCssHSLA(v)], - to: cssHSLAToHex, - }, - text: { - from: strToCssHSLA, - to: v => strToCssHSLA(v)[1], - }, - }, - }; - - class ElementView extends View { - constructor(tag, className) { - super(createElem(tag, {className})); - } - } - - // TODO: remove this? Should just be user side - class Canvas extends LabelController { - #canvasElem; - - constructor() { - super('muigui-canvas'); - this.#canvasElem = this.add( - new ElementView('canvas', 'muigui-canvas'), - ).domElement; - } - get canvas() { - return this.#canvasElem; - } - } - - class ColorView extends EditView { - #to; - #from; - #colorElem; - #skipUpdate; - #options = { - converters: identity, - }; - - constructor(setter, options) { - const colorElem = createElem('input', { - type: 'color', - onInput: () => { - const [valid, newV] = this.#from(colorElem.value); - if (valid) { - this.#skipUpdate = true; - setter.setValue(newV); - } - }, - onChange: () => { - const [valid, newV] = this.#from(colorElem.value); - if (valid) { - this.#skipUpdate = true; - setter.setFinalValue(newV); - } - }, - }); - super(createElem('div', {}, [colorElem])); - this.setOptions(options); - this.#colorElem = colorElem; - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.#colorElem.value = this.#to(v); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {converters: {to, from}} = this.#options; - this.#to = to; - this.#from = from; - return this; - } - } - - class Color extends ValueController { - #colorView; - #textView; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-color'); - const format = options.format || guessFormat(this.getValue()); - const {color, text} = colorFormatConverters[format]; - this.#colorView = this.add(new ColorView(this, {converters: color})); - this.#textView = this.add(new TextView(this, {converters: text})); - this.updateDisplay(); - } - setOptions(options) { - const {format} = options; - if (format) { - const {color, text} = colorFormatConverters[format]; - this.#colorView.setOptions({converters: color}); - this.#textView.setOptions({converters: text}); - } - super.setOptions(options); - return this; - } - } - - // This feels like it should be something else like - // gui.addController({className: 'muigui-divider')}; - class Divider extends Controller { - constructor() { - super('muigui-divider'); - } - } - - class Container extends Controller { - #controllers; - #childDestController; - - constructor(className) { - super(className); - this.#controllers = []; - this.#childDestController = this; - } - get children() { - return this.#controllers; // should we return a copy? - } - get controllers() { - return this.#controllers.filter(c => !(c instanceof Container)); - } - get folders() { - return this.#controllers.filter(c => c instanceof Container); - } - reset(recursive = true) { - for (const controller of this.#controllers) { - if (!(controller instanceof Container) || recursive) { - controller.reset(recursive); - } - } - return this; - } - updateDisplay() { - for (const controller of this.#controllers) { - controller.updateDisplay(); - } - return this; - } - remove(controller) { - const ndx = this.#controllers.indexOf(controller); - if (ndx >= 0) { - const c = this.#controllers.splice(ndx, 1); - const c0 = c[0]; - const elem = c0.domElement; - elem.remove(); - c0.setParent(null); - } - return this; - } - _addControllerImpl(controller) { - this.domElement.appendChild(controller.domElement); - this.#controllers.push(controller); - controller.setParent(this); - return controller; - } - addController(controller) { - return this.#childDestController._addControllerImpl(controller); - } - pushContainer(container) { - this.addController(container); - this.#childDestController = container; - return container; - } - popContainer() { - this.#childDestController = this.#childDestController.parent; - return this; - } - } - - class Folder extends Container { - #labelElem; - - constructor(name = 'Controls', className = 'muigui-menu') { - super(className); - this.#labelElem = createElem('label'); - this.addElem(createElem('button', { - type: 'button', - onClick: () => this.toggleOpen(), - }, [this.#labelElem])); - this.pushContainer(new Container()); - this.name(name); - this.open(); - } - open(open = true) { - this.domElement.classList.toggle('muigui-closed', !open); - this.domElement.classList.toggle('muigui-open', open); - return this; - } - close() { - return this.open(false); - } - name(name) { - this.#labelElem.textContent = name; - return this; - } - title(title) { - return this.name(title); - } - toggleOpen() { - this.open(!this.domElement.classList.contains('muigui-open')); - return this; - } - } - - // This feels like it should be something else like - // gui.addDividing = new Controller() - class Label extends Controller { - constructor(text) { - super('muigui-label'); - this.text(text); - } - text(text) { - this.domElement.textContent = text; - return this; - } - } - - function noop$1() { - } - - function computeRelativePosition(elem, event, start) { - const rect = elem.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const nx = x / rect.width; - const ny = y / rect.height; - start = start || [x, y]; - const dx = x - start[0]; - const dy = y - start[1]; - const ndx = dx / rect.width; - const ndy = dy / rect.width; - return {x, y, nx, ny, dx, dy, ndx, ndy}; - } - - function addTouchEvents(elem, {onDown = noop$1, onMove = noop$1, onUp = noop$1}) { - let start; - const pointerMove = function(event) { - const e = { - type: 'move', - ...computeRelativePosition(elem, event, start), - }; - onMove(e); - }; - - const pointerUp = function(event) { - elem.releasePointerCapture(event.pointerId); - elem.removeEventListener('pointermove', pointerMove); - elem.removeEventListener('pointerup', pointerUp); - - document.body.style.backgroundColor = ''; - - onUp('up'); - }; - - const pointerDown = function(event) { - elem.addEventListener('pointermove', pointerMove); - elem.addEventListener('pointerup', pointerUp); - elem.setPointerCapture(event.pointerId); - - const rel = computeRelativePosition(elem, event); - start = [rel.x, rel.y]; - onDown({ - type: 'down', - ...rel, - }); - }; - - elem.addEventListener('pointerdown', pointerDown); - - return function() { - elem.removeEventListener('pointerdown', pointerDown); - }; - } - - const svg$3 = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - - function connectFillTargets(elem) { - elem.querySelectorAll('[data-src]').forEach(srcElem => { - const id = getNewId(); - srcElem.id = id; - elem.querySelectorAll(`[data-target=${srcElem.dataset.src}]`).forEach(targetElem => { - targetElem.setAttribute('fill', `url(#${id})`); - }); - }); - return elem; - } - - // Was originally going to make alpha an option. Issue is - // hard coded conversions? - class ColorChooserView extends EditView { - #to; - #from; - #satLevelElem; - #circleElem; - #hueUIElem; - #hueElem; - #hueCursorElem; - #alphaUIElem; - #alphaElem; - #alphaCursorElem; - #hsva; - #skipHueUpdate; - #skipSatLevelUpdate; - #skipAlphaUpdate; - #options = { - converters: identity, - alpha: false, - }; - #convertInternalToHex; - #convertHexToInternal; - - constructor(setter, options) { - super(createElem('div', { - innerHTML: svg$3, - className: 'muigui-no-scroll', - })); - this.#satLevelElem = this.domElement.children[0]; - this.#hueUIElem = this.domElement.children[1]; - this.#alphaUIElem = this.domElement.children[2]; - connectFillTargets(this.#satLevelElem); - connectFillTargets(this.#hueUIElem); - connectFillTargets(this.#alphaUIElem); - this.#circleElem = this.$('.muigui-color-chooser-circle'); - this.#hueElem = this.$('[data-src=muigui-color-chooser-hue]'); - this.#hueCursorElem = this.$('.muigui-color-chooser-hue-cursor'); - this.#alphaElem = this.$('[data-src=muigui-color-chooser-alpha]'); - this.#alphaCursorElem = this.$('.muigui-color-chooser-alpha-cursor'); - - const handleSatLevelChange = (e) => { - const s = clamp$1(e.nx, 0, 1); - const v = clamp$1(e.ny, 0, 1); - this.#hsva[1] = s; - this.#hsva[2] = (1 - v); - this.#skipHueUpdate = true; - this.#skipAlphaUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - const handleHueChange = (e) => { - const h = clamp$1(e.nx, 0, 1); - this.#hsva[0] = h; - this.#skipSatLevelUpdate = true; - this.#skipAlphaUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - const handleAlphaChange = (e) => { - const a = clamp$1(e.nx, 0, 1); - this.#hsva[3] = a; - this.#skipHueUpdate = true; - this.#skipSatLevelUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - addTouchEvents(this.#satLevelElem, { - onDown: handleSatLevelChange, - onMove: handleSatLevelChange, - }); - addTouchEvents(this.#hueUIElem, { - onDown: handleHueChange, - onMove: handleHueChange, - }); - addTouchEvents(this.#alphaUIElem, { - onDown: handleAlphaChange, - onMove: handleAlphaChange, - }); - this.setOptions(options); - } - updateDisplay(newV) { - if (!this.#hsva) { - this.#hsva = this.#convertHexToInternal(this.#to(newV)); - } - { - const [h, s, v, a = 1] = this.#convertHexToInternal(this.#to(newV)); - // Don't copy the hue if it was un-computable. - if (!this.#skipHueUpdate) { - this.#hsva[0] = s > 0.001 && v > 0.001 ? h : this.#hsva[0]; - } - if (!this.#skipSatLevelUpdate) { - this.#hsva[1] = s; - this.#hsva[2] = v; - } - if (!this.#skipAlphaUpdate) { - this.#hsva[3] = a; - } - } - { - const [h, s, v, a] = this.#hsva; - const [hue, sat, lum] = rgbaFloatToHsla01(hsva01ToRGBAFloat(this.#hsva)); - - if (!this.#skipHueUpdate) { - this.#hueCursorElem.setAttribute('transform', `translate(${h * 64}, 0)`); - } - this.#hueElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} 0% 100% / ${a})`); - this.#hueElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} 100% 50% / ${a})`); - if (!this.#skipAlphaUpdate) { - this.#alphaCursorElem.setAttribute('transform', `translate(${a * 64}, 0)`); - } - this.#alphaElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 0)`); - this.#alphaElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 1)`); - - if (!this.#skipSatLevelUpdate) { - this.#circleElem.setAttribute('cx', `${s * 64}`); - this.#circleElem.setAttribute('cy', `${(1 - v) * 48}`); - } - } - this.#skipHueUpdate = false; - this.#skipSatLevelUpdate = false; - this.#skipAlphaUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {converters: {to, from}, alpha} = this.#options; - this.#alphaUIElem.style.display = alpha ? '' : 'none'; - this.#convertInternalToHex = alpha - ? v => floatRGBAToHex(hsva01ToRGBAFloat(v)) - : v => floatRGBToHex(hsv01ToRGBFloat(v)); - this.#convertHexToInternal = alpha - ? v => rgbaFloatToHSVA01(hexToFloatRGBA(v)) - : v => rgbFloatToHSV01(hexToFloatRGB(v)); - this.#to = to; - this.#from = from; - return this; - } - } - - /* - - holder = new TabHolder - tab = holder.add(new Tab("name")) - tab.add(...) - - - pc = new PopdownController - top = pc.add(new Row()) - top.add(new Button()); - values = topRow.add(new Div()) - bottom = pc.add(new Row()); - - - - pc = new PopdownController - pc.addTop - pc.addTop - - pc.addBottom - - - */ - - class PopDownController extends ValueController { - #top; - #valuesView; - #checkboxElem; - #bottom; - #options = { - open: false, - }; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-pop-down-controller'); - /* - [ValueView - [[B][values]] upper row - [[ visual ]] lower row - ] - */ - this.#top = this.add(new ElementView('div', 'muigui-pop-down-top')); - // this.#top.add(new CheckboxView(makeSetter(this.#options, 'open'))); - const checkboxElem = this.#top.addElem(createElem('input', { - type: 'checkbox', - onChange: () => { - this.#options.open = checkboxElem.checked; - this.updateDisplay(); - }, - })); - this.#checkboxElem = checkboxElem; - this.#valuesView = this.#top.add(new ElementView('div', 'muigui-pop-down-values')); - this.#bottom = this.add(new ElementView('div', 'muigui-pop-down-bottom')); - this.setOptions(options); - } - setKnobColor(bgCssColor/*, fgCssColor*/) { - if (this.#checkboxElem) { - this.#checkboxElem.style = ` - --range-color: ${bgCssColor}; - --value-bg-color: ${bgCssColor}; - `; - } - } - updateDisplay() { - super.updateDisplay(); - const {open} = this.#options; - this.domElement.children[1].classList.toggle('muigui-open', open); - this.domElement.children[1].classList.toggle('muigui-closed', !open); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - super.setOptions(options); - this.updateDisplay(); - } - addTop(view) { - return this.#valuesView.add(view); - } - addBottom(view) { - return this.#bottom.add(view); - } - } - - class ColorChooser extends PopDownController { - #colorView; - #textView; - #to; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-color-chooser'); - const format = options.format || guessFormat(this.getValue()); - const {color, text} = colorFormatConverters[format]; - this.#to = color.to; - this.#textView = new TextView(this, {converters: text, alpha: hasAlpha(format)}); - this.#colorView = new ColorChooserView(this, {converters: color, alpha: hasAlpha(format)}); - this.addTop(this.#textView); - this.addBottom(this.#colorView); - // WTF! FIX! - this.__setKnobHelper = () => { - if (this.#to) { - const hex6Or8 = this.#to(this.getValue()); - const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8)); - hsl[2] = (hsl[2] + 50) % 100; - const hex = uint8RGBToHex(hslToRgbUint8(hsl)); - this.setKnobColor(`${hex6Or8.substring(0, 7)}FF`, hex); - } - }; - this.updateDisplay(); - } - updateDisplay() { - super.updateDisplay(); - if (this.__setKnobHelper) { - this.__setKnobHelper(); - } - } - setOptions(options) { - super.setOptions(options); - return this; - } - } - - function showCSS(ob) { - if (ob.prototype.css) { - showCSS(ob.prototype); - } - } - - class Layout extends View { - static css = 'bar'; - constructor(tag, className) { - super(createElem(tag, {className})); - - showCSS(this); - } - } - - /* - class ValueController ?? { - const row = this.add(new Row()); - const label = row.add(new Label()); - const div = row.add(new Div()); - const row = div.add(new Row()); - } - */ - - /* - class MyCustomThing extends ValueController { - constructor(object, property, options) { - const topRow = this.add(new Row()); - const bottomRow = this.add(new Row()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - } - } - new Grid([ - [new - ] - */ - - class Column extends Layout { - constructor() { - super('div', 'muigui-row'); - } - } - - class Frame extends Layout { - static css = 'foo'; - constructor() { - super('div', 'muigui-frame'); - } - static get foo() { - return 'boo'; - } - } - - class Grid extends Layout { - constructor() { - super('div', 'muigui-grid'); - } - } - - class Row extends Layout { - constructor() { - super('div', 'muigui-row'); - } - } - - class GUIFolder extends Folder { - add(object, property, ...args) { - const controller = object instanceof Controller - ? object - : createController(object, property, ...args); - return this.addController(controller); - } - addCanvas(name) { - return this.addController(new Canvas(name)); - } - addColor(object, property, options = {}) { - const value = object[property]; - if (hasAlpha(options.format || guessFormat(value))) { - return this.addController(new ColorChooser(object, property, options)); - } else { - return this.addController(new Color(object, property, options)); - } - } - addDivider() { - return this.addController(new Divider()); - } - addFolder(name) { - return this.addController(new GUIFolder(name)); - } - addLabel(text) { - return this.addController(new Label(text)); - } - } - - class MuiguiElement extends HTMLElement { - constructor() { - super(); - this.shadow = this.attachShadow({mode: 'open'}); - } - } - - customElements.define('muigui-element', MuiguiElement); - - const baseStyleSheet = new CSSStyleSheet(); - baseStyleSheet.replaceSync(css.default); - const userStyleSheet = new CSSStyleSheet(); - - function makeStyleSheetUpdater(styleSheet) { - let newCss; - let newCssPromise; - - function updateStyle() { - if (newCss && !newCssPromise) { - const s = newCss; - newCss = undefined; - newCssPromise = styleSheet.replace(s).then(() => { - newCssPromise = undefined; - updateStyle(); - }); - } - } - - return function updateStyleSheet(css) { - newCss = css; - updateStyle(); - }; - } - - const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet); - const updateUserStyle = makeStyleSheetUpdater(userStyleSheet); - - class GUI extends GUIFolder { - static converters = converters; - static mapRange = mapRange; - static makeRangeConverters = makeRangeConverters; - static makeRangeOptions = makeRangeOptions; - static makeMinMaxPair = makeMinMaxPair; - #localStyleSheet = new CSSStyleSheet(); - - constructor(options = {}) { - super('Controls', 'muigui-root'); - if (options instanceof HTMLElement) { - options = {parent: options}; - } - const { - autoPlace = true, - width, - title = 'Controls', - } = options; - let { - parent, - } = options; - - if (width) { - this.domElement.style.width = /^\d+$/.test(width) ? `${width}px` : width; - } - if (parent === undefined && autoPlace) { - parent = document.body; - this.domElement.classList.add('muigui-auto-place'); - } - if (parent) { - const muiguiElement = createElem('muigui-element'); - muiguiElement.shadowRoot.adoptedStyleSheets = [baseStyleSheet, userStyleSheet, this.#localStyleSheet]; - muiguiElement.shadow.appendChild(this.domElement); - parent.appendChild(muiguiElement); - } - if (title) { - this.title(title); - } - this.domElement.classList.add('muigui', 'muigui-colors'); - } - setStyle(css) { - this.#localStyleSheet.replace(css); - } - static setBaseStyles(css) { - updateBaseStyle(css); - } - static getBaseStyleSheet() { - return baseStyleSheet; - } - static setUserStyles(css) { - updateUserStyle(css); - } - static getUserStyleSheet() { - return userStyleSheet; - } - static setTheme(name) { - GUI.setBaseStyles(`${css.default}\n${css.themes[name] || ''}`); - } - } - - function noop() { - } - - const keyDirections = { - ArrowLeft: [-1, 0], - ArrowRight: [1, 0], - ArrowUp: [0, -1], - ArrowDown: [0, 1], - }; - - // This probably needs to be global - function addKeyboardEvents(elem, {onDown = noop, onUp = noop}) { - const keyDown = function(event) { - const mult = event.shiftKey ? 10 : 1; - const [dx, dy] = (keyDirections[event.key] || [0, 0]).map(v => v * mult); - const fn = event.type === 'keydown' ? onDown : onUp; - fn({ - type: event.type.substring(3), - dx, - dy, - event, - }); - }; - - elem.addEventListener('keydown', keyDown); - elem.addEventListener('keyup', keyDown); - - return function() { - elem.removeEventListener('keydown', keyDown); - elem.removeEventListener('keyup', keyDown); - }; - } - - function assert(truthy, msg = '') { - if (!truthy) { - throw new Error(msg); - } - } - - function getEllipsePointForAngle(cx, cy, rx, ry, phi, theta) { - const m = Math.abs(rx) * Math.cos(theta); - const n = Math.abs(ry) * Math.sin(theta); - - return [ - cx + Math.cos(phi) * m - Math.sin(phi) * n, - cy + Math.sin(phi) * m + Math.cos(phi) * n, - ]; - } - - function getEndpointParameters(cx, cy, rx, ry, phi, theta, dTheta) { - const [x1, y1] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta); - const [x2, y2] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta + dTheta); - - const fa = Math.abs(dTheta) > Math.PI ? 1 : 0; - const fs = dTheta > 0 ? 1 : 0; - - return { x1, y1, x2, y2, fa, fs }; - } - - function arc(cx, cy, r, start, end) { - assert(Math.abs(start - end) <= Math.PI * 2); - assert(start >= -Math.PI && start <= Math.PI * 2); - assert(start <= end); - assert(end >= -Math.PI && end <= Math.PI * 4); - - const { x1, y1, x2, y2, fa, fs } = getEndpointParameters(cx, cy, r, r, 0, start, end - start); - return Math.abs(Math.abs(start - end) - Math.PI * 2) > Number.EPSILON - ? `M${cx} ${cy} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2} L${cx} ${cy}` - : `M${x1} ${y1} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2}`; - } - - const svg$2 = ` - - - - - - - - - -`; - - const twoPiMod = v => euclideanModulo$1(v + Math.PI, Math.PI * 2) - Math.PI; - - class DirectionView extends EditView { - #arrowElem; - #rangeElem; - #lastV; - #wrap; - #options = { - step: 1, - min: -180, - max: 180, - - /* - -------- - / -ฯ€/2 \ - / | \ - |<- -ฯ€ * | - | * 0 ->| zero is down the positive X axis - |<- +ฯ€ * | - \ | / - \ ฯ€/2 / - -------- - */ - dirMin: -Math.PI, - dirMax: Math.PI, - //dirMin: Math.PI * 0.5, - //dirMax: Math.PI * 2.5, - //dirMin: -Math.PI * 0.75, // test 10:30 to 7:30 - //dirMax: Math.PI * 0.75, - //dirMin: Math.PI * 0.75, // test 7:30 to 10:30 - //dirMax: -Math.PI * 0.75, - //dirMin: -Math.PI * 0.75, // test 10:30 to 1:30 - //dirMax: -Math.PI * 0.25, - //dirMin: Math.PI * 0.25, // test 4:30 to 7:30 - //dirMax: Math.PI * 0.75, - //dirMin: Math.PI * 0.75, // test 4:30 to 7:30 - //dirMax: Math.PI * 0.25, - wrap: undefined, - converters: identity, - }; - - constructor(setter, options = {}) { - const wheelHelper = createWheelHelper(); - super(createElem('div', { - className: 'muigui-direction muigui-no-scroll', - innerHTML: svg$2, - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - let tempV = this.#lastV + delta; - if (this.#wrap) { - tempV = euclideanModulo$1(tempV - min, max - min) + min; - } - const newV = clamp$1(stepify(tempV, v => v, step), min, max); - setter.setValue(newV); - }, - })); - const handleTouch = (e) => { - const {min, max, step, dirMin, dirMax} = this.#options; - const nx = e.nx * 2 - 1; - const ny = e.ny * 2 - 1; - const a = Math.atan2(ny, nx); - - const center = (dirMin + dirMax) / 2; - - const centeredAngle = twoPiMod(a - center); - const centeredStart = twoPiMod(dirMin - center); - const diff = dirMax - dirMin; - - const n = clamp$1((centeredAngle - centeredStart) / (diff), 0, 1); - const newV = stepify(min + (max - min) * n, v => v, step); - setter.setValue(newV); - }; - addTouchEvents(this.domElement, { - onDown: handleTouch, - onMove: handleTouch, - }); - addKeyboardEvents(this.domElement, { - onDown: (e) => { - const {min, max, step} = this.#options; - const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max); - setter.setValue(newV); - }, - }); - this.#arrowElem = this.$('#muigui-arrow'); - this.#rangeElem = this.$('#muigui-range'); - this.setOptions(options); - } - updateDisplay(v) { - this.#lastV = v; - const {min, max} = this.#options; - const n = (v - min) / (max - min); - const angle = lerp$1(this.#options.dirMin, this.#options.dirMax, n); - this.#arrowElem.style.transform = `rotate(${angle}rad)`; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {dirMin, dirMax, wrap} = this.#options; - this.#wrap = wrap !== undefined - ? wrap - : Math.abs(dirMin - dirMax) >= Math.PI * 2 - Number.EPSILON; - const [min, max] = dirMin < dirMax ? [dirMin, dirMax] : [dirMax , dirMin]; - this.#rangeElem.setAttribute('d', arc(0, 0, 28.87, min, max)); - } - } - - // deg2rad - // where is 0 - // range (0, 360), (-180, +180), (0,0) Really this is a range - - class Direction extends PopDownController { - #options; - constructor(object, property, options) { - super(object, property, 'muigui-direction'); - this.#options = options; // FIX - this.addTop(new NumberView(this, - identity)); - this.addBottom(new DirectionView(this, options)); - this.updateDisplay(); - } - } - - class RadioGridView extends EditView { - #values; - - constructor(setter, keyValues, cols = 3) { - const values = []; - const name = makeId(); - super(createElem('div', {}, keyValues.map(([key, value], ndx) => { - values.push(value); - return createElem('label', {}, [ - createElem('input', { - type: 'radio', - name, - value: ndx, - onChange: function() { - if (this.checked) { - setter.setFinalValue(that.#values[this.value]); - } - }, - }), - createElem('button', { - type: 'button', - textContent: key, - onClick: function() { - this.previousElementSibling.click(); - }, - }), - ]); - }))); - const that = this; - this.#values = values; - this.cols(cols); - } - updateDisplay(v) { - const ndx = this.#values.indexOf(v); - for (let i = 0; i < this.domElement.children.length; ++i) { - this.domElement.children[i].children[0].checked = i === ndx; - } - } - cols(cols) { - this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; - } - } - - class RadioGrid extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-radio-grid'); - const valueIsNumber = typeof this.getValue() === 'number'; - const { - keyValues: keyValuesInput, - cols = 3, - } = options; - const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); - this.add(new RadioGridView(this, keyValues, cols)); - this.updateDisplay(); - } - } - - function onResize(elem, callback) { - new ResizeObserver(() => { - callback({rect: elem.getBoundingClientRect(), elem}); - }).observe(elem); - } - - function onResizeSVGNoScale(elem, hAnchor, vAnchor, callback) { - onResize(elem, ({rect}) => { - const {width, height} = rect; - elem.setAttribute('viewBox', `-${width * hAnchor} -${height * vAnchor} ${width} ${height}`); - callback({elem, rect}); - }); - } - - function onResizeCanvas(elem, callback) { - onResize(elem, ({rect}) => { - const {width, height} = rect; - elem.width = width; - elem.height = height; - callback({elem, rect}); - }); - } - - const svg$1 = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - - function createSVGTicks(start, end, step, min, max, height) { - const p = []; - if (start < min) { - start += stepify(min - start, v => v, step); - } - end = Math.min(end, max); - for (let i = start; i <= end; i += step) { - p.push(`M${i} 0 l0 ${height}`); - } - return p.join(' '); - } - - function createSVGNumbers(start, end, unitSize, unit, minusSize, min, max, labelFn) { - const texts = []; - if (start < min) { - start += stepify(min - start, v => v, unitSize); - } - end = Math.min(end, max); - const digits = Math.max(0, -Math.log10(unit)); - const f = v => labelFn(v.toFixed(digits)); - for (let i = start; i <= end; i += unitSize) { - texts.push(`${f(i / unitSize * unit)}`); - } - return texts.join('\n'); - } - - function computeSizeOfMinus(elem) { - const oldHTML = elem.innerHTML; - elem.innerHTML = '- '; - const text = elem.querySelector('text'); - const size = text.getComputedTextLength(); - elem.innerHTML = oldHTML; - return size; - } - - class SliderView extends EditView { - #svgElem; - #originElem; - #ticksElem; - #thicksElem; - #numbersElem; - #leftGradElem; - #rightGradElem; - #width; - #height; - #lastV; - #minusSize; - #options = { - min: -100, - max: 100, - step: 1, - unit: 10, - unitSize: 10, - ticksPerUnit: 5, - labelFn: v => v, - tickHeight: 1, - limits: true, - thicksColor: undefined, - orientation: undefined, - }; - - constructor(setter, options) { - const wheelHelper = createWheelHelper(); - super(createElem('div', { - innerHTML: svg$1, - className: 'muigui-no-v-scroll', - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const newV = clamp$1(stepify(this.#lastV + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.#svgElem = this.$('svg'); - this.#originElem = this.$('#muigui-origin'); - this.#ticksElem = this.$('#muigui-ticks'); - this.#thicksElem = this.$('#muigui-thicks'); - this.#numbersElem = this.$('#muigui-numbers'); - this.#leftGradElem = this.$('#muigui-left-grad'); - this.#rightGradElem = this.$('#muigui-right-grad'); - this.setOptions(options); - let startV; - addTouchEvents(this.domElement, { - onDown: () => { - startV = this.#lastV; - }, - onMove: (e) => { - const {min, max, unitSize, unit, step} = this.#options; - const newV = clamp$1(stepify(startV - e.dx / unitSize * unit, v => v, step), min, max); - setter.setValue(newV); - }, - }); - addKeyboardEvents(this.domElement, { - onDown: (e) => { - const {min, max, step} = this.#options; - const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max); - setter.setValue(newV); - }, - }); - onResizeSVGNoScale(this.#svgElem, 0.5, 0, ({rect: {width}}) => { - this.#leftGradElem.setAttribute('x', -width / 2); - this.#rightGradElem.setAttribute('x', width / 2 - 20); - this.#minusSize = computeSizeOfMinus(this.#numbersElem); - this.#width = width; - this.#updateSlider(); - }); - } - // |--------V--------| - // . . | . . . | . . . | - // - #updateSlider() { - // There's no size if ResizeObserver has not fired yet. - if (!this.#width || this.#lastV === undefined) { - return; - } - const { - labelFn, - limits, - min, - max, - orientation, - tickHeight, - ticksPerUnit, - unit, - unitSize, - thicksColor, - } = this.#options; - const unitsAcross = Math.ceil(this.#width / unitSize); - const center = this.#lastV; - const centerUnitSpace = center / unit; - const startUnitSpace = Math.round(centerUnitSpace - unitsAcross); - const endUnitSpace = startUnitSpace + unitsAcross * 2; - const start = startUnitSpace * unitSize; - const end = endUnitSpace * unitSize; - const minUnitSpace = limits ? min * unitSize / unit : start; - const maxUnitSpace = limits ? max * unitSize / unit : end; - const height = labelFn(1) === '' ? 10 : 5; - if (ticksPerUnit > 1) { - this.#ticksElem.setAttribute('d', createSVGTicks(start, end, unitSize / ticksPerUnit, minUnitSpace, maxUnitSpace, height * tickHeight)); - } - this.#thicksElem.style.stroke = thicksColor; //setAttribute('stroke', thicksColor); - this.#thicksElem.setAttribute('d', createSVGTicks(start, end, unitSize, minUnitSpace, maxUnitSpace, height)); - this.#numbersElem.innerHTML = createSVGNumbers(start, end, unitSize, unit, this.#minusSize, minUnitSpace, maxUnitSpace, labelFn); - this.#originElem.setAttribute('transform', `translate(${-this.#lastV * unitSize / unit} 0)`); - this.#svgElem.classList.toggle('muigui-slider-up', orientation === 'up'); - } - updateDisplay(v) { - this.#lastV = v; - this.#updateSlider(); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - return this; - } - } - - class Slider extends ValueController { - constructor(object, property, options = {}) { - super(object, property, 'muigui-slider'); - this.add(new SliderView(this, options)); - this.add(new NumberView(this, options)); - this.updateDisplay(); - } - } - - const svg = ` - - - - - - - -`; - - class Vec2View extends EditView { - #svgElem; - #arrowElem; - #circleElem; - #lastV = []; - - constructor(setter) { - super(createElem('div', { - innerHTML: svg, - className: 'muigui-no-scroll', - })); - const onTouch = (e) => { - const {width, height} = this.#svgElem.getBoundingClientRect(); - const nx = e.nx * 2 - 1; - const ny = e.ny * 2 - 1; - setter.setValue([nx * width * 0.5, ny * height * 0.5]); - }; - addTouchEvents(this.domElement, { - onDown: onTouch, - onMove: onTouch, - }); - this.#svgElem = this.$('svg'); - this.#arrowElem = this.$('#muigui-arrow'); - this.#circleElem = this.$('#muigui-circle'); - onResizeSVGNoScale(this.#svgElem, 0.5, 0.5, () => this.#updateDisplayImpl); - } - #updateDisplayImpl() { - const [x, y] = this.#lastV; - this.#arrowElem.setAttribute('d', `M0,0L${x},${y}`); - this.#circleElem.setAttribute('transform', `translate(${x}, ${y})`); - } - updateDisplay(v) { - this.#lastV[0] = v[0]; - this.#lastV[1] = v[1]; - this.#updateDisplayImpl(); - } - } - - // TODO: zoom with wheel and pinch? - // TODO: grid? - // // options - // scale: - // range: number (both x and y + /) - // range: array (min, max) - // xRange: - // deg/rad/turn - - class Vec2 extends PopDownController { - constructor(object, property) { - super(object, property, 'muigui-vec2'); - - const makeSetter = (ndx) => { - return { - setValue: (v) => { - const newV = this.getValue(); - newV[ndx] = v; - this.setValue(newV); - }, - setFinalValue: (v) => { - const newV = this.getValue(); - newV[ndx] = v; - this.setFinalValue(newV); - }, - }; - }; - - this.addTop(new NumberView(makeSetter(0), { - converters: { - to: v => v[0], - from: strToNumber.from, - }, - })); - this.addTop(new NumberView(makeSetter(1), { - converters: { - to: v => v[1], - from: strToNumber.from, - }, - })); - this.addBottom(new Vec2View(this)); - this.updateDisplay(); - } - } - - GUI.ColorChooser = ColorChooser; - GUI.Direction = Direction; - GUI.RadioGrid = RadioGrid; - GUI.Range = Range; - GUI.Select = Select; - GUI.Slider = Slider; - GUI.TextNumber = TextNumber; - GUI.Vec2 = Vec2; - - return GUI; - -})); -//# sourceMappingURL=muigui.js.map diff --git a/dist/0.x/muigui.min.js b/dist/0.x/muigui.min.js deleted file mode 100644 index 9a859d1..0000000 --- a/dist/0.x/muigui.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).GUI=e()}(this,(function(){"use strict";var t={default:'\n.muigui {\n --bg-color: #ddd;\n --color: #222;\n --contrast-color: #eee;\n --value-color: #145 ;\n --value-bg-color: #eeee;\n --disabled-color: #999;\n --menu-bg-color: #f8f8f8;\n --menu-sep-color: #bbb;\n --hover-bg-color: #999;\n --focus-color: #68C;\n --range-color: #888888;\n --invalid-color: #FF0000;\n --selected-color: rgb(255, 255, 255, 0.9);\n\n --button-bg-color: var(--value-bg-color);\n\n --range-left-color: var(--value-color);\n --range-right-color: var(--value-bg-color); \n --range-right-hover-color: var(--hover-bg-color);\n\n color: var(--color);\n background-color: var(--bg-color);\n}\n\n@media (prefers-color-scheme: dark) {\n .muigui {\n --bg-color: #222222;\n --color: #dddddd;\n --contrast-color: #000;\n --value-color: #43e5f7;\n --value-bg-color: #444444;\n --disabled-color: #666666;\n --menu-bg-color: #080808;\n --menu-sep-color: #444444;\n --hover-bg-color: #666666;\n --focus-color: #88AAFF;\n --range-color: #888888;\n --invalid-color: #FF6666;\n --selected-color: rgba(255, 255, 255, 0.3);\n\n --button-bg-color: var(--value-bg-color);\n\n --range-left-color: var(--value-color);\n --range-right-color: var(--value-bg-color); \n --range-right-hover-color: var(--hover-bg-color);\n\n color: var(--color);\n background-color: var(--bg-color);\n }\n}\n\n.muigui {\n --width: 250px;\n --label-width: 45%;\n --number-width: 40%;\n\n\n --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;\n --font-size: 11px;\n --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;\n --font-size-mono: 11px;\n\n --line-height: 1.7em;\n --border-radius: 0px;\n\n width: var(--width);\n font-family: var(--font-family);\n font-size: var(--font-size);\n box-sizing: border-box;\n line-height: 100%;\n}\n.muigui * {\n box-sizing: inherit;\n}\n\n.muigui-no-scroll {\n touch-action: none;\n}\n.muigui-no-h-scroll {\n touch-action: pan-y;\n}\n.muigui-no-v-scroll {\n touch-action: pan-x;\n}\n\n.muigui-invalid-value {\n background-color: red !important;\n color: white !important;\n}\n\n.muigui-grid {\n display: grid;\n}\n.muigui-rows {\n display: flex;\n flex-direction: column;\n\n min-height: 20px;\n border: 2px solid red;\n}\n.muigui-columns {\n display: flex;\n flex-direction: row;\n\n height: 20px;\n border: 2px solid green;\n}\n.muigui-rows>*,\n.muigui-columns>* {\n flex: 1 1 auto;\n align-items: stretch;\n min-height: 0;\n min-width: 0;\n}\n\n.muigui-row {\n border: 2px solid yellow;\n min-height: 10px\n}\n.muigui-column {\n border: 2px solid lightgreen;\n}\n\n/* -------- */\n\n.muigui-show { /* */ }\n.muigui-hide { \n display: none !important;\n}\n.muigui-disabled {\n pointer-events: none;\n --color: var(--disabled-color) !important;\n --value-color: var(--disabled-color) !important;\n --range-left-color: var(--disabled-color) !important;\n}\n\n.muigui canvas,\n.muigui svg {\n display: block;\n border-radius: var(--border-radius);\n}\n.muigui canvas {\n background-color: var(--value-bg-color);\n}\n\n.muigui-controller {\n min-width: 0;\n min-height: var(--line-height);\n}\n.muigui-root,\n.muigui-menu {\n display: flex;\n flex-direction: column;\n position: relative;\n user-select: none;\n height: fit-content;\n margin: 0;\n padding-bottom: 0.1em;\n border-radius: var(--border-radius);\n}\n.muigui-menu {\n border-bottom: 1px solid var(--menu-sep-color);\n}\n\n.muigui-root>button:nth-child(1),\n.muigui-menu>button:nth-child(1) {\n border-top: 1px solid var(--menu-sep-color);\n border-bottom: 1px solid var(--menu-sep-color);\n position: relative;\n text-align: left;\n color: var(--color);\n background-color: var(--menu-bg-color);\n min-height: var(--line-height);\n padding-top: 0.2em;\n padding-bottom: 0.2em;\n cursor: pointer;\n border-radius: var(--border-radius);\n}\n.muigui-root>div:nth-child(2),\n.muigui-menu>div:nth-child(2) {\n flex: 1 1 auto;\n}\n\n.muigui-controller {\n margin-left: 0.2em;\n margin-right: 0.2em;\n}\n.muigui-root.muigui-controller,\n.muigui-menu.muigui-controller {\n margin-left: 0;\n margin-right: 0;\n}\n.muigui-controller>*:nth-child(1) {\n flex: 1 0 var(--label-width);\n min-width: 0;\n white-space: pre;\n}\n.muigui-controller>label:nth-child(1) {\n place-content: center start;\n display: inline-grid;\n overflow: hidden;\n}\n.muigui-controller>*:nth-child(2) {\n flex: 1 1 75%;\n min-width: 0;\n}\n\n/* -----------------------------------------\n a label controller is [[label][value]]\n*/\n\n.muigui-label-controller {\n display: flex;\n margin: 0.4em 0 0.4em 0;\n word-wrap: initial;\n align-items: stretch;\n}\n\n.muigui-value {\n display: flex;\n align-items: stretch;\n}\n.muigui-value>* {\n flex: 1 1 auto;\n min-width: 0;\n}\n.muigui-value>*:nth-child(1) {\n flex: 1 1 calc(100% - var(--number-width));\n}\n.muigui-value>*:nth-child(2) {\n flex: 1 1 var(--number-width);\n margin-left: 0.2em;\n}\n\n/* fix! */\n.muigui-open>button>label::before,\n.muigui-closed>button>label::before {\n width: 1.25em;\n height: var(--line-height);\n display: inline-grid;\n place-content: center start;\n pointer-events: none;\n}\n.muigui-open>button>label::before {\n content: "โ“ง"; /*"โ–ผ";*/\n}\n.muigui-closed>button>label::before {\n content: "โจ"; /*"โ–ถ";*/\n}\n.muigui-open>*:nth-child(2) {\n transition: max-height 0.2s ease-out,\n opacity 0.5s ease-out;\n max-height: 100vh;\n overflow: auto;\n opacity: 1;\n}\n\n.muigui-closed>*:nth-child(2) {\n transition: max-height 0.2s ease-out,\n opacity 1s;\n max-height: 0;\n opacity: 0;\n overflow: hidden;\n}\n\n/* ---- popdown ---- */\n\n.muigui-pop-down-top {\n display: flex;\n}\n/* fix? */\n.muigui-value>*:nth-child(1).muigui-pop-down-top {\n flex: 0;\n}\n.muigui-pop-down-bottom {\n\n}\n\n.muigui-pop-down-values {\n min-width: 0;\n display: flex;\n}\n.muigui-pop-down-values>* {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.muigui-value.muigui-pop-down-controller {\n flex-direction: column;\n}\n\n.muigui-pop-down-top input[type=checkbox] {\n -webkit-appearance: none;\n appearance: none;\n width: auto;\n color: var(--value-color);\n background-color: var(--value-bg-color);\n cursor: pointer;\n\n display: grid;\n place-content: center;\n margin: 0;\n font: inherit;\n color: currentColor;\n width: 1.7em;\n height: 1.7em;\n transform: translateY(-0.075em);\n}\n\n.muigui-pop-down-top input[type=checkbox]::before {\n content: "+";\n display: grid;\n place-content: center;\n border-radius: calc(var(--border-radius) + 2px);\n border-left: 1px solid rgba(255,255,255,0.3);\n border-top: 1px solid rgba(255,255,255,0.3);\n border-bottom: 1px solid rgba(0,0,0,0.2);\n border-right: 1px solid rgba(0,0,0,0.2);\n background-color: var(--range-color);\n color: var(--value-bg-color);\n width: calc(var(--line-height) - 4px);\n height: calc(var(--line-height) - 4px);\n}\n\n.muigui-pop-down-top input[type=checkbox]:checked::before {\n content: "๏ผธ";\n}\n\n\n/* ---- select ---- */\n\n.muigui select,\n.muigui option,\n.muigui input,\n.muigui button {\n color: var(--value-color);\n background-color: var(--value-bg-color);\n font-family: var(--font-family);\n font-size: var(--font-size);\n border: none;\n margin: 0;\n border-radius: var(--border-radius);\n}\n.muigui select {\n appearance: none;\n margin: 0;\n margin-left: 0; /*?*/\n overflow: hidden; /* Safari */\n}\n\n.muigui select:focus,\n.muigui input:focus,\n.muigui button:focus {\n outline: 1px solid var(--focus-color);\n}\n\n.muigui select:hover,\n.muigui option:hover,\n.muigui input:hover,\n.muigui button:hover {\n background-color: var(--hover-bg-color); \n}\n\n/* ------ [ label ] ------ */\n\n.muigui-label {\n border-top: 1px solid var(--menu-sep-color);\n border-bottom: 1px solid var(--menu-sep-color);\n padding-top: 0.4em;\n padding-bottom: 0.3em;\n place-content: center start;\n background-color: var(--menu-bg-color);\n white-space: pre;\n border-radius: var(--border-radius);\n}\n\n/* ------ [ divider] ------ */\n\n.muigui-divider {\n min-height: 6px;\n border-top: 2px solid var(--menu-sep-color);\n margin-top: 6px;\n}\n\n/* ------ [ button ] ------ */\n\n.muigui-button {\n display: grid;\n\n}\n.muigui-button button {\n border: none;\n color: var(--value-color);\n background-color: var(--button-bg-color);\n cursor: pointer;\n place-content: center center;\n}\n\n/* ------ [ color ] ------ */\n\n.muigui-color>div {\n overflow: hidden;\n position: relative;\n margin-left: 0;\n margin-right: 0; /* why? */\n max-width: var(--line-height);\n border-radius: var(--border-radius);\n}\n\n.muigui-color>div:focus-within {\n outline: 1px solid var(--focus-color);\n}\n\n.muigui-color input[type=color] {\n border: none;\n padding: 0;\n background: inherit;\n cursor: pointer;\n position: absolute;\n width: 200%;\n left: -10px;\n top: -10px;\n height: 200%;\n}\n.muigui-disabled canvas,\n.muigui-disabled svg,\n.muigui-disabled img,\n.muigui-disabled .muigui-color input[type=color] {\n opacity: 0.2;\n}\n\n/* ------ [ checkbox ] ------ */\n\n.muigui-checkbox>label:nth-child(2) {\n display: grid;\n place-content: center start;\n margin: 0;\n}\n\n.muigui-checkbox input[type=checkbox] {\n -webkit-appearance: none;\n appearance: none;\n width: auto;\n color: var(--value-color);\n background-color: var(--value-bg-color);\n cursor: pointer;\n\n display: grid;\n place-content: center;\n margin: 0;\n font: inherit;\n color: currentColor;\n width: 1.7em;\n height: 1.7em;\n transform: translateY(-0.075em);\n}\n\n.muigui-checkbox input[type=checkbox]::before {\n content: "";\n color: var(--value-color);\n display: grid;\n place-content: center;\n}\n\n.muigui-checkbox input[type=checkbox]:checked::before {\n content: "โœ”";\n}\n\n.muigui input[type=number]::-webkit-inner-spin-button, \n.muigui input[type=number]::-webkit-outer-spin-button { \n -webkit-appearance: none;\n appearance: none;\n margin: 0; \n}\n.muigui input[type=number] {\n -moz-appearance: textfield;\n}\n\n/* ------ [ radio grid ] ------ */\n\n.muigui-radio-grid>div {\n display: grid;\n gap: 2px;\n}\n\n.muigui-radio-grid input {\n appearance: none;\n display: none;\n}\n\n.muigui-radio-grid button {\n color: var(--color);\n width: 100%;\n text-align: left;\n}\n\n.muigui-radio-grid input:checked + button {\n color: var(--value-color);\n background-color: var(--selected-color);\n}\n\n/* ------ [ color-chooser ] ------ */\n\n.muigui-color-chooser-cursor {\n stroke-width: 1px;\n stroke: white;\n fill: none;\n}\n.muigui-color-chooser-circle {\n stroke-width: 1px;\n stroke: white;\n fill: none;\n}\n\n\n/* ------ [ vec2 ] ------ */\n\n.muigui-vec2 svg {\n background-color: var(--value-bg-color);\n}\n\n.muigui-vec2-axis {\n stroke: 1px;\n stroke: var(--focus-color);\n}\n\n.muigui-vec2-line {\n stroke-width: 1px;\n stroke: var(--value-color);\n fill: var(--value-color);\n}\n\n/* ------ [ direction ] ------ */\n\n.muigui-direction svg {\n background-color: rgba(0,0,0,0.2);\n}\n\n.muigui-direction:focus-within svg {\n outline: none;\n}\n.muigui-direction-range {\n fill: var(--value-bg-color);\n}\n.muigui-direction svg:focus {\n outline: none;\n}\n.muigui-direction svg:focus .muigui-direction-range {\n stroke-width: 0.5px;\n stroke: var(--focus-color);\n}\n\n.muigui-direction-arrow {\n fill: var(--value-color);\n}\n\n/* ------ [ slider ] ------ */\n\n.muigui-slider>div {\n display: flex;\n align-items: stretch;\n height: var(--line-height);\n}\n.muigui-slider svg {\n flex: 1 1 auto;\n}\n.muigui-slider .muigui-slider-up #muigui-orientation {\n transform: scale(1, -1) translateY(-100%);\n}\n\n.muigui-slider .muigui-slider-up #muigui-number-orientation {\n transform: scale(1,-1);\n}\n\n.muigui-ticks {\n stroke: var(--range-color);\n}\n.muigui-thicks {\n stroke: var(--color);\n stroke-width: 2px;\n}\n.muigui-svg-text {\n fill: var(--color);\n font-size: 7px;\n}\n.muigui-mark {\n fill: var(--value-color);\n}\n\n/* ------ [ range ] ------ */\n\n\n.muigui-range input[type=range] {\n -webkit-appearance: none;\n appearance: none;\n background-color: transparent;\n}\n\n.muigui-range input[type=range]::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n border-radius: calc(var(--border-radius) + 2px);\n border-left: 1px solid rgba(255,255,255,0.3);\n border-top: 1px solid rgba(255,255,255,0.3);\n border-bottom: 1px solid rgba(0,0,0,0.2);\n border-right: 1px solid rgba(0,0,0,0.2);\n background-color: var(--range-color);\n margin-top: calc((var(--line-height) - 2px) / -2);\n width: calc(var(--line-height) - 2px);\n height: calc(var(--line-height) - 2px);\n}\n\n.muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: none;\n appearance: none;\n border: 1px solid var(--menu-sep-color);\n height: 2px;\n}\n\n\n/* dat.gui style - doesn\'t work on Safari iOS */\n\n/*\n.muigui-range input[type=range] {\n cursor: ew-resize;\n overflow: hidden;\n}\n\n.muigui-range input[type=range] {\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--range-right-color);\n margin: 0;\n}\n.muigui-range input[type=range]:hover {\n background-color: var(--range-right-hover-color);\n}\n\n.muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: none;\n appearance: none;\n height: max-content;\n color: var(--range-left-color);\n margin-top: -1px;\n}\n\n.muigui-range input[type=range]::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 0px;\n height: max-content;\n box-shadow: -1000px 0 0 1000px var(--range-left-color);\n}\n*/\n\n/* FF */\n/*\n.muigui-range input[type=range]::-moz-slider-progress {\n background-color: var(--range-left-color); \n}\n.muigui-range input[type=range]::-moz-slider-thumb {\n height: max-content;\n width: 0;\n border: none;\n box-shadow: -1000px 0 0 1000px var(--range-left-color);\n box-sizing: border-box;\n}\n*/\n\n.muigui-checkered-background {\n background-color: #404040;\n background-image:\n linear-gradient(45deg, #808080 25%, transparent 25%),\n linear-gradient(-45deg, #808080 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #808080 75%),\n linear-gradient(-45deg, transparent 75%, #808080 75%);\n background-size: 16px 16px;\n background-position: 0 0, 0 8px, 8px -8px, -8px 0px;\n}\n\n/* ---------------------------------------------------------- */\n\n/* needs to be at bottom to take precedence */\n.muigui-auto-place {\n max-height: 100%;\n position: fixed;\n top: 0;\n right: 15px;\n z-index: 100001;\n}\n\n',themes:{default:"",float:"\n :root {\n color-scheme: light dark,\n }\n\n .muigui {\n --width: 400px;\n --bg-color: initial;\n --label-width: 25%;\n --number-width: 20%;\n }\n\n input,\n .muigui-label-controller>label {\n text-shadow:\n -1px -1px 0 var(--contrast-color),\n 1px -1px 0 var(--contrast-color),\n -1px 1px 0 var(--contrast-color),\n 1px 1px 0 var(--contrast-color);\n }\n\n .muigui-controller > label:nth-child(1) {\n place-content: center end;\n margin-right: 1em;\n }\n\n .muigui-value > :nth-child(2) {\n margin-left: 1em;\n }\n\n .muigui-root>*:nth-child(1) {\n display: none;\n }\n\n .muigui-range input[type=range]::-webkit-slider-thumb {\n border-radius: 1em;\n }\n\n .muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: initial;\n appearance: none;\n border: 1px solid rgba(0, 0, 0, 0.25);\n height: 2px;\n }\n\n .muigui-colors {\n --value-color: var(--color );\n --value-bg-color: rgba(0, 0, 0, 0.1);\n --disabled-color: #cccccc;\n --menu-bg-color: rgba(0, 0, 0, 0.1);\n --menu-sep-color: #bbbbbb;\n --hover-bg-color: rgba(0, 0, 0, 0);\n --invalid-color: #FF0000;\n --selected-color: rgba(0, 0, 0, 0.3);\n --range-color: rgba(0, 0, 0, 0.125);\n }\n"}};function e(t,e={},n=[]){const i=document.createElement(t);return function(t,e,n){for(const[n,i]of Object.entries(e))if("function"==typeof i&&n.startsWith("on")){const e=n.substring(2).toLowerCase();t.addEventListener(e,i,{passive:!1})}else if("object"==typeof i)for(const[e,o]of Object.entries(i))t[n][e]=o;else void 0===t[n]?t.setAttribute(n,i):t[n]=i;for(const e of n)t.appendChild(e)}(i,e,n),i}let n=0;function i(t,e){const n=t.indexOf(e);return n&&t.splice(n,1),t}function o(t,e,n){return Math.max(e,Math.min(n,t))}const r="undefined"!=typeof SharedArrayBuffer?function(t){return t&&t.buffer&&(t.buffer instanceof ArrayBuffer||t.buffer instanceof SharedArrayBuffer)}:function(t){return t&&t.buffer&&t.buffer instanceof ArrayBuffer},s=(t,e,n)=>Math.round(e(t)/n)/(1/n),a=(t,e)=>(t%e+e)%e;function l(t,e){for(const n in e)n in t&&(t[n]=e[n]);return t}const u=(t,e,n,i,o)=>(t-e)*(o-i)/(n-e)+i,c=({from:t,to:e})=>({to:n=>u(n,...t,...e),from:n=>[!0,u(n,...e,...t)]}),h=({from:t,to:e,step:n})=>({min:e[0],max:e[1],...n&&{step:n},converters:c({from:t,to:e})}),d={to:t=>t,from:t=>[!0,t]};function p(t,e,n,i,o){const{converters:{from:r}=d}=o,{min:s,max:a}=o,l=o.minRange||0,u=r(l)[1],c=t.add(e,n,{...o,min:s,max:a-l}).onChange((t=>{h.setValue(Math.min(a,Math.max(t+u,e[i])))})),h=t.add(e,i,{...o,min:s+l,max:a}).onChange((t=>{c.setValue(Math.max(s,Math.min(t-u,e[n])))}));return[c,h]}class m{domElement;#t;#e=[];constructor(t){this.domElement=t,this.#t=t}addElem(t){return this.#t.appendChild(t),t}removeElem(t){return this.#t.removeChild(t),t}pushSubElem(t){this.#t.appendChild(t),this.#t=t}popSubElem(){this.#t=this.#t.parentElement}add(t){return this.#e.push(t),this.addElem(t.domElement),t}remove(t){return this.removeElem(t.domElement),i(this.#e,t),t}pushSubView(t){this.pushSubElem(t.domElement)}popSubView(){this.popSubElem()}setOptions(t){for(const e of this.#e)e.setOptions(t)}updateDisplayIfNeeded(t,e){for(const n of this.#e)n.updateDisplayIfNeeded(t,e);return this}$(t){return this.domElement.querySelector(t)}}class g extends m{#n;#i;#o;constructor(t){super(e("div",{className:"muigui-controller"})),this.#n=[],this.#i=[],t&&this.domElement.classList.add(t)}get parent(){return this.#o}setParent(t){this.#o=t,this.enable(!this.disabled())}show(t=!0){return this.domElement.classList.toggle("muigui-hide",!t),this.domElement.classList.toggle("muigui-show",t),this}hide(){return this.show(!1)}disabled(){return!!this.domElement.closest(".muigui-disabled")}enable(t=!0){return this.domElement.classList.toggle("muigui-disabled",!t),["input","button","select","textarea"].forEach((t=>{this.domElement.querySelectorAll(t).forEach((t=>{const e=!!t.closest(".muigui-disabled");t.disabled=e}))})),this}disable(t=!0){return this.enable(!t)}onChange(t){return this.removeChange(t),this.#n.push(t),this}removeChange(t){return i(this.#n,t),this}onFinishChange(t){return this.removeFinishChange(t),this.#i.push(t),this}removeFinishChange(t){return i(this.#i,t),this}#r(t,e){for(const n of t)n.call(this,e)}emitChange(t,e,n){this.#r(this.#n,t),this.#o&&(void 0===e?this.#o.emitChange(t):this.#o.emitChange({object:e,property:n,value:t,controller:this}))}emitFinalChange(t,e,n){this.#r(this.#i,t),this.#o&&(void 0===e?this.#o.emitChange(t):this.#o.emitFinalChange({object:e,property:n,value:t,controller:this}))}updateDisplay(){}getColors(){const t=t=>t.replace(/-([a-z])/g,((t,e)=>e.toUpperCase())),n=e("div");this.domElement.appendChild(n);const i=Object.fromEntries(["color","bg-color","value-color","value-bg-color","hover-bg-color","menu-bg-color","menu-sep-color","disabled-color"].map((e=>{n.style.color=`var(--${e})`;const i=getComputedStyle(n);return[t(e),i.color]})));return n.remove(),i}}class f extends g{#s;#a;#l;#u={name:""};constructor(t,n,i={}){super("muigui-button",""),this.#s=t,this.#a=n,this.#l=this.addElem(e("button",{type:"button",onClick:()=>{this.#s[this.#a](this)}})),this.setOptions({name:n,...i})}setOptions(t){l(this.#u,t);const{name:e}=this.#u;this.#l.textContent=e}}function b(t,e){if(t.length!==e.length)return!1;for(let n=0;n{t.setValue(i.checked)},onChange:()=>{t.setFinalValue(i.checked)}});super(e("label",{},[i])),this.#b=i}updateDisplay(t){this.#b.checked=t}}const w=[],y=new Set;let k,E;function $(){k=void 0,E=!0;for(const t of w)y.has(t)||t();E=!1,y.size&&(E?C():(y.forEach((t=>{i(w,t)})),y.clear())),C()}function C(){!k&&w.length&&(k=requestAnimationFrame($))}let V=0;function I(){return"muigui-"+ ++V}class M extends m{constructor(t=""){super(e("div",{className:"muigui-value"})),t&&this.domElement.classList.add(t)}}class S extends g{#v;#x;constructor(t="",n=""){super("muigui-label-controller"),this.#v=I(),this.#x=e("label",{for:this.#v}),this.domElement.appendChild(this.#x),this.pushSubView(new M(t)),this.name(n)}get id(){return this.#v}name(t){return this.#x.title===this.#x.textContent&&(this.#x.title=t),this.#x.textContent=t,this}tooltip(t){this.#x.title=t}}class D extends S{#s;#a;#w;#y;#e;#k;constructor(t,e,n=""){super(n,e),this.#s=t,this.#a=e,this.#w=this.getValue(),this.#y=!1,this.#e=[]}get initialValue(){return this.#w}get object(){return this.#s}get property(){return this.#a}add(t){return this.#e.push(t),super.add(t),this.updateDisplay(),t}#E(t,e){let n=!1;if("object"==typeof t){const e=this.#s[this.#a];if(Array.isArray(t)||r(t))for(let i=0;i=0&&w.splice(e,1)}(this.#k)),this}}class N extends D{constructor(t,e){super(t,e,"muigui-checkbox");const n=this.id;this.add(new x(this,n)),this.updateDisplay()}}const F={to:t=>t,from:t=>[!0,t]},A={to:t=>t.toString(),from:t=>{const e=parseFloat(t);return[!Number.isNaN(e),e]}},U={radToDeg:c({to:[0,180],from:[0,Math.PI]})};function L(){let t=0;return function(e,n,i=5){t-=e.deltaY*n/i;const o=Math.floor(Math.abs(t)/n)*Math.sign(t)*n;return t-=o,o}}class O extends v{#$;#C;#V;#I;#u={step:.01,converters:A,min:Number.NEGATIVE_INFINITY,max:Number.POSITIVE_INFINITY};constructor(t,n){const i=t.setValue.bind(t),r=t.setFinalValue.bind(t),a=L();super(e("input",{type:"number",onInput:()=>this.#M(i,!0),onChange:()=>this.#M(r,!1),onWheel:e=>{e.preventDefault();const{min:n,max:i,step:r}=this.#u,l=a(e,r),u=parseFloat(this.domElement.value),c=o(s(u+l,(t=>t),r),n,i);t.setValue(c)}})),this.setOptions(n)}#M(t,e){const n=parseFloat(this.domElement.value),[i,r]=this.#C(n);let s;if(i&&!Number.isNaN(n)){const{min:n,max:i}=this.#u;s=r>=n&&r<=i,this.#I=e,t(o(r,n,i))}this.domElement.classList.toggle("muigui-invalid-value",!i||!s)}updateDisplay(t){this.#I||(this.domElement.value=s(t,this.#$,this.#V)),this.#I=!1}setOptions(t){l(this.#u,t);const{step:e,converters:{to:n,from:i}}=this.#u;return this.#$=n,this.#C=i,this.#V=e,this}}class j extends D{#S;#V;constructor(t,e,n={}){super(t,e,"muigui-checkbox"),this.#S=this.add(new O(this,n)),this.updateDisplay()}}class T extends v{#D;constructor(t,n){const i=[];super(e("select",{onChange:()=>{t.setFinalValue(this.#D[this.domElement.selectedIndex])}},n.map((([t,n])=>(i.push(n),e("option",{textContent:t})))))),this.#D=i}updateDisplay(t){const e=this.#D.indexOf(t);this.domElement.selectedIndex=e}}function H(t,e){return Array.isArray(t)?Array.isArray(t[0])?t:e?t.map(((t,e)=>[t,e])):t.map((t=>[t,t])):[...Object.entries(t)]}class z extends D{constructor(t,e,n){super(t,e,"muigui-select");const i="number"==typeof this.getValue(),{keyValues:o}=n,r=H(o,i);this.add(new T(this,r)),this.updateDisplay()}}class P extends v{#$;#C;#V;#I;#u={step:.01,min:0,max:1,converters:F};constructor(t,n){const i=L();super(e("input",{type:"range",onInput:()=>{this.#I=!0;const{min:e,max:n,step:i}=this.#u,r=parseFloat(this.domElement.value),a=o(s(r,(t=>t),i),e,n),[l,u]=this.#C(a);l&&t.setValue(u)},onChange:()=>{this.#I=!0;const{min:e,max:n,step:i}=this.#u,r=parseFloat(this.domElement.value),a=o(s(r,(t=>t),i),e,n),[l,u]=this.#C(a);l&&t.setFinalValue(u)},onWheel:e=>{e.preventDefault();const[n,r]=this.#C(parseFloat(this.domElement.value));if(!n)return;const{min:a,max:l,step:u}=this.#u,c=i(e,u),h=o(s(r+c,(t=>t),u),a,l);t.setValue(h)}})),this.setOptions(n)}updateDisplay(t){this.#I||(this.domElement.value=s(t,this.#$,this.#V)),this.#I=!1}setOptions(t){l(this.#u,t);const{step:e,min:n,max:i,converters:{to:o,from:r}}=this.#u;return this.#$=o,this.#C=r,this.#V=e,this.domElement.step=e,this.domElement.min=n,this.domElement.max=i,this}}class B extends D{constructor(t,e,n){super(t,e,"muigui-range"),this.add(new P(this,n)),this.add(new O(this,n))}}class G extends v{#$;#C;#I;#u={converters:F};constructor(t,n){const i=t.setValue.bind(t),o=t.setFinalValue.bind(t);super(e("input",{type:"text",onInput:()=>this.#M(i,!0),onChange:()=>this.#M(o,!1)})),this.setOptions(n)}#M(t,e){const[n,i]=this.#C(this.domElement.value);n&&(this.#I=e,t(i)),this.domElement.style.color=n?"":"var(--invalid-color)"}updateDisplay(t){this.#I||(this.domElement.value=this.#$(t),this.domElement.style.color=""),this.#I=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n}}=this.#u;return this.#$=e,this.#C=n,this}}class R extends D{constructor(t,e){super(t,e,"muigui-checkbox"),this.add(new G(this)),this.updateDisplay()}}const _=(t,e,n)=>Math.max(e,Math.min(n,t)),Y=(t,e,n)=>t+(e-t)*n,W=t=>t>=0?t%1:1-t%1,q=t=>+t.toFixed(0),K=t=>+t.toFixed(3),J=t=>parseInt(t.substring(1,3),16)<<16|parseInt(t.substring(3,5),16)<<8|parseInt(t.substring(5,7),16),X=t=>parseInt(t.substring(1,3),16)*2**24+65536*parseInt(t.substring(3,5),16)+256*parseInt(t.substring(5,7),16)+parseInt(t.substring(7,9),16),Z=t=>[parseInt(t.substring(1,3),16),parseInt(t.substring(3,5),16),parseInt(t.substring(5,7),16)],Q=t=>`#${Array.from(t).map((t=>t.toString(16).padStart(2,"0"))).join("")}`,tt=t=>[parseInt(t.substring(1,3),16),parseInt(t.substring(3,5),16),parseInt(t.substring(5,7),16),parseInt(t.substring(7,9),16)],et=t=>`#${Array.from(t).map((t=>t.toString(16).padStart(2,"0"))).join("")}`,nt=t=>Z(t).map((t=>K(t/255))),it=t=>Q(Array.from(t).map((t=>Math.round(_(255*t,0,255))))),ot=t=>tt(t).map((t=>K(t/255))),rt=t=>et(Array.from(t).map((t=>Math.round(_(255*t,0,255))))),st=t=>_(Math.round(255*t),0,255).toString(16).padStart(2,"0"),at=t=>({r:parseInt(t.substring(1,3),16)/255,g:parseInt(t.substring(3,5),16)/255,b:parseInt(t.substring(5,7),16)/255}),lt=t=>({r:parseInt(t.substring(1,3),16)/255,g:parseInt(t.substring(3,5),16)/255,b:parseInt(t.substring(5,7),16)/255,a:parseInt(t.substring(7,9),16)/255}),ut=t=>`rgb(${Z(t).join(", ")})`,ct=/^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,ht=t=>`rgba(${tt(t).map(((t,e)=>3===e?t/255:t)).join(", ")})`,dt=/^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+\.\d+|\d+)\s*\)\s*$/,pt=t=>{const e=yt(Z(t)).map((t=>q(t)));return`hsl(${e[0]}, ${e[1]}%, ${e[2]}%)`},mt=t=>{const e=kt(tt(t)).map(((t,e)=>3===e?K(t):q(t)));return`hsl(${e[0]} ${e[1]}% ${e[2]}% / ${e[3]})`},gt=/^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\)\s*$/,ft=/^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\/\s*(\d+\.\d+|\d+)\s*\)\s*$/,bt=(t,e)=>(t%e+e)%e;function vt([t,e,n]){t=bt(t,360),e=_(e/100,0,1),n=_(n/100,0,1);const i=e*Math.min(n,1-n);function o(e){const o=(e+t/30)%12;return n-i*Math.max(-1,Math.min(o-3,9-o,1))}return[o(0),o(8),o(4)].map((t=>Math.round(255*t)))}function xt([t,e,n]){const i=Math.max(t,e,n),o=Math.min(t,e,n),r=.5*(o+i),s=i-o;let a=0,l=0;if(0!==s)switch(l=0===r||1===r?0:(i-r)/Math.min(r,1-r),i){case t:a=(e-n)/s+(e{const[e,n,i]=xt(t.map((t=>t/255)));return[360*e,100*n,100*i]},kt=t=>{const[e,n,i,o]=wt(t.map((t=>t/255)));return[360*e,100*n,100*i,o]};function Et([t,e,n]){return e=_(e,0,1),n=_(n,0,1),[t,t+2/3,t+1/3].map((t=>Y(1,_(Math.abs(6*W(t)-3)-1,0,1),e)*n))}function $t([t,e,n,i]){return[...Et([t,e,n]),i]}const Ct=t=>Math.round(1e3*t)/1e3;function Vt([t,e,n]){const i=n>e?[n,e,-1,2/3]:[e,n,0,-1/3],o=i[0]>t?[i[0],i[1],i[3],t]:[t,i[1],i[2],i[0]],r=o[0]-Math.min(o[3],o[1]);return[Math.abs(o[2]+(o[3]-o[1])/(6*r+Number.EPSILON)),r/(o[0]+Number.EPSILON),o[0]].map(Ct)}const It=t=>t.endsWith("a")||t.startsWith("hex8"),Mt=[{re:/^#(?:[0-9a-f]){6}$/i,format:"hex6"},{re:/^(?:[0-9a-f]){6}$/i,format:"hex6-no-hash"},{re:/^#(?:[0-9a-f]){8}$/i,format:"hex8"},{re:/^(?:[0-9a-f]){8}$/i,format:"hex8-no-hash"},{re:/^#(?:[0-9a-f]){3}$/i,format:"hex3"},{re:/^(?:[0-9a-f]){3}$/i,format:"hex3-no-hash"},{re:ct,format:"css-rgb"},{re:gt,format:"css-hsl"},{re:dt,format:"css-rgba"},{re:ft,format:"css-hsla"}];function St(t){switch(typeof t){case"number":return console.warn('can not reliably guess format based on a number. You should pass in a format like {format: "uint32-rgb"} or {format: "uint32-rgb"}'),t<=16777215?"uint32-rgb":"uint32-rgba";case"string":{const e=function(t){for(const e of Mt)if(e.re.test(t))return e}(t.trim());if(e)return e.format;break}case"object":if(t instanceof Uint8Array||t instanceof Uint8ClampedArray){if(3===t.length)return"uint8-rgb";if(4===t.length)return"uint8-rgba"}else if(t instanceof Float32Array){if(3===t.length)return"float-rgb";if(4===t.length)return"float-rgba"}else if(Array.isArray(t)){if(3===t.length)return"float-rgb";if(4===t.length)return"float-rgba"}else if("r"in t&&"g"in t&&"b"in t)return"a"in t?"object-rgba":"object-rgb"}throw new Error(`unknown color format: ${t}`)}function Dt(t){return t.trim(t)}function Nt(t){return t.trim(t)}function Ft(t){return t[1]===t[2]&&t[3]===t[4]&&t[5]===t[6]?`#${t[1]}${t[3]}${t[5]}`:t}const At=/^(#|)([0-9a-f]{3})$/i;function Ut(t){const e=At.exec(t);if(e){const[,,t]=e;return"#"+`${(n=t)[0]}${n[0]}${n[1]}${n[1]}${n[2]}${n[2]}`}var n;return t}function Lt(t){return Ft(Dt(t))}const Ot=t=>{const e=ct.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),`rgb(${n.join(", ")})`]},jt=t=>{const e=dt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map(((t,e)=>3===e?parseFloat(t):parseInt(t)));return[!n.find((t=>t>255)),`rgba(${n.join(", ")})`]},Tt=t=>{const e=gt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseFloat(t)));return[!n.find((t=>Number.isNaN(t))),`hsl(${n[0]}, ${n[1]}%, ${n[2]}%)`]},Ht=t=>{const e=ft.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map((t=>parseFloat(t)));return[!n.find((t=>Number.isNaN(t))),`hsl(${n[0]} ${n[1]}% ${n[2]}% / ${n[3]})`]},zt=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/,Pt=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/,Bt=/^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i,Gt=/^\s*(?:0x){0,1}([0-9a-z]{1,8})\s*$/i,Rt=/^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i,_t=/^\s*[a-f0-9]{6}\s*$/i,Yt=/^\s*#[a-f0-9]{8}\s*$/i,Wt=/^\s*[a-f0-9]{8}\s*$/i,qt={hex6:{color:{from:t=>[!0,t],to:Dt},text:{from:t=>[Rt.test(t),t.trim()],to:t=>t}},hex8:{color:{from:t=>[!0,t],to:Nt},text:{from:t=>[Yt.test(t),t.trim()],to:t=>t}},hex3:{color:{from:t=>[!0,Lt(t)],to:Ut},text:{from:t=>[Rt.test(t),Ft(t.trim())],to:t=>t}},"hex6-no-hash":{color:{from:t=>[!0,t.substring(1)],to:t=>`#${Dt(t)}`},text:{from:t=>[_t.test(t),t.trim()],to:t=>t}},"hex8-no-hash":{color:{from:t=>[!0,t.substring(1)],to:t=>`#${Nt(t)}`},text:{from:t=>[Wt.test(t),t.trim()],to:t=>t}},"hex3-no-hash":{color:{from:t=>[!0,Lt(t).substring(1)],to:Ut},text:{from:t=>[_t.test(t),Ft(t.trim())],to:t=>t}},"uint32-rgb":{color:{from:t=>[!0,J(t)],to:t=>`#${Math.round(t).toString(16).padStart(6,"0")}`},text:{from:t=>(t=>{const e=Bt.exec(t);return e?[!0,parseInt(e[1],16)]:[!1]})(t),to:t=>`0x${t.toString(16).padStart(6,"0")}`}},"uint32-rgba":{color:{from:t=>[!0,X(t)],to:t=>`#${Math.round(t).toString(16).padStart(8,"0")}`},text:{from:t=>(t=>{const e=Gt.exec(t);return e?[!0,parseInt(e[1],16)]:[!1]})(t),to:t=>`0x${t.toString(16).padStart(8,"0")}`}},"uint8-rgb":{color:{from:t=>[!0,Z(t)],to:Q},text:{from:t=>{const e=zt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),n]},to:t=>t.join(", ")}},"uint8-rgba":{color:{from:t=>[!0,tt(t)],to:et},text:{from:t=>{const e=Pt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),n]},to:t=>t.join(", ")}},"float-rgb":{color:{from:t=>[!0,nt(t)],to:it},text:{from:t=>{const e=t.split(",").map((t=>t.trim())),n=e.map((t=>parseFloat(t)));if(3!==n.length)return[!1];const i=e.findIndex((t=>isNaN(t)));return[i<0,n.map((t=>K(t)))]},to:t=>Array.from(t).map((t=>K(t))).join(", ")}},"float-rgba":{color:{from:t=>[!0,ot(t)],to:rt},text:{from:t=>{const e=t.split(",").map((t=>t.trim())),n=e.map((t=>parseFloat(t)));if(4!==n.length)return[!1];const i=e.findIndex((t=>isNaN(t)));return[i<0,n.map((t=>K(t)))]},to:t=>Array.from(t).map((t=>K(t))).join(", ")}},"object-rgb":{color:{from:t=>[!0,at(t)],to:t=>`#${st(t.r)}${st(t.g)}${st(t.b)}`},text:{from:t=>{try{const e=t.replace(/([a-z])/g,'"$1"'),n=JSON.parse(e);if(Number.isNaN(n.r)||Number.isNaN(n.g)||Number.isNaN(n.b))throw new Error("not {r, g, b}");return[!0,n]}catch(t){return[!1]}},to:t=>`{r:${K(t.r)}, g:${K(t.g)}, b:${K(t.b)}}`}},"object-rgba":{color:{from:t=>[!0,lt(t)],to:t=>`#${st(t.r)}${st(t.g)}${st(t.b)}${st(t.a)}`},text:{from:t=>{try{const e=t.replace(/([a-z])/g,'"$1"'),n=JSON.parse(e);if(Number.isNaN(n.r)||Number.isNaN(n.g)||Number.isNaN(n.b)||Number.isNaN(n.a))throw new Error("not {r, g, b, a}");return[!0,n]}catch(t){return[!1]}},to:t=>`{r:${K(t.r)}, g:${K(t.g)}, b:${K(t.b)}}, a:${K(t.a)}}`}},"css-rgb":{color:{from:t=>[!0,ut(t)],to:t=>{const e=ct.exec(t);return Q([e[1],e[2],e[3]].map((t=>parseInt(t))))}},text:{from:Ot,to:t=>Ot(t)[1]}},"css-rgba":{color:{from:t=>[!0,ht(t)],to:t=>{const e=dt.exec(t);return et([e[1],e[2],e[3],e[4]].map(((t,e)=>3===e?255*parseFloat(t)|0:parseInt(t))))}},text:{from:jt,to:t=>jt(t)[1]}},"css-hsl":{color:{from:t=>[!0,pt(t)],to:t=>{const e=gt.exec(t),n=vt([e[1],e[2],e[3]].map((t=>parseFloat(t))));return Q(n)}},text:{from:Tt,to:t=>Tt(t)[1]}},"css-hsla":{color:{from:t=>[!0,mt(t)],to:t=>{const e=ft.exec(t),n=function([t,e,n,i]){return[...vt([t,e,n]),255*i|0]}([e[1],e[2],e[3],e[4]].map((t=>parseFloat(t))));return et(n)}},text:{from:Ht,to:t=>Ht(t)[1]}}};class Kt extends m{constructor(t,n){super(e(t,{className:n}))}}class Jt extends S{#N;constructor(){super("muigui-canvas"),this.#N=this.add(new Kt("canvas","muigui-canvas")).domElement}get canvas(){return this.#N}}class Xt extends v{#$;#C;#F;#I;#u={converters:F};constructor(t,n){const i=e("input",{type:"color",onInput:()=>{const[e,n]=this.#C(i.value);e&&(this.#I=!0,t.setValue(n))},onChange:()=>{const[e,n]=this.#C(i.value);e&&(this.#I=!0,t.setFinalValue(n))}});super(e("div",{},[i])),this.setOptions(n),this.#F=i}updateDisplay(t){this.#I||(this.#F.value=this.#$(t)),this.#I=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n}}=this.#u;return this.#$=e,this.#C=n,this}}class Zt extends D{#A;#S;constructor(t,e,n={}){super(t,e,"muigui-color");const i=n.format||St(this.getValue()),{color:o,text:r}=qt[i];this.#A=this.add(new Xt(this,{converters:o})),this.#S=this.add(new G(this,{converters:r})),this.updateDisplay()}setOptions(t){const{format:e}=t;if(e){const{color:t,text:n}=qt[e];this.#A.setOptions({converters:t}),this.#S.setOptions({converters:n})}return super.setOptions(t),this}}class Qt extends g{constructor(){super("muigui-divider")}}class te extends g{#U;#L;constructor(t){super(t),this.#U=[],this.#L=this}get children(){return this.#U}get controllers(){return this.#U.filter((t=>!(t instanceof te)))}get folders(){return this.#U.filter((t=>t instanceof te))}reset(t=!0){for(const e of this.#U)e instanceof te&&!t||e.reset(t);return this}updateDisplay(){for(const t of this.#U)t.updateDisplay();return this}remove(t){const e=this.#U.indexOf(t);if(e>=0){const t=this.#U.splice(e,1)[0];t.domElement.remove(),t.setParent(null)}return this}_addControllerImpl(t){return this.domElement.appendChild(t.domElement),this.#U.push(t),t.setParent(this),t}addController(t){return this.#L._addControllerImpl(t)}pushContainer(t){return this.addController(t),this.#L=t,t}popContainer(){return this.#L=this.#L.parent,this}}class ee extends te{#O;constructor(t="Controls",n="muigui-menu"){super(n),this.#O=e("label"),this.addElem(e("button",{type:"button",onClick:()=>this.toggleOpen()},[this.#O])),this.pushContainer(new te),this.name(t),this.open()}open(t=!0){return this.domElement.classList.toggle("muigui-closed",!t),this.domElement.classList.toggle("muigui-open",t),this}close(){return this.open(!1)}name(t){return this.#O.textContent=t,this}title(t){return this.name(t)}toggleOpen(){return this.open(!this.domElement.classList.contains("muigui-open")),this}}class ne extends g{constructor(t){super("muigui-label"),this.text(t)}text(t){return this.domElement.textContent=t,this}}function ie(){}function oe(t,e,n){const i=t.getBoundingClientRect(),o=e.clientX-i.left,r=e.clientY-i.top,s=o/i.width,a=r/i.height,l=o-(n=n||[o,r])[0],u=r-n[1];return{x:o,y:r,nx:s,ny:a,dx:l,dy:u,ndx:l/i.width,ndy:u/i.width}}function re(t,{onDown:e=ie,onMove:n=ie,onUp:i=ie}){let o;const r=function(e){const i={type:"move",...oe(t,e,o)};n(i)},s=function(e){t.releasePointerCapture(e.pointerId),t.removeEventListener("pointermove",r),t.removeEventListener("pointerup",s),document.body.style.backgroundColor="",i("up")},a=function(n){t.addEventListener("pointermove",r),t.addEventListener("pointerup",s),t.setPointerCapture(n.pointerId);const i=oe(t,n);o=[i.x,i.y],e({type:"down",...i})};return t.addEventListener("pointerdown",a),function(){t.removeEventListener("pointerdown",a)}}function se(t){return t.querySelectorAll("[data-src]").forEach((e=>{const i="muigui-id-"+n++;e.id=i,t.querySelectorAll(`[data-target=${e.dataset.src}]`).forEach((t=>{t.setAttribute("fill",`url(#${i})`)}))})),t}class ae extends v{#$;#C;#j;#T;#H;#z;#P;#B;#G;#R;#_;#Y;#W;#q;#u={converters:F,alpha:!1};#K;#J;constructor(t,n){super(e("div",{innerHTML:'\n\n \n \n \n \n \n \n \n \n\n \n \n \n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n',className:"muigui-no-scroll"})),this.#j=this.domElement.children[0],this.#H=this.domElement.children[1],this.#B=this.domElement.children[2],se(this.#j),se(this.#H),se(this.#B),this.#T=this.$(".muigui-color-chooser-circle"),this.#z=this.$("[data-src=muigui-color-chooser-hue]"),this.#P=this.$(".muigui-color-chooser-hue-cursor"),this.#G=this.$("[data-src=muigui-color-chooser-alpha]"),this.#R=this.$(".muigui-color-chooser-alpha-cursor");const i=e=>{const n=o(e.nx,0,1),i=o(e.ny,0,1);this.#_[1]=n,this.#_[2]=1-i,this.#Y=!0,this.#q=!0;const[r,s]=this.#C(this.#K(this.#_));r&&t.setValue(s)},r=e=>{const n=o(e.nx,0,1);this.#_[0]=n,this.#W=!0,this.#q=!0;const[i,r]=this.#C(this.#K(this.#_));i&&t.setValue(r)},s=e=>{const n=o(e.nx,0,1);this.#_[3]=n,this.#Y=!0,this.#W=!0;const[i,r]=this.#C(this.#K(this.#_));i&&t.setValue(r)};re(this.#j,{onDown:i,onMove:i}),re(this.#H,{onDown:r,onMove:r}),re(this.#B,{onDown:s,onMove:s}),this.setOptions(n)}updateDisplay(t){this.#_||(this.#_=this.#J(this.#$(t)));{const[e,n,i,o=1]=this.#J(this.#$(t));this.#Y||(this.#_[0]=n>.001&&i>.001?e:this.#_[0]),this.#W||(this.#_[1]=n,this.#_[2]=i),this.#q||(this.#_[3]=o)}{const[t,e,n,i]=this.#_,[o,r,s]=wt($t(this.#_));this.#Y||this.#P.setAttribute("transform",`translate(${64*t}, 0)`),this.#z.children[0].setAttribute("stop-color",`hsl(${360*o} 0% 100% / ${i})`),this.#z.children[1].setAttribute("stop-color",`hsl(${360*o} 100% 50% / ${i})`),this.#q||this.#R.setAttribute("transform",`translate(${64*i}, 0)`),this.#G.children[0].setAttribute("stop-color",`hsl(${360*o} ${100*r}% ${100*s}% / 0)`),this.#G.children[1].setAttribute("stop-color",`hsl(${360*o} ${100*r}% ${100*s}% / 1)`),this.#W||(this.#T.setAttribute("cx",""+64*e),this.#T.setAttribute("cy",""+48*(1-n)))}this.#Y=!1,this.#W=!1,this.#q=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n},alpha:i}=this.#u;return this.#B.style.display=i?"":"none",this.#K=i?t=>rt($t(t)):t=>it(Et(t)),this.#J=i?t=>function([t,e,n,i]){return[...Vt([t,e,n]),i]}(ot(t)):t=>Vt(nt(t)),this.#$=e,this.#C=n,this}}class le extends D{#X;#Z;#b;#Q;#u={open:!1};constructor(t,n,i={}){super(t,n,"muigui-pop-down-controller"),this.#X=this.add(new Kt("div","muigui-pop-down-top"));const o=this.#X.addElem(e("input",{type:"checkbox",onChange:()=>{this.#u.open=o.checked,this.updateDisplay()}}));this.#b=o,this.#Z=this.#X.add(new Kt("div","muigui-pop-down-values")),this.#Q=this.add(new Kt("div","muigui-pop-down-bottom")),this.setOptions(i)}setKnobColor(t){this.#b&&(this.#b.style=`\n --range-color: ${t};\n --value-bg-color: ${t};\n `)}updateDisplay(){super.updateDisplay();const{open:t}=this.#u;this.domElement.children[1].classList.toggle("muigui-open",t),this.domElement.children[1].classList.toggle("muigui-closed",!t)}setOptions(t){l(this.#u,t),super.setOptions(t),this.updateDisplay()}addTop(t){return this.#Z.add(t)}addBottom(t){return this.#Q.add(t)}}class ue extends le{#A;#S;#$;constructor(t,e,n={}){super(t,e,"muigui-color-chooser");const i=n.format||St(this.getValue()),{color:o,text:r}=qt[i];this.#$=o.to,this.#S=new G(this,{converters:r,alpha:It(i)}),this.#A=new ae(this,{converters:o,alpha:It(i)}),this.addTop(this.#S),this.addBottom(this.#A),this.__setKnobHelper=()=>{if(this.#$){const t=this.#$(this.getValue()),e=yt(Z(t));e[2]=(e[2]+50)%100;const n=Q(vt(e));this.setKnobColor(`${t.substring(0,7)}FF`,n)}},this.updateDisplay()}updateDisplay(){super.updateDisplay(),this.__setKnobHelper&&this.__setKnobHelper()}setOptions(t){return super.setOptions(t),this}}class ce extends ee{add(t,e,...n){const i=t instanceof g?t:function(t,e,...n){const[i]=n;if(Array.isArray(i))return new z(t,e,{keyValues:i});const o=typeof t[e];switch(o){case"number":if("number"==typeof n[0]&&"number"==typeof n[1]){const i=n[0],o=n[1],r=n[2];return new B(t,e,{min:i,max:o,...r&&{step:r}})}return 0===n.length?new j(t,e,...n):new B(t,e,...n);case"boolean":return new N(t,e,...n);case"function":return new f(t,e,...n);case"string":return new R(t,e,...n);case"undefined":throw new Error(`no property named ${e}`);default:throw new Error(`unhandled type ${o} for property ${e}`)}}(t,e,...n);return this.addController(i)}addCanvas(t){return this.addController(new Jt(t))}addColor(t,e,n={}){const i=t[e];return It(n.format||St(i))?this.addController(new ue(t,e,n)):this.addController(new Zt(t,e,n))}addDivider(){return this.addController(new Qt)}addFolder(t){return this.addController(new ce(t))}addLabel(t){return this.addController(new ne(t))}}class he extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"})}}customElements.define("muigui-element",he);const de=new CSSStyleSheet;de.replaceSync(t.default);const pe=new CSSStyleSheet;function me(t){let e,n;function i(){if(e&&!n){const o=e;e=void 0,n=t.replace(o).then((()=>{n=void 0,i()}))}}return function(t){e=t,i()}}const ge=me(de),fe=me(pe);class be extends ce{static converters=U;static mapRange=u;static makeRangeConverters=c;static makeRangeOptions=h;static makeMinMaxPair=p;#tt=new CSSStyleSheet;constructor(t={}){super("Controls","muigui-root"),t instanceof HTMLElement&&(t={parent:t});const{autoPlace:n=!0,width:i,title:o="Controls"}=t;let{parent:r}=t;if(i&&(this.domElement.style.width=/^\d+$/.test(i)?`${i}px`:i),void 0===r&&n&&(r=document.body,this.domElement.classList.add("muigui-auto-place")),r){const t=e("muigui-element");t.shadowRoot.adoptedStyleSheets=[de,pe,this.#tt],t.shadow.appendChild(this.domElement),r.appendChild(t)}o&&this.title(o),this.domElement.classList.add("muigui","muigui-colors")}setStyle(t){this.#tt.replace(t)}static setBaseStyles(t){ge(t)}static getBaseStyleSheet(){return de}static setUserStyles(t){fe(t)}static getUserStyleSheet(){return pe}static setTheme(e){be.setBaseStyles(`${t.default}\n${t.themes[e]||""}`)}}function ve(){}const xe={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]};function we(t,{onDown:e=ve,onUp:n=ve}){const i=function(t){const i=t.shiftKey?10:1,[o,r]=(xe[t.key]||[0,0]).map((t=>t*i));("keydown"===t.type?e:n)({type:t.type.substring(3),dx:o,dy:r,event:t})};return t.addEventListener("keydown",i),t.addEventListener("keyup",i),function(){t.removeEventListener("keydown",i),t.removeEventListener("keyup",i)}}function ye(t,e=""){if(!t)throw new Error(e)}function ke(t,e,n,i,o,r){const s=Math.abs(n)*Math.cos(r),a=Math.abs(i)*Math.sin(r);return[t+Math.cos(o)*s-Math.sin(o)*a,e+Math.sin(o)*s+Math.cos(o)*a]}function Ee(t,e,n,i,o){ye(Math.abs(i-o)<=2*Math.PI),ye(i>=-Math.PI&&i<=2*Math.PI),ye(i<=o),ye(o>=-Math.PI&&o<=4*Math.PI);const{x1:r,y1:s,x2:a,y2:l,fa:u,fs:c}=function(t,e,n,i,o,r,s){const[a,l]=ke(t,e,n,i,o,r),[u,c]=ke(t,e,n,i,o,r+s);return{x1:a,y1:l,x2:u,y2:c,fa:Math.abs(s)>Math.PI?1:0,fs:s>0?1:0}}(t,e,n,n,0,i,o-i);return Math.abs(Math.abs(i-o)-2*Math.PI)>Number.EPSILON?`M${t} ${e} L${r} ${s} A ${n} ${n} 0 ${u} ${c} ${a} ${l} L${t} ${e}`:`M${r} ${s} L${r} ${s} A ${n} ${n} 0 ${u} ${c} ${a} ${l}`}const $e=t=>a(t+Math.PI,2*Math.PI)-Math.PI;class Ce extends v{#et;#nt;#it;#ot;#u={step:1,min:-180,max:180,dirMin:-Math.PI,dirMax:Math.PI,wrap:void 0,converters:F};constructor(t,n={}){const i=L();super(e("div",{className:"muigui-direction muigui-no-scroll",innerHTML:'\n\n \x3c!----\x3e\n \n \n \n \n \n \n\n',onWheel:e=>{e.preventDefault();const{min:n,max:r,step:l}=this.#u,u=i(e,l);let c=this.#it+u;this.#ot&&(c=a(c-n,r-n)+n);const h=o(s(c,(t=>t),l),n,r);t.setValue(h)}}));const r=e=>{const{min:n,max:i,step:r,dirMin:a,dirMax:l}=this.#u,u=2*e.nx-1,c=2*e.ny-1,h=Math.atan2(c,u),d=(a+l)/2,p=o(($e(h-d)-$e(a-d))/(l-a),0,1),m=s(n+(i-n)*p,(t=>t),r);t.setValue(m)};re(this.domElement,{onDown:r,onMove:r}),we(this.domElement,{onDown:e=>{const{min:n,max:i,step:r}=this.#u,a=o(s(this.#it+e.dx*r,(t=>t),r),n,i);t.setValue(a)}}),this.#et=this.$("#muigui-arrow"),this.#nt=this.$("#muigui-range"),this.setOptions(n)}updateDisplay(t){this.#it=t;const{min:e,max:n}=this.#u,i=(t-e)/(n-e),o=(r=this.#u.dirMin,s=this.#u.dirMax,r+(s-r)*i);var r,s;this.#et.style.transform=`rotate(${o}rad)`}setOptions(t){l(this.#u,t);const{dirMin:e,dirMax:n,wrap:i}=this.#u;this.#ot=void 0!==i?i:Math.abs(e-n)>=2*Math.PI-Number.EPSILON;const[o,r]=e(o.push(i),e("label",{},[e("input",{type:"radio",name:r,value:a,onChange:function(){this.checked&&t.setFinalValue(s.#D[this.value])}}),e("button",{type:"button",textContent:n,onClick:function(){this.previousElementSibling.click()}})]))))));const s=this;this.#D=o,this.cols(i)}updateDisplay(t){const e=this.#D.indexOf(t);for(let t=0;t{e({rect:t.getBoundingClientRect(),elem:t})})).observe(t)}function Me(t,e,n,i){Ie(t,(({rect:o})=>{const{width:r,height:s}=o;t.setAttribute("viewBox",`-${r*e} -${s*n} ${r} ${s}`),i({elem:t,rect:o})}))}function Se(t,e,n,i,o,r){const a=[];tt),n)),e=Math.min(e,o);for(let i=t;i<=e;i+=n)a.push(`M${i} 0 l0 ${r}`);return a.join(" ")}class De extends v{#rt;#st;#at;#lt;#ut;#ct;#ht;#dt;#pt;#it;#mt;#u={min:-100,max:100,step:1,unit:10,unitSize:10,ticksPerUnit:5,labelFn:t=>t,tickHeight:1,limits:!0,thicksColor:void 0,orientation:void 0};constructor(t,n){const i=L();let r;super(e("div",{innerHTML:'\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \x3c!----\x3e\n \x3c!----\x3e\n \n \n \n \n\n',className:"muigui-no-v-scroll",onWheel:e=>{e.preventDefault();const{min:n,max:r,step:a}=this.#u,l=i(e,a),u=o(s(this.#it+l,(t=>t),a),n,r);t.setValue(u)}})),this.#rt=this.$("svg"),this.#st=this.$("#muigui-origin"),this.#at=this.$("#muigui-ticks"),this.#lt=this.$("#muigui-thicks"),this.#ut=this.$("#muigui-numbers"),this.#ct=this.$("#muigui-left-grad"),this.#ht=this.$("#muigui-right-grad"),this.setOptions(n),re(this.domElement,{onDown:()=>{r=this.#it},onMove:e=>{const{min:n,max:i,unitSize:a,unit:l,step:u}=this.#u,c=o(s(r-e.dx/a*l,(t=>t),u),n,i);t.setValue(c)}}),we(this.domElement,{onDown:e=>{const{min:n,max:i,step:r}=this.#u,a=o(s(this.#it+e.dx*r,(t=>t),r),n,i);t.setValue(a)}}),Me(this.#rt,.5,0,(({rect:{width:t}})=>{this.#ct.setAttribute("x",-t/2),this.#ht.setAttribute("x",t/2-20),this.#mt=function(t){const e=t.innerHTML;t.innerHTML="- ";const n=t.querySelector("text").getComputedTextLength();return t.innerHTML=e,n}(this.#ut),this.#dt=t,this.#gt()}))}#gt(){if(!this.#dt||void 0===this.#it)return;const{labelFn:t,limits:e,min:n,max:i,orientation:o,tickHeight:r,ticksPerUnit:a,unit:l,unitSize:u,thicksColor:c}=this.#u,h=Math.ceil(this.#dt/u),d=this.#it/l,p=Math.round(d-h),m=p*u,g=(p+2*h)*u,f=e?n*u/l:m,b=e?i*u/l:g,v=""===t(1)?10:5;a>1&&this.#at.setAttribute("d",Se(m,g,u/a,f,b,v*r)),this.#lt.style.stroke=c,this.#lt.setAttribute("d",Se(m,g,u,f,b,v)),this.#ut.innerHTML=function(t,e,n,i,o,r,a,l){const u=[];tt),n)),e=Math.min(e,a);const c=Math.max(0,-Math.log10(i));for(let r=t;r<=e;r+=n)u.push(`${h=r/n*i,l(h.toFixed(c))}`);var h;return u.join("\n")}(m,g,u,l,this.#mt,f,b,t),this.#st.setAttribute("transform",`translate(${-this.#it*u/l} 0)`),this.#rt.classList.toggle("muigui-slider-up","up"===o)}updateDisplay(t){this.#it=t,this.#gt()}setOptions(t){return l(this.#u,t),this}}class Ne extends v{#rt;#et;#T;#it=[];constructor(t){super(e("div",{innerHTML:'\n\n \n \n \n \n \n\n',className:"muigui-no-scroll"}));const n=e=>{const{width:n,height:i}=this.#rt.getBoundingClientRect(),o=2*e.nx-1,r=2*e.ny-1;t.setValue([o*n*.5,r*i*.5])};re(this.domElement,{onDown:n,onMove:n}),this.#rt=this.$("svg"),this.#et=this.$("#muigui-arrow"),this.#T=this.$("#muigui-circle"),Me(this.#rt,.5,.5,(()=>this.#ft))}#ft(){const[t,e]=this.#it;this.#et.setAttribute("d",`M0,0L${t},${e}`),this.#T.setAttribute("transform",`translate(${t}, ${e})`)}updateDisplay(t){this.#it[0]=t[0],this.#it[1]=t[1],this.#ft()}}return be.ColorChooser=ue,be.Direction=class extends le{#u;constructor(t,e,n){super(t,e,"muigui-direction"),this.#u=n,this.addTop(new O(this,F)),this.addBottom(new Ce(this,n)),this.updateDisplay()}},be.RadioGrid=class extends D{constructor(t,e,n){super(t,e,"muigui-radio-grid");const i="number"==typeof this.getValue(),{keyValues:o,cols:r=3}=n,s=H(o,i);this.add(new Ve(this,s,r)),this.updateDisplay()}},be.Range=B,be.Select=z,be.Slider=class extends D{constructor(t,e,n={}){super(t,e,"muigui-slider"),this.add(new De(this,n)),this.add(new O(this,n)),this.updateDisplay()}},be.TextNumber=j,be.Vec2=class extends le{constructor(t,e){super(t,e,"muigui-vec2");const n=t=>({setValue:e=>{const n=this.getValue();n[t]=e,this.setValue(n)},setFinalValue:e=>{const n=this.getValue();n[t]=e,this.setFinalValue(n)}});this.addTop(new O(n(0),{converters:{to:t=>t[0],from:A.from}})),this.addTop(new O(n(1),{converters:{to:t=>t[1],from:A.from}})),this.addBottom(new Ne(this)),this.updateDisplay()}},be})); -//# sourceMappingURL=muigui.min.js.map diff --git a/dist/0.x/muigui.module.js b/dist/0.x/muigui.module.js deleted file mode 100644 index e0992db..0000000 --- a/dist/0.x/muigui.module.js +++ /dev/null @@ -1,3814 +0,0 @@ -/* muigui@0.0.10, license MIT */ -var css = { - default: ` -.muigui { - --bg-color: #ddd; - --color: #222; - --contrast-color: #eee; - --value-color: #145 ; - --value-bg-color: #eeee; - --disabled-color: #999; - --menu-bg-color: #f8f8f8; - --menu-sep-color: #bbb; - --hover-bg-color: #999; - --focus-color: #68C; - --range-color: #888888; - --invalid-color: #FF0000; - --selected-color: rgb(255, 255, 255, 0.9); - - --button-bg-color: var(--value-bg-color); - - --range-left-color: var(--value-color); - --range-right-color: var(--value-bg-color); - --range-right-hover-color: var(--hover-bg-color); - - color: var(--color); - background-color: var(--bg-color); -} - -@media (prefers-color-scheme: dark) { - .muigui { - --bg-color: #222222; - --color: #dddddd; - --contrast-color: #000; - --value-color: #43e5f7; - --value-bg-color: #444444; - --disabled-color: #666666; - --menu-bg-color: #080808; - --menu-sep-color: #444444; - --hover-bg-color: #666666; - --focus-color: #88AAFF; - --range-color: #888888; - --invalid-color: #FF6666; - --selected-color: rgba(255, 255, 255, 0.3); - - --button-bg-color: var(--value-bg-color); - - --range-left-color: var(--value-color); - --range-right-color: var(--value-bg-color); - --range-right-hover-color: var(--hover-bg-color); - - color: var(--color); - background-color: var(--bg-color); - } -} - -.muigui { - --width: 250px; - --label-width: 45%; - --number-width: 40%; - - - --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; - --font-size: 11px; - --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace; - --font-size-mono: 11px; - - --line-height: 1.7em; - --border-radius: 0px; - - width: var(--width); - font-family: var(--font-family); - font-size: var(--font-size); - box-sizing: border-box; - line-height: 100%; -} -.muigui * { - box-sizing: inherit; -} - -.muigui-no-scroll { - touch-action: none; -} -.muigui-no-h-scroll { - touch-action: pan-y; -} -.muigui-no-v-scroll { - touch-action: pan-x; -} - -.muigui-invalid-value { - background-color: red !important; - color: white !important; -} - -.muigui-grid { - display: grid; -} -.muigui-rows { - display: flex; - flex-direction: column; - - min-height: 20px; - border: 2px solid red; -} -.muigui-columns { - display: flex; - flex-direction: row; - - height: 20px; - border: 2px solid green; -} -.muigui-rows>*, -.muigui-columns>* { - flex: 1 1 auto; - align-items: stretch; - min-height: 0; - min-width: 0; -} - -.muigui-row { - border: 2px solid yellow; - min-height: 10px -} -.muigui-column { - border: 2px solid lightgreen; -} - -/* -------- */ - -.muigui-show { /* */ } -.muigui-hide { - display: none !important; -} -.muigui-disabled { - pointer-events: none; - --color: var(--disabled-color) !important; - --value-color: var(--disabled-color) !important; - --range-left-color: var(--disabled-color) !important; -} - -.muigui canvas, -.muigui svg { - display: block; - border-radius: var(--border-radius); -} -.muigui canvas { - background-color: var(--value-bg-color); -} - -.muigui-controller { - min-width: 0; - min-height: var(--line-height); -} -.muigui-root, -.muigui-menu { - display: flex; - flex-direction: column; - position: relative; - user-select: none; - height: fit-content; - margin: 0; - padding-bottom: 0.1em; - border-radius: var(--border-radius); -} -.muigui-menu { - border-bottom: 1px solid var(--menu-sep-color); -} - -.muigui-root>button:nth-child(1), -.muigui-menu>button:nth-child(1) { - border-top: 1px solid var(--menu-sep-color); - border-bottom: 1px solid var(--menu-sep-color); - position: relative; - text-align: left; - color: var(--color); - background-color: var(--menu-bg-color); - min-height: var(--line-height); - padding-top: 0.2em; - padding-bottom: 0.2em; - cursor: pointer; - border-radius: var(--border-radius); -} -.muigui-root>div:nth-child(2), -.muigui-menu>div:nth-child(2) { - flex: 1 1 auto; -} - -.muigui-controller { - margin-left: 0.2em; - margin-right: 0.2em; -} -.muigui-root.muigui-controller, -.muigui-menu.muigui-controller { - margin-left: 0; - margin-right: 0; -} -.muigui-controller>*:nth-child(1) { - flex: 1 0 var(--label-width); - min-width: 0; - white-space: pre; -} -.muigui-controller>label:nth-child(1) { - place-content: center start; - display: inline-grid; - overflow: hidden; -} -.muigui-controller>*:nth-child(2) { - flex: 1 1 75%; - min-width: 0; -} - -/* ----------------------------------------- - a label controller is [[label][value]] -*/ - -.muigui-label-controller { - display: flex; - margin: 0.4em 0 0.4em 0; - word-wrap: initial; - align-items: stretch; -} - -.muigui-value { - display: flex; - align-items: stretch; -} -.muigui-value>* { - flex: 1 1 auto; - min-width: 0; -} -.muigui-value>*:nth-child(1) { - flex: 1 1 calc(100% - var(--number-width)); -} -.muigui-value>*:nth-child(2) { - flex: 1 1 var(--number-width); - margin-left: 0.2em; -} - -/* fix! */ -.muigui-open>button>label::before, -.muigui-closed>button>label::before { - width: 1.25em; - height: var(--line-height); - display: inline-grid; - place-content: center start; - pointer-events: none; -} -.muigui-open>button>label::before { - content: "โ“ง"; /*"โ–ผ";*/ -} -.muigui-closed>button>label::before { - content: "โจ"; /*"โ–ถ";*/ -} -.muigui-open>*:nth-child(2) { - transition: max-height 0.2s ease-out, - opacity 0.5s ease-out; - max-height: 100vh; - overflow: auto; - opacity: 1; -} - -.muigui-closed>*:nth-child(2) { - transition: max-height 0.2s ease-out, - opacity 1s; - max-height: 0; - opacity: 0; - overflow: hidden; -} - -/* ---- popdown ---- */ - -.muigui-pop-down-top { - display: flex; -} -/* fix? */ -.muigui-value>*:nth-child(1).muigui-pop-down-top { - flex: 0; -} -.muigui-pop-down-bottom { - -} - -.muigui-pop-down-values { - min-width: 0; - display: flex; -} -.muigui-pop-down-values>* { - flex: 1 1 auto; - min-width: 0; -} - -.muigui-value.muigui-pop-down-controller { - flex-direction: column; -} - -.muigui-pop-down-top input[type=checkbox] { - -webkit-appearance: none; - appearance: none; - width: auto; - color: var(--value-color); - background-color: var(--value-bg-color); - cursor: pointer; - - display: grid; - place-content: center; - margin: 0; - font: inherit; - color: currentColor; - width: 1.7em; - height: 1.7em; - transform: translateY(-0.075em); -} - -.muigui-pop-down-top input[type=checkbox]::before { - content: "+"; - display: grid; - place-content: center; - border-radius: calc(var(--border-radius) + 2px); - border-left: 1px solid rgba(255,255,255,0.3); - border-top: 1px solid rgba(255,255,255,0.3); - border-bottom: 1px solid rgba(0,0,0,0.2); - border-right: 1px solid rgba(0,0,0,0.2); - background-color: var(--range-color); - color: var(--value-bg-color); - width: calc(var(--line-height) - 4px); - height: calc(var(--line-height) - 4px); -} - -.muigui-pop-down-top input[type=checkbox]:checked::before { - content: "๏ผธ"; -} - - -/* ---- select ---- */ - -.muigui select, -.muigui option, -.muigui input, -.muigui button { - color: var(--value-color); - background-color: var(--value-bg-color); - font-family: var(--font-family); - font-size: var(--font-size); - border: none; - margin: 0; - border-radius: var(--border-radius); -} -.muigui select { - appearance: none; - margin: 0; - margin-left: 0; /*?*/ - overflow: hidden; /* Safari */ -} - -.muigui select:focus, -.muigui input:focus, -.muigui button:focus { - outline: 1px solid var(--focus-color); -} - -.muigui select:hover, -.muigui option:hover, -.muigui input:hover, -.muigui button:hover { - background-color: var(--hover-bg-color); -} - -/* ------ [ label ] ------ */ - -.muigui-label { - border-top: 1px solid var(--menu-sep-color); - border-bottom: 1px solid var(--menu-sep-color); - padding-top: 0.4em; - padding-bottom: 0.3em; - place-content: center start; - background-color: var(--menu-bg-color); - white-space: pre; - border-radius: var(--border-radius); -} - -/* ------ [ divider] ------ */ - -.muigui-divider { - min-height: 6px; - border-top: 2px solid var(--menu-sep-color); - margin-top: 6px; -} - -/* ------ [ button ] ------ */ - -.muigui-button { - display: grid; - -} -.muigui-button button { - border: none; - color: var(--value-color); - background-color: var(--button-bg-color); - cursor: pointer; - place-content: center center; -} - -/* ------ [ color ] ------ */ - -.muigui-color>div { - overflow: hidden; - position: relative; - margin-left: 0; - margin-right: 0; /* why? */ - max-width: var(--line-height); - border-radius: var(--border-radius); -} - -.muigui-color>div:focus-within { - outline: 1px solid var(--focus-color); -} - -.muigui-color input[type=color] { - border: none; - padding: 0; - background: inherit; - cursor: pointer; - position: absolute; - width: 200%; - left: -10px; - top: -10px; - height: 200%; -} -.muigui-disabled canvas, -.muigui-disabled svg, -.muigui-disabled img, -.muigui-disabled .muigui-color input[type=color] { - opacity: 0.2; -} - -/* ------ [ checkbox ] ------ */ - -.muigui-checkbox>label:nth-child(2) { - display: grid; - place-content: center start; - margin: 0; -} - -.muigui-checkbox input[type=checkbox] { - -webkit-appearance: none; - appearance: none; - width: auto; - color: var(--value-color); - background-color: var(--value-bg-color); - cursor: pointer; - - display: grid; - place-content: center; - margin: 0; - font: inherit; - color: currentColor; - width: 1.7em; - height: 1.7em; - transform: translateY(-0.075em); -} - -.muigui-checkbox input[type=checkbox]::before { - content: ""; - color: var(--value-color); - display: grid; - place-content: center; -} - -.muigui-checkbox input[type=checkbox]:checked::before { - content: "โœ”"; -} - -.muigui input[type=number]::-webkit-inner-spin-button, -.muigui input[type=number]::-webkit-outer-spin-button { - -webkit-appearance: none; - appearance: none; - margin: 0; -} -.muigui input[type=number] { - -moz-appearance: textfield; -} - -/* ------ [ radio grid ] ------ */ - -.muigui-radio-grid>div { - display: grid; - gap: 2px; -} - -.muigui-radio-grid input { - appearance: none; - display: none; -} - -.muigui-radio-grid button { - color: var(--color); - width: 100%; - text-align: left; -} - -.muigui-radio-grid input:checked + button { - color: var(--value-color); - background-color: var(--selected-color); -} - -/* ------ [ color-chooser ] ------ */ - -.muigui-color-chooser-cursor { - stroke-width: 1px; - stroke: white; - fill: none; -} -.muigui-color-chooser-circle { - stroke-width: 1px; - stroke: white; - fill: none; -} - - -/* ------ [ vec2 ] ------ */ - -.muigui-vec2 svg { - background-color: var(--value-bg-color); -} - -.muigui-vec2-axis { - stroke: 1px; - stroke: var(--focus-color); -} - -.muigui-vec2-line { - stroke-width: 1px; - stroke: var(--value-color); - fill: var(--value-color); -} - -/* ------ [ direction ] ------ */ - -.muigui-direction svg { - background-color: rgba(0,0,0,0.2); -} - -.muigui-direction:focus-within svg { - outline: none; -} -.muigui-direction-range { - fill: var(--value-bg-color); -} -.muigui-direction svg:focus { - outline: none; -} -.muigui-direction svg:focus .muigui-direction-range { - stroke-width: 0.5px; - stroke: var(--focus-color); -} - -.muigui-direction-arrow { - fill: var(--value-color); -} - -/* ------ [ slider ] ------ */ - -.muigui-slider>div { - display: flex; - align-items: stretch; - height: var(--line-height); -} -.muigui-slider svg { - flex: 1 1 auto; -} -.muigui-slider .muigui-slider-up #muigui-orientation { - transform: scale(1, -1) translateY(-100%); -} - -.muigui-slider .muigui-slider-up #muigui-number-orientation { - transform: scale(1,-1); -} - -.muigui-ticks { - stroke: var(--range-color); -} -.muigui-thicks { - stroke: var(--color); - stroke-width: 2px; -} -.muigui-svg-text { - fill: var(--color); - font-size: 7px; -} -.muigui-mark { - fill: var(--value-color); -} - -/* ------ [ range ] ------ */ - - -.muigui-range input[type=range] { - -webkit-appearance: none; - appearance: none; - background-color: transparent; -} - -.muigui-range input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - border-radius: calc(var(--border-radius) + 2px); - border-left: 1px solid rgba(255,255,255,0.3); - border-top: 1px solid rgba(255,255,255,0.3); - border-bottom: 1px solid rgba(0,0,0,0.2); - border-right: 1px solid rgba(0,0,0,0.2); - background-color: var(--range-color); - margin-top: calc((var(--line-height) - 2px) / -2); - width: calc(var(--line-height) - 2px); - height: calc(var(--line-height) - 2px); -} - -.muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - appearance: none; - border: 1px solid var(--menu-sep-color); - height: 2px; -} - - -/* dat.gui style - doesn't work on Safari iOS */ - -/* -.muigui-range input[type=range] { - cursor: ew-resize; - overflow: hidden; -} - -.muigui-range input[type=range] { - -webkit-appearance: none; - appearance: none; - background-color: var(--range-right-color); - margin: 0; -} -.muigui-range input[type=range]:hover { - background-color: var(--range-right-hover-color); -} - -.muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: none; - appearance: none; - height: max-content; - color: var(--range-left-color); - margin-top: -1px; -} - -.muigui-range input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 0px; - height: max-content; - box-shadow: -1000px 0 0 1000px var(--range-left-color); -} -*/ - -/* FF */ -/* -.muigui-range input[type=range]::-moz-slider-progress { - background-color: var(--range-left-color); -} -.muigui-range input[type=range]::-moz-slider-thumb { - height: max-content; - width: 0; - border: none; - box-shadow: -1000px 0 0 1000px var(--range-left-color); - box-sizing: border-box; -} -*/ - -.muigui-checkered-background { - background-color: #404040; - background-image: - linear-gradient(45deg, #808080 25%, transparent 25%), - linear-gradient(-45deg, #808080 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #808080 75%), - linear-gradient(-45deg, transparent 75%, #808080 75%); - background-size: 16px 16px; - background-position: 0 0, 0 8px, 8px -8px, -8px 0px; -} - -/* ---------------------------------------------------------- */ - -/* needs to be at bottom to take precedence */ -.muigui-auto-place { - max-height: 100%; - position: fixed; - top: 0; - right: 15px; - z-index: 100001; -} - -`, -themes: { - default: '', - float: ` - :root { - color-scheme: light dark, - } - - .muigui { - --width: 400px; - --bg-color: initial; - --label-width: 25%; - --number-width: 20%; - } - - input, - .muigui-label-controller>label { - text-shadow: - -1px -1px 0 var(--contrast-color), - 1px -1px 0 var(--contrast-color), - -1px 1px 0 var(--contrast-color), - 1px 1px 0 var(--contrast-color); - } - - .muigui-controller > label:nth-child(1) { - place-content: center end; - margin-right: 1em; - } - - .muigui-value > :nth-child(2) { - margin-left: 1em; - } - - .muigui-root>*:nth-child(1) { - display: none; - } - - .muigui-range input[type=range]::-webkit-slider-thumb { - border-radius: 1em; - } - - .muigui-range input[type=range]::-webkit-slider-runnable-track { - -webkit-appearance: initial; - appearance: none; - border: 1px solid rgba(0, 0, 0, 0.25); - height: 2px; - } - - .muigui-colors { - --value-color: var(--color ); - --value-bg-color: rgba(0, 0, 0, 0.1); - --disabled-color: #cccccc; - --menu-bg-color: rgba(0, 0, 0, 0.1); - --menu-sep-color: #bbbbbb; - --hover-bg-color: rgba(0, 0, 0, 0); - --invalid-color: #FF0000; - --selected-color: rgba(0, 0, 0, 0.3); - --range-color: rgba(0, 0, 0, 0.125); - } -`, -}, -}; - -function setElemProps(elem, attrs, children) { - for (const [key, value] of Object.entries(attrs)) { - if (typeof value === 'function' && key.startsWith('on')) { - const eventName = key.substring(2).toLowerCase(); - elem.addEventListener(eventName, value, {passive: false}); - } else if (typeof value === 'object') { - for (const [k, v] of Object.entries(value)) { - elem[key][k] = v; - } - } else if (elem[key] === undefined) { - elem.setAttribute(key, value); - } else { - elem[key] = value; - } - } - for (const child of children) { - elem.appendChild(child); - } - return elem; -} - -function createElem(tag, attrs = {}, children = []) { - const elem = document.createElement(tag); - setElemProps(elem, attrs, children); - return elem; -} - -function addElem(tag, parent, attrs = {}, children = []) { - const elem = createElem(tag, attrs, children); - parent.appendChild(elem); - return elem; -} - -let nextId = 0; -function getNewId() { - return `muigui-id-${nextId++}`; -} - -function removeArrayElem(array, value) { - const ndx = array.indexOf(value); - if (ndx) { - array.splice(ndx, 1); - } - return array; -} - -/** - * Converts an camelCase or snake_case id to "camel case" or "snake case" - * @param {string} id - */ -const underscoreRE = /_/g; -const upperLowerRE = /([A-Z])([a-z])/g; -function idToLabel(id) { - return id.replace(underscoreRE, ' ') - .replace(upperLowerRE, (m, m1, m2) => `${m1.toLowerCase()} ${m2}`); -} - -function clamp$1(v, min, max) { - return Math.max(min, Math.min(max, v)); -} - -const isTypedArray = typeof SharedArrayBuffer !== 'undefined' - ? function isArrayBufferOrSharedArrayBuffer(a) { - return a && a.buffer && (a.buffer instanceof ArrayBuffer || a.buffer instanceof SharedArrayBuffer); - } - : function isArrayBuffer(a) { - return a && a.buffer && a.buffer instanceof ArrayBuffer; - }; - -const isArrayOrTypedArray = v => Array.isArray(v) || isTypedArray(v); - -// Yea, I know this should be `Math.round(v / step) * step -// but try step = 0.1, newV = 19.95 -// -// I get -// Math.round(19.95 / 0.1) * 0.1 -// 19.900000000000002 -// vs -// Math.round(19.95 / 0.1) / (1 / 0.1) -// 19.9 -// -const stepify = (v, from, step) => Math.round(from(v) / step) / (1 / step); - -const euclideanModulo$1 = (v, n) => ((v % n) + n) % n; -const lerp$1 = (a, b, t) => a + (b - a) * t; -function copyExistingProperties(dst, src) { - for (const key in src) { - if (key in dst) { - dst[key] = src[key]; - } - } - return dst; -} - -const mapRange = (v, inMin, inMax, outMin, outMax) => (v - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; - -const makeRangeConverters = ({from, to}) => { - return { - to: v => mapRange(v, ...from, ...to), - from: v => [true, mapRange(v, ...to, ...from)], - }; -}; - -const makeRangeOptions = ({from, to, step}) => { - return { - min: to[0], - max: to[1], - ...(step && {step}), - converters: makeRangeConverters({from, to}), - }; -}; - -// TODO: remove an use one in conversions. Move makeRangeConverters there? -const identity$1 = { - to: v => v, - from: v => [true, v], -}; -function makeMinMaxPair(gui, properties, minPropName, maxPropName, options) { - const { converters: { from } = identity$1 } = options; - const { min, max } = options; - const guiMinRange = options.minRange || 0; - const valueMinRange = from(guiMinRange)[1]; - const minGui = gui - .add(properties, minPropName, { - ...options, - min, - max: max - guiMinRange, - }) - .onChange(v => { - maxGui.setValue(Math.min(max, Math.max(v + valueMinRange, properties[maxPropName]))); - }); - const maxGui = gui - .add(properties, maxPropName, { - ...options, - min: min + guiMinRange, - max, - }) - .onChange(v => { - minGui.setValue(Math.max(min, Math.min(v - valueMinRange, properties[minPropName]))); - }); - return [ minGui, maxGui ]; -} - -class View { - domElement; - #childDestElem; - #views = []; - constructor(elem) { - this.domElement = elem; - this.#childDestElem = elem; - } - addElem(elem) { - this.#childDestElem.appendChild(elem); - return elem; - } - removeElem(elem) { - this.#childDestElem.removeChild(elem); - return elem; - } - pushSubElem(elem) { - this.#childDestElem.appendChild(elem); - this.#childDestElem = elem; - } - popSubElem() { - this.#childDestElem = this.#childDestElem.parentElement; - } - add(view) { - this.#views.push(view); - this.addElem(view.domElement); - return view; - } - remove(view) { - this.removeElem(view.domElement); - removeArrayElem(this.#views, view); - return view; - } - pushSubView(view) { - this.pushSubElem(view.domElement); - } - popSubView() { - this.popSubElem(); - } - setOptions(options) { - for (const view of this.#views) { - view.setOptions(options); - } - } - updateDisplayIfNeeded(newV, ignoreCache) { - for (const view of this.#views) { - view.updateDisplayIfNeeded(newV, ignoreCache); - } - return this; - } - $(selector) { - return this.domElement.querySelector(selector); - } -} - -class Controller extends View { - #changeFns; - #finishChangeFns; - #parent; - - constructor(className) { - super(createElem('div', {className: 'muigui-controller'})); - this.#changeFns = []; - this.#finishChangeFns = []; - // we need the specialization to come last so it takes precedence. - if (className) { - this.domElement.classList.add(className); - } - } - get parent() { - return this.#parent; - } - setParent(parent) { - this.#parent = parent; - this.enable(!this.disabled()); - } - show(show = true) { - this.domElement.classList.toggle('muigui-hide', !show); - this.domElement.classList.toggle('muigui-show', show); - return this; - } - hide() { - return this.show(false); - } - disabled() { - return !!this.domElement.closest('.muigui-disabled'); - } - - enable(enable = true) { - this.domElement.classList.toggle('muigui-disabled', !enable); - - // If disabled we need to set the attribute 'disabled=true' to all - // input/select/button/textarea's below - // - // If enabled we need to set the attribute 'disabled=false' to all below - // until we hit a disabled controller. - // - // ATM the problem is we can find the input/select/button/textarea elements - // but we can't easily find which controller they belong do. - // But we don't need to? We can just check up if it or parent has - // '.muigui-disabled' - ['input', 'button', 'select', 'textarea'].forEach(tag => { - this.domElement.querySelectorAll(tag).forEach(elem => { - const disabled = !!elem.closest('.muigui-disabled'); - elem.disabled = disabled; - }); - }); - - return this; - } - disable(disable = true) { - return this.enable(!disable); - } - onChange(fn) { - this.removeChange(fn); - this.#changeFns.push(fn); - return this; - } - removeChange(fn) { - removeArrayElem(this.#changeFns, fn); - return this; - } - onFinishChange(fn) { - this.removeFinishChange(fn); - this.#finishChangeFns.push(fn); - return this; - } - removeFinishChange(fn) { - removeArrayElem(this.#finishChangeFns, fn); - return this; - } - #callListeners(fns, newV) { - for (const fn of fns) { - fn.call(this, newV); - } - } - emitChange(value, object, property) { - this.#callListeners(this.#changeFns, value); - if (this.#parent) { - if (object === undefined) { - this.#parent.emitChange(value); - } else { - this.#parent.emitChange({ - object, - property, - value, - controller: this, - }); - } - } - } - emitFinalChange(value, object, property) { - this.#callListeners(this.#finishChangeFns, value); - if (this.#parent) { - if (object === undefined) { - this.#parent.emitChange(value); - } else { - this.#parent.emitFinalChange({ - object, - property, - value, - controller: this, - }); - } - } - } - updateDisplay() { - // placeholder. override - } - getColors() { - const toCamelCase = s => s.replace(/-([a-z])/g, (m, m1) => m1.toUpperCase()); - const keys = [ - 'color', - 'bg-color', - 'value-color', - 'value-bg-color', - 'hover-bg-color', - 'menu-bg-color', - 'menu-sep-color', - 'disabled-color', - ]; - const div = createElem('div'); - this.domElement.appendChild(div); - const colors = Object.fromEntries(keys.map(key => { - div.style.color = `var(--${key})`; - const s = getComputedStyle(div); - return [toCamelCase(key), s.color]; - })); - div.remove(); - return colors; - } -} - -class Button extends Controller { - #object; - #property; - #buttonElem; - #options = { - name: '', - }; - - constructor(object, property, options = {}) { - super('muigui-button', ''); - this.#object = object; - this.#property = property; - - this.#buttonElem = this.addElem( - createElem('button', { - type: 'button', - onClick: () => { - this.#object[this.#property](this); - }, - })); - this.setOptions({name: property, ...options}); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {name} = this.#options; - this.#buttonElem.textContent = name; - } -} - -function arraysEqual(a, b) { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - return true; -} - -function copyArrayElementsFromTo(src, dst) { - dst.length = src.length; - for (let i = 0; i < src.length; ++i) { - dst[i] = src[i]; - } -} - -class EditView extends View { - #oldV; - #updateCheck; - - #checkArrayNeedsUpdate(newV) { - // It's an array, we need to compare all elements - // Example, vec2, [r,g,b], ... - const needUpdate = !arraysEqual(newV, this.#oldV); - if (needUpdate) { - copyArrayElementsFromTo(newV, this.#oldV); - } - return needUpdate; - } - - #checkTypedArrayNeedsUpdate() { - let once = true; - return function checkTypedArrayNeedsUpdateImpl(newV) { - // It's a typedarray, we need to compare all elements - // Example: Float32Array([r, g, b]) - let needUpdate = once; - once = false; - if (!needUpdate) { - needUpdate = !arraysEqual(newV, this.#oldV); - } - return needUpdate; - }; - } - - #checkObjectNeedsUpdate(newV) { - let needUpdate = false; - for (const key in newV) { - if (newV[key] !== this.#oldV[key]) { - needUpdate = true; - this.#oldV[key] = newV[key]; - } - } - return needUpdate; - } - - #checkValueNeedsUpdate(newV) { - const needUpdate = newV !== this.#oldV; - this.#oldV = newV; - return needUpdate; - } - - #getUpdateCheckForType(newV) { - if (Array.isArray(newV)) { - this.#oldV = []; - return this.#checkArrayNeedsUpdate.bind(this); - } else if (isTypedArray(newV)) { - this.#oldV = new newV.constructor(newV); - return this.#checkTypedArrayNeedsUpdate(this); - } else if (typeof newV === 'object') { - this.#oldV = {}; - return this.#checkObjectNeedsUpdate.bind(this); - } else { - return this.#checkValueNeedsUpdate.bind(this); - } - } - - // The point of this is updating DOM elements - // is slow but if we've called `listen` then - // every frame we're going to try to update - // things with the current value so if nothing - // has changed then skip it. - updateDisplayIfNeeded(newV, ignoreCache) { - this.#updateCheck = this.#updateCheck || this.#getUpdateCheckForType(newV); - // Note: We call #updateCheck first because it updates - // the cache - if (this.#updateCheck(newV) || ignoreCache) { - this.updateDisplay(newV); - } - } - setOptions(/*options*/) { - // override this - return this; - } -} - -class CheckboxView extends EditView { - #checkboxElem; - constructor(setter, id) { - const checkboxElem = createElem('input', { - type: 'checkbox', - id, - onInput: () => { - setter.setValue(checkboxElem.checked); - }, - onChange: () => { - setter.setFinalValue(checkboxElem.checked); - }, - }); - super(createElem('label', {}, [checkboxElem])); - this.#checkboxElem = checkboxElem; - } - updateDisplay(v) { - this.#checkboxElem.checked = v; - } -} - -const tasks = []; -const tasksToRemove = new Set(); - -let requestId; -let processing; - -function removeTasks() { - if (!tasksToRemove.size) { - return; - } - - if (processing) { - queueProcessing(); - return; - } - - tasksToRemove.forEach(task => { - removeArrayElem(tasks, task); - }); - tasksToRemove.clear(); -} - -function processTasks() { - requestId = undefined; - processing = true; - for (const task of tasks) { - if (!tasksToRemove.has(task)) { - task(); - } - } - processing = false; - removeTasks(); - queueProcessing(); -} - -function queueProcessing() { - if (!requestId && tasks.length) { - requestId = requestAnimationFrame(processTasks); - } -} - -function addTask(fn) { - tasks.push(fn); - queueProcessing(); -} - -function removeTask(fn) { - tasksToRemove.set(fn); - - const ndx = tasks.indexOf(fn); - if (ndx >= 0) { - tasks.splice(ndx, 1); - } -} - -let id = 0; - -function makeId() { - return `muigui-${++id}`; -} - -class ValueView extends View { - constructor(className = '') { - super(createElem('div', {className: 'muigui-value'})); - if (className) { - this.domElement.classList.add(className); - } - } -} - -class LabelController extends Controller { - #id; - #nameElem; - - constructor(className = '', name = '') { - super('muigui-label-controller'); - this.#id = makeId(); - this.#nameElem = createElem('label', {for: this.#id}); - this.domElement.appendChild(this.#nameElem); - this.pushSubView(new ValueView(className)); - this.name(name); - } - get id() { - return this.#id; - } - name(name) { - if (this.#nameElem.title === this.#nameElem.textContent) { - this.#nameElem.title = name; - } - this.#nameElem.textContent = name; - return this; - } - tooltip(tip) { - this.#nameElem.title = tip; - } -} - -class ValueController extends LabelController { - #object; - #property; - #initialValue; - #listening; - #views; - #updateFn; - - constructor(object, property, className = '') { - super(className, property); - this.#object = object; - this.#property = property; - this.#initialValue = this.getValue(); - this.#listening = false; - this.#views = []; - } - get initialValue() { - return this.#initialValue; - } - get object() { - return this.#object; - } - get property() { - return this.#property; - } - add(view) { - this.#views.push(view); - super.add(view); - this.updateDisplay(); - return view; - } - #setValueImpl(v, ignoreCache) { - let isDifferent = false; - if (typeof v === 'object') { - const dst = this.#object[this.#property]; - // don't replace objects, just their values. - if (Array.isArray(v) || isTypedArray(v)) { - for (let i = 0; i < v.length; ++i) { - isDifferent ||= dst[i] !== v[i]; - dst[i] = v[i]; - } - } else { - for (const key of Object.keys(v)) { - isDifferent ||= dst[key] !== v[key]; - } - Object.assign(dst, v); - } - } else { - isDifferent = this.#object[this.#property] !== v; - this.#object[this.#property] = v; - } - this.updateDisplay(ignoreCache); - if (isDifferent) { - this.emitChange(this.getValue(), this.#object, this.#property); - } - return isDifferent; - } - setValue(v) { - this.#setValueImpl(v); - } - setFinalValue(v) { - const isDifferent = this.#setValueImpl(v, true); - if (isDifferent) { - this.emitFinalChange(this.getValue(), this.#object, this.#property); - } - return this; - } - updateDisplay(ignoreCache) { - const newV = this.getValue(); - for (const view of this.#views) { - view.updateDisplayIfNeeded(newV, ignoreCache); - } - return this; - } - setOptions(options) { - for (const view of this.#views) { - view.setOptions(options); - } - this.updateDisplay(); - return this; - } - getValue() { - return this.#object[this.#property]; - } - value(v) { - this.setValue(v); - return this; - } - reset() { - this.setValue(this.#initialValue); - return this; - } - listen(listen = true) { - if (!this.#updateFn) { - this.#updateFn = this.updateDisplay.bind(this); - } - if (listen) { - if (!this.#listening) { - this.#listening = true; - addTask(this.#updateFn); - } - } else { - if (this.#listening) { - this.#listening = false; - removeTask(this.#updateFn); - } - } - return this; - } -} - -class Checkbox extends ValueController { - constructor(object, property) { - super(object, property, 'muigui-checkbox'); - const id = this.id; - this.add(new CheckboxView(this, id)); - this.updateDisplay(); - } -} - -const identity = { - to: v => v, - from: v => [true, v], -}; - -// from: from string to value -// to: from value to string -const strToNumber = { - to: v => v.toString(), - from: v => { - const newV = parseFloat(v); - return [!Number.isNaN(newV), newV]; - }, -}; - -const converters = { - radToDeg: makeRangeConverters({to: [0, 180], from: [0, Math.PI]}), -}; - -function createWheelHelper() { - let wheelAccum = 0; - return function(e, step, wheelScale = 5) { - wheelAccum -= e.deltaY * step / wheelScale; - const wheelSteps = Math.floor(Math.abs(wheelAccum) / step) * Math.sign(wheelAccum); - const delta = wheelSteps * step; - wheelAccum -= delta; - return delta; - }; -} - -class NumberView extends EditView { - #to; - #from; - #step; - #skipUpdate; - #options = { - step: 0.01, - converters: strToNumber, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - }; - - constructor(setter, options) { - const setValue = setter.setValue.bind(setter); - const setFinalValue = setter.setFinalValue.bind(setter); - const wheelHelper = createWheelHelper(); - super(createElem('input', { - type: 'number', - onInput: () => this.#handleInput(setValue, true), - onChange: () => this.#handleInput(setFinalValue, false), - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.setOptions(options); - } - #handleInput(setFn, skipUpdate) { - const v = parseFloat(this.domElement.value); - const [valid, newV] = this.#from(v); - let inRange; - if (valid && !Number.isNaN(v)) { - const {min, max} = this.#options; - inRange = newV >= min && newV <= max; - this.#skipUpdate = skipUpdate; - setFn(clamp$1(newV, min, max)); - } - this.domElement.classList.toggle('muigui-invalid-value', !valid || !inRange); - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = stepify(v, this.#to, this.#step); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - step, - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - this.#step = step; - return this; - } -} - -// Wanted to name this `Number` but it conflicts with -// JavaScript `Number`. It most likely wouldn't be -// an issue? But users might `import {Number} ...` and -// things would break. -class TextNumber extends ValueController { - #textView; - #step; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-checkbox'); - this.#textView = this.add(new NumberView(this, options)); - this.updateDisplay(); - } -} - -class SelectView extends EditView { - #values; - - constructor(setter, keyValues) { - const values = []; - super(createElem('select', { - onChange: () => { - setter.setFinalValue(this.#values[this.domElement.selectedIndex]); - }, - }, keyValues.map(([key, value]) => { - values.push(value); - return createElem('option', {textContent: key}); - }))); - this.#values = values; - } - updateDisplay(v) { - const ndx = this.#values.indexOf(v); - this.domElement.selectedIndex = ndx; - } -} - -// 4 cases -// (a) keyValues is array of arrays, each sub array is key value -// (b) keyValues is array and value is number then keys = array contents, value = index -// (c) keyValues is array and value is not number, key = array contents, value = array contents -// (d) keyValues is object then key->value -function convertToKeyValues(keyValues, valueIsNumber) { - if (Array.isArray(keyValues)) { - if (Array.isArray(keyValues[0])) { - // (a) keyValues is array of arrays, each sub array is key value - return keyValues; - } else { - if (valueIsNumber) { - // (b) keyValues is array and value is number then keys = array contents, value = index - return keyValues.map((v, ndx) => [v, ndx]); - } else { - // (c) keyValues is array and value is not number, key = array contents, value = array contents - return keyValues.map(v => [v, v]); - } - } - } else { - // (d) - return [...Object.entries(keyValues)]; - } -} - -class Select extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-select'); - const valueIsNumber = typeof this.getValue() === 'number'; - const {keyValues: keyValuesInput} = options; - const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); - this.add(new SelectView(this, keyValues)); - this.updateDisplay(); - } -} - -class RangeView extends EditView { - #to; - #from; - #step; - #skipUpdate; - #options = { - step: 0.01, - min: 0, - max: 1, - converters: identity, - }; - - constructor(setter, options) { - const wheelHelper = createWheelHelper(); - super(createElem('input', { - type: 'range', - onInput: () => { - this.#skipUpdate = true; - const {min, max, step} = this.#options; - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v, v => v, step), min, max); - const [valid, validV] = this.#from(newV); - if (valid) { - setter.setValue(validV); - } - }, - onChange: () => { - this.#skipUpdate = true; - const {min, max, step} = this.#options; - const v = parseFloat(this.domElement.value); - const newV = clamp$1(stepify(v, v => v, step), min, max); - const [valid, validV] = this.#from(newV); - if (valid) { - setter.setFinalValue(validV); - } - }, - onWheel: e => { - e.preventDefault(); - const [valid, v] = this.#from(parseFloat(this.domElement.value)); - if (!valid) { - return; - } - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const newV = clamp$1(stepify(v + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.setOptions(options); - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = stepify(v, this.#to, this.#step); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - step, - min, - max, - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - this.#step = step; - this.domElement.step = step; - this.domElement.min = min; - this.domElement.max = max; - return this; - } -} - -class Range extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-range'); - this.add(new RangeView(this, options)); - this.add(new NumberView(this, options)); - } -} - -class TextView extends EditView { - #to; - #from; - #skipUpdate; - #options = { - converters: identity, - }; - - constructor(setter, options) { - const setValue = setter.setValue.bind(setter); - const setFinalValue = setter.setFinalValue.bind(setter); - super(createElem('input', { - type: 'text', - onInput: () => this.#handleInput(setValue, true), - onChange: () => this.#handleInput(setFinalValue, false), - })); - this.setOptions(options); - } - #handleInput(setFn, skipUpdate) { - const [valid, newV] = this.#from(this.domElement.value); - if (valid) { - this.#skipUpdate = skipUpdate; - setFn(newV); - } - this.domElement.style.color = valid ? '' : 'var(--invalid-color)'; - - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.domElement.value = this.#to(v); - this.domElement.style.color = ''; - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const { - converters: {to, from}, - } = this.#options; - this.#to = to; - this.#from = from; - return this; - } -} - -class Text extends ValueController { - constructor(object, property) { - super(object, property, 'muigui-checkbox'); - this.add(new TextView(this)); - this.updateDisplay(); - } -} - -// const isConversion = o => typeof o.to === 'function' && typeof o.from === 'function'; - -/** - * possible inputs - * add(o, p, min: number, max: number) - * add(o, p, min: number, max: number, step: number) - * add(o, p, array: [value]) - * add(o, p, array: [[key, value]]) - * - * @param {*} object - * @param {string} property - * @param {...any} args - * @returns {Controller} - */ -function createController(object, property, ...args) { - const [arg1] = args; - if (Array.isArray(arg1)) { - return new Select(object, property, {keyValues: arg1}); - } - - const t = typeof object[property]; - switch (t) { - case 'number': - if (typeof args[0] === 'number' && typeof args[1] === 'number') { - const min = args[0]; - const max = args[1]; - const step = args[2]; - return new Range(object, property, {min, max, ...(step && {step})}); - } - return args.length === 0 - ? new TextNumber(object, property, ...args) - : new Range(object, property, ...args); - case 'boolean': - return new Checkbox(object, property, ...args); - case 'function': - return new Button(object, property, ...args); - case 'string': - return new Text(object, property, ...args); - case 'undefined': - throw new Error(`no property named ${property}`); - default: - throw new Error(`unhandled type ${t} for property ${property}`); - } -} - -const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); -const lerp = (a, b, t) => a + (b - a) * t; -const fract = v => v >= 0 ? v % 1 : 1 - (v % 1); - -const f0 = v => +v.toFixed(0); // converts to string (eg 1.2 => "1"), then converts back to number (eg, "1.200" => 1.2) -const f3 = v => +v.toFixed(3); // converts to string (eg 1.2 => "1.200"), then converts back to number (eg, "1.200" => 1.2) - -const hexToUint32RGB = v => (parseInt(v.substring(1, 3), 16) << 16) | - (parseInt(v.substring(3, 5), 16) << 8 ) | - (parseInt(v.substring(5, 7), 16) ); -const uint32RGBToHex = v => `#${(Math.round(v)).toString(16).padStart(6, '0')}`; -const hexToUint32RGBA = v => (parseInt(v.substring(1, 3), 16) * 2 ** 24) + - (parseInt(v.substring(3, 5), 16) * 2 ** 16) + - (parseInt(v.substring(5, 7), 16) * 2 ** 8) + - (parseInt(v.substring(7, 9), 16) ); -const uint32RGBAToHex = v => `#${(Math.round(v)).toString(16).padStart(8, '0')}`; - -const hexToUint8RGB = v => [ - parseInt(v.substring(1, 3), 16), - parseInt(v.substring(3, 5), 16), - parseInt(v.substring(5, 7), 16), -]; -const uint8RGBToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`; - -const hexToUint8RGBA = v => [ - parseInt(v.substring(1, 3), 16), - parseInt(v.substring(3, 5), 16), - parseInt(v.substring(5, 7), 16), - parseInt(v.substring(7, 9), 16), -]; -const uint8RGBAToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`; - -const hexToFloatRGB = v => hexToUint8RGB(v).map(v => f3(v / 255)); -const floatRGBToHex = v => uint8RGBToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255)))); - -const hexToFloatRGBA = v => hexToUint8RGBA(v).map(v => f3(v / 255)); -const floatRGBAToHex = v => uint8RGBAToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255)))); - -const scaleAndClamp = v => clamp(Math.round(v * 255), 0, 255).toString(16).padStart(2, '0'); - -const hexToObjectRGB = v => ({ - r: parseInt(v.substring(1, 3), 16) / 255, - g: parseInt(v.substring(3, 5), 16) / 255, - b: parseInt(v.substring(5, 7), 16) / 255, -}); -const objectRGBToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}`; -const hexToObjectRGBA = v => ({ - r: parseInt(v.substring(1, 3), 16) / 255, - g: parseInt(v.substring(3, 5), 16) / 255, - b: parseInt(v.substring(5, 7), 16) / 255, - a: parseInt(v.substring(7, 9), 16) / 255, -}); -const objectRGBAToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}${scaleAndClamp(v.a)}`; - -const hexToCssRGB = v => `rgb(${hexToUint8RGB(v).join(', ')})`; -const cssRGBRegex = /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/; -const cssRGBToHex = v => { - const m = cssRGBRegex.exec(v); - return uint8RGBToHex([m[1], m[2], m[3]].map(v => parseInt(v))); -}; -const hexToCssRGBA = v => `rgba(${hexToUint8RGBA(v).map((v, i) => i === 3 ? v / 255 : v).join(', ')})`; -const cssRGBARegex = /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+\.\d+|\d+)\s*\)\s*$/; -const cssRGBAToHex = v => { - const m = cssRGBARegex.exec(v); - return uint8RGBAToHex([m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? (parseFloat(v) * 255 | 0) : parseInt(v))); -}; - -const hexToCssHSL = v => { - const hsl = rgbUint8ToHsl(hexToUint8RGB(v)).map(v => f0(v)); - return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`; -}; -const hexToCssHSLA = v => { - const hsla = rgbaUint8ToHsla(hexToUint8RGBA(v)).map((v, i) => i === 3 ? f3(v) : f0(v)); - return `hsl(${hsla[0]} ${hsla[1]}% ${hsla[2]}% / ${hsla[3]})`; -}; -const cssHSLRegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\)\s*$/; -const cssHSLARegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\/\s*(\d+\.\d+|\d+)\s*\)\s*$/; - -const hex3DigitTo6Digit = v => `${v[0]}${v[0]}${v[1]}${v[1]}${v[2]}${v[2]}`; -const cssHSLToHex = v => { - const m = cssHSLRegex.exec(v); - const rgb = hslToRgbUint8([m[1], m[2], m[3]].map(v => parseFloat(v))); - return uint8RGBToHex(rgb); -}; -const cssHSLAToHex = v => { - const m = cssHSLARegex.exec(v); - const rgba = hslaToRgbaUint8([m[1], m[2], m[3], m[4]].map(v => parseFloat(v))); - return uint8RGBAToHex(rgba); -}; - -const euclideanModulo = (v, n) => ((v % n) + n) % n; - -function hslToRgbUint8([h, s, l]) { - h = euclideanModulo(h, 360); - s = clamp(s / 100, 0, 1); - l = clamp(l / 100, 0, 1); - - const a = s * Math.min(l, 1 - l); - - function f(n) { - const k = (n + h / 30) % 12; - return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); - } - - return [f(0), f(8), f(4)].map(v => Math.round(v * 255)); -} - -function hslaToRgbaUint8([h, s, l, a]) { - const rgb = hslToRgbUint8([h, s, l]); - return [...rgb, a * 255 | 0]; -} - -function rgbFloatToHsl01([r, g, b]) { - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (min + max) * 0.5; - const d = max - min; - let h = 0; - let s = 0; - - if (d !== 0) { - s = (l === 0 || l === 1) - ? 0 - : (max - l) / Math.min(l, 1 - l); - - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; - } - } - - return [h / 6, s, l]; -} - -function rgbaFloatToHsla01([r, g, b, a]) { - const hsl = rgbFloatToHsl01([r, g, b]); - return [...hsl, a]; -} - -const rgbUint8ToHsl = (rgb) => { - const [h, s, l] = rgbFloatToHsl01(rgb.map(v => v / 255)); - return [h * 360, s * 100, l * 100]; -}; - -const rgbaUint8ToHsla = (rgba) => { - const [h, s, l, a] = rgbaFloatToHsla01(rgba.map(v => v / 255)); - return [h * 360, s * 100, l * 100, a]; -}; - -function hsv01ToRGBFloat([hue, sat, val]) { - sat = clamp(sat, 0, 1); - val = clamp(val, 0, 1); - return [hue, hue + 2 / 3, hue + 1 / 3].map( - v => lerp(1, clamp(Math.abs(fract(v) * 6 - 3.0) - 1, 0, 1), sat) * val - ); -} - -function hsva01ToRGBAFloat([hue, sat, val, alpha]) { - const rgb = hsv01ToRGBFloat([hue, sat, val]); - return [...rgb, alpha]; -} - -const round3 = v => Math.round(v * 1000) / 1000; - -function rgbFloatToHSV01([r, g, b]) { - const p = b > g - ? [b, g, -1, 2 / 3] - : [g, b, 0, -1 / 3]; - const q = p[0] > r - ? [p[0], p[1], p[3], r] - : [r, p[1], p[2], p[0]]; - const d = q[0] - Math.min(q[3], q[1]); - return [ - Math.abs(q[2] + (q[3] - q[1]) / (6 * d + Number.EPSILON)), - d / (q[0] + Number.EPSILON), - q[0], - ].map(round3); -} - -function rgbaFloatToHSVA01([r, g, b, a]) { - const hsv = rgbFloatToHSV01([r, g, b]); - return [...hsv, a]; -} - -// window.hsv01ToRGBFloat = hsv01ToRGBFloat; -// window.rgbFloatToHSV01 = rgbFloatToHSV01; - -// Yea, meh! -const hasAlpha = format => format.endsWith('a') || format.startsWith('hex8'); - -const cssStringFormats = [ - { re: /^#(?:[0-9a-f]){6}$/i, format: 'hex6' }, - { re: /^(?:[0-9a-f]){6}$/i, format: 'hex6-no-hash' }, - { re: /^#(?:[0-9a-f]){8}$/i, format: 'hex8' }, - { re: /^(?:[0-9a-f]){8}$/i, format: 'hex8-no-hash' }, - { re: /^#(?:[0-9a-f]){3}$/i, format: 'hex3' }, - { re: /^(?:[0-9a-f]){3}$/i, format: 'hex3-no-hash' }, - { re: cssRGBRegex, format: 'css-rgb' }, - { re: cssHSLRegex, format: 'css-hsl' }, - { re: cssRGBARegex, format: 'css-rgba' }, - { re: cssHSLARegex, format: 'css-hsla' }, -]; - -function guessStringColorFormat(v) { - for (const formatInfo of cssStringFormats) { - if (formatInfo.re.test(v)) { - return formatInfo; - } - } - return undefined; -} - -function guessFormat(v) { - switch (typeof v) { - case 'number': - console.warn('can not reliably guess format based on a number. You should pass in a format like {format: "uint32-rgb"} or {format: "uint32-rgb"}'); - return v <= 0xFFFFFF ? 'uint32-rgb' : 'uint32-rgba'; - case 'string': { - const formatInfo = guessStringColorFormat(v.trim()); - if (formatInfo) { - return formatInfo.format; - } - break; - } - case 'object': - if (v instanceof Uint8Array || v instanceof Uint8ClampedArray) { - if (v.length === 3) { - return 'uint8-rgb'; - } else if (v.length === 4) { - return 'uint8-rgba'; - } - } else if (v instanceof Float32Array) { - if (v.length === 3) { - return 'float-rgb'; - } else if (v.length === 4) { - return 'float-rgba'; - } - } else if (Array.isArray(v)) { - if (v.length === 3) { - return 'float-rgb'; - } else if (v.length === 4) { - return 'float-rgba'; - } - } else { - if ('r' in v && 'g' in v && 'b' in v) { - if ('a' in v) { - return 'object-rgba'; - } else { - return 'object-rgb'; - } - } - } - } - throw new Error(`unknown color format: ${v}`); -} - -function fixHex6(v) { - return v.trim(v); - //const formatInfo = guessStringColorFormat(v.trim()); - //const fix = formatInfo ? formatInfo.fix : v => v; - //return fix(v.trim()); -} - -function fixHex8(v) { - return v.trim(v); - //const formatInfo = guessStringColorFormat(v.trim()); - //const fix = formatInfo ? formatInfo.fix : v => v; - //return fix(v.trim()); -} - -function hex6ToHex3(hex6) { - return (hex6[1] === hex6[2] && - hex6[3] === hex6[4] && - hex6[5] === hex6[6]) - ? `#${hex6[1]}${hex6[3]}${hex6[5]}` - : hex6; -} - -const hex3RE = /^(#|)([0-9a-f]{3})$/i; -function hex3ToHex6(hex3) { - const m = hex3RE.exec(hex3); - if (m) { - const [, , m2] = m; - return `#${hex3DigitTo6Digit(m2)}`; - } - return hex3; -} - -function fixHex3(v) { - return hex6ToHex3(fixHex6(v)); -} - -const strToRGBObject = (s) => { - try { - const json = s.replace(/([a-z])/g, '"$1"'); - const rgb = JSON.parse(json); - if (Number.isNaN(rgb.r) || Number.isNaN(rgb.g) || Number.isNaN(rgb.b)) { - throw new Error('not {r, g, b}'); - } - return [true, rgb]; - } catch (e) { - return [false]; - } -}; - -const strToRGBAObject = (s) => { - try { - const json = s.replace(/([a-z])/g, '"$1"'); - const rgba = JSON.parse(json); - if (Number.isNaN(rgba.r) || Number.isNaN(rgba.g) || Number.isNaN(rgba.b) || Number.isNaN(rgba.a)) { - throw new Error('not {r, g, b, a}'); - } - return [true, rgba]; - } catch (e) { - return [false]; - } -}; - -const strToCssRGB = s => { - const m = cssRGBRegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, `rgb(${v.join(', ')})`]; -}; - -const strToCssRGBA = s => { - const m = cssRGBARegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map((v, i) => i === 3 ? parseFloat(v) : parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, `rgba(${v.join(', ')})`]; -}; - -const strToCssHSL = s => { - const m = cssHSLRegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseFloat(v)); - const outOfRange = v.find(v => Number.isNaN(v)); - return [!outOfRange, `hsl(${v[0]}, ${v[1]}%, ${v[2]}%)`]; -}; - -const strToCssHSLA = s => { - const m = cssHSLARegex.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map(v => parseFloat(v)); - const outOfRange = v.find(v => Number.isNaN(v)); - return [!outOfRange, `hsl(${v[0]} ${v[1]}% ${v[2]}% / ${v[3]})`]; -}; - -const rgbObjectToStr = rgb => { - return `{r:${f3(rgb.r)}, g:${f3(rgb.g)}, b:${f3(rgb.b)}}`; -}; -const rgbaObjectToStr = rgba => { - return `{r:${f3(rgba.r)}, g:${f3(rgba.g)}, b:${f3(rgba.b)}}, a:${f3(rgba.a)}}`; -}; - -const strTo3IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/; -const strTo3Ints = s => { - const m = strTo3IntsRE.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, v]; -}; - -const strTo4IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/; -const strTo4Ints = s => { - const m = strTo4IntsRE.exec(s); - if (!m) { - return [false]; - } - const v = [m[1], m[2], m[3], m[4]].map(v => parseInt(v)); - const outOfRange = v.find(v => v > 255); - return [!outOfRange, v]; -}; - -const strTo3Floats = s => { - const numbers = s.split(',').map(s => s.trim()); - const v = numbers.map(v => parseFloat(v)); - if (v.length !== 3) { - return [false]; - } - // Note: using isNaN not Number.isNaN - const badNdx = numbers.findIndex(v => isNaN(v)); - return [badNdx < 0, v.map(v => f3(v))]; -}; - -const strTo4Floats = s => { - const numbers = s.split(',').map(s => s.trim()); - const v = numbers.map(v => parseFloat(v)); - if (v.length !== 4) { - return [false]; - } - // Note: using isNaN not Number.isNaN - const badNdx = numbers.findIndex(v => isNaN(v)); - return [badNdx < 0, v.map(v => f3(v))]; -}; - -const strToUint32RGBRegex = /^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i; -const strToUint32RGB = s => { - const m = strToUint32RGBRegex.exec(s); - if (!m) { - return [false]; - } - return [true, parseInt(m[1], 16)]; -}; - -const strToUint32RGBARegex = /^\s*(?:0x){0,1}([0-9a-z]{1,8})\s*$/i; -const strToUint32RGBA = s => { - const m = strToUint32RGBARegex.exec(s); - if (!m) { - return [false]; - } - return [true, parseInt(m[1], 16)]; -}; - -const hex6RE = /^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i; -const hexNoHash6RE = /^\s*[a-f0-9]{6}\s*$/i; -const hex8RE = /^\s*#[a-f0-9]{8}\s*$/i; -const hexNoHash8RE = /^\s*[a-f0-9]{8}\s*$/i; - -// For each format converter -// -// fromHex/toHex convert from/to '#RRGGBB' -// -// fromHex converts from the string '#RRBBGG' to the format -// (eg: for uint32-rgb, '#123456' becomes 0x123456) -// -// toHex converts from the format to '#RRGGBB' -// (eg: for uint8-rgb, [16, 33, 50] becomes '#102132') -// -// -// fromStr/toStr convert from/to what's in the input[type=text] element -// -// toStr converts from the format to its string representation -// (eg, for object-rgb, {r: 1, g: 0.5, b:0} becomes "{r: 1, g: 0.5, b:0}") -// ^object ^string -// -// fromStr converts its string representation to its format -// (eg, for object-rgb) "{r: 1, g: 0.5, b:0}" becomes {r: 1, g: 0.5, b:0}) -// ^string ^object -// fromString returns an array which is [valid, v] -// where valid is true if the string was a valid and v is the converted -// format if v is true. -// -// Note: toStr should convert to "ideal" form (whatever that is). -// (eg, for css-rgb -// "{ r: 0.10000, g: 001, b: 0}" becomes "{r: 0.1, g: 1, b: 0}" -// notice that css-rgb is a string to a string -// ) -const colorFormatConverters = { - 'hex6': { - color: { - from: v => [true, v], - to: fixHex6, - }, - text: { - from: v => [hex6RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex8': { - color: { - from: v => [true, v], - to: fixHex8, - }, - text: { - from: v => [hex8RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex3': { - color: { - from: v => [true, fixHex3(v)], - to: hex3ToHex6, - }, - text: { - from: v => [hex6RE.test(v), hex6ToHex3(v.trim())], - to: v => v, - }, - }, - 'hex6-no-hash': { - color: { - from: v => [true, v.substring(1)], - to: v => `#${fixHex6(v)}`, - }, - text: { - from: v => [hexNoHash6RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex8-no-hash': { - color: { - from: v => [true, v.substring(1)], - to: v => `#${fixHex8(v)}`, - }, - text: { - from: v => [hexNoHash8RE.test(v), v.trim()], - to: v => v, - }, - }, - 'hex3-no-hash': { - color: { - from: v => [true, fixHex3(v).substring(1)], - to: hex3ToHex6, - }, - text: { - from: v => [hexNoHash6RE.test(v), hex6ToHex3(v.trim())], - to: v => v, - }, - }, - 'uint32-rgb': { - color: { - from: v => [true, hexToUint32RGB(v)], - to: uint32RGBToHex, - }, - text: { - from: v => strToUint32RGB(v), - to: v => `0x${v.toString(16).padStart(6, '0')}`, - }, - }, - 'uint32-rgba': { - color: { - from: v => [true, hexToUint32RGBA(v)], - to: uint32RGBAToHex, - }, - text: { - from: v => strToUint32RGBA(v), - to: v => `0x${v.toString(16).padStart(8, '0')}`, - }, - }, - 'uint8-rgb': { - color: { - from: v => [true, hexToUint8RGB(v)], - to: uint8RGBToHex, - }, - text: { - from: strTo3Ints, - to: v => v.join(', '), - }, - }, - 'uint8-rgba': { - color: { - from: v => [true, hexToUint8RGBA(v)], - to: uint8RGBAToHex, - }, - text: { - from: strTo4Ints, - to: v => v.join(', '), - }, - }, - 'float-rgb': { - color: { - from: v => [true, hexToFloatRGB(v)], - to: floatRGBToHex, - }, - text: { - from: strTo3Floats, - // need Array.from because map of Float32Array makes a Float32Array - to: v => Array.from(v).map(v => f3(v)).join(', '), - }, - }, - 'float-rgba': { - color: { - from: v => [true, hexToFloatRGBA(v)], - to: floatRGBAToHex, - }, - text: { - from: strTo4Floats, - // need Array.from because map of Float32Array makes a Float32Array - to: v => Array.from(v).map(v => f3(v)).join(', '), - }, - }, - 'object-rgb': { - color: { - from: v => [true, hexToObjectRGB(v)], - to: objectRGBToHex, - }, - text: { - from: strToRGBObject, - to: rgbObjectToStr, - }, - }, - 'object-rgba': { - color: { - from: v => [true, hexToObjectRGBA(v)], - to: objectRGBAToHex, - }, - text: { - from: strToRGBAObject, - to: rgbaObjectToStr, - }, - }, - 'css-rgb': { - color: { - from: v => [true, hexToCssRGB(v)], - to: cssRGBToHex, - }, - text: { - from: strToCssRGB, - to: v => strToCssRGB(v)[1], - }, - }, - 'css-rgba': { - color: { - from: v => [true, hexToCssRGBA(v)], - to: cssRGBAToHex, - }, - text: { - from: strToCssRGBA, - to: v => strToCssRGBA(v)[1], - }, - }, - 'css-hsl': { - color: { - from: v => [true, hexToCssHSL(v)], - to: cssHSLToHex, - }, - text: { - from: strToCssHSL, - to: v => strToCssHSL(v)[1], - }, - }, - 'css-hsla': { - color: { - from: v => [true, hexToCssHSLA(v)], - to: cssHSLAToHex, - }, - text: { - from: strToCssHSLA, - to: v => strToCssHSLA(v)[1], - }, - }, -}; - -class ElementView extends View { - constructor(tag, className) { - super(createElem(tag, {className})); - } -} - -// TODO: remove this? Should just be user side -class Canvas extends LabelController { - #canvasElem; - - constructor() { - super('muigui-canvas'); - this.#canvasElem = this.add( - new ElementView('canvas', 'muigui-canvas'), - ).domElement; - } - get canvas() { - return this.#canvasElem; - } -} - -class ColorView extends EditView { - #to; - #from; - #colorElem; - #skipUpdate; - #options = { - converters: identity, - }; - - constructor(setter, options) { - const colorElem = createElem('input', { - type: 'color', - onInput: () => { - const [valid, newV] = this.#from(colorElem.value); - if (valid) { - this.#skipUpdate = true; - setter.setValue(newV); - } - }, - onChange: () => { - const [valid, newV] = this.#from(colorElem.value); - if (valid) { - this.#skipUpdate = true; - setter.setFinalValue(newV); - } - }, - }); - super(createElem('div', {}, [colorElem])); - this.setOptions(options); - this.#colorElem = colorElem; - } - updateDisplay(v) { - if (!this.#skipUpdate) { - this.#colorElem.value = this.#to(v); - } - this.#skipUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {converters: {to, from}} = this.#options; - this.#to = to; - this.#from = from; - return this; - } -} - -class Color extends ValueController { - #colorView; - #textView; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-color'); - const format = options.format || guessFormat(this.getValue()); - const {color, text} = colorFormatConverters[format]; - this.#colorView = this.add(new ColorView(this, {converters: color})); - this.#textView = this.add(new TextView(this, {converters: text})); - this.updateDisplay(); - } - setOptions(options) { - const {format} = options; - if (format) { - const {color, text} = colorFormatConverters[format]; - this.#colorView.setOptions({converters: color}); - this.#textView.setOptions({converters: text}); - } - super.setOptions(options); - return this; - } -} - -// This feels like it should be something else like -// gui.addController({className: 'muigui-divider')}; -class Divider extends Controller { - constructor() { - super('muigui-divider'); - } -} - -class Container extends Controller { - #controllers; - #childDestController; - - constructor(className) { - super(className); - this.#controllers = []; - this.#childDestController = this; - } - get children() { - return this.#controllers; // should we return a copy? - } - get controllers() { - return this.#controllers.filter(c => !(c instanceof Container)); - } - get folders() { - return this.#controllers.filter(c => c instanceof Container); - } - reset(recursive = true) { - for (const controller of this.#controllers) { - if (!(controller instanceof Container) || recursive) { - controller.reset(recursive); - } - } - return this; - } - updateDisplay() { - for (const controller of this.#controllers) { - controller.updateDisplay(); - } - return this; - } - remove(controller) { - const ndx = this.#controllers.indexOf(controller); - if (ndx >= 0) { - const c = this.#controllers.splice(ndx, 1); - const c0 = c[0]; - const elem = c0.domElement; - elem.remove(); - c0.setParent(null); - } - return this; - } - _addControllerImpl(controller) { - this.domElement.appendChild(controller.domElement); - this.#controllers.push(controller); - controller.setParent(this); - return controller; - } - addController(controller) { - return this.#childDestController._addControllerImpl(controller); - } - pushContainer(container) { - this.addController(container); - this.#childDestController = container; - return container; - } - popContainer() { - this.#childDestController = this.#childDestController.parent; - return this; - } -} - -class Folder extends Container { - #labelElem; - - constructor(name = 'Controls', className = 'muigui-menu') { - super(className); - this.#labelElem = createElem('label'); - this.addElem(createElem('button', { - type: 'button', - onClick: () => this.toggleOpen(), - }, [this.#labelElem])); - this.pushContainer(new Container()); - this.name(name); - this.open(); - } - open(open = true) { - this.domElement.classList.toggle('muigui-closed', !open); - this.domElement.classList.toggle('muigui-open', open); - return this; - } - close() { - return this.open(false); - } - name(name) { - this.#labelElem.textContent = name; - return this; - } - title(title) { - return this.name(title); - } - toggleOpen() { - this.open(!this.domElement.classList.contains('muigui-open')); - return this; - } -} - -// This feels like it should be something else like -// gui.addDividing = new Controller() -class Label extends Controller { - constructor(text) { - super('muigui-label'); - this.text(text); - } - text(text) { - this.domElement.textContent = text; - return this; - } -} - -function noop$1() { -} - -function computeRelativePosition(elem, event, start) { - const rect = elem.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const nx = x / rect.width; - const ny = y / rect.height; - start = start || [x, y]; - const dx = x - start[0]; - const dy = y - start[1]; - const ndx = dx / rect.width; - const ndy = dy / rect.width; - return {x, y, nx, ny, dx, dy, ndx, ndy}; -} - -function addTouchEvents(elem, {onDown = noop$1, onMove = noop$1, onUp = noop$1}) { - let start; - const pointerMove = function(event) { - const e = { - type: 'move', - ...computeRelativePosition(elem, event, start), - }; - onMove(e); - }; - - const pointerUp = function(event) { - elem.releasePointerCapture(event.pointerId); - elem.removeEventListener('pointermove', pointerMove); - elem.removeEventListener('pointerup', pointerUp); - - document.body.style.backgroundColor = ''; - - onUp('up'); - }; - - const pointerDown = function(event) { - elem.addEventListener('pointermove', pointerMove); - elem.addEventListener('pointerup', pointerUp); - elem.setPointerCapture(event.pointerId); - - const rel = computeRelativePosition(elem, event); - start = [rel.x, rel.y]; - onDown({ - type: 'down', - ...rel, - }); - }; - - elem.addEventListener('pointerdown', pointerDown); - - return function() { - elem.removeEventListener('pointerdown', pointerDown); - }; -} - -const svg$3 = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -function connectFillTargets(elem) { - elem.querySelectorAll('[data-src]').forEach(srcElem => { - const id = getNewId(); - srcElem.id = id; - elem.querySelectorAll(`[data-target=${srcElem.dataset.src}]`).forEach(targetElem => { - targetElem.setAttribute('fill', `url(#${id})`); - }); - }); - return elem; -} - -// Was originally going to make alpha an option. Issue is -// hard coded conversions? -class ColorChooserView extends EditView { - #to; - #from; - #satLevelElem; - #circleElem; - #hueUIElem; - #hueElem; - #hueCursorElem; - #alphaUIElem; - #alphaElem; - #alphaCursorElem; - #hsva; - #skipHueUpdate; - #skipSatLevelUpdate; - #skipAlphaUpdate; - #options = { - converters: identity, - alpha: false, - }; - #convertInternalToHex; - #convertHexToInternal; - - constructor(setter, options) { - super(createElem('div', { - innerHTML: svg$3, - className: 'muigui-no-scroll', - })); - this.#satLevelElem = this.domElement.children[0]; - this.#hueUIElem = this.domElement.children[1]; - this.#alphaUIElem = this.domElement.children[2]; - connectFillTargets(this.#satLevelElem); - connectFillTargets(this.#hueUIElem); - connectFillTargets(this.#alphaUIElem); - this.#circleElem = this.$('.muigui-color-chooser-circle'); - this.#hueElem = this.$('[data-src=muigui-color-chooser-hue]'); - this.#hueCursorElem = this.$('.muigui-color-chooser-hue-cursor'); - this.#alphaElem = this.$('[data-src=muigui-color-chooser-alpha]'); - this.#alphaCursorElem = this.$('.muigui-color-chooser-alpha-cursor'); - - const handleSatLevelChange = (e) => { - const s = clamp$1(e.nx, 0, 1); - const v = clamp$1(e.ny, 0, 1); - this.#hsva[1] = s; - this.#hsva[2] = (1 - v); - this.#skipHueUpdate = true; - this.#skipAlphaUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - const handleHueChange = (e) => { - const h = clamp$1(e.nx, 0, 1); - this.#hsva[0] = h; - this.#skipSatLevelUpdate = true; - this.#skipAlphaUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - const handleAlphaChange = (e) => { - const a = clamp$1(e.nx, 0, 1); - this.#hsva[3] = a; - this.#skipHueUpdate = true; - this.#skipSatLevelUpdate = true; - const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); - if (valid) { - setter.setValue(newV); - } - }; - - addTouchEvents(this.#satLevelElem, { - onDown: handleSatLevelChange, - onMove: handleSatLevelChange, - }); - addTouchEvents(this.#hueUIElem, { - onDown: handleHueChange, - onMove: handleHueChange, - }); - addTouchEvents(this.#alphaUIElem, { - onDown: handleAlphaChange, - onMove: handleAlphaChange, - }); - this.setOptions(options); - } - updateDisplay(newV) { - if (!this.#hsva) { - this.#hsva = this.#convertHexToInternal(this.#to(newV)); - } - { - const [h, s, v, a = 1] = this.#convertHexToInternal(this.#to(newV)); - // Don't copy the hue if it was un-computable. - if (!this.#skipHueUpdate) { - this.#hsva[0] = s > 0.001 && v > 0.001 ? h : this.#hsva[0]; - } - if (!this.#skipSatLevelUpdate) { - this.#hsva[1] = s; - this.#hsva[2] = v; - } - if (!this.#skipAlphaUpdate) { - this.#hsva[3] = a; - } - } - { - const [h, s, v, a] = this.#hsva; - const [hue, sat, lum] = rgbaFloatToHsla01(hsva01ToRGBAFloat(this.#hsva)); - - if (!this.#skipHueUpdate) { - this.#hueCursorElem.setAttribute('transform', `translate(${h * 64}, 0)`); - } - this.#hueElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} 0% 100% / ${a})`); - this.#hueElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} 100% 50% / ${a})`); - if (!this.#skipAlphaUpdate) { - this.#alphaCursorElem.setAttribute('transform', `translate(${a * 64}, 0)`); - } - this.#alphaElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 0)`); - this.#alphaElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 1)`); - - if (!this.#skipSatLevelUpdate) { - this.#circleElem.setAttribute('cx', `${s * 64}`); - this.#circleElem.setAttribute('cy', `${(1 - v) * 48}`); - } - } - this.#skipHueUpdate = false; - this.#skipSatLevelUpdate = false; - this.#skipAlphaUpdate = false; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {converters: {to, from}, alpha} = this.#options; - this.#alphaUIElem.style.display = alpha ? '' : 'none'; - this.#convertInternalToHex = alpha - ? v => floatRGBAToHex(hsva01ToRGBAFloat(v)) - : v => floatRGBToHex(hsv01ToRGBFloat(v)); - this.#convertHexToInternal = alpha - ? v => rgbaFloatToHSVA01(hexToFloatRGBA(v)) - : v => rgbFloatToHSV01(hexToFloatRGB(v)); - this.#to = to; - this.#from = from; - return this; - } -} - -/* - -holder = new TabHolder -tab = holder.add(new Tab("name")) -tab.add(...) - - -pc = new PopdownController -top = pc.add(new Row()) -top.add(new Button()); -values = topRow.add(new Div()) -bottom = pc.add(new Row()); - - - -pc = new PopdownController -pc.addTop -pc.addTop - -pc.addBottom - - -*/ - -class PopDownController extends ValueController { - #top; - #valuesView; - #checkboxElem; - #bottom; - #options = { - open: false, - }; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-pop-down-controller'); - /* - [ValueView - [[B][values]] upper row - [[ visual ]] lower row - ] - */ - this.#top = this.add(new ElementView('div', 'muigui-pop-down-top')); -// this.#top.add(new CheckboxView(makeSetter(this.#options, 'open'))); - const checkboxElem = this.#top.addElem(createElem('input', { - type: 'checkbox', - onChange: () => { - this.#options.open = checkboxElem.checked; - this.updateDisplay(); - }, - })); - this.#checkboxElem = checkboxElem; - this.#valuesView = this.#top.add(new ElementView('div', 'muigui-pop-down-values')); - this.#bottom = this.add(new ElementView('div', 'muigui-pop-down-bottom')); - this.setOptions(options); - } - setKnobColor(bgCssColor/*, fgCssColor*/) { - if (this.#checkboxElem) { - this.#checkboxElem.style = ` - --range-color: ${bgCssColor}; - --value-bg-color: ${bgCssColor}; - `; - } - } - updateDisplay() { - super.updateDisplay(); - const {open} = this.#options; - this.domElement.children[1].classList.toggle('muigui-open', open); - this.domElement.children[1].classList.toggle('muigui-closed', !open); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - super.setOptions(options); - this.updateDisplay(); - } - addTop(view) { - return this.#valuesView.add(view); - } - addBottom(view) { - return this.#bottom.add(view); - } -} - -class ColorChooser extends PopDownController { - #colorView; - #textView; - #to; - - constructor(object, property, options = {}) { - super(object, property, 'muigui-color-chooser'); - const format = options.format || guessFormat(this.getValue()); - const {color, text} = colorFormatConverters[format]; - this.#to = color.to; - this.#textView = new TextView(this, {converters: text, alpha: hasAlpha(format)}); - this.#colorView = new ColorChooserView(this, {converters: color, alpha: hasAlpha(format)}); - this.addTop(this.#textView); - this.addBottom(this.#colorView); - // WTF! FIX! - this.__setKnobHelper = () => { - if (this.#to) { - const hex6Or8 = this.#to(this.getValue()); - const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8)); - hsl[2] = (hsl[2] + 50) % 100; - const hex = uint8RGBToHex(hslToRgbUint8(hsl)); - this.setKnobColor(`${hex6Or8.substring(0, 7)}FF`, hex); - } - }; - this.updateDisplay(); - } - updateDisplay() { - super.updateDisplay(); - if (this.__setKnobHelper) { - this.__setKnobHelper(); - } - } - setOptions(options) { - super.setOptions(options); - return this; - } -} - -function showCSS(ob) { - if (ob.prototype.css) { - showCSS(ob.prototype); - } -} - -class Layout extends View { - static css = 'bar'; - constructor(tag, className) { - super(createElem(tag, {className})); - - showCSS(this); - } -} - -/* -class ValueController ?? { - const row = this.add(new Row()); - const label = row.add(new Label()); - const div = row.add(new Div()); - const row = div.add(new Row()); -} -*/ - -/* -class MyCustomThing extends ValueController { - constructor(object, property, options) { - const topRow = this.add(new Row()); - const bottomRow = this.add(new Row()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - topRow.add(new NumberView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - bottomRow.add(new DirectionView()); - } -} - new Grid([ - [new - ] - */ - -class Column extends Layout { - constructor() { - super('div', 'muigui-row'); - } -} - -class Frame extends Layout { - static css = 'foo'; - constructor() { - super('div', 'muigui-frame'); - } - static get foo() { - return 'boo'; - } -} - -class Grid extends Layout { - constructor() { - super('div', 'muigui-grid'); - } -} - -class Row extends Layout { - constructor() { - super('div', 'muigui-row'); - } -} - -class GUIFolder extends Folder { - add(object, property, ...args) { - const controller = object instanceof Controller - ? object - : createController(object, property, ...args); - return this.addController(controller); - } - addCanvas(name) { - return this.addController(new Canvas(name)); - } - addColor(object, property, options = {}) { - const value = object[property]; - if (hasAlpha(options.format || guessFormat(value))) { - return this.addController(new ColorChooser(object, property, options)); - } else { - return this.addController(new Color(object, property, options)); - } - } - addDivider() { - return this.addController(new Divider()); - } - addFolder(name) { - return this.addController(new GUIFolder(name)); - } - addLabel(text) { - return this.addController(new Label(text)); - } -} - -class MuiguiElement extends HTMLElement { - constructor() { - super(); - this.shadow = this.attachShadow({mode: 'open'}); - } -} - -customElements.define('muigui-element', MuiguiElement); - -const baseStyleSheet = new CSSStyleSheet(); -baseStyleSheet.replaceSync(css.default); -const userStyleSheet = new CSSStyleSheet(); - -function makeStyleSheetUpdater(styleSheet) { - let newCss; - let newCssPromise; - - function updateStyle() { - if (newCss && !newCssPromise) { - const s = newCss; - newCss = undefined; - newCssPromise = styleSheet.replace(s).then(() => { - newCssPromise = undefined; - updateStyle(); - }); - } - } - - return function updateStyleSheet(css) { - newCss = css; - updateStyle(); - }; -} - -const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet); -const updateUserStyle = makeStyleSheetUpdater(userStyleSheet); - -class GUI extends GUIFolder { - static converters = converters; - static mapRange = mapRange; - static makeRangeConverters = makeRangeConverters; - static makeRangeOptions = makeRangeOptions; - static makeMinMaxPair = makeMinMaxPair; - #localStyleSheet = new CSSStyleSheet(); - - constructor(options = {}) { - super('Controls', 'muigui-root'); - if (options instanceof HTMLElement) { - options = {parent: options}; - } - const { - autoPlace = true, - width, - title = 'Controls', - } = options; - let { - parent, - } = options; - - if (width) { - this.domElement.style.width = /^\d+$/.test(width) ? `${width}px` : width; - } - if (parent === undefined && autoPlace) { - parent = document.body; - this.domElement.classList.add('muigui-auto-place'); - } - if (parent) { - const muiguiElement = createElem('muigui-element'); - muiguiElement.shadowRoot.adoptedStyleSheets = [baseStyleSheet, userStyleSheet, this.#localStyleSheet]; - muiguiElement.shadow.appendChild(this.domElement); - parent.appendChild(muiguiElement); - } - if (title) { - this.title(title); - } - this.domElement.classList.add('muigui', 'muigui-colors'); - } - setStyle(css) { - this.#localStyleSheet.replace(css); - } - static setBaseStyles(css) { - updateBaseStyle(css); - } - static getBaseStyleSheet() { - return baseStyleSheet; - } - static setUserStyles(css) { - updateUserStyle(css); - } - static getUserStyleSheet() { - return userStyleSheet; - } - static setTheme(name) { - GUI.setBaseStyles(`${css.default}\n${css.themes[name] || ''}`); - } -} - -function noop() { -} - -const keyDirections = { - ArrowLeft: [-1, 0], - ArrowRight: [1, 0], - ArrowUp: [0, -1], - ArrowDown: [0, 1], -}; - -// This probably needs to be global -function addKeyboardEvents(elem, {onDown = noop, onUp = noop}) { - const keyDown = function(event) { - const mult = event.shiftKey ? 10 : 1; - const [dx, dy] = (keyDirections[event.key] || [0, 0]).map(v => v * mult); - const fn = event.type === 'keydown' ? onDown : onUp; - fn({ - type: event.type.substring(3), - dx, - dy, - event, - }); - }; - - elem.addEventListener('keydown', keyDown); - elem.addEventListener('keyup', keyDown); - - return function() { - elem.removeEventListener('keydown', keyDown); - elem.removeEventListener('keyup', keyDown); - }; -} - -function assert(truthy, msg = '') { - if (!truthy) { - throw new Error(msg); - } -} - -function getEllipsePointForAngle(cx, cy, rx, ry, phi, theta) { - const m = Math.abs(rx) * Math.cos(theta); - const n = Math.abs(ry) * Math.sin(theta); - - return [ - cx + Math.cos(phi) * m - Math.sin(phi) * n, - cy + Math.sin(phi) * m + Math.cos(phi) * n, - ]; -} - -function getEndpointParameters(cx, cy, rx, ry, phi, theta, dTheta) { - const [x1, y1] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta); - const [x2, y2] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta + dTheta); - - const fa = Math.abs(dTheta) > Math.PI ? 1 : 0; - const fs = dTheta > 0 ? 1 : 0; - - return { x1, y1, x2, y2, fa, fs }; -} - -function arc(cx, cy, r, start, end) { - assert(Math.abs(start - end) <= Math.PI * 2); - assert(start >= -Math.PI && start <= Math.PI * 2); - assert(start <= end); - assert(end >= -Math.PI && end <= Math.PI * 4); - - const { x1, y1, x2, y2, fa, fs } = getEndpointParameters(cx, cy, r, r, 0, start, end - start); - return Math.abs(Math.abs(start - end) - Math.PI * 2) > Number.EPSILON - ? `M${cx} ${cy} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2} L${cx} ${cy}` - : `M${x1} ${y1} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2}`; -} - -const svg$2 = ` - - - - - - - - - -`; - -const twoPiMod = v => euclideanModulo$1(v + Math.PI, Math.PI * 2) - Math.PI; - -class DirectionView extends EditView { - #arrowElem; - #rangeElem; - #lastV; - #wrap; - #options = { - step: 1, - min: -180, - max: 180, - - /* - -------- - / -ฯ€/2 \ - / | \ - |<- -ฯ€ * | - | * 0 ->| zero is down the positive X axis - |<- +ฯ€ * | - \ | / - \ ฯ€/2 / - -------- - */ - dirMin: -Math.PI, - dirMax: Math.PI, - //dirMin: Math.PI * 0.5, - //dirMax: Math.PI * 2.5, - //dirMin: -Math.PI * 0.75, // test 10:30 to 7:30 - //dirMax: Math.PI * 0.75, - //dirMin: Math.PI * 0.75, // test 7:30 to 10:30 - //dirMax: -Math.PI * 0.75, - //dirMin: -Math.PI * 0.75, // test 10:30 to 1:30 - //dirMax: -Math.PI * 0.25, - //dirMin: Math.PI * 0.25, // test 4:30 to 7:30 - //dirMax: Math.PI * 0.75, - //dirMin: Math.PI * 0.75, // test 4:30 to 7:30 - //dirMax: Math.PI * 0.25, - wrap: undefined, - converters: identity, - }; - - constructor(setter, options = {}) { - const wheelHelper = createWheelHelper(); - super(createElem('div', { - className: 'muigui-direction muigui-no-scroll', - innerHTML: svg$2, - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - let tempV = this.#lastV + delta; - if (this.#wrap) { - tempV = euclideanModulo$1(tempV - min, max - min) + min; - } - const newV = clamp$1(stepify(tempV, v => v, step), min, max); - setter.setValue(newV); - }, - })); - const handleTouch = (e) => { - const {min, max, step, dirMin, dirMax} = this.#options; - const nx = e.nx * 2 - 1; - const ny = e.ny * 2 - 1; - const a = Math.atan2(ny, nx); - - const center = (dirMin + dirMax) / 2; - - const centeredAngle = twoPiMod(a - center); - const centeredStart = twoPiMod(dirMin - center); - const diff = dirMax - dirMin; - - const n = clamp$1((centeredAngle - centeredStart) / (diff), 0, 1); - const newV = stepify(min + (max - min) * n, v => v, step); - setter.setValue(newV); - }; - addTouchEvents(this.domElement, { - onDown: handleTouch, - onMove: handleTouch, - }); - addKeyboardEvents(this.domElement, { - onDown: (e) => { - const {min, max, step} = this.#options; - const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max); - setter.setValue(newV); - }, - }); - this.#arrowElem = this.$('#muigui-arrow'); - this.#rangeElem = this.$('#muigui-range'); - this.setOptions(options); - } - updateDisplay(v) { - this.#lastV = v; - const {min, max} = this.#options; - const n = (v - min) / (max - min); - const angle = lerp$1(this.#options.dirMin, this.#options.dirMax, n); - this.#arrowElem.style.transform = `rotate(${angle}rad)`; - } - setOptions(options) { - copyExistingProperties(this.#options, options); - const {dirMin, dirMax, wrap} = this.#options; - this.#wrap = wrap !== undefined - ? wrap - : Math.abs(dirMin - dirMax) >= Math.PI * 2 - Number.EPSILON; - const [min, max] = dirMin < dirMax ? [dirMin, dirMax] : [dirMax , dirMin]; - this.#rangeElem.setAttribute('d', arc(0, 0, 28.87, min, max)); - } -} - -// deg2rad -// where is 0 -// range (0, 360), (-180, +180), (0,0) Really this is a range - -class Direction extends PopDownController { - #options; - constructor(object, property, options) { - super(object, property, 'muigui-direction'); -this.#options = options; // FIX - this.addTop(new NumberView(this, -identity)); - this.addBottom(new DirectionView(this, options)); - this.updateDisplay(); - } -} - -class RadioGridView extends EditView { - #values; - - constructor(setter, keyValues, cols = 3) { - const values = []; - const name = makeId(); - super(createElem('div', {}, keyValues.map(([key, value], ndx) => { - values.push(value); - return createElem('label', {}, [ - createElem('input', { - type: 'radio', - name, - value: ndx, - onChange: function() { - if (this.checked) { - setter.setFinalValue(that.#values[this.value]); - } - }, - }), - createElem('button', { - type: 'button', - textContent: key, - onClick: function() { - this.previousElementSibling.click(); - }, - }), - ]); - }))); - const that = this; - this.#values = values; - this.cols(cols); - } - updateDisplay(v) { - const ndx = this.#values.indexOf(v); - for (let i = 0; i < this.domElement.children.length; ++i) { - this.domElement.children[i].children[0].checked = i === ndx; - } - } - cols(cols) { - this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; - } -} - -class RadioGrid extends ValueController { - constructor(object, property, options) { - super(object, property, 'muigui-radio-grid'); - const valueIsNumber = typeof this.getValue() === 'number'; - const { - keyValues: keyValuesInput, - cols = 3, - } = options; - const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); - this.add(new RadioGridView(this, keyValues, cols)); - this.updateDisplay(); - } -} - -function onResize(elem, callback) { - new ResizeObserver(() => { - callback({rect: elem.getBoundingClientRect(), elem}); - }).observe(elem); -} - -function onResizeSVGNoScale(elem, hAnchor, vAnchor, callback) { - onResize(elem, ({rect}) => { - const {width, height} = rect; - elem.setAttribute('viewBox', `-${width * hAnchor} -${height * vAnchor} ${width} ${height}`); - callback({elem, rect}); - }); -} - -function onResizeCanvas(elem, callback) { - onResize(elem, ({rect}) => { - const {width, height} = rect; - elem.width = width; - elem.height = height; - callback({elem, rect}); - }); -} - -const svg$1 = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -function createSVGTicks(start, end, step, min, max, height) { - const p = []; - if (start < min) { - start += stepify(min - start, v => v, step); - } - end = Math.min(end, max); - for (let i = start; i <= end; i += step) { - p.push(`M${i} 0 l0 ${height}`); - } - return p.join(' '); -} - -function createSVGNumbers(start, end, unitSize, unit, minusSize, min, max, labelFn) { - const texts = []; - if (start < min) { - start += stepify(min - start, v => v, unitSize); - } - end = Math.min(end, max); - const digits = Math.max(0, -Math.log10(unit)); - const f = v => labelFn(v.toFixed(digits)); - for (let i = start; i <= end; i += unitSize) { - texts.push(`${f(i / unitSize * unit)}`); - } - return texts.join('\n'); -} - -function computeSizeOfMinus(elem) { - const oldHTML = elem.innerHTML; - elem.innerHTML = '- '; - const text = elem.querySelector('text'); - const size = text.getComputedTextLength(); - elem.innerHTML = oldHTML; - return size; -} - -class SliderView extends EditView { - #svgElem; - #originElem; - #ticksElem; - #thicksElem; - #numbersElem; - #leftGradElem; - #rightGradElem; - #width; - #height; - #lastV; - #minusSize; - #options = { - min: -100, - max: 100, - step: 1, - unit: 10, - unitSize: 10, - ticksPerUnit: 5, - labelFn: v => v, - tickHeight: 1, - limits: true, - thicksColor: undefined, - orientation: undefined, - }; - - constructor(setter, options) { - const wheelHelper = createWheelHelper(); - super(createElem('div', { - innerHTML: svg$1, - className: 'muigui-no-v-scroll', - onWheel: e => { - e.preventDefault(); - const {min, max, step} = this.#options; - const delta = wheelHelper(e, step); - const newV = clamp$1(stepify(this.#lastV + delta, v => v, step), min, max); - setter.setValue(newV); - }, - })); - this.#svgElem = this.$('svg'); - this.#originElem = this.$('#muigui-origin'); - this.#ticksElem = this.$('#muigui-ticks'); - this.#thicksElem = this.$('#muigui-thicks'); - this.#numbersElem = this.$('#muigui-numbers'); - this.#leftGradElem = this.$('#muigui-left-grad'); - this.#rightGradElem = this.$('#muigui-right-grad'); - this.setOptions(options); - let startV; - addTouchEvents(this.domElement, { - onDown: () => { - startV = this.#lastV; - }, - onMove: (e) => { - const {min, max, unitSize, unit, step} = this.#options; - const newV = clamp$1(stepify(startV - e.dx / unitSize * unit, v => v, step), min, max); - setter.setValue(newV); - }, - }); - addKeyboardEvents(this.domElement, { - onDown: (e) => { - const {min, max, step} = this.#options; - const newV = clamp$1(stepify(this.#lastV + e.dx * step, v => v, step), min, max); - setter.setValue(newV); - }, - }); - onResizeSVGNoScale(this.#svgElem, 0.5, 0, ({rect: {width}}) => { - this.#leftGradElem.setAttribute('x', -width / 2); - this.#rightGradElem.setAttribute('x', width / 2 - 20); - this.#minusSize = computeSizeOfMinus(this.#numbersElem); - this.#width = width; - this.#updateSlider(); - }); - } - // |--------V--------| - // . . | . . . | . . . | - // - #updateSlider() { - // There's no size if ResizeObserver has not fired yet. - if (!this.#width || this.#lastV === undefined) { - return; - } - const { - labelFn, - limits, - min, - max, - orientation, - tickHeight, - ticksPerUnit, - unit, - unitSize, - thicksColor, - } = this.#options; - const unitsAcross = Math.ceil(this.#width / unitSize); - const center = this.#lastV; - const centerUnitSpace = center / unit; - const startUnitSpace = Math.round(centerUnitSpace - unitsAcross); - const endUnitSpace = startUnitSpace + unitsAcross * 2; - const start = startUnitSpace * unitSize; - const end = endUnitSpace * unitSize; - const minUnitSpace = limits ? min * unitSize / unit : start; - const maxUnitSpace = limits ? max * unitSize / unit : end; - const height = labelFn(1) === '' ? 10 : 5; - if (ticksPerUnit > 1) { - this.#ticksElem.setAttribute('d', createSVGTicks(start, end, unitSize / ticksPerUnit, minUnitSpace, maxUnitSpace, height * tickHeight)); - } - this.#thicksElem.style.stroke = thicksColor; //setAttribute('stroke', thicksColor); - this.#thicksElem.setAttribute('d', createSVGTicks(start, end, unitSize, minUnitSpace, maxUnitSpace, height)); - this.#numbersElem.innerHTML = createSVGNumbers(start, end, unitSize, unit, this.#minusSize, minUnitSpace, maxUnitSpace, labelFn); - this.#originElem.setAttribute('transform', `translate(${-this.#lastV * unitSize / unit} 0)`); - this.#svgElem.classList.toggle('muigui-slider-up', orientation === 'up'); - } - updateDisplay(v) { - this.#lastV = v; - this.#updateSlider(); - } - setOptions(options) { - copyExistingProperties(this.#options, options); - return this; - } -} - -class Slider extends ValueController { - constructor(object, property, options = {}) { - super(object, property, 'muigui-slider'); - this.add(new SliderView(this, options)); - this.add(new NumberView(this, options)); - this.updateDisplay(); - } -} - -const svg = ` - - - - - - - -`; - -class Vec2View extends EditView { - #svgElem; - #arrowElem; - #circleElem; - #lastV = []; - - constructor(setter) { - super(createElem('div', { - innerHTML: svg, - className: 'muigui-no-scroll', - })); - const onTouch = (e) => { - const {width, height} = this.#svgElem.getBoundingClientRect(); - const nx = e.nx * 2 - 1; - const ny = e.ny * 2 - 1; - setter.setValue([nx * width * 0.5, ny * height * 0.5]); - }; - addTouchEvents(this.domElement, { - onDown: onTouch, - onMove: onTouch, - }); - this.#svgElem = this.$('svg'); - this.#arrowElem = this.$('#muigui-arrow'); - this.#circleElem = this.$('#muigui-circle'); - onResizeSVGNoScale(this.#svgElem, 0.5, 0.5, () => this.#updateDisplayImpl); - } - #updateDisplayImpl() { - const [x, y] = this.#lastV; - this.#arrowElem.setAttribute('d', `M0,0L${x},${y}`); - this.#circleElem.setAttribute('transform', `translate(${x}, ${y})`); - } - updateDisplay(v) { - this.#lastV[0] = v[0]; - this.#lastV[1] = v[1]; - this.#updateDisplayImpl(); - } -} - -// TODO: zoom with wheel and pinch? -// TODO: grid? -// // options -// scale: -// range: number (both x and y + /) -// range: array (min, max) -// xRange: -// deg/rad/turn - -class Vec2 extends PopDownController { - constructor(object, property) { - super(object, property, 'muigui-vec2'); - - const makeSetter = (ndx) => { - return { - setValue: (v) => { - const newV = this.getValue(); - newV[ndx] = v; - this.setValue(newV); - }, - setFinalValue: (v) => { - const newV = this.getValue(); - newV[ndx] = v; - this.setFinalValue(newV); - }, - }; - }; - - this.addTop(new NumberView(makeSetter(0), { - converters: { - to: v => v[0], - from: strToNumber.from, - }, - })); - this.addTop(new NumberView(makeSetter(1), { - converters: { - to: v => v[1], - from: strToNumber.from, - }, - })); - this.addBottom(new Vec2View(this)); - this.updateDisplay(); - } -} - -export { ColorChooser, Direction, RadioGrid, Range, Select, Slider, TextNumber, Vec2, GUI as default }; -//# sourceMappingURL=muigui.module.js.map diff --git a/dist/0.x/muigui.module.min.js b/dist/0.x/muigui.module.min.js deleted file mode 100644 index ed1d8b4..0000000 --- a/dist/0.x/muigui.module.min.js +++ /dev/null @@ -1,2 +0,0 @@ -var t={default:'\n.muigui {\n --bg-color: #ddd;\n --color: #222;\n --contrast-color: #eee;\n --value-color: #145 ;\n --value-bg-color: #eeee;\n --disabled-color: #999;\n --menu-bg-color: #f8f8f8;\n --menu-sep-color: #bbb;\n --hover-bg-color: #999;\n --focus-color: #68C;\n --range-color: #888888;\n --invalid-color: #FF0000;\n --selected-color: rgb(255, 255, 255, 0.9);\n\n --button-bg-color: var(--value-bg-color);\n\n --range-left-color: var(--value-color);\n --range-right-color: var(--value-bg-color); \n --range-right-hover-color: var(--hover-bg-color);\n\n color: var(--color);\n background-color: var(--bg-color);\n}\n\n@media (prefers-color-scheme: dark) {\n .muigui {\n --bg-color: #222222;\n --color: #dddddd;\n --contrast-color: #000;\n --value-color: #43e5f7;\n --value-bg-color: #444444;\n --disabled-color: #666666;\n --menu-bg-color: #080808;\n --menu-sep-color: #444444;\n --hover-bg-color: #666666;\n --focus-color: #88AAFF;\n --range-color: #888888;\n --invalid-color: #FF6666;\n --selected-color: rgba(255, 255, 255, 0.3);\n\n --button-bg-color: var(--value-bg-color);\n\n --range-left-color: var(--value-color);\n --range-right-color: var(--value-bg-color); \n --range-right-hover-color: var(--hover-bg-color);\n\n color: var(--color);\n background-color: var(--bg-color);\n }\n}\n\n.muigui {\n --width: 250px;\n --label-width: 45%;\n --number-width: 40%;\n\n\n --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;\n --font-size: 11px;\n --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;\n --font-size-mono: 11px;\n\n --line-height: 1.7em;\n --border-radius: 0px;\n\n width: var(--width);\n font-family: var(--font-family);\n font-size: var(--font-size);\n box-sizing: border-box;\n line-height: 100%;\n}\n.muigui * {\n box-sizing: inherit;\n}\n\n.muigui-no-scroll {\n touch-action: none;\n}\n.muigui-no-h-scroll {\n touch-action: pan-y;\n}\n.muigui-no-v-scroll {\n touch-action: pan-x;\n}\n\n.muigui-invalid-value {\n background-color: red !important;\n color: white !important;\n}\n\n.muigui-grid {\n display: grid;\n}\n.muigui-rows {\n display: flex;\n flex-direction: column;\n\n min-height: 20px;\n border: 2px solid red;\n}\n.muigui-columns {\n display: flex;\n flex-direction: row;\n\n height: 20px;\n border: 2px solid green;\n}\n.muigui-rows>*,\n.muigui-columns>* {\n flex: 1 1 auto;\n align-items: stretch;\n min-height: 0;\n min-width: 0;\n}\n\n.muigui-row {\n border: 2px solid yellow;\n min-height: 10px\n}\n.muigui-column {\n border: 2px solid lightgreen;\n}\n\n/* -------- */\n\n.muigui-show { /* */ }\n.muigui-hide { \n display: none !important;\n}\n.muigui-disabled {\n pointer-events: none;\n --color: var(--disabled-color) !important;\n --value-color: var(--disabled-color) !important;\n --range-left-color: var(--disabled-color) !important;\n}\n\n.muigui canvas,\n.muigui svg {\n display: block;\n border-radius: var(--border-radius);\n}\n.muigui canvas {\n background-color: var(--value-bg-color);\n}\n\n.muigui-controller {\n min-width: 0;\n min-height: var(--line-height);\n}\n.muigui-root,\n.muigui-menu {\n display: flex;\n flex-direction: column;\n position: relative;\n user-select: none;\n height: fit-content;\n margin: 0;\n padding-bottom: 0.1em;\n border-radius: var(--border-radius);\n}\n.muigui-menu {\n border-bottom: 1px solid var(--menu-sep-color);\n}\n\n.muigui-root>button:nth-child(1),\n.muigui-menu>button:nth-child(1) {\n border-top: 1px solid var(--menu-sep-color);\n border-bottom: 1px solid var(--menu-sep-color);\n position: relative;\n text-align: left;\n color: var(--color);\n background-color: var(--menu-bg-color);\n min-height: var(--line-height);\n padding-top: 0.2em;\n padding-bottom: 0.2em;\n cursor: pointer;\n border-radius: var(--border-radius);\n}\n.muigui-root>div:nth-child(2),\n.muigui-menu>div:nth-child(2) {\n flex: 1 1 auto;\n}\n\n.muigui-controller {\n margin-left: 0.2em;\n margin-right: 0.2em;\n}\n.muigui-root.muigui-controller,\n.muigui-menu.muigui-controller {\n margin-left: 0;\n margin-right: 0;\n}\n.muigui-controller>*:nth-child(1) {\n flex: 1 0 var(--label-width);\n min-width: 0;\n white-space: pre;\n}\n.muigui-controller>label:nth-child(1) {\n place-content: center start;\n display: inline-grid;\n overflow: hidden;\n}\n.muigui-controller>*:nth-child(2) {\n flex: 1 1 75%;\n min-width: 0;\n}\n\n/* -----------------------------------------\n a label controller is [[label][value]]\n*/\n\n.muigui-label-controller {\n display: flex;\n margin: 0.4em 0 0.4em 0;\n word-wrap: initial;\n align-items: stretch;\n}\n\n.muigui-value {\n display: flex;\n align-items: stretch;\n}\n.muigui-value>* {\n flex: 1 1 auto;\n min-width: 0;\n}\n.muigui-value>*:nth-child(1) {\n flex: 1 1 calc(100% - var(--number-width));\n}\n.muigui-value>*:nth-child(2) {\n flex: 1 1 var(--number-width);\n margin-left: 0.2em;\n}\n\n/* fix! */\n.muigui-open>button>label::before,\n.muigui-closed>button>label::before {\n width: 1.25em;\n height: var(--line-height);\n display: inline-grid;\n place-content: center start;\n pointer-events: none;\n}\n.muigui-open>button>label::before {\n content: "โ“ง"; /*"โ–ผ";*/\n}\n.muigui-closed>button>label::before {\n content: "โจ"; /*"โ–ถ";*/\n}\n.muigui-open>*:nth-child(2) {\n transition: max-height 0.2s ease-out,\n opacity 0.5s ease-out;\n max-height: 100vh;\n overflow: auto;\n opacity: 1;\n}\n\n.muigui-closed>*:nth-child(2) {\n transition: max-height 0.2s ease-out,\n opacity 1s;\n max-height: 0;\n opacity: 0;\n overflow: hidden;\n}\n\n/* ---- popdown ---- */\n\n.muigui-pop-down-top {\n display: flex;\n}\n/* fix? */\n.muigui-value>*:nth-child(1).muigui-pop-down-top {\n flex: 0;\n}\n.muigui-pop-down-bottom {\n\n}\n\n.muigui-pop-down-values {\n min-width: 0;\n display: flex;\n}\n.muigui-pop-down-values>* {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.muigui-value.muigui-pop-down-controller {\n flex-direction: column;\n}\n\n.muigui-pop-down-top input[type=checkbox] {\n -webkit-appearance: none;\n appearance: none;\n width: auto;\n color: var(--value-color);\n background-color: var(--value-bg-color);\n cursor: pointer;\n\n display: grid;\n place-content: center;\n margin: 0;\n font: inherit;\n color: currentColor;\n width: 1.7em;\n height: 1.7em;\n transform: translateY(-0.075em);\n}\n\n.muigui-pop-down-top input[type=checkbox]::before {\n content: "+";\n display: grid;\n place-content: center;\n border-radius: calc(var(--border-radius) + 2px);\n border-left: 1px solid rgba(255,255,255,0.3);\n border-top: 1px solid rgba(255,255,255,0.3);\n border-bottom: 1px solid rgba(0,0,0,0.2);\n border-right: 1px solid rgba(0,0,0,0.2);\n background-color: var(--range-color);\n color: var(--value-bg-color);\n width: calc(var(--line-height) - 4px);\n height: calc(var(--line-height) - 4px);\n}\n\n.muigui-pop-down-top input[type=checkbox]:checked::before {\n content: "๏ผธ";\n}\n\n\n/* ---- select ---- */\n\n.muigui select,\n.muigui option,\n.muigui input,\n.muigui button {\n color: var(--value-color);\n background-color: var(--value-bg-color);\n font-family: var(--font-family);\n font-size: var(--font-size);\n border: none;\n margin: 0;\n border-radius: var(--border-radius);\n}\n.muigui select {\n appearance: none;\n margin: 0;\n margin-left: 0; /*?*/\n overflow: hidden; /* Safari */\n}\n\n.muigui select:focus,\n.muigui input:focus,\n.muigui button:focus {\n outline: 1px solid var(--focus-color);\n}\n\n.muigui select:hover,\n.muigui option:hover,\n.muigui input:hover,\n.muigui button:hover {\n background-color: var(--hover-bg-color); \n}\n\n/* ------ [ label ] ------ */\n\n.muigui-label {\n border-top: 1px solid var(--menu-sep-color);\n border-bottom: 1px solid var(--menu-sep-color);\n padding-top: 0.4em;\n padding-bottom: 0.3em;\n place-content: center start;\n background-color: var(--menu-bg-color);\n white-space: pre;\n border-radius: var(--border-radius);\n}\n\n/* ------ [ divider] ------ */\n\n.muigui-divider {\n min-height: 6px;\n border-top: 2px solid var(--menu-sep-color);\n margin-top: 6px;\n}\n\n/* ------ [ button ] ------ */\n\n.muigui-button {\n display: grid;\n\n}\n.muigui-button button {\n border: none;\n color: var(--value-color);\n background-color: var(--button-bg-color);\n cursor: pointer;\n place-content: center center;\n}\n\n/* ------ [ color ] ------ */\n\n.muigui-color>div {\n overflow: hidden;\n position: relative;\n margin-left: 0;\n margin-right: 0; /* why? */\n max-width: var(--line-height);\n border-radius: var(--border-radius);\n}\n\n.muigui-color>div:focus-within {\n outline: 1px solid var(--focus-color);\n}\n\n.muigui-color input[type=color] {\n border: none;\n padding: 0;\n background: inherit;\n cursor: pointer;\n position: absolute;\n width: 200%;\n left: -10px;\n top: -10px;\n height: 200%;\n}\n.muigui-disabled canvas,\n.muigui-disabled svg,\n.muigui-disabled img,\n.muigui-disabled .muigui-color input[type=color] {\n opacity: 0.2;\n}\n\n/* ------ [ checkbox ] ------ */\n\n.muigui-checkbox>label:nth-child(2) {\n display: grid;\n place-content: center start;\n margin: 0;\n}\n\n.muigui-checkbox input[type=checkbox] {\n -webkit-appearance: none;\n appearance: none;\n width: auto;\n color: var(--value-color);\n background-color: var(--value-bg-color);\n cursor: pointer;\n\n display: grid;\n place-content: center;\n margin: 0;\n font: inherit;\n color: currentColor;\n width: 1.7em;\n height: 1.7em;\n transform: translateY(-0.075em);\n}\n\n.muigui-checkbox input[type=checkbox]::before {\n content: "";\n color: var(--value-color);\n display: grid;\n place-content: center;\n}\n\n.muigui-checkbox input[type=checkbox]:checked::before {\n content: "โœ”";\n}\n\n.muigui input[type=number]::-webkit-inner-spin-button, \n.muigui input[type=number]::-webkit-outer-spin-button { \n -webkit-appearance: none;\n appearance: none;\n margin: 0; \n}\n.muigui input[type=number] {\n -moz-appearance: textfield;\n}\n\n/* ------ [ radio grid ] ------ */\n\n.muigui-radio-grid>div {\n display: grid;\n gap: 2px;\n}\n\n.muigui-radio-grid input {\n appearance: none;\n display: none;\n}\n\n.muigui-radio-grid button {\n color: var(--color);\n width: 100%;\n text-align: left;\n}\n\n.muigui-radio-grid input:checked + button {\n color: var(--value-color);\n background-color: var(--selected-color);\n}\n\n/* ------ [ color-chooser ] ------ */\n\n.muigui-color-chooser-cursor {\n stroke-width: 1px;\n stroke: white;\n fill: none;\n}\n.muigui-color-chooser-circle {\n stroke-width: 1px;\n stroke: white;\n fill: none;\n}\n\n\n/* ------ [ vec2 ] ------ */\n\n.muigui-vec2 svg {\n background-color: var(--value-bg-color);\n}\n\n.muigui-vec2-axis {\n stroke: 1px;\n stroke: var(--focus-color);\n}\n\n.muigui-vec2-line {\n stroke-width: 1px;\n stroke: var(--value-color);\n fill: var(--value-color);\n}\n\n/* ------ [ direction ] ------ */\n\n.muigui-direction svg {\n background-color: rgba(0,0,0,0.2);\n}\n\n.muigui-direction:focus-within svg {\n outline: none;\n}\n.muigui-direction-range {\n fill: var(--value-bg-color);\n}\n.muigui-direction svg:focus {\n outline: none;\n}\n.muigui-direction svg:focus .muigui-direction-range {\n stroke-width: 0.5px;\n stroke: var(--focus-color);\n}\n\n.muigui-direction-arrow {\n fill: var(--value-color);\n}\n\n/* ------ [ slider ] ------ */\n\n.muigui-slider>div {\n display: flex;\n align-items: stretch;\n height: var(--line-height);\n}\n.muigui-slider svg {\n flex: 1 1 auto;\n}\n.muigui-slider .muigui-slider-up #muigui-orientation {\n transform: scale(1, -1) translateY(-100%);\n}\n\n.muigui-slider .muigui-slider-up #muigui-number-orientation {\n transform: scale(1,-1);\n}\n\n.muigui-ticks {\n stroke: var(--range-color);\n}\n.muigui-thicks {\n stroke: var(--color);\n stroke-width: 2px;\n}\n.muigui-svg-text {\n fill: var(--color);\n font-size: 7px;\n}\n.muigui-mark {\n fill: var(--value-color);\n}\n\n/* ------ [ range ] ------ */\n\n\n.muigui-range input[type=range] {\n -webkit-appearance: none;\n appearance: none;\n background-color: transparent;\n}\n\n.muigui-range input[type=range]::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n border-radius: calc(var(--border-radius) + 2px);\n border-left: 1px solid rgba(255,255,255,0.3);\n border-top: 1px solid rgba(255,255,255,0.3);\n border-bottom: 1px solid rgba(0,0,0,0.2);\n border-right: 1px solid rgba(0,0,0,0.2);\n background-color: var(--range-color);\n margin-top: calc((var(--line-height) - 2px) / -2);\n width: calc(var(--line-height) - 2px);\n height: calc(var(--line-height) - 2px);\n}\n\n.muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: none;\n appearance: none;\n border: 1px solid var(--menu-sep-color);\n height: 2px;\n}\n\n\n/* dat.gui style - doesn\'t work on Safari iOS */\n\n/*\n.muigui-range input[type=range] {\n cursor: ew-resize;\n overflow: hidden;\n}\n\n.muigui-range input[type=range] {\n -webkit-appearance: none;\n appearance: none;\n background-color: var(--range-right-color);\n margin: 0;\n}\n.muigui-range input[type=range]:hover {\n background-color: var(--range-right-hover-color);\n}\n\n.muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: none;\n appearance: none;\n height: max-content;\n color: var(--range-left-color);\n margin-top: -1px;\n}\n\n.muigui-range input[type=range]::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 0px;\n height: max-content;\n box-shadow: -1000px 0 0 1000px var(--range-left-color);\n}\n*/\n\n/* FF */\n/*\n.muigui-range input[type=range]::-moz-slider-progress {\n background-color: var(--range-left-color); \n}\n.muigui-range input[type=range]::-moz-slider-thumb {\n height: max-content;\n width: 0;\n border: none;\n box-shadow: -1000px 0 0 1000px var(--range-left-color);\n box-sizing: border-box;\n}\n*/\n\n.muigui-checkered-background {\n background-color: #404040;\n background-image:\n linear-gradient(45deg, #808080 25%, transparent 25%),\n linear-gradient(-45deg, #808080 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #808080 75%),\n linear-gradient(-45deg, transparent 75%, #808080 75%);\n background-size: 16px 16px;\n background-position: 0 0, 0 8px, 8px -8px, -8px 0px;\n}\n\n/* ---------------------------------------------------------- */\n\n/* needs to be at bottom to take precedence */\n.muigui-auto-place {\n max-height: 100%;\n position: fixed;\n top: 0;\n right: 15px;\n z-index: 100001;\n}\n\n',themes:{default:"",float:"\n :root {\n color-scheme: light dark,\n }\n\n .muigui {\n --width: 400px;\n --bg-color: initial;\n --label-width: 25%;\n --number-width: 20%;\n }\n\n input,\n .muigui-label-controller>label {\n text-shadow:\n -1px -1px 0 var(--contrast-color),\n 1px -1px 0 var(--contrast-color),\n -1px 1px 0 var(--contrast-color),\n 1px 1px 0 var(--contrast-color);\n }\n\n .muigui-controller > label:nth-child(1) {\n place-content: center end;\n margin-right: 1em;\n }\n\n .muigui-value > :nth-child(2) {\n margin-left: 1em;\n }\n\n .muigui-root>*:nth-child(1) {\n display: none;\n }\n\n .muigui-range input[type=range]::-webkit-slider-thumb {\n border-radius: 1em;\n }\n\n .muigui-range input[type=range]::-webkit-slider-runnable-track {\n -webkit-appearance: initial;\n appearance: none;\n border: 1px solid rgba(0, 0, 0, 0.25);\n height: 2px;\n }\n\n .muigui-colors {\n --value-color: var(--color );\n --value-bg-color: rgba(0, 0, 0, 0.1);\n --disabled-color: #cccccc;\n --menu-bg-color: rgba(0, 0, 0, 0.1);\n --menu-sep-color: #bbbbbb;\n --hover-bg-color: rgba(0, 0, 0, 0);\n --invalid-color: #FF0000;\n --selected-color: rgba(0, 0, 0, 0.3);\n --range-color: rgba(0, 0, 0, 0.125);\n }\n"}};function e(t,e={},n=[]){const i=document.createElement(t);return function(t,e,n){for(const[n,i]of Object.entries(e))if("function"==typeof i&&n.startsWith("on")){const e=n.substring(2).toLowerCase();t.addEventListener(e,i,{passive:!1})}else if("object"==typeof i)for(const[e,o]of Object.entries(i))t[n][e]=o;else void 0===t[n]?t.setAttribute(n,i):t[n]=i;for(const e of n)t.appendChild(e)}(i,e,n),i}let n=0;function i(t,e){const n=t.indexOf(e);return n&&t.splice(n,1),t}function o(t,e,n){return Math.max(e,Math.min(n,t))}const r="undefined"!=typeof SharedArrayBuffer?function(t){return t&&t.buffer&&(t.buffer instanceof ArrayBuffer||t.buffer instanceof SharedArrayBuffer)}:function(t){return t&&t.buffer&&t.buffer instanceof ArrayBuffer},s=(t,e,n)=>Math.round(e(t)/n)/(1/n),a=(t,e)=>(t%e+e)%e;function l(t,e){for(const n in e)n in t&&(t[n]=e[n]);return t}const u=(t,e,n,i,o)=>(t-e)*(o-i)/(n-e)+i,c=({from:t,to:e})=>({to:n=>u(n,...t,...e),from:n=>[!0,u(n,...e,...t)]}),h=({from:t,to:e,step:n})=>({min:e[0],max:e[1],...n&&{step:n},converters:c({from:t,to:e})}),d={to:t=>t,from:t=>[!0,t]};function p(t,e,n,i,o){const{converters:{from:r}=d}=o,{min:s,max:a}=o,l=o.minRange||0,u=r(l)[1],c=t.add(e,n,{...o,min:s,max:a-l}).onChange((t=>{h.setValue(Math.min(a,Math.max(t+u,e[i])))})),h=t.add(e,i,{...o,min:s+l,max:a}).onChange((t=>{c.setValue(Math.max(s,Math.min(t-u,e[n])))}));return[c,h]}class m{domElement;#t;#e=[];constructor(t){this.domElement=t,this.#t=t}addElem(t){return this.#t.appendChild(t),t}removeElem(t){return this.#t.removeChild(t),t}pushSubElem(t){this.#t.appendChild(t),this.#t=t}popSubElem(){this.#t=this.#t.parentElement}add(t){return this.#e.push(t),this.addElem(t.domElement),t}remove(t){return this.removeElem(t.domElement),i(this.#e,t),t}pushSubView(t){this.pushSubElem(t.domElement)}popSubView(){this.popSubElem()}setOptions(t){for(const e of this.#e)e.setOptions(t)}updateDisplayIfNeeded(t,e){for(const n of this.#e)n.updateDisplayIfNeeded(t,e);return this}$(t){return this.domElement.querySelector(t)}}class g extends m{#n;#i;#o;constructor(t){super(e("div",{className:"muigui-controller"})),this.#n=[],this.#i=[],t&&this.domElement.classList.add(t)}get parent(){return this.#o}setParent(t){this.#o=t,this.enable(!this.disabled())}show(t=!0){return this.domElement.classList.toggle("muigui-hide",!t),this.domElement.classList.toggle("muigui-show",t),this}hide(){return this.show(!1)}disabled(){return!!this.domElement.closest(".muigui-disabled")}enable(t=!0){return this.domElement.classList.toggle("muigui-disabled",!t),["input","button","select","textarea"].forEach((t=>{this.domElement.querySelectorAll(t).forEach((t=>{const e=!!t.closest(".muigui-disabled");t.disabled=e}))})),this}disable(t=!0){return this.enable(!t)}onChange(t){return this.removeChange(t),this.#n.push(t),this}removeChange(t){return i(this.#n,t),this}onFinishChange(t){return this.removeFinishChange(t),this.#i.push(t),this}removeFinishChange(t){return i(this.#i,t),this}#r(t,e){for(const n of t)n.call(this,e)}emitChange(t,e,n){this.#r(this.#n,t),this.#o&&(void 0===e?this.#o.emitChange(t):this.#o.emitChange({object:e,property:n,value:t,controller:this}))}emitFinalChange(t,e,n){this.#r(this.#i,t),this.#o&&(void 0===e?this.#o.emitChange(t):this.#o.emitFinalChange({object:e,property:n,value:t,controller:this}))}updateDisplay(){}getColors(){const t=t=>t.replace(/-([a-z])/g,((t,e)=>e.toUpperCase())),n=e("div");this.domElement.appendChild(n);const i=Object.fromEntries(["color","bg-color","value-color","value-bg-color","hover-bg-color","menu-bg-color","menu-sep-color","disabled-color"].map((e=>{n.style.color=`var(--${e})`;const i=getComputedStyle(n);return[t(e),i.color]})));return n.remove(),i}}class f extends g{#s;#a;#l;#u={name:""};constructor(t,n,i={}){super("muigui-button",""),this.#s=t,this.#a=n,this.#l=this.addElem(e("button",{type:"button",onClick:()=>{this.#s[this.#a](this)}})),this.setOptions({name:n,...i})}setOptions(t){l(this.#u,t);const{name:e}=this.#u;this.#l.textContent=e}}function b(t,e){if(t.length!==e.length)return!1;for(let n=0;n{t.setValue(i.checked)},onChange:()=>{t.setFinalValue(i.checked)}});super(e("label",{},[i])),this.#b=i}updateDisplay(t){this.#b.checked=t}}const w=[],y=new Set;let k,E;function $(){k=void 0,E=!0;for(const t of w)y.has(t)||t();E=!1,y.size&&(E?C():(y.forEach((t=>{i(w,t)})),y.clear())),C()}function C(){!k&&w.length&&(k=requestAnimationFrame($))}let V=0;function I(){return"muigui-"+ ++V}class M extends m{constructor(t=""){super(e("div",{className:"muigui-value"})),t&&this.domElement.classList.add(t)}}class S extends g{#v;#x;constructor(t="",n=""){super("muigui-label-controller"),this.#v=I(),this.#x=e("label",{for:this.#v}),this.domElement.appendChild(this.#x),this.pushSubView(new M(t)),this.name(n)}get id(){return this.#v}name(t){return this.#x.title===this.#x.textContent&&(this.#x.title=t),this.#x.textContent=t,this}tooltip(t){this.#x.title=t}}class D extends S{#s;#a;#w;#y;#e;#k;constructor(t,e,n=""){super(n,e),this.#s=t,this.#a=e,this.#w=this.getValue(),this.#y=!1,this.#e=[]}get initialValue(){return this.#w}get object(){return this.#s}get property(){return this.#a}add(t){return this.#e.push(t),super.add(t),this.updateDisplay(),t}#E(t,e){let n=!1;if("object"==typeof t){const e=this.#s[this.#a];if(Array.isArray(t)||r(t))for(let i=0;i=0&&w.splice(e,1)}(this.#k)),this}}class N extends D{constructor(t,e){super(t,e,"muigui-checkbox");const n=this.id;this.add(new x(this,n)),this.updateDisplay()}}const F={to:t=>t,from:t=>[!0,t]},A={to:t=>t.toString(),from:t=>{const e=parseFloat(t);return[!Number.isNaN(e),e]}},U={radToDeg:c({to:[0,180],from:[0,Math.PI]})};function L(){let t=0;return function(e,n,i=5){t-=e.deltaY*n/i;const o=Math.floor(Math.abs(t)/n)*Math.sign(t)*n;return t-=o,o}}class O extends v{#$;#C;#V;#I;#u={step:.01,converters:A,min:Number.NEGATIVE_INFINITY,max:Number.POSITIVE_INFINITY};constructor(t,n){const i=t.setValue.bind(t),r=t.setFinalValue.bind(t),a=L();super(e("input",{type:"number",onInput:()=>this.#M(i,!0),onChange:()=>this.#M(r,!1),onWheel:e=>{e.preventDefault();const{min:n,max:i,step:r}=this.#u,l=a(e,r),u=parseFloat(this.domElement.value),c=o(s(u+l,(t=>t),r),n,i);t.setValue(c)}})),this.setOptions(n)}#M(t,e){const n=parseFloat(this.domElement.value),[i,r]=this.#C(n);let s;if(i&&!Number.isNaN(n)){const{min:n,max:i}=this.#u;s=r>=n&&r<=i,this.#I=e,t(o(r,n,i))}this.domElement.classList.toggle("muigui-invalid-value",!i||!s)}updateDisplay(t){this.#I||(this.domElement.value=s(t,this.#$,this.#V)),this.#I=!1}setOptions(t){l(this.#u,t);const{step:e,converters:{to:n,from:i}}=this.#u;return this.#$=n,this.#C=i,this.#V=e,this}}class j extends D{#S;#V;constructor(t,e,n={}){super(t,e,"muigui-checkbox"),this.#S=this.add(new O(this,n)),this.updateDisplay()}}class T extends v{#D;constructor(t,n){const i=[];super(e("select",{onChange:()=>{t.setFinalValue(this.#D[this.domElement.selectedIndex])}},n.map((([t,n])=>(i.push(n),e("option",{textContent:t})))))),this.#D=i}updateDisplay(t){const e=this.#D.indexOf(t);this.domElement.selectedIndex=e}}function H(t,e){return Array.isArray(t)?Array.isArray(t[0])?t:e?t.map(((t,e)=>[t,e])):t.map((t=>[t,t])):[...Object.entries(t)]}class z extends D{constructor(t,e,n){super(t,e,"muigui-select");const i="number"==typeof this.getValue(),{keyValues:o}=n,r=H(o,i);this.add(new T(this,r)),this.updateDisplay()}}class P extends v{#$;#C;#V;#I;#u={step:.01,min:0,max:1,converters:F};constructor(t,n){const i=L();super(e("input",{type:"range",onInput:()=>{this.#I=!0;const{min:e,max:n,step:i}=this.#u,r=parseFloat(this.domElement.value),a=o(s(r,(t=>t),i),e,n),[l,u]=this.#C(a);l&&t.setValue(u)},onChange:()=>{this.#I=!0;const{min:e,max:n,step:i}=this.#u,r=parseFloat(this.domElement.value),a=o(s(r,(t=>t),i),e,n),[l,u]=this.#C(a);l&&t.setFinalValue(u)},onWheel:e=>{e.preventDefault();const[n,r]=this.#C(parseFloat(this.domElement.value));if(!n)return;const{min:a,max:l,step:u}=this.#u,c=i(e,u),h=o(s(r+c,(t=>t),u),a,l);t.setValue(h)}})),this.setOptions(n)}updateDisplay(t){this.#I||(this.domElement.value=s(t,this.#$,this.#V)),this.#I=!1}setOptions(t){l(this.#u,t);const{step:e,min:n,max:i,converters:{to:o,from:r}}=this.#u;return this.#$=o,this.#C=r,this.#V=e,this.domElement.step=e,this.domElement.min=n,this.domElement.max=i,this}}class B extends D{constructor(t,e,n){super(t,e,"muigui-range"),this.add(new P(this,n)),this.add(new O(this,n))}}class G extends v{#$;#C;#I;#u={converters:F};constructor(t,n){const i=t.setValue.bind(t),o=t.setFinalValue.bind(t);super(e("input",{type:"text",onInput:()=>this.#M(i,!0),onChange:()=>this.#M(o,!1)})),this.setOptions(n)}#M(t,e){const[n,i]=this.#C(this.domElement.value);n&&(this.#I=e,t(i)),this.domElement.style.color=n?"":"var(--invalid-color)"}updateDisplay(t){this.#I||(this.domElement.value=this.#$(t),this.domElement.style.color=""),this.#I=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n}}=this.#u;return this.#$=e,this.#C=n,this}}class R extends D{constructor(t,e){super(t,e,"muigui-checkbox"),this.add(new G(this)),this.updateDisplay()}}const _=(t,e,n)=>Math.max(e,Math.min(n,t)),Y=(t,e,n)=>t+(e-t)*n,W=t=>t>=0?t%1:1-t%1,q=t=>+t.toFixed(0),K=t=>+t.toFixed(3),J=t=>parseInt(t.substring(1,3),16)<<16|parseInt(t.substring(3,5),16)<<8|parseInt(t.substring(5,7),16),X=t=>parseInt(t.substring(1,3),16)*2**24+65536*parseInt(t.substring(3,5),16)+256*parseInt(t.substring(5,7),16)+parseInt(t.substring(7,9),16),Z=t=>[parseInt(t.substring(1,3),16),parseInt(t.substring(3,5),16),parseInt(t.substring(5,7),16)],Q=t=>`#${Array.from(t).map((t=>t.toString(16).padStart(2,"0"))).join("")}`,tt=t=>[parseInt(t.substring(1,3),16),parseInt(t.substring(3,5),16),parseInt(t.substring(5,7),16),parseInt(t.substring(7,9),16)],et=t=>`#${Array.from(t).map((t=>t.toString(16).padStart(2,"0"))).join("")}`,nt=t=>Z(t).map((t=>K(t/255))),it=t=>Q(Array.from(t).map((t=>Math.round(_(255*t,0,255))))),ot=t=>tt(t).map((t=>K(t/255))),rt=t=>et(Array.from(t).map((t=>Math.round(_(255*t,0,255))))),st=t=>_(Math.round(255*t),0,255).toString(16).padStart(2,"0"),at=t=>({r:parseInt(t.substring(1,3),16)/255,g:parseInt(t.substring(3,5),16)/255,b:parseInt(t.substring(5,7),16)/255}),lt=t=>({r:parseInt(t.substring(1,3),16)/255,g:parseInt(t.substring(3,5),16)/255,b:parseInt(t.substring(5,7),16)/255,a:parseInt(t.substring(7,9),16)/255}),ut=t=>`rgb(${Z(t).join(", ")})`,ct=/^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,ht=t=>`rgba(${tt(t).map(((t,e)=>3===e?t/255:t)).join(", ")})`,dt=/^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+\.\d+|\d+)\s*\)\s*$/,pt=t=>{const e=yt(Z(t)).map((t=>q(t)));return`hsl(${e[0]}, ${e[1]}%, ${e[2]}%)`},mt=t=>{const e=kt(tt(t)).map(((t,e)=>3===e?K(t):q(t)));return`hsl(${e[0]} ${e[1]}% ${e[2]}% / ${e[3]})`},gt=/^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\)\s*$/,ft=/^\s*hsl\(\s*(\d+)(?:deg|)\s*(?:,|)\s*(\d+)%\s*(?:,|)\s*(\d+)%\s*\/\s*(\d+\.\d+|\d+)\s*\)\s*$/,bt=(t,e)=>(t%e+e)%e;function vt([t,e,n]){t=bt(t,360),e=_(e/100,0,1),n=_(n/100,0,1);const i=e*Math.min(n,1-n);function o(e){const o=(e+t/30)%12;return n-i*Math.max(-1,Math.min(o-3,9-o,1))}return[o(0),o(8),o(4)].map((t=>Math.round(255*t)))}function xt([t,e,n]){const i=Math.max(t,e,n),o=Math.min(t,e,n),r=.5*(o+i),s=i-o;let a=0,l=0;if(0!==s)switch(l=0===r||1===r?0:(i-r)/Math.min(r,1-r),i){case t:a=(e-n)/s+(e{const[e,n,i]=xt(t.map((t=>t/255)));return[360*e,100*n,100*i]},kt=t=>{const[e,n,i,o]=wt(t.map((t=>t/255)));return[360*e,100*n,100*i,o]};function Et([t,e,n]){return e=_(e,0,1),n=_(n,0,1),[t,t+2/3,t+1/3].map((t=>Y(1,_(Math.abs(6*W(t)-3)-1,0,1),e)*n))}function $t([t,e,n,i]){return[...Et([t,e,n]),i]}const Ct=t=>Math.round(1e3*t)/1e3;function Vt([t,e,n]){const i=n>e?[n,e,-1,2/3]:[e,n,0,-1/3],o=i[0]>t?[i[0],i[1],i[3],t]:[t,i[1],i[2],i[0]],r=o[0]-Math.min(o[3],o[1]);return[Math.abs(o[2]+(o[3]-o[1])/(6*r+Number.EPSILON)),r/(o[0]+Number.EPSILON),o[0]].map(Ct)}const It=t=>t.endsWith("a")||t.startsWith("hex8"),Mt=[{re:/^#(?:[0-9a-f]){6}$/i,format:"hex6"},{re:/^(?:[0-9a-f]){6}$/i,format:"hex6-no-hash"},{re:/^#(?:[0-9a-f]){8}$/i,format:"hex8"},{re:/^(?:[0-9a-f]){8}$/i,format:"hex8-no-hash"},{re:/^#(?:[0-9a-f]){3}$/i,format:"hex3"},{re:/^(?:[0-9a-f]){3}$/i,format:"hex3-no-hash"},{re:ct,format:"css-rgb"},{re:gt,format:"css-hsl"},{re:dt,format:"css-rgba"},{re:ft,format:"css-hsla"}];function St(t){switch(typeof t){case"number":return console.warn('can not reliably guess format based on a number. You should pass in a format like {format: "uint32-rgb"} or {format: "uint32-rgb"}'),t<=16777215?"uint32-rgb":"uint32-rgba";case"string":{const e=function(t){for(const e of Mt)if(e.re.test(t))return e}(t.trim());if(e)return e.format;break}case"object":if(t instanceof Uint8Array||t instanceof Uint8ClampedArray){if(3===t.length)return"uint8-rgb";if(4===t.length)return"uint8-rgba"}else if(t instanceof Float32Array){if(3===t.length)return"float-rgb";if(4===t.length)return"float-rgba"}else if(Array.isArray(t)){if(3===t.length)return"float-rgb";if(4===t.length)return"float-rgba"}else if("r"in t&&"g"in t&&"b"in t)return"a"in t?"object-rgba":"object-rgb"}throw new Error(`unknown color format: ${t}`)}function Dt(t){return t.trim(t)}function Nt(t){return t.trim(t)}function Ft(t){return t[1]===t[2]&&t[3]===t[4]&&t[5]===t[6]?`#${t[1]}${t[3]}${t[5]}`:t}const At=/^(#|)([0-9a-f]{3})$/i;function Ut(t){const e=At.exec(t);if(e){const[,,t]=e;return"#"+`${(n=t)[0]}${n[0]}${n[1]}${n[1]}${n[2]}${n[2]}`}var n;return t}function Lt(t){return Ft(Dt(t))}const Ot=t=>{const e=ct.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),`rgb(${n.join(", ")})`]},jt=t=>{const e=dt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map(((t,e)=>3===e?parseFloat(t):parseInt(t)));return[!n.find((t=>t>255)),`rgba(${n.join(", ")})`]},Tt=t=>{const e=gt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseFloat(t)));return[!n.find((t=>Number.isNaN(t))),`hsl(${n[0]}, ${n[1]}%, ${n[2]}%)`]},Ht=t=>{const e=ft.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map((t=>parseFloat(t)));return[!n.find((t=>Number.isNaN(t))),`hsl(${n[0]} ${n[1]}% ${n[2]}% / ${n[3]})`]},zt=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/,Pt=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/,Bt=/^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i,Gt=/^\s*(?:0x){0,1}([0-9a-z]{1,8})\s*$/i,Rt=/^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i,_t=/^\s*[a-f0-9]{6}\s*$/i,Yt=/^\s*#[a-f0-9]{8}\s*$/i,Wt=/^\s*[a-f0-9]{8}\s*$/i,qt={hex6:{color:{from:t=>[!0,t],to:Dt},text:{from:t=>[Rt.test(t),t.trim()],to:t=>t}},hex8:{color:{from:t=>[!0,t],to:Nt},text:{from:t=>[Yt.test(t),t.trim()],to:t=>t}},hex3:{color:{from:t=>[!0,Lt(t)],to:Ut},text:{from:t=>[Rt.test(t),Ft(t.trim())],to:t=>t}},"hex6-no-hash":{color:{from:t=>[!0,t.substring(1)],to:t=>`#${Dt(t)}`},text:{from:t=>[_t.test(t),t.trim()],to:t=>t}},"hex8-no-hash":{color:{from:t=>[!0,t.substring(1)],to:t=>`#${Nt(t)}`},text:{from:t=>[Wt.test(t),t.trim()],to:t=>t}},"hex3-no-hash":{color:{from:t=>[!0,Lt(t).substring(1)],to:Ut},text:{from:t=>[_t.test(t),Ft(t.trim())],to:t=>t}},"uint32-rgb":{color:{from:t=>[!0,J(t)],to:t=>`#${Math.round(t).toString(16).padStart(6,"0")}`},text:{from:t=>(t=>{const e=Bt.exec(t);return e?[!0,parseInt(e[1],16)]:[!1]})(t),to:t=>`0x${t.toString(16).padStart(6,"0")}`}},"uint32-rgba":{color:{from:t=>[!0,X(t)],to:t=>`#${Math.round(t).toString(16).padStart(8,"0")}`},text:{from:t=>(t=>{const e=Gt.exec(t);return e?[!0,parseInt(e[1],16)]:[!1]})(t),to:t=>`0x${t.toString(16).padStart(8,"0")}`}},"uint8-rgb":{color:{from:t=>[!0,Z(t)],to:Q},text:{from:t=>{const e=zt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),n]},to:t=>t.join(", ")}},"uint8-rgba":{color:{from:t=>[!0,tt(t)],to:et},text:{from:t=>{const e=Pt.exec(t);if(!e)return[!1];const n=[e[1],e[2],e[3],e[4]].map((t=>parseInt(t)));return[!n.find((t=>t>255)),n]},to:t=>t.join(", ")}},"float-rgb":{color:{from:t=>[!0,nt(t)],to:it},text:{from:t=>{const e=t.split(",").map((t=>t.trim())),n=e.map((t=>parseFloat(t)));if(3!==n.length)return[!1];const i=e.findIndex((t=>isNaN(t)));return[i<0,n.map((t=>K(t)))]},to:t=>Array.from(t).map((t=>K(t))).join(", ")}},"float-rgba":{color:{from:t=>[!0,ot(t)],to:rt},text:{from:t=>{const e=t.split(",").map((t=>t.trim())),n=e.map((t=>parseFloat(t)));if(4!==n.length)return[!1];const i=e.findIndex((t=>isNaN(t)));return[i<0,n.map((t=>K(t)))]},to:t=>Array.from(t).map((t=>K(t))).join(", ")}},"object-rgb":{color:{from:t=>[!0,at(t)],to:t=>`#${st(t.r)}${st(t.g)}${st(t.b)}`},text:{from:t=>{try{const e=t.replace(/([a-z])/g,'"$1"'),n=JSON.parse(e);if(Number.isNaN(n.r)||Number.isNaN(n.g)||Number.isNaN(n.b))throw new Error("not {r, g, b}");return[!0,n]}catch(t){return[!1]}},to:t=>`{r:${K(t.r)}, g:${K(t.g)}, b:${K(t.b)}}`}},"object-rgba":{color:{from:t=>[!0,lt(t)],to:t=>`#${st(t.r)}${st(t.g)}${st(t.b)}${st(t.a)}`},text:{from:t=>{try{const e=t.replace(/([a-z])/g,'"$1"'),n=JSON.parse(e);if(Number.isNaN(n.r)||Number.isNaN(n.g)||Number.isNaN(n.b)||Number.isNaN(n.a))throw new Error("not {r, g, b, a}");return[!0,n]}catch(t){return[!1]}},to:t=>`{r:${K(t.r)}, g:${K(t.g)}, b:${K(t.b)}}, a:${K(t.a)}}`}},"css-rgb":{color:{from:t=>[!0,ut(t)],to:t=>{const e=ct.exec(t);return Q([e[1],e[2],e[3]].map((t=>parseInt(t))))}},text:{from:Ot,to:t=>Ot(t)[1]}},"css-rgba":{color:{from:t=>[!0,ht(t)],to:t=>{const e=dt.exec(t);return et([e[1],e[2],e[3],e[4]].map(((t,e)=>3===e?255*parseFloat(t)|0:parseInt(t))))}},text:{from:jt,to:t=>jt(t)[1]}},"css-hsl":{color:{from:t=>[!0,pt(t)],to:t=>{const e=gt.exec(t),n=vt([e[1],e[2],e[3]].map((t=>parseFloat(t))));return Q(n)}},text:{from:Tt,to:t=>Tt(t)[1]}},"css-hsla":{color:{from:t=>[!0,mt(t)],to:t=>{const e=ft.exec(t),n=function([t,e,n,i]){return[...vt([t,e,n]),255*i|0]}([e[1],e[2],e[3],e[4]].map((t=>parseFloat(t))));return et(n)}},text:{from:Ht,to:t=>Ht(t)[1]}}};class Kt extends m{constructor(t,n){super(e(t,{className:n}))}}class Jt extends S{#N;constructor(){super("muigui-canvas"),this.#N=this.add(new Kt("canvas","muigui-canvas")).domElement}get canvas(){return this.#N}}class Xt extends v{#$;#C;#F;#I;#u={converters:F};constructor(t,n){const i=e("input",{type:"color",onInput:()=>{const[e,n]=this.#C(i.value);e&&(this.#I=!0,t.setValue(n))},onChange:()=>{const[e,n]=this.#C(i.value);e&&(this.#I=!0,t.setFinalValue(n))}});super(e("div",{},[i])),this.setOptions(n),this.#F=i}updateDisplay(t){this.#I||(this.#F.value=this.#$(t)),this.#I=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n}}=this.#u;return this.#$=e,this.#C=n,this}}class Zt extends D{#A;#S;constructor(t,e,n={}){super(t,e,"muigui-color");const i=n.format||St(this.getValue()),{color:o,text:r}=qt[i];this.#A=this.add(new Xt(this,{converters:o})),this.#S=this.add(new G(this,{converters:r})),this.updateDisplay()}setOptions(t){const{format:e}=t;if(e){const{color:t,text:n}=qt[e];this.#A.setOptions({converters:t}),this.#S.setOptions({converters:n})}return super.setOptions(t),this}}class Qt extends g{constructor(){super("muigui-divider")}}class te extends g{#U;#L;constructor(t){super(t),this.#U=[],this.#L=this}get children(){return this.#U}get controllers(){return this.#U.filter((t=>!(t instanceof te)))}get folders(){return this.#U.filter((t=>t instanceof te))}reset(t=!0){for(const e of this.#U)e instanceof te&&!t||e.reset(t);return this}updateDisplay(){for(const t of this.#U)t.updateDisplay();return this}remove(t){const e=this.#U.indexOf(t);if(e>=0){const t=this.#U.splice(e,1)[0];t.domElement.remove(),t.setParent(null)}return this}_addControllerImpl(t){return this.domElement.appendChild(t.domElement),this.#U.push(t),t.setParent(this),t}addController(t){return this.#L._addControllerImpl(t)}pushContainer(t){return this.addController(t),this.#L=t,t}popContainer(){return this.#L=this.#L.parent,this}}class ee extends te{#O;constructor(t="Controls",n="muigui-menu"){super(n),this.#O=e("label"),this.addElem(e("button",{type:"button",onClick:()=>this.toggleOpen()},[this.#O])),this.pushContainer(new te),this.name(t),this.open()}open(t=!0){return this.domElement.classList.toggle("muigui-closed",!t),this.domElement.classList.toggle("muigui-open",t),this}close(){return this.open(!1)}name(t){return this.#O.textContent=t,this}title(t){return this.name(t)}toggleOpen(){return this.open(!this.domElement.classList.contains("muigui-open")),this}}class ne extends g{constructor(t){super("muigui-label"),this.text(t)}text(t){return this.domElement.textContent=t,this}}function ie(){}function oe(t,e,n){const i=t.getBoundingClientRect(),o=e.clientX-i.left,r=e.clientY-i.top,s=o/i.width,a=r/i.height,l=o-(n=n||[o,r])[0],u=r-n[1];return{x:o,y:r,nx:s,ny:a,dx:l,dy:u,ndx:l/i.width,ndy:u/i.width}}function re(t,{onDown:e=ie,onMove:n=ie,onUp:i=ie}){let o;const r=function(e){const i={type:"move",...oe(t,e,o)};n(i)},s=function(e){t.releasePointerCapture(e.pointerId),t.removeEventListener("pointermove",r),t.removeEventListener("pointerup",s),document.body.style.backgroundColor="",i("up")},a=function(n){t.addEventListener("pointermove",r),t.addEventListener("pointerup",s),t.setPointerCapture(n.pointerId);const i=oe(t,n);o=[i.x,i.y],e({type:"down",...i})};return t.addEventListener("pointerdown",a),function(){t.removeEventListener("pointerdown",a)}}function se(t){return t.querySelectorAll("[data-src]").forEach((e=>{const i="muigui-id-"+n++;e.id=i,t.querySelectorAll(`[data-target=${e.dataset.src}]`).forEach((t=>{t.setAttribute("fill",`url(#${i})`)}))})),t}class ae extends v{#$;#C;#j;#T;#H;#z;#P;#B;#G;#R;#_;#Y;#W;#q;#u={converters:F,alpha:!1};#K;#J;constructor(t,n){super(e("div",{innerHTML:'\n\n \n \n \n \n \n \n \n \n\n \n \n \n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n',className:"muigui-no-scroll"})),this.#j=this.domElement.children[0],this.#H=this.domElement.children[1],this.#B=this.domElement.children[2],se(this.#j),se(this.#H),se(this.#B),this.#T=this.$(".muigui-color-chooser-circle"),this.#z=this.$("[data-src=muigui-color-chooser-hue]"),this.#P=this.$(".muigui-color-chooser-hue-cursor"),this.#G=this.$("[data-src=muigui-color-chooser-alpha]"),this.#R=this.$(".muigui-color-chooser-alpha-cursor");const i=e=>{const n=o(e.nx,0,1),i=o(e.ny,0,1);this.#_[1]=n,this.#_[2]=1-i,this.#Y=!0,this.#q=!0;const[r,s]=this.#C(this.#K(this.#_));r&&t.setValue(s)},r=e=>{const n=o(e.nx,0,1);this.#_[0]=n,this.#W=!0,this.#q=!0;const[i,r]=this.#C(this.#K(this.#_));i&&t.setValue(r)},s=e=>{const n=o(e.nx,0,1);this.#_[3]=n,this.#Y=!0,this.#W=!0;const[i,r]=this.#C(this.#K(this.#_));i&&t.setValue(r)};re(this.#j,{onDown:i,onMove:i}),re(this.#H,{onDown:r,onMove:r}),re(this.#B,{onDown:s,onMove:s}),this.setOptions(n)}updateDisplay(t){this.#_||(this.#_=this.#J(this.#$(t)));{const[e,n,i,o=1]=this.#J(this.#$(t));this.#Y||(this.#_[0]=n>.001&&i>.001?e:this.#_[0]),this.#W||(this.#_[1]=n,this.#_[2]=i),this.#q||(this.#_[3]=o)}{const[t,e,n,i]=this.#_,[o,r,s]=wt($t(this.#_));this.#Y||this.#P.setAttribute("transform",`translate(${64*t}, 0)`),this.#z.children[0].setAttribute("stop-color",`hsl(${360*o} 0% 100% / ${i})`),this.#z.children[1].setAttribute("stop-color",`hsl(${360*o} 100% 50% / ${i})`),this.#q||this.#R.setAttribute("transform",`translate(${64*i}, 0)`),this.#G.children[0].setAttribute("stop-color",`hsl(${360*o} ${100*r}% ${100*s}% / 0)`),this.#G.children[1].setAttribute("stop-color",`hsl(${360*o} ${100*r}% ${100*s}% / 1)`),this.#W||(this.#T.setAttribute("cx",""+64*e),this.#T.setAttribute("cy",""+48*(1-n)))}this.#Y=!1,this.#W=!1,this.#q=!1}setOptions(t){l(this.#u,t);const{converters:{to:e,from:n},alpha:i}=this.#u;return this.#B.style.display=i?"":"none",this.#K=i?t=>rt($t(t)):t=>it(Et(t)),this.#J=i?t=>function([t,e,n,i]){return[...Vt([t,e,n]),i]}(ot(t)):t=>Vt(nt(t)),this.#$=e,this.#C=n,this}}class le extends D{#X;#Z;#b;#Q;#u={open:!1};constructor(t,n,i={}){super(t,n,"muigui-pop-down-controller"),this.#X=this.add(new Kt("div","muigui-pop-down-top"));const o=this.#X.addElem(e("input",{type:"checkbox",onChange:()=>{this.#u.open=o.checked,this.updateDisplay()}}));this.#b=o,this.#Z=this.#X.add(new Kt("div","muigui-pop-down-values")),this.#Q=this.add(new Kt("div","muigui-pop-down-bottom")),this.setOptions(i)}setKnobColor(t){this.#b&&(this.#b.style=`\n --range-color: ${t};\n --value-bg-color: ${t};\n `)}updateDisplay(){super.updateDisplay();const{open:t}=this.#u;this.domElement.children[1].classList.toggle("muigui-open",t),this.domElement.children[1].classList.toggle("muigui-closed",!t)}setOptions(t){l(this.#u,t),super.setOptions(t),this.updateDisplay()}addTop(t){return this.#Z.add(t)}addBottom(t){return this.#Q.add(t)}}class ue extends le{#A;#S;#$;constructor(t,e,n={}){super(t,e,"muigui-color-chooser");const i=n.format||St(this.getValue()),{color:o,text:r}=qt[i];this.#$=o.to,this.#S=new G(this,{converters:r,alpha:It(i)}),this.#A=new ae(this,{converters:o,alpha:It(i)}),this.addTop(this.#S),this.addBottom(this.#A),this.__setKnobHelper=()=>{if(this.#$){const t=this.#$(this.getValue()),e=yt(Z(t));e[2]=(e[2]+50)%100;const n=Q(vt(e));this.setKnobColor(`${t.substring(0,7)}FF`,n)}},this.updateDisplay()}updateDisplay(){super.updateDisplay(),this.__setKnobHelper&&this.__setKnobHelper()}setOptions(t){return super.setOptions(t),this}}class ce extends ee{add(t,e,...n){const i=t instanceof g?t:function(t,e,...n){const[i]=n;if(Array.isArray(i))return new z(t,e,{keyValues:i});const o=typeof t[e];switch(o){case"number":if("number"==typeof n[0]&&"number"==typeof n[1]){const i=n[0],o=n[1],r=n[2];return new B(t,e,{min:i,max:o,...r&&{step:r}})}return 0===n.length?new j(t,e,...n):new B(t,e,...n);case"boolean":return new N(t,e,...n);case"function":return new f(t,e,...n);case"string":return new R(t,e,...n);case"undefined":throw new Error(`no property named ${e}`);default:throw new Error(`unhandled type ${o} for property ${e}`)}}(t,e,...n);return this.addController(i)}addCanvas(t){return this.addController(new Jt(t))}addColor(t,e,n={}){const i=t[e];return It(n.format||St(i))?this.addController(new ue(t,e,n)):this.addController(new Zt(t,e,n))}addDivider(){return this.addController(new Qt)}addFolder(t){return this.addController(new ce(t))}addLabel(t){return this.addController(new ne(t))}}class he extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"})}}customElements.define("muigui-element",he);const de=new CSSStyleSheet;de.replaceSync(t.default);const pe=new CSSStyleSheet;function me(t){let e,n;function i(){if(e&&!n){const o=e;e=void 0,n=t.replace(o).then((()=>{n=void 0,i()}))}}return function(t){e=t,i()}}const ge=me(de),fe=me(pe);class be extends ce{static converters=U;static mapRange=u;static makeRangeConverters=c;static makeRangeOptions=h;static makeMinMaxPair=p;#tt=new CSSStyleSheet;constructor(t={}){super("Controls","muigui-root"),t instanceof HTMLElement&&(t={parent:t});const{autoPlace:n=!0,width:i,title:o="Controls"}=t;let{parent:r}=t;if(i&&(this.domElement.style.width=/^\d+$/.test(i)?`${i}px`:i),void 0===r&&n&&(r=document.body,this.domElement.classList.add("muigui-auto-place")),r){const t=e("muigui-element");t.shadowRoot.adoptedStyleSheets=[de,pe,this.#tt],t.shadow.appendChild(this.domElement),r.appendChild(t)}o&&this.title(o),this.domElement.classList.add("muigui","muigui-colors")}setStyle(t){this.#tt.replace(t)}static setBaseStyles(t){ge(t)}static getBaseStyleSheet(){return de}static setUserStyles(t){fe(t)}static getUserStyleSheet(){return pe}static setTheme(e){be.setBaseStyles(`${t.default}\n${t.themes[e]||""}`)}}function ve(){}const xe={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]};function we(t,{onDown:e=ve,onUp:n=ve}){const i=function(t){const i=t.shiftKey?10:1,[o,r]=(xe[t.key]||[0,0]).map((t=>t*i));("keydown"===t.type?e:n)({type:t.type.substring(3),dx:o,dy:r,event:t})};return t.addEventListener("keydown",i),t.addEventListener("keyup",i),function(){t.removeEventListener("keydown",i),t.removeEventListener("keyup",i)}}function ye(t,e=""){if(!t)throw new Error(e)}function ke(t,e,n,i,o,r){const s=Math.abs(n)*Math.cos(r),a=Math.abs(i)*Math.sin(r);return[t+Math.cos(o)*s-Math.sin(o)*a,e+Math.sin(o)*s+Math.cos(o)*a]}function Ee(t,e,n,i,o){ye(Math.abs(i-o)<=2*Math.PI),ye(i>=-Math.PI&&i<=2*Math.PI),ye(i<=o),ye(o>=-Math.PI&&o<=4*Math.PI);const{x1:r,y1:s,x2:a,y2:l,fa:u,fs:c}=function(t,e,n,i,o,r,s){const[a,l]=ke(t,e,n,i,o,r),[u,c]=ke(t,e,n,i,o,r+s);return{x1:a,y1:l,x2:u,y2:c,fa:Math.abs(s)>Math.PI?1:0,fs:s>0?1:0}}(t,e,n,n,0,i,o-i);return Math.abs(Math.abs(i-o)-2*Math.PI)>Number.EPSILON?`M${t} ${e} L${r} ${s} A ${n} ${n} 0 ${u} ${c} ${a} ${l} L${t} ${e}`:`M${r} ${s} L${r} ${s} A ${n} ${n} 0 ${u} ${c} ${a} ${l}`}const $e=t=>a(t+Math.PI,2*Math.PI)-Math.PI;class Ce extends v{#et;#nt;#it;#ot;#u={step:1,min:-180,max:180,dirMin:-Math.PI,dirMax:Math.PI,wrap:void 0,converters:F};constructor(t,n={}){const i=L();super(e("div",{className:"muigui-direction muigui-no-scroll",innerHTML:'\n\n \x3c!----\x3e\n \n \n \n \n \n \n\n',onWheel:e=>{e.preventDefault();const{min:n,max:r,step:l}=this.#u,u=i(e,l);let c=this.#it+u;this.#ot&&(c=a(c-n,r-n)+n);const h=o(s(c,(t=>t),l),n,r);t.setValue(h)}}));const r=e=>{const{min:n,max:i,step:r,dirMin:a,dirMax:l}=this.#u,u=2*e.nx-1,c=2*e.ny-1,h=Math.atan2(c,u),d=(a+l)/2,p=o(($e(h-d)-$e(a-d))/(l-a),0,1),m=s(n+(i-n)*p,(t=>t),r);t.setValue(m)};re(this.domElement,{onDown:r,onMove:r}),we(this.domElement,{onDown:e=>{const{min:n,max:i,step:r}=this.#u,a=o(s(this.#it+e.dx*r,(t=>t),r),n,i);t.setValue(a)}}),this.#et=this.$("#muigui-arrow"),this.#nt=this.$("#muigui-range"),this.setOptions(n)}updateDisplay(t){this.#it=t;const{min:e,max:n}=this.#u,i=(t-e)/(n-e),o=(r=this.#u.dirMin,s=this.#u.dirMax,r+(s-r)*i);var r,s;this.#et.style.transform=`rotate(${o}rad)`}setOptions(t){l(this.#u,t);const{dirMin:e,dirMax:n,wrap:i}=this.#u;this.#ot=void 0!==i?i:Math.abs(e-n)>=2*Math.PI-Number.EPSILON;const[o,r]=e(o.push(i),e("label",{},[e("input",{type:"radio",name:r,value:a,onChange:function(){this.checked&&t.setFinalValue(s.#D[this.value])}}),e("button",{type:"button",textContent:n,onClick:function(){this.previousElementSibling.click()}})]))))));const s=this;this.#D=o,this.cols(i)}updateDisplay(t){const e=this.#D.indexOf(t);for(let t=0;t{e({rect:t.getBoundingClientRect(),elem:t})})).observe(t)}function De(t,e,n,i){Se(t,(({rect:o})=>{const{width:r,height:s}=o;t.setAttribute("viewBox",`-${r*e} -${s*n} ${r} ${s}`),i({elem:t,rect:o})}))}function Ne(t,e,n,i,o,r){const a=[];tt),n)),e=Math.min(e,o);for(let i=t;i<=e;i+=n)a.push(`M${i} 0 l0 ${r}`);return a.join(" ")}class Fe extends v{#rt;#st;#at;#lt;#ut;#ct;#ht;#dt;#pt;#it;#mt;#u={min:-100,max:100,step:1,unit:10,unitSize:10,ticksPerUnit:5,labelFn:t=>t,tickHeight:1,limits:!0,thicksColor:void 0,orientation:void 0};constructor(t,n){const i=L();let r;super(e("div",{innerHTML:'\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \x3c!----\x3e\n \x3c!----\x3e\n \n \n \n \n\n',className:"muigui-no-v-scroll",onWheel:e=>{e.preventDefault();const{min:n,max:r,step:a}=this.#u,l=i(e,a),u=o(s(this.#it+l,(t=>t),a),n,r);t.setValue(u)}})),this.#rt=this.$("svg"),this.#st=this.$("#muigui-origin"),this.#at=this.$("#muigui-ticks"),this.#lt=this.$("#muigui-thicks"),this.#ut=this.$("#muigui-numbers"),this.#ct=this.$("#muigui-left-grad"),this.#ht=this.$("#muigui-right-grad"),this.setOptions(n),re(this.domElement,{onDown:()=>{r=this.#it},onMove:e=>{const{min:n,max:i,unitSize:a,unit:l,step:u}=this.#u,c=o(s(r-e.dx/a*l,(t=>t),u),n,i);t.setValue(c)}}),we(this.domElement,{onDown:e=>{const{min:n,max:i,step:r}=this.#u,a=o(s(this.#it+e.dx*r,(t=>t),r),n,i);t.setValue(a)}}),De(this.#rt,.5,0,(({rect:{width:t}})=>{this.#ct.setAttribute("x",-t/2),this.#ht.setAttribute("x",t/2-20),this.#mt=function(t){const e=t.innerHTML;t.innerHTML="- ";const n=t.querySelector("text").getComputedTextLength();return t.innerHTML=e,n}(this.#ut),this.#dt=t,this.#gt()}))}#gt(){if(!this.#dt||void 0===this.#it)return;const{labelFn:t,limits:e,min:n,max:i,orientation:o,tickHeight:r,ticksPerUnit:a,unit:l,unitSize:u,thicksColor:c}=this.#u,h=Math.ceil(this.#dt/u),d=this.#it/l,p=Math.round(d-h),m=p*u,g=(p+2*h)*u,f=e?n*u/l:m,b=e?i*u/l:g,v=""===t(1)?10:5;a>1&&this.#at.setAttribute("d",Ne(m,g,u/a,f,b,v*r)),this.#lt.style.stroke=c,this.#lt.setAttribute("d",Ne(m,g,u,f,b,v)),this.#ut.innerHTML=function(t,e,n,i,o,r,a,l){const u=[];tt),n)),e=Math.min(e,a);const c=Math.max(0,-Math.log10(i));for(let r=t;r<=e;r+=n)u.push(`${h=r/n*i,l(h.toFixed(c))}`);var h;return u.join("\n")}(m,g,u,l,this.#mt,f,b,t),this.#st.setAttribute("transform",`translate(${-this.#it*u/l} 0)`),this.#rt.classList.toggle("muigui-slider-up","up"===o)}updateDisplay(t){this.#it=t,this.#gt()}setOptions(t){return l(this.#u,t),this}}class Ae extends D{constructor(t,e,n={}){super(t,e,"muigui-slider"),this.add(new Fe(this,n)),this.add(new O(this,n)),this.updateDisplay()}}class Ue extends v{#rt;#et;#T;#it=[];constructor(t){super(e("div",{innerHTML:'\n\n \n \n \n \n \n\n',className:"muigui-no-scroll"}));const n=e=>{const{width:n,height:i}=this.#rt.getBoundingClientRect(),o=2*e.nx-1,r=2*e.ny-1;t.setValue([o*n*.5,r*i*.5])};re(this.domElement,{onDown:n,onMove:n}),this.#rt=this.$("svg"),this.#et=this.$("#muigui-arrow"),this.#T=this.$("#muigui-circle"),De(this.#rt,.5,.5,(()=>this.#ft))}#ft(){const[t,e]=this.#it;this.#et.setAttribute("d",`M0,0L${t},${e}`),this.#T.setAttribute("transform",`translate(${t}, ${e})`)}updateDisplay(t){this.#it[0]=t[0],this.#it[1]=t[1],this.#ft()}}class Le extends le{constructor(t,e){super(t,e,"muigui-vec2");const n=t=>({setValue:e=>{const n=this.getValue();n[t]=e,this.setValue(n)},setFinalValue:e=>{const n=this.getValue();n[t]=e,this.setFinalValue(n)}});this.addTop(new O(n(0),{converters:{to:t=>t[0],from:A.from}})),this.addTop(new O(n(1),{converters:{to:t=>t[1],from:A.from}})),this.addBottom(new Ue(this)),this.updateDisplay()}}export{ue as ColorChooser,Ve as Direction,Me as RadioGrid,B as Range,z as Select,Ae as Slider,j as TextNumber,Le as Vec2,be as default}; -//# sourceMappingURL=muigui.module.min.js.map diff --git a/package-lock.json b/package-lock.json index f14f763..22931c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "rollup": "^3.20.2", "servez": "^2.1.2", "tslib": "^2.6.2", - "typescript": "^5.3.2" + "typescript": "5.2" } }, "node_modules/@babel/code-frame": { @@ -4000,9 +4000,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7159,9 +7159,9 @@ } }, "typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, "unbzip2-stream": { diff --git a/package.json b/package.json index 7168ce7..db52dde 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,17 @@ "module": "src/muigui.js", "type": "module", "scripts": { - "start": "node build/serve.js", - "watch": "npm run start", "build": "npm run build-normal", + "build-ci": "npm run build && node build/prep-for-deploy.js", "build-normal": "rollup -c", "build-min": "rollup -c", + "check-ci": "npm run pre-push", "eslint": "eslint \"**/*.js\"", "fix": "eslint --fix", "pre-push": "npm run eslint && npm run test", - "test": "node test/puppeteer.js" + "start": "node build/serve.js", + "test": "node test/puppeteer.js", + "watch": "npm run start" }, "repository": { "type": "git", @@ -52,6 +54,6 @@ "rollup": "^3.20.2", "servez": "^2.1.2", "tslib": "^2.6.2", - "typescript": "^5.3.2" + "typescript": "5.2" } }