├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── build-and-deploy.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CNAME ├── LICENSE.md ├── README.md ├── TODO.md ├── build ├── copy.js ├── prep-for-deploy.js ├── pull-vsa.js ├── serve.js └── tsconfig-serve.json ├── examples ├── 3rdParty │ ├── threejs │ │ ├── build │ │ │ └── three.module.js │ │ └── examples │ │ │ └── jsm │ │ │ └── controls │ │ │ └── TrackballControls.js │ └── twgl-full.module.js ├── controllers │ └── direction.html ├── custom.html ├── js │ ├── index │ │ ├── VSAEffect.js │ │ ├── effects.js │ │ ├── effects │ │ │ ├── admo.js │ │ │ ├── bwow.js │ │ │ ├── codez.js │ │ │ ├── cyty.js │ │ │ ├── discus.js │ │ │ ├── dotto-chouhoukei.js │ │ │ ├── hexit2.js │ │ │ ├── loop-test.js │ │ │ ├── pookymelon.js │ │ │ ├── rollin.js │ │ │ ├── starfield.js │ │ │ └── ung.js │ │ └── index.js │ ├── logger.js │ ├── long-hide.js │ ├── lots-umd.js │ ├── lots.js │ ├── model.js │ └── utils.js ├── layout │ ├── layout.html │ └── layout.js ├── long-hide.html ├── lots-umd.html └── lots.html ├── images ├── muigui-icon.png ├── muigui-screenshot.png └── muigui.png ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── controllers │ ├── Button.js │ ├── Canvas.js │ ├── Checkbox.js │ ├── Color.js │ ├── ColorChooser.js │ ├── Container.js │ ├── Controller.js │ ├── Direction.js │ ├── Divider.js │ ├── Folder.js │ ├── Label.js │ ├── LabelController.js │ ├── PopDownController.js │ ├── RadioGrid.js │ ├── Range.js │ ├── Select.js │ ├── Slider.js │ ├── TabHolder.js │ ├── Text.js │ ├── TextNumber.js │ ├── ValueController.js │ ├── Vec2.js │ └── create-controller.js ├── esm.ts ├── layout │ ├── Column.js │ ├── Frame.js │ ├── Grid.js │ ├── Layout.js │ └── Row.js ├── libs │ ├── assert.js │ ├── color-utils.js │ ├── conversions.js │ ├── css-utils.js │ ├── elem.js │ ├── emitter.js │ ├── graph.js │ ├── ids.js │ ├── iterable-array.js │ ├── key-values.js │ ├── keyboard.js │ ├── monitor.js │ ├── resize-helpers.js │ ├── svg.js │ ├── taskrunner.js │ ├── touch.js │ ├── utils.js │ └── wheel.js ├── muigui.js ├── styles │ └── muigui.css.js ├── umd.js └── views │ ├── CheckboxView.js │ ├── ColorChooserView.js │ ├── ColorView.js │ ├── DirectionView.js │ ├── EditView.js │ ├── ElementView.js │ ├── GridView.js │ ├── NumberView.js │ ├── RadioGridView.js │ ├── RangeView.js │ ├── SelectView.js │ ├── SliderView.js │ ├── TextView.js │ ├── ValueView.js │ ├── Vec2View.js │ └── View.ts ├── test ├── assert.js ├── index.html ├── index.js ├── mocha-support.js ├── mocha.css ├── mocha.js ├── puppeteer.js └── tests │ ├── color-utils-tests.js │ └── muigui-tests.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/js 2 | test/mocha.js 3 | dist 4 | examples/3rdParty 5 | out 6 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | /* global __dirname */ 3 | 4 | module.exports = { 5 | parser: '@typescript-eslint/parser', 6 | env: { 7 | es2022: true, 8 | browser: true, 9 | }, 10 | parserOptions: { 11 | sourceType: 'module', 12 | ecmaVersion: 'latest', 13 | tsconfigRootDir: __dirname, 14 | project: ['./tsconfig.json'], 15 | }, 16 | root: true, 17 | 18 | plugins: [ 19 | '@typescript-eslint', 20 | 'eslint-plugin-html', 21 | 'eslint-plugin-optional-comma-spacing', 22 | 'eslint-plugin-require-trailing-comma', 23 | ], 24 | extends: [ 25 | 'eslint:recommended', 26 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 27 | ], 28 | rules: { 29 | 'brace-style': [2, '1tbs', { allowSingleLine: false }], 30 | camelcase: [0], 31 | 'comma-dangle': 0, 32 | 'comma-spacing': 0, 33 | 'comma-style': [2, 'last'], 34 | 'consistent-return': 2, 35 | curly: [2, 'all'], 36 | 'dot-notation': 0, 37 | 'eol-last': [0], 38 | eqeqeq: 2, 39 | 'global-strict': [0], 40 | 'key-spacing': [0], 41 | 'keyword-spacing': [1, { before: true, after: true, overrides: {} }], 42 | 'new-cap': 2, 43 | 'new-parens': 2, 44 | 'no-alert': 2, 45 | 'no-array-constructor': 2, 46 | 'no-caller': 2, 47 | 'no-catch-shadow': 2, 48 | 'no-comma-dangle': [0], 49 | 'no-const-assign': 2, 50 | 'no-eval': 2, 51 | 'no-extend-native': 2, 52 | 'no-extra-bind': 2, 53 | 'no-extra-parens': [2, 'functions'], 54 | 'no-implied-eval': 2, 55 | 'no-irregular-whitespace': 2, 56 | 'no-iterator': 2, 57 | 'no-label-var': 2, 58 | 'no-labels': 2, 59 | 'no-lone-blocks': 2, 60 | 'no-loop-func': 2, 61 | 'no-multi-spaces': [0], 62 | 'no-multi-str': 2, 63 | 'no-native-reassign': 2, 64 | 'no-new-func': 2, 65 | 'no-new-object': 2, 66 | 'no-new-wrappers': 2, 67 | 'no-new': 2, 68 | 'no-obj-calls': 2, 69 | 'no-octal-escape': 2, 70 | 'no-process-exit': 2, 71 | 'no-proto': 2, 72 | 'no-return-assign': 2, 73 | 'no-script-url': 2, 74 | 'no-sequences': 2, 75 | 'no-shadow-restricted-names': 2, 76 | 'no-shadow': [0], 77 | 'no-spaced-func': 2, 78 | 'no-trailing-spaces': 2, 79 | 'no-undef-init': 2, 80 | //'no-undef': 2, // ts recommends this be off: https://typescript-eslint.io/linting/troubleshooting 81 | 'no-underscore-dangle': 2, 82 | 'no-unreachable': 2, 83 | 'no-unused-expressions': 2, 84 | 'no-use-before-define': 0, 85 | 'no-var': 2, 86 | 'no-with': 2, 87 | 'one-var': ["error", "never"], 88 | 'optional-comma-spacing/optional-comma-spacing': [2, { after: true }], 89 | 'prefer-const': 2, 90 | 'require-trailing-comma/require-trailing-comma': [2], 91 | 'semi-spacing': [2, { before: false, after: true }], 92 | semi: [2, 'always'], 93 | 'space-before-function-paren': [ 94 | 2, 95 | { 96 | anonymous: 'always', 97 | named: 'never', 98 | asyncArrow: 'always', 99 | }, 100 | ], 101 | 'space-infix-ops': 2, 102 | 'space-unary-ops': [2, { words: true, nonwords: false }], 103 | strict: [2, 'function'], 104 | yoda: [2, 'never'], 105 | '@typescript-eslint/no-empty-function': 'off', 106 | '@typescript-eslint/no-explicit-any': 'off', // TODO: Reenable this and figure out how to fix code. 107 | '@typescript-eslint/no-non-null-assertion': 'off', 108 | '@typescript-eslint/no-unused-vars': 2, 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | permissions: 7 | contents: write 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | environment: deploy 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - name: Install and Build 🔧 22 | run: | 23 | npm ci 24 | npm run build-ci 25 | npm run check-ci 26 | 27 | - name: Deploy 🚀 28 | uses: JamesIves/github-pages-deploy-action@v4 29 | with: 30 | folder: . 31 | 32 | - name: Publish to NPM 📖 33 | uses: JS-DevTools/npm-publish@v2 34 | with: 35 | token: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🍔🍟🥤 12 | uses: actions/checkout@v3 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - name: Test 🧪 22 | run: | 23 | npm ci 24 | npm run check-ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- clip-for-deploy-start -- 2 | 3 | /docs 4 | /dist 5 | 6 | # -- clip-for-deploy-end -- 7 | 8 | *.pyc 9 | .DS_Store 10 | node_modules 11 | out 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Accum", 4 | "elems", 5 | "muigui", 6 | "stepify", 7 | "treeshake" 8 | ] 9 | } -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | muigui.org -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Gregg Tavares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To Do 2 | 3 | - [ ] Tests 4 | - [ ] DirectionView to use converters 5 | - [ ] Color RGB, HSL, LAB, Alpha? (just input=color for now?) 6 | - [x] red, green, blue color formats 7 | - [x] #RGB 8 | - [x] RGB 9 | - [x] #RRGGBB 10 | - [x] RRGGBB 11 | - [x] 0xRRGGBB 12 | - [x] [255, 255, 255] 13 | - [x] [1, 1, 1] 14 | - [ ] red, green, blue, alpha color formats 15 | 16 | note: the default color editor doesn't have alpha so this is a big ask. 17 | I think this is best as an extra add on since it requires a non-small 18 | amount of code to build a color editor? (actually, maybe I don't care about size) 19 | 20 | note: kind of leading toward not carrying about size and also 21 | making own color editor as the default one, at least in Chrome, 22 | kinda sucks. 23 | 24 | - [ ] 0xRRGGBBAA 25 | - [ ] #RRGGBBAA 26 | - [ ] [255, 255, 255, 255] 27 | - [ ] [1, 1, 1, 1] 28 | 29 | - [ ] hdr colors (this is really just not limited them to 1.0 on float colors, and/or CSS strings) 30 | - [x] (no for now) wrapping slider? (0-360) 31 | - [x] (no for now) Direction? (an arrow, low-pri) 32 | - [x] onChange 33 | - [x] name 34 | - [x] listen 35 | - [x] update 36 | - [x] disable 37 | - [x] remove 38 | - [x] hide 39 | - [x] fix 'period'. It's stepping by 0.1 and only going to 3.1 40 | - [ ] camelCase to Camel Case? 41 | - [x] get rid of min/max/step etc... as setters and add setOptions 42 | - [ ] interval 43 | - [ ] radio 44 | - [x] scroll wheel (not sure, opt in?) 45 | - [ ] make direction roll with min/max? 46 | - [ ] add ticks like volume control to direction (rename knob) 47 | - [x] text red if invalid? 48 | - [ ] fix editing text (forgot where it's failing) 49 | - [ ] what should 0x123 do for hex colors because we want it to be 0x000123. I guess it just works. 50 | - [ ] angle (circle with arrow) - both display, and editor 51 | - [ ] slider with ticks and number 52 | - [ ] circle input (circle with 2 arrows) - both display and editor 53 | - [ ] add keyboard controls to direction 54 | - [ ] add keyboard controls to vec2 55 | - [ ] add keyboard controls to color picker 56 | - [x] Label (not interactive) 57 | - [x] Label multi line (use as log) 58 | - [x] ask for file? (nah, drag and drop is better) 59 | - [ ] styles (form) 60 | - [x] deg to rad 61 | - [x] fix step with conversions 62 | - [x] format slider number? 63 | - [ ] TextArea (edit larger text) 64 | - [ ] pop-out text (or expand in place?) 65 | - [ ] submenus 66 | - [x] menu open/close 67 | - [x] scroll on long (css) 68 | - [ ] x, y, z 69 | - [x] copy paste all (no, what would that even look like? right click? Shift-Ctrl-C?) 70 | - [x] single line text 71 | - [x] splitter 72 | - [x] functions to query colors? 73 | - [x] (doesn't work) maybe just add color-scheme to CSS 74 | - [ ] make svg ids start with muigui 75 | - [ ] on enter in text field we need to invalidate value cache 76 | - [ ] add all 77 | - [ ] add with filter? (pass in filter so you can make positive or negative) 78 | - [ ] add with list of fields? 79 | - [ ] list of options by field name 80 | - [ ] recursive (max depth?) 81 | - [ ] match by instanceof ? 82 | - [ ] let user provide matcher? 83 | - [x] add hover for long name 84 | - [ ] try making custom controllers. In particular a list editor like unity 85 | - [x] fix "RGB" 86 | - [x] fix first column when changing width 87 | - [x] do autoplace test 88 | - [ ] Create layout units? Instead of using CSS directly on types maybe make 89 | components that do nothing but layout? 90 | `Column`, `Row`, etc. Then for layout it becomes 91 | 92 | Column[ 93 | title, 94 | Column[ 95 | Row[ 96 | Column[label, Color, Text], 97 | ], 98 | Row[ 99 | Column[label, Slider, Number], 100 | ] 101 | Row[ 102 | Button, 103 | ] 104 | ], 105 | ] 106 | 107 | No idea if that makes sense 108 | 109 | - [ ] try to refactor Text, Number, Slider, Color, Checkbox, etc, into more reusable components 110 | so you can combine them into a new component. Like ideally an X,Y,Z might be 111 | 3 sliders. So maybe instead of Checkbox extends ValueComponent it should just 112 | `ValueComponent.add(new Checkbox())` or something to that effect. Same with ValueComponent 113 | vs Component. Maybe it's `Component.add(new ValueComponent())` 114 | 115 | Can we separate text and number and reuse them? 116 | 117 | - [x] auto step (not going to do this for now) 118 | - [x] fix can't enter trailing '.' on input number (FF only?) (maybe don't set text when value from text) 119 | - [x] consider more explicit layout 120 | - [x] (1 part, 2 parts, 3 parts) or (1 part, 2 parts where 3 is [[1][2[1][2]]]) 121 | - [ ] Docs 122 | - [ ] API docs (jsdoc) 123 | - [ ] TypeScript 124 | - [x] add folder.onChange/onFinishChange 125 | - [x] Fix Safari Style 126 | - [x] Fix Safari overflow on long names 127 | - [x] Change menu to button or at least make it so you can focus 128 | - [x] add focus to color 129 | - [x] fix disabled so it disables all inputs (otherwise focus goes there) 130 | 131 | --- 132 | 133 | - [ ] look into add without object. eg 134 | 135 | ```js 136 | gui.addButton(title, fn); 137 | gui.addSlider(title, get, set, min, max, step); 138 | gui.addNumber(title, get, set, converter, step); 139 | gui.addText(title, get, set); 140 | gui.addCheckbox(title, get, set); 141 | gui.addColor(title, get, set, format); 142 | ``` 143 | 144 | or 145 | 146 | ```js 147 | gui.addController(new Button(title, fn)); 148 | gui.addController(new Slider(title, get, set, min, max, step)); 149 | gui.addController(new Number(title, get, set, converter, step)); 150 | gui.addController(new Text(title, get, set)); 151 | gui.addController(new Checkbox(title, get, set)); 152 | gui.addController(new Color(title, get, set, format)); 153 | ``` 154 | 155 | It's kind of gross but given this is probably a not a common desire, if you really want to mod a 156 | local variable you can do this 157 | 158 | ```js 159 | let temperature = 72.0; 160 | 161 | const helper = { 162 | get v() { return temperature; } 163 | set v(newV) { temperature = newV; } 164 | }; 165 | 166 | gui.add(helper, 'v').name('temperature'); 167 | ``` 168 | 169 | 170 | The second is arguably better than the first? The first is cluttered API, having to add every widget. 171 | Sadly the `addColor` and `addFolder` are already the expected API 😭 172 | 173 | - [ ] Decide what to do about to/from 174 | 175 | DirectionView: pass in what you want 176 | NumberView: pass in what you want 177 | what if I want hex? (use text view?) 178 | SliderView: pass in what you want 179 | TextView: ??? 180 | ColorView: ??? 181 | Vec2View: pass in what you want -------------------------------------------------------------------------------- /build/copy.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fsPromise from 'fs/promises'; 3 | import chokidar from 'chokidar'; 4 | 5 | export function copy({watch, transformFn = v => v}) { 6 | return new Promise(resolve => { 7 | const outDir = 'out'; 8 | 9 | async function copyFile(srcFilename) { 10 | const dstFilename = path.join(outDir, srcFilename); 11 | const dirname = path.dirname(dstFilename); 12 | try { 13 | await fsPromise.stat(dirname); 14 | } catch { 15 | await fsPromise.mkdir(dirname, { recursive: true }); 16 | } 17 | console.log('copy', srcFilename, '->', dstFilename); 18 | const src = await fsPromise.readFile(srcFilename); 19 | const dst = transformFn(src, srcFilename, dstFilename); 20 | await fsPromise.writeFile(dstFilename, dst); 21 | } 22 | 23 | chokidar.watch('.', { 24 | ignored: [ 25 | '.git', 26 | 'node_modules', 27 | 'build', 28 | 'out', 29 | 'src', 30 | 'dist', 31 | '**/.*', 32 | ], 33 | }).on('all', (event, path) => { 34 | switch (event) { 35 | case 'add': 36 | case 'change': 37 | copyFile(path); 38 | break; 39 | case 'ready': 40 | if (!watch) { 41 | resolve(); 42 | } 43 | break; 44 | } 45 | }); 46 | }); 47 | } -------------------------------------------------------------------------------- /build/prep-for-deploy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import * as url from 'url'; 4 | const dirname = url.fileURLToPath(new URL('.', import.meta.url)); 5 | 6 | 7 | const ignoreFilename = path.join(dirname, '..', '.gitignore'); 8 | const ignore = fs.readFileSync(ignoreFilename, {encoding: 'utf8'}); 9 | const newIgnore = ignore.replace(/# -- clip-for-deploy-start --[\s\S]*?# -- clip-for-deploy-end --/, ''); 10 | fs.writeFileSync(ignoreFilename, newIgnore); 11 | 12 | const version = parseInt(JSON.parse(fs.readFileSync('package.json', {encoding: 'utf8'})).version); 13 | 14 | function transformJS(src) { 15 | return src.replace(/'.*?';\s+\/\*\s+muigui-include\s+\*\//g, `'/dist/${version}.x/muigui.module.js';`); 16 | } 17 | 18 | [ 19 | 'examples/js/index/index.js', 20 | ].forEach(filename => { 21 | const src = fs.readFileSync(filename, {encoding: 'utf8'}); 22 | const dst = transformJS(src); 23 | if (src !== dst) { 24 | fs.writeFileSync(filename, dst); 25 | } 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /build/pull-vsa.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | 4 | // node build/pull-vsa.js ...url-to-vsa 5 | const urls = process.argv.slice(2); 6 | 7 | for (const url of urls) { 8 | const req = await fetch(`${url}?format=json`); 9 | const data = await req.json(); 10 | const filename = `examples/js/index/effects/${data.name}.js`; 11 | const shader = data.settings.shader; 12 | data.settings.shader = '--shader--'; 13 | const s = `\ 14 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 15 | /* eslint-disable no-useless-escape */ 16 | export default ${JSON.stringify(data, null, 2) 17 | .replace(/"(.*?)":/g, '$1:') 18 | .replace('"--shader--"', `\`${shader}\``)};\n`; 19 | console.log('write', filename); 20 | fs.writeFileSync(filename, s); 21 | } -------------------------------------------------------------------------------- /build/serve.js: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process'; 2 | import {copy} from './copy.js'; 3 | 4 | spawn('./node_modules/.bin/tsc', [ 5 | '--watch', 6 | '--project', 'build/tsconfig-serve.json', 7 | ], { 8 | stdio: 'inherit', 9 | }); 10 | 11 | spawn('./node_modules/.bin/servez', [ 12 | 'out', 13 | ], { 14 | shell: true, 15 | stdio: 'inherit', 16 | }); 17 | 18 | copy({watch: true}); 19 | -------------------------------------------------------------------------------- /build/tsconfig-serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/controllers/direction.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /examples/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | muigui - custom 7 | 8 | 9 | 10 | 54 | 55 | -------------------------------------------------------------------------------- /examples/js/index/effects.js: -------------------------------------------------------------------------------- 1 | import admo from './effects/admo.js'; 2 | import bwow from './effects/bwow.js'; 3 | import codez from './effects/codez.js'; 4 | import cyty from './effects/cyty.js'; 5 | import discus from './effects/discus.js'; 6 | import dottoChoukoukei from './effects/dotto-chouhoukei.js'; 7 | import hexit2 from './effects/hexit2.js'; 8 | import loopTest from './effects/loop-test.js'; 9 | import pookymelon from './effects/pookymelon.js'; 10 | import rollin from './effects/rollin.js'; 11 | import starfield from './effects/starfield.js'; 12 | import ung from './effects/ung.js'; 13 | 14 | export default { 15 | admo, 16 | bwow, 17 | codez, 18 | cyty, 19 | discus, 20 | dottoChoukoukei, 21 | hexit2, 22 | loopTest, 23 | pookymelon, 24 | rollin, 25 | starfield, 26 | ung, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/js/index/effects/bwow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 2 | /* eslint-disable no-useless-escape */ 3 | export default { 4 | _id: "6Yx2A7TQ6NnyHhFPQ", 5 | createdAt: "2016-09-02T17:38:52.975Z", 6 | modifiedAt: "2016-09-18T04:57:25.845Z", 7 | origId: "qjkP6BDvEFyD6CfZC", 8 | name: "bwow", 9 | username: "gman", 10 | avatarUrl: "https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200", 11 | settings: { 12 | num: 10404, 13 | mode: "TRIANGLES", 14 | sound: "", 15 | lineSize: "NATIVE", 16 | backgroundColor: [ 17 | 1, 18 | 1, 19 | 1, 20 | 1 21 | ], 22 | shader: `/* 23 | 24 | Challenge: 01 25 | 26 | */ 27 | 28 | 29 | 30 | 31 | #define PI radians(180.) 32 | 33 | vec3 hsv2rgb(vec3 c) { 34 | c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); 35 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 36 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 37 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 38 | } 39 | 40 | mat4 rotX(float angleInRadians) { 41 | float s = sin(angleInRadians); 42 | float c = cos(angleInRadians); 43 | 44 | return mat4( 45 | 1, 0, 0, 0, 46 | 0, c, s, 0, 47 | 0, -s, c, 0, 48 | 0, 0, 0, 1); 49 | } 50 | 51 | mat4 rotY(float angleInRadians) { 52 | float s = sin(angleInRadians); 53 | float c = cos(angleInRadians); 54 | 55 | return mat4( 56 | c, 0,-s, 0, 57 | 0, 1, 0, 0, 58 | s, 0, c, 0, 59 | 0, 0, 0, 1); 60 | } 61 | 62 | mat4 rotZ(float angleInRadians) { 63 | float s = sin(angleInRadians); 64 | float c = cos(angleInRadians); 65 | 66 | return mat4( 67 | c,-s, 0, 0, 68 | s, c, 0, 0, 69 | 0, 0, 1, 0, 70 | 0, 0, 0, 1); 71 | } 72 | 73 | mat4 trans(vec3 trans) { 74 | return mat4( 75 | 1, 0, 0, 0, 76 | 0, 1, 0, 0, 77 | 0, 0, 1, 0, 78 | trans, 1); 79 | } 80 | 81 | mat4 ident() { 82 | return mat4( 83 | 1, 0, 0, 0, 84 | 0, 1, 0, 0, 85 | 0, 0, 1, 0, 86 | 0, 0, 0, 1); 87 | } 88 | 89 | mat4 scale(vec3 s) { 90 | return mat4( 91 | s[0], 0, 0, 0, 92 | 0, s[1], 0, 0, 93 | 0, 0, s[2], 0, 94 | 0, 0, 0, 1); 95 | } 96 | 97 | mat4 uniformScale(float s) { 98 | return mat4( 99 | s, 0, 0, 0, 100 | 0, s, 0, 0, 101 | 0, 0, s, 0, 102 | 0, 0, 0, 1); 103 | } 104 | 105 | mat4 persp(float fov, float aspect, float zNear, float zFar) { 106 | float f = tan(PI * 0.5 - 0.5 * fov); 107 | float rangeInv = 1.0 / (zNear - zFar); 108 | 109 | return mat4( 110 | f / aspect, 0, 0, 0, 111 | 0, f, 0, 0, 112 | 0, 0, (zNear + zFar) * rangeInv, -1, 113 | 0, 0, zNear * zFar * rangeInv * 2., 0); 114 | } 115 | 116 | mat4 trInv(mat4 m) { 117 | mat3 i = mat3( 118 | m[0][0], m[1][0], m[2][0], 119 | m[0][1], m[1][1], m[2][1], 120 | m[0][2], m[1][2], m[2][2]); 121 | vec3 t = -i * m[3].xyz; 122 | 123 | return mat4( 124 | i[0], t[0], 125 | i[1], t[1], 126 | i[2], t[2], 127 | 0, 0, 0, 1); 128 | } 129 | 130 | mat4 transpose(mat4 m) { 131 | return mat4( 132 | m[0][0], m[1][0], m[2][0], m[3][0], 133 | m[0][1], m[1][1], m[2][1], m[3][1], 134 | m[0][2], m[1][2], m[2][2], m[3][2], 135 | m[0][3], m[1][3], m[2][3], m[3][3]); 136 | } 137 | 138 | mat4 lookAt(vec3 eye, vec3 target, vec3 up) { 139 | vec3 zAxis = normalize(eye - target); 140 | vec3 xAxis = normalize(cross(up, zAxis)); 141 | vec3 yAxis = cross(zAxis, xAxis); 142 | 143 | return mat4( 144 | xAxis, 0, 145 | yAxis, 0, 146 | zAxis, 0, 147 | eye, 1); 148 | } 149 | 150 | mat4 inverse(mat4 m) { 151 | float 152 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 153 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 154 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 155 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 156 | 157 | b00 = a00 * a11 - a01 * a10, 158 | b01 = a00 * a12 - a02 * a10, 159 | b02 = a00 * a13 - a03 * a10, 160 | b03 = a01 * a12 - a02 * a11, 161 | b04 = a01 * a13 - a03 * a11, 162 | b05 = a02 * a13 - a03 * a12, 163 | b06 = a20 * a31 - a21 * a30, 164 | b07 = a20 * a32 - a22 * a30, 165 | b08 = a20 * a33 - a23 * a30, 166 | b09 = a21 * a32 - a22 * a31, 167 | b10 = a21 * a33 - a23 * a31, 168 | b11 = a22 * a33 - a23 * a32, 169 | 170 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 171 | 172 | return mat4( 173 | a11 * b11 - a12 * b10 + a13 * b09, 174 | a02 * b10 - a01 * b11 - a03 * b09, 175 | a31 * b05 - a32 * b04 + a33 * b03, 176 | a22 * b04 - a21 * b05 - a23 * b03, 177 | a12 * b08 - a10 * b11 - a13 * b07, 178 | a00 * b11 - a02 * b08 + a03 * b07, 179 | a32 * b02 - a30 * b05 - a33 * b01, 180 | a20 * b05 - a22 * b02 + a23 * b01, 181 | a10 * b10 - a11 * b08 + a13 * b06, 182 | a01 * b08 - a00 * b10 - a03 * b06, 183 | a30 * b04 - a31 * b02 + a33 * b00, 184 | a21 * b02 - a20 * b04 - a23 * b00, 185 | a11 * b07 - a10 * b09 - a12 * b06, 186 | a00 * b09 - a01 * b07 + a02 * b06, 187 | a31 * b01 - a30 * b03 - a32 * b00, 188 | a20 * b03 - a21 * b01 + a22 * b00) / det; 189 | } 190 | 191 | mat4 cameraLookAt(vec3 eye, vec3 target, vec3 up) { 192 | #if 1 193 | return inverse(lookAt(eye, target, up)); 194 | #else 195 | vec3 zAxis = normalize(target - eye); 196 | vec3 xAxis = normalize(cross(up, zAxis)); 197 | vec3 yAxis = cross(zAxis, xAxis); 198 | 199 | return mat4( 200 | xAxis, 0, 201 | yAxis, 0, 202 | zAxis, 0, 203 | -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1); 204 | #endif 205 | 206 | } 207 | 208 | 209 | 210 | // hash function from https://www.shadertoy.com/view/4djSRW 211 | float hash(float p) { 212 | vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); 213 | p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); 214 | return fract(p2.x * p2.y * 95.4337); 215 | } 216 | 217 | // times 2 minus 1 218 | float t2m1(float v) { 219 | return v * 2. - 1.; 220 | } 221 | 222 | // times .5 plus .5 223 | float t5p5(float v) { 224 | return v * 0.5 + 0.5; 225 | } 226 | 227 | float inv(float v) { 228 | return 1. - v; 229 | } 230 | 231 | void getCirclePoint(const float numEdgePointsPerCircle, const float id, const float inner, const float start, const float end, out vec3 pos) { 232 | float outId = id - floor(id / 3.) * 2. - 1.; // 0 1 2 3 4 5 6 7 8 .. 0 1 2, 1 2 3, 2 3 4 233 | float ux = floor(id / 6.) + mod(id, 2.); 234 | float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx 235 | float u = ux / numEdgePointsPerCircle; 236 | float v = mix(inner, 1., vy); 237 | float a = mix(start, end, u) * PI * 2. + PI * 0.0; 238 | float s = sin(a); 239 | float c = cos(a); 240 | float x = c * v; 241 | float y = s * v; 242 | float z = 0.; 243 | pos = vec3(x, y, z); 244 | } 245 | 246 | 247 | #define CUBE_POINTS_PER_FACE 6. 248 | #define FACES_PER_CUBE 6. 249 | #define POINTS_PER_CUBE (CUBE_POINTS_PER_FACE * FACES_PER_CUBE) 250 | void getCubePoint(const float id, out vec3 position, out vec3 normal) { 251 | float quadId = floor(mod(id, POINTS_PER_CUBE) / CUBE_POINTS_PER_FACE); 252 | float sideId = mod(quadId, 3.); 253 | float flip = mix(1., -1., step(2.5, quadId)); 254 | // 0 1 2 1 2 3 255 | float facePointId = mod(id, CUBE_POINTS_PER_FACE); 256 | float pointId = mod(facePointId - floor(facePointId / 3.0), 6.0); 257 | float a = pointId * PI * 2. / 4. + PI * 0.25; 258 | vec3 p = vec3(cos(a), 0.707106781, sin(a)) * flip; 259 | vec3 n = vec3(0, 1, 0) * flip; 260 | float lr = mod(sideId, 2.); 261 | float ud = step(2., sideId); 262 | mat4 mat = rotX(lr * PI * 0.5); 263 | mat *= rotZ(ud * PI * 0.5); 264 | position = (mat * vec4(p, 1)).xyz; 265 | normal = (mat * vec4(n, 0)).xyz; 266 | } 267 | 268 | void main() { 269 | float pointId = vertexId; 270 | 271 | vec3 pos; 272 | vec3 normal; 273 | getCubePoint(pointId, pos, normal); 274 | float cubeId = floor(pointId / 36.); 275 | float faceId = mod(floor(pointId / 6.), 6.); 276 | float numCubes = floor(vertexCount / 36.); 277 | float down = floor(sqrt(numCubes)); 278 | float across = floor(numCubes / down); 279 | 280 | float cx = mod(cubeId, across); 281 | float cy = floor(cubeId / across); 282 | 283 | float cu = cx / (across - 1.); 284 | float cv = cy / (down - 1.); 285 | 286 | float ca = cu * 2. - 1.; 287 | float cd = cv * 2. - 1.; 288 | 289 | float tm = PI * 1.75;time * 0.1; 290 | mat4 mat = persp(radians(60.0), resolution.x / resolution.y, 0.1, 1000.0); 291 | vec3 eye = vec3(cos(tm) * 1., sin(tm * 0.9) * .1 + 0.7, sin(tm) * 1.); 292 | vec3 target = vec3(0); 293 | vec3 up = vec3(0,1,0); 294 | 295 | mat *= cameraLookAt(eye, target, up); 296 | mat *= trans(vec3(ca, 0, cd) * .3); 297 | // mat *= rotX(time + abs(ca) * 5.); 298 | // mat *= rotZ(time + abs(cd) * 6.); 299 | float d = length(vec2(ca, cd)); 300 | mat *= scale(vec3( 301 | 1, 302 | mix(2., 303 | 12., 304 | pow(sin(time * -2. + d * PI * 1.) * 0.5 + 0.5, 305 | 1.5 + (1. - d) * 2.)), 306 | 1)); 307 | mat *= uniformScale(0.03); 308 | 309 | 310 | gl_Position = mat * vec4(pos, 1); 311 | vec3 n = normalize((mat * vec4(normal, 0)).xyz); 312 | 313 | vec3 lightDir = normalize(vec3(3.3, 0.4, -1)); 314 | 315 | float hue = .15 + mod(faceId + 2., 6.) * 0.17; 316 | float sat = 0.3; 317 | float val = 1.; 318 | vec3 color = hsv2rgb(vec3(hue, sat, val)) * 1.125; 319 | v_color = vec4(color * (dot(n, lightDir) * 0.5 + 0.5), 1); 320 | } 321 | 322 | ` 323 | }, 324 | revisionId: "vWR26Fhpcy3b2KLme", 325 | revisionUrl: "https://www.vertexshaderart.com/art/6Yx2A7TQ6NnyHhFPQ/revision/vWR26Fhpcy3b2KLme", 326 | artUrl: "https://www.vertexshaderart.com/art/undefined", 327 | origUrl: "https://www.vertexshaderart.com/art/qjkP6BDvEFyD6CfZC" 328 | }; 329 | -------------------------------------------------------------------------------- /examples/js/index/effects/discus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 2 | /* eslint-disable no-useless-escape */ 3 | export default { 4 | _id: "yX9SGHv6RPPqcsXvh", 5 | createdAt: "2017-01-28T04:48:49.529Z", 6 | modifiedAt: "2017-01-28T05:56:37.779Z", 7 | origId: "yX9SGHv6RPPqcsXvh", 8 | name: "discus", 9 | username: "gman", 10 | avatarUrl: "https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200", 11 | settings: { 12 | num: 100000, 13 | mode: "TRIANGLES", 14 | sound: "https://soundcloud.com/beatsfar/grand-mas-mandoline", 15 | lineSize: "NATIVE", 16 | backgroundColor: [ 17 | 1, 18 | 0, 19 | 0, 20 | 1 21 | ], 22 | shader: ` 23 | 24 | 25 | #define PI radians(180.) 26 | 27 | vec3 hsv2rgb(vec3 c) { 28 | c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); 29 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 30 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 31 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 32 | } 33 | 34 | mat4 rotY( float angle ) { 35 | float s = sin( angle ); 36 | float c = cos( angle ); 37 | 38 | return mat4( 39 | c, 0,-s, 0, 40 | 0, 1, 0, 0, 41 | s, 0, c, 0, 42 | 0, 0, 0, 1); 43 | } 44 | 45 | 46 | mat4 rotZ( float angle ) { 47 | float s = sin( angle ); 48 | float c = cos( angle ); 49 | 50 | return mat4( 51 | c,-s, 0, 0, 52 | s, c, 0, 0, 53 | 0, 0, 1, 0, 54 | 0, 0, 0, 1); 55 | } 56 | 57 | mat4 trans(vec3 trans) { 58 | return mat4( 59 | 1, 0, 0, 0, 60 | 0, 1, 0, 0, 61 | 0, 0, 1, 0, 62 | trans, 1); 63 | } 64 | 65 | mat4 ident() { 66 | return mat4( 67 | 1, 0, 0, 0, 68 | 0, 1, 0, 0, 69 | 0, 0, 1, 0, 70 | 0, 0, 0, 1); 71 | } 72 | 73 | mat4 scale(vec3 s) { 74 | return mat4( 75 | s[0], 0, 0, 0, 76 | 0, s[1], 0, 0, 77 | 0, 0, s[2], 0, 78 | 0, 0, 0, 1); 79 | } 80 | 81 | mat4 uniformScale(float s) { 82 | return mat4( 83 | s, 0, 0, 0, 84 | 0, s, 0, 0, 85 | 0, 0, s, 0, 86 | 0, 0, 0, 1); 87 | } 88 | 89 | // hash function from https://www.shadertoy.com/view/4djSRW 90 | float hash(float p) { 91 | vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); 92 | p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); 93 | return fract(p2.x * p2.y * 95.4337); 94 | } 95 | 96 | float m1p1(float v) { 97 | return v * 2. - 1.; 98 | } 99 | 100 | float p1m1(float v) { 101 | return v * 0.5 + 0.5; 102 | } 103 | 104 | float inv(float v) { 105 | return 1. - v; 106 | } 107 | 108 | uniform float numSides; 109 | #define NUM_EDGE_POINTS_PER_CIRCLE numSides 110 | #define NUM_POINTS_PER_CIRCLE (NUM_EDGE_POINTS_PER_CIRCLE * 6.) 111 | #define NUM_CIRCLES_PER_GROUP 2. 112 | void getCirclePoint(const float id, const float inner, const float start, const float end, out vec3 pos) { 113 | float outId = id - floor(id / 3.) * 2. - 1.; // 0 1 2 3 4 5 6 7 8 .. 0 1 2, 1 2 3, 2 3 4 114 | float ux = floor(id / 6.) + mod(id, 2.); 115 | float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx 116 | float u = ux / NUM_EDGE_POINTS_PER_CIRCLE; 117 | float v = mix(inner, 1., vy); 118 | float a = mix(start, end, u) * PI * 2. + PI * 0.0; 119 | float s = sin(a); 120 | float c = cos(a); 121 | float x = c * v; 122 | float y = s * v; 123 | float z = 0.; 124 | pos = vec3(x, y, z); 125 | } 126 | 127 | float goop(float t) { 128 | return sin(t) + sin(t * 0.27) + sin(t * 0.13) + sin(t * 0.73); 129 | } 130 | 131 | float easeInOutSine(float t) { 132 | return (-0.5 * (cos(PI * t) - 1.)); 133 | } 134 | 135 | float mixer(float t, float timeOff, float duration) { 136 | t = mod(t, duration * 2.0); 137 | t = t - timeOff; 138 | if (t > duration) { 139 | t = duration + 1. - t; 140 | } 141 | return easeInOutSine(clamp(t, 0., 1.)); 142 | } 143 | 144 | uniform float speed; 145 | uniform float brightness; 146 | uniform vec3 color1; 147 | uniform vec3 color2; 148 | uniform float rotation; 149 | uniform float split; 150 | 151 | void main() { 152 | float circleId = floor(vertexId / NUM_POINTS_PER_CIRCLE); 153 | float groupId = floor(circleId / NUM_CIRCLES_PER_GROUP); 154 | float pointId = mod(vertexId, NUM_POINTS_PER_CIRCLE); 155 | float sliceId = mod(floor(vertexId / 6.), 2.); 156 | float side = mix(-1., 1., step(0.5, mod(circleId, 2.))); 157 | float numCircles = floor(vertexCount / NUM_POINTS_PER_CIRCLE); 158 | float numGroups = floor(numCircles / NUM_CIRCLES_PER_GROUP); 159 | float cu = circleId / numCircles; 160 | float gv = groupId / numGroups; 161 | float cgId = mod(circleId, NUM_CIRCLES_PER_GROUP); 162 | float cgv = cgId / NUM_CIRCLES_PER_GROUP; 163 | float ncgv = 1. - cgv; 164 | 165 | 166 | float tm = time - cgv * 0.2; 167 | float su = hash(groupId); 168 | float snd = texture2D(sound, vec2(mix(0.01, 0.14, su), gv * 0.05)).a; 169 | 170 | //snd = pow(snd, mix(2., 0.5, su)); 171 | 172 | 173 | vec3 pos; 174 | float inner = mix(0.0, 1. - pow(snd, 4.), cgId); 175 | float start = 0.;//fract(hash(sideId * 0.33) + sin(time * 0.1 + sideId) * 1.1); 176 | float end = 1.; //start + hash(sideId + 1.); 177 | getCirclePoint(pointId, inner, start, end, pos); 178 | pos.z = cgv; 179 | 180 | // float historyX = mix(0.01, 0.14, u); 181 | // snd = pow(snd, mix(2., 0.5, u)); 182 | 183 | 184 | 185 | // ---- 186 | float gDown = floor(sqrt(numGroups)); 187 | float gAcross = floor(numGroups / gDown); 188 | vec3 offset0 = vec3( 189 | mod(groupId, gAcross) - (gAcross - 1.) / 2., 190 | floor(groupId / gAcross) - (gDown - 1.) / 2., 191 | 0) * 0.2; 192 | 193 | // ---- 194 | float ang = gv * 10.0; 195 | vec3 offset1 = vec3(cos(ang), sin(ang), 0) * gv * 0.5; 196 | 197 | // ---- 198 | vec3 offset2 = (vec3(hash(groupId), hash(groupId * 0.37), 0) * 2. - 1.) * 0.8; 199 | 200 | // ---- 201 | ang = gv * 20.0; 202 | float rad = floor(groupId / pow(2., gv + 3.)); 203 | vec3 offset3 = vec3(cos(ang), sin(ang), 0) * mix(0.3, 0.7, rad); 204 | 205 | // 0-6 206 | float m = 0.;tm; //mod(tm, 4. * 3.); 207 | float mix01 = mixer(m, 0., 3.); 208 | float mix23 = mixer(m, 0., 3.); 209 | float mix0123 = mixer(m, 0., 6.); 210 | vec3 offset = 211 | mix( 212 | mix(offset0, offset1, mix01), 213 | mix(offset2, offset3, mix23), 214 | mix0123); 215 | 216 | // vec3 offset = vec3(hash(groupId) * 0.8, m1p1(hash(groupId * 0.37)), cgv); 217 | // offset.x += m1p1(pow(snd, 5.0) + goop(groupId + time * 0.) * 0.1); 218 | // offset.y += goop(groupId + time * 0.) * 0.1; 219 | vec3 aspect = vec3(1, resolution.x / resolution.y, 1); 220 | 221 | mat4 mat = ident(); 222 | mat *= scale(aspect / gAcross * 12.); 223 | mat *= trans(vec3(0.25,0,0)); 224 | mat *= rotZ(rotation); 225 | mat *= trans(offset); 226 | mat *= rotZ(offset.x * offset.y); 227 | float sp = pow(snd, 5.0); 228 | 229 | mat *= uniformScale(mix(sp, 1. - sp, cgId) * 0.1 + sliceId * 0.0); 230 | gl_Position = vec4((mat * vec4(pos, 1)).xyz, 1); 231 | gl_PointSize = 4.; 232 | 233 | float hue = tm * 0.0 + mix(0., 20.2, hash(groupId * 0.23)); 234 | float sat = 1. - pow(snd, 5.); 235 | float pump = step(snd, split); //pow(snd, 2.); 236 | float val = pump * brightness;//ncgv;//1.;//mix(0.0, 0.0, fract(circleId * 0.79)) + sliceId * .65; 237 | // v_color = vec4(mix(color1, hsv2rgb(vec3(hue, sat, val)), pump), 1); 238 | v_color = vec4(mix(color1, color2, pump), 1); 239 | v_color.rgb *= v_color.a; 240 | } 241 | ` 242 | }, 243 | revisionId: "yX9SGHv6RPPqcsXvh", 244 | revisionUrl: "https://www.vertexshaderart.com/art/yX9SGHv6RPPqcsXvh/revision/yX9SGHv6RPPqcsXvh", 245 | artUrl: "https://www.vertexshaderart.com/art/yX9SGHv6RPPqcsXvh", 246 | origUrl: "https://www.vertexshaderart.com/art/yX9SGHv6RPPqcsXvh" 247 | }; 248 | -------------------------------------------------------------------------------- /examples/js/index/effects/hexit2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 2 | /* eslint-disable no-useless-escape */ 3 | export default { 4 | _id: "d7anES7ef6WrrDwsy", 5 | createdAt: "2017-01-28T04:48:49.529Z", 6 | modifiedAt: "2017-01-28T05:56:37.779Z", 7 | origId: "yey7qrMtmhZZhq2K6", 8 | name: "hexit2", 9 | username: "gman", 10 | avatarUrl: "https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200", 11 | settings: { 12 | num: 100000, 13 | mode: "TRIANGLES", 14 | sound: "https://soundcloud.com/beatsfar/grand-mas-mandoline", 15 | lineSize: "NATIVE", 16 | backgroundColor: [ 17 | 1, 18 | 0, 19 | 0, 20 | 1 21 | ], 22 | shader: ` 23 | #define PI radians(180.0) 24 | 25 | vec3 hsv2rgb(vec3 c) { 26 | c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); 27 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 28 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 29 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 30 | } 31 | 32 | mat4 rotY( float angle ) { 33 | float s = sin( angle ); 34 | float c = cos( angle ); 35 | 36 | return mat4( 37 | c, 0,-s, 0, 38 | 0, 1, 0, 0, 39 | s, 0, c, 0, 40 | 0, 0, 0, 1); 41 | } 42 | 43 | 44 | mat4 rotZ( float angle ) { 45 | float s = sin( angle ); 46 | float c = cos( angle ); 47 | 48 | return mat4( 49 | c,-s, 0, 0, 50 | s, c, 0, 0, 51 | 0, 0, 1, 0, 52 | 0, 0, 0, 1); 53 | } 54 | 55 | mat4 trans(vec3 trans) { 56 | return mat4( 57 | 1, 0, 0, 0, 58 | 0, 1, 0, 0, 59 | 0, 0, 1, 0, 60 | trans, 1); 61 | } 62 | 63 | mat4 ident() { 64 | return mat4( 65 | 1, 0, 0, 0, 66 | 0, 1, 0, 0, 67 | 0, 0, 1, 0, 68 | 0, 0, 0, 1); 69 | } 70 | 71 | mat4 scale(vec3 s) { 72 | return mat4( 73 | s[0], 0, 0, 0, 74 | 0, s[1], 0, 0, 75 | 0, 0, s[2], 0, 76 | 0, 0, 0, 1); 77 | } 78 | 79 | mat4 uniformScale(float s) { 80 | return mat4( 81 | s, 0, 0, 0, 82 | 0, s, 0, 0, 83 | 0, 0, s, 0, 84 | 0, 0, 0, 1); 85 | } 86 | 87 | // hash function from https://www.shadertoy.com/view/4djSRW 88 | float hash(float p) { 89 | vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); 90 | p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); 91 | return fract(p2.x * p2.y * 95.4337); 92 | } 93 | 94 | float m1p1(float v) { 95 | return v * 2. - 1.; 96 | } 97 | 98 | float p1m1(float v) { 99 | return v * 0.5 + 0.5; 100 | } 101 | 102 | float inv(float v) { 103 | return 1. - v; 104 | } 105 | 106 | uniform float numSides; 107 | 108 | #define NUM_EDGE_POINTS_PER_CIRCLE numSides 109 | #define NUM_POINTS_PER_CIRCLE (NUM_EDGE_POINTS_PER_CIRCLE * 6.) 110 | #define NUM_CIRCLES_PER_GROUP 1. 111 | void getCirclePoint(const float id, const float inner, const float start, const float end, out vec3 pos) { 112 | float outId = id - floor(id / 3.) * 2. - 1.; // 0 1 2 3 4 5 6 7 8 .. 0 1 2, 1 2 3, 2 3 4 113 | float ux = floor(id / 6.) + mod(id, 2.); 114 | float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx 115 | float u = ux / NUM_EDGE_POINTS_PER_CIRCLE; 116 | float v = mix(inner, 1., vy); 117 | float a = mix(start, end, u) * PI * 2. + PI * 0.0; 118 | float s = sin(a); 119 | float c = cos(a); 120 | float x = c * v; 121 | float y = s * v; 122 | float z = 0.; 123 | pos = vec3(x, y, z); 124 | } 125 | 126 | float goop(float t) { 127 | return sin(t) + sin(t * 0.27) + sin(t * 0.13) + sin(t * 0.73); 128 | } 129 | 130 | float easeInOutSine(float t) { 131 | return (-0.5 * (cos(PI * t) - 1.)); 132 | } 133 | 134 | float mixer(float t, float timeOff, float duration) { 135 | t = mod(t, duration * 2.0); 136 | t = t - timeOff; 137 | if (t > duration) { 138 | t = duration + 1. - t; 139 | } 140 | return easeInOutSine(clamp(t, 0., 1.)); 141 | } 142 | 143 | uniform float s1; 144 | uniform float s2; 145 | uniform float s3; 146 | uniform float s4; 147 | 148 | void main() { 149 | float circleId = floor(vertexId / NUM_POINTS_PER_CIRCLE); 150 | float groupId = floor(circleId / NUM_CIRCLES_PER_GROUP); 151 | float pointId = mod(vertexId, NUM_POINTS_PER_CIRCLE); 152 | float sliceId = mod(floor(vertexId / 6.), 2.); 153 | float side = mix(-1., 1., step(0.5, mod(circleId, 2.))); 154 | float numCircles = floor(vertexCount / NUM_POINTS_PER_CIRCLE); 155 | float numGroups = floor(numCircles / NUM_CIRCLES_PER_GROUP); 156 | float cu = circleId / numCircles; 157 | float gv = groupId / numGroups; 158 | float cgId = mod(circleId, NUM_CIRCLES_PER_GROUP); 159 | float cgv = cgId / NUM_CIRCLES_PER_GROUP; 160 | float ncgv = 1. - cgv; 161 | 162 | 163 | //snd = pow(snd, mix(2., 0.5, su)); 164 | 165 | 166 | vec3 pos; 167 | float inner = 0.;//mix(0.0, 1. - pow(snd, 4.), cgId); 168 | float start = 0.;//fract(hash(sideId * 0.33) + sin(time * 0.1 + sideId) * 1.1); 169 | float end = 1.; //start + hash(sideId + 1.); 170 | getCirclePoint(pointId, inner, start, end, pos); 171 | pos.z = cgv; 172 | 173 | // float historyX = mix(0.01, 0.14, u); 174 | // snd = pow(snd, mix(2., 0.5, u)); 175 | 176 | 177 | 178 | // ---- 179 | float gDown = floor(sqrt(numGroups)); 180 | float gAcross = floor(numGroups / gDown); 181 | float gx = mod(groupId, gAcross); 182 | float gy = floor(groupId / gAcross); 183 | vec3 offset = vec3( 184 | gx - (gAcross - 1.) / 2. + mod(gy, 2.) * 0.5, 185 | gy - (gDown - 1.) / 2., 186 | 0) * 0.17; 187 | 188 | float tm = time - cgv * 0.2; 189 | float su = hash(groupId); 190 | float snd = texture2D(sound, vec2(mix(0.001, 0.015, su), length(offset) * 0.125)).a; 191 | 192 | 193 | 194 | // vec3 offset = vec3(hash(groupId) * 0.8, m1p1(hash(groupId * 0.37)), cgv); 195 | // offset.x += m1p1(pow(snd, 5.0) + goop(groupId + time * 0.) * 0.1); 196 | // offset.y += goop(groupId + time * 0.) * 0.1; 197 | vec3 aspect = vec3(1, resolution.x / resolution.y, 1); 198 | 199 | mat4 mat = ident(); 200 | mat *= scale(aspect * (0.2 / 3.0) * numSides); 201 | mat *= trans(vec3(0.25,0,0)); 202 | mat *= rotZ(-time * 0. + snd * .0); 203 | mat *= trans(offset); 204 | float sp = pow(snd, 5.0); 205 | 206 | mat *= rotZ(time * 0.01 + sin(gx * s1 * 0.1) + cos(gy * s2 * 0.1)); 207 | mat *= uniformScale(0.1 * pow(snd, 0.));// + -sin((time + 0.5) * 6.) * 0.01); 208 | //mat *= uniformScale(mix(sp, 1. - sp, cgId) * 0.1 + sliceId * 0.0); 209 | gl_Position = vec4((mat * vec4(pos, 1)).xyz, 1); 210 | gl_PointSize = 4.; 211 | 212 | float hue = tm * 0.05 + fract(snd * 2.5) * 0.2 + 0.3 + mix(0., .02, length(offset)); 213 | float sat = pow(snd, 5.); 214 | float val = mix(hash(groupId), 1.0, step(0.98, snd));//ncgv;//1.;//mix(0.0, 0.0, fract(circleId * 0.79)) + sliceId * .65; 215 | v_color = vec4(hsv2rgb(vec3(hue, sat, val)), 1); 216 | v_color.rgb *= v_color.a; 217 | } 218 | ` 219 | }, 220 | revisionId: "yey7qrMtmhZZhq2K6", 221 | revisionUrl: "https://www.vertexshaderart.com/art/yey7qrMtmhZZhq2K6/revision/yey7qrMtmhZZhq2K6", 222 | artUrl: "https://www.vertexshaderart.com/art/yey7qrMtmhZZhq2K6", 223 | origUrl: "https://www.vertexshaderart.com/art/yey7qrMtmhZZhq2K6" 224 | }; 225 | -------------------------------------------------------------------------------- /examples/js/index/effects/loop-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 2 | /* eslint-disable no-useless-escape */ 3 | export default { 4 | _id: "ZFSiQpx33DLDg9hmd", 5 | createdAt: "2018-06-06T08:31:10.941Z", 6 | modifiedAt: "2018-06-06T08:31:10.941Z", 7 | origId: null, 8 | name: "loop-test", 9 | username: "gman", 10 | avatarUrl: "https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200", 11 | settings: { 12 | num: 100000, 13 | mode: "TRIANGLES", 14 | sound: "https://soundcloud.com/greggman/testing-1-2-3", 15 | lineSize: "NATIVE", 16 | backgroundColor: [ 17 | 1, 18 | 1, 19 | 1, 20 | 1 21 | ], 22 | shader: `/* 23 | 24 | VertexShaderArt Boilerplate Library 25 | 26 | */ 27 | 28 | 29 | 30 | 31 | #define PI radians(180.) 32 | 33 | vec3 hsv2rgb(vec3 c) { 34 | c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); 35 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 36 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 37 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 38 | } 39 | 40 | mat4 rotX(float angleInRadians) { 41 | float s = sin(angleInRadians); 42 | float c = cos(angleInRadians); 43 | 44 | return mat4( 45 | 1, 0, 0, 0, 46 | 0, c, s, 0, 47 | 0, -s, c, 0, 48 | 0, 0, 0, 1); 49 | } 50 | 51 | mat4 rotY(float angleInRadians) { 52 | float s = sin(angleInRadians); 53 | float c = cos(angleInRadians); 54 | 55 | return mat4( 56 | c, 0,-s, 0, 57 | 0, 1, 0, 0, 58 | s, 0, c, 0, 59 | 0, 0, 0, 1); 60 | } 61 | 62 | mat4 rotZ(float angleInRadians) { 63 | float s = sin(angleInRadians); 64 | float c = cos(angleInRadians); 65 | 66 | return mat4( 67 | c,-s, 0, 0, 68 | s, c, 0, 0, 69 | 0, 0, 1, 0, 70 | 0, 0, 0, 1); 71 | } 72 | 73 | mat4 trans(vec3 trans) { 74 | return mat4( 75 | 1, 0, 0, 0, 76 | 0, 1, 0, 0, 77 | 0, 0, 1, 0, 78 | trans, 1); 79 | } 80 | 81 | mat4 ident() { 82 | return mat4( 83 | 1, 0, 0, 0, 84 | 0, 1, 0, 0, 85 | 0, 0, 1, 0, 86 | 0, 0, 0, 1); 87 | } 88 | 89 | mat4 scale(vec3 s) { 90 | return mat4( 91 | s[0], 0, 0, 0, 92 | 0, s[1], 0, 0, 93 | 0, 0, s[2], 0, 94 | 0, 0, 0, 1); 95 | } 96 | 97 | mat4 uniformScale(float s) { 98 | return mat4( 99 | s, 0, 0, 0, 100 | 0, s, 0, 0, 101 | 0, 0, s, 0, 102 | 0, 0, 0, 1); 103 | } 104 | 105 | mat4 persp(float fov, float aspect, float zNear, float zFar) { 106 | float f = tan(PI * 0.5 - 0.5 * fov); 107 | float rangeInv = 1.0 / (zNear - zFar); 108 | 109 | return mat4( 110 | f / aspect, 0, 0, 0, 111 | 0, f, 0, 0, 112 | 0, 0, (zNear + zFar) * rangeInv, -1, 113 | 0, 0, zNear * zFar * rangeInv * 2., 0); 114 | } 115 | 116 | mat4 trInv(mat4 m) { 117 | mat3 i = mat3( 118 | m[0][0], m[1][0], m[2][0], 119 | m[0][1], m[1][1], m[2][1], 120 | m[0][2], m[1][2], m[2][2]); 121 | vec3 t = -i * m[3].xyz; 122 | 123 | return mat4( 124 | i[0], t[0], 125 | i[1], t[1], 126 | i[2], t[2], 127 | 0, 0, 0, 1); 128 | } 129 | 130 | mat4 transpose(mat4 m) { 131 | return mat4( 132 | m[0][0], m[1][0], m[2][0], m[3][0], 133 | m[0][1], m[1][1], m[2][1], m[3][1], 134 | m[0][2], m[1][2], m[2][2], m[3][2], 135 | m[0][3], m[1][3], m[2][3], m[3][3]); 136 | } 137 | 138 | mat4 lookAt(vec3 eye, vec3 target, vec3 up) { 139 | vec3 zAxis = normalize(eye - target); 140 | vec3 xAxis = normalize(cross(up, zAxis)); 141 | vec3 yAxis = cross(zAxis, xAxis); 142 | 143 | return mat4( 144 | xAxis, 0, 145 | yAxis, 0, 146 | zAxis, 0, 147 | eye, 1); 148 | } 149 | 150 | mat4 inverse(mat4 m) { 151 | float 152 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 153 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 154 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 155 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 156 | 157 | b00 = a00 * a11 - a01 * a10, 158 | b01 = a00 * a12 - a02 * a10, 159 | b02 = a00 * a13 - a03 * a10, 160 | b03 = a01 * a12 - a02 * a11, 161 | b04 = a01 * a13 - a03 * a11, 162 | b05 = a02 * a13 - a03 * a12, 163 | b06 = a20 * a31 - a21 * a30, 164 | b07 = a20 * a32 - a22 * a30, 165 | b08 = a20 * a33 - a23 * a30, 166 | b09 = a21 * a32 - a22 * a31, 167 | b10 = a21 * a33 - a23 * a31, 168 | b11 = a22 * a33 - a23 * a32, 169 | 170 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 171 | 172 | return mat4( 173 | a11 * b11 - a12 * b10 + a13 * b09, 174 | a02 * b10 - a01 * b11 - a03 * b09, 175 | a31 * b05 - a32 * b04 + a33 * b03, 176 | a22 * b04 - a21 * b05 - a23 * b03, 177 | a12 * b08 - a10 * b11 - a13 * b07, 178 | a00 * b11 - a02 * b08 + a03 * b07, 179 | a32 * b02 - a30 * b05 - a33 * b01, 180 | a20 * b05 - a22 * b02 + a23 * b01, 181 | a10 * b10 - a11 * b08 + a13 * b06, 182 | a01 * b08 - a00 * b10 - a03 * b06, 183 | a30 * b04 - a31 * b02 + a33 * b00, 184 | a21 * b02 - a20 * b04 - a23 * b00, 185 | a11 * b07 - a10 * b09 - a12 * b06, 186 | a00 * b09 - a01 * b07 + a02 * b06, 187 | a31 * b01 - a30 * b03 - a32 * b00, 188 | a20 * b03 - a21 * b01 + a22 * b00) / det; 189 | } 190 | 191 | mat4 cameraLookAt(vec3 eye, vec3 target, vec3 up) { 192 | #if 1 193 | return inverse(lookAt(eye, target, up)); 194 | #else 195 | vec3 zAxis = normalize(target - eye); 196 | vec3 xAxis = normalize(cross(up, zAxis)); 197 | vec3 yAxis = cross(zAxis, xAxis); 198 | 199 | return mat4( 200 | xAxis, 0, 201 | yAxis, 0, 202 | zAxis, 0, 203 | -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1); 204 | #endif 205 | 206 | } 207 | 208 | 209 | 210 | // hash function from https://www.shadertoy.com/view/4djSRW 211 | float hash(float p) { 212 | vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); 213 | p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); 214 | return fract(p2.x * p2.y * 95.4337); 215 | } 216 | 217 | // times 2 minus 1 218 | float t2m1(float v) { 219 | return v * 2. - 1.; 220 | } 221 | 222 | // times .5 plus .5 223 | float t5p5(float v) { 224 | return v * 0.5 + 0.5; 225 | } 226 | 227 | float inv(float v) { 228 | return 1. - v; 229 | } 230 | 231 | void getCirclePoint(const float numEdgePointsPerCircle, const float id, const float inner, const float start, const float end, out vec3 pos) { 232 | float outId = id - floor(id / 3.) * 2. - 1.; // 0 1 2 3 4 5 6 7 8 .. 0 1 2, 1 2 3, 2 3 4 233 | float ux = floor(id / 6.) + mod(id, 2.); 234 | float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx 235 | float u = ux / numEdgePointsPerCircle; 236 | float v = mix(inner, 1., vy); 237 | float a = mix(start, end, u) * PI * 2. + PI * 0.0; 238 | float s = sin(a); 239 | float c = cos(a); 240 | float x = c * v; 241 | float y = s * v; 242 | float z = 0.; 243 | pos = vec3(x, y, z); 244 | } 245 | 246 | 247 | #define CUBE_POINTS_PER_FACE 6. 248 | #define FACES_PER_CUBE 6. 249 | #define POINTS_PER_CUBE (CUBE_POINTS_PER_FACE * FACES_PER_CUBE) 250 | void getCubePoint(const float id, out vec3 position, out vec3 normal) { 251 | float quadId = floor(mod(id, POINTS_PER_CUBE) / CUBE_POINTS_PER_FACE); 252 | float sideId = mod(quadId, 3.); 253 | float flip = mix(1., -1., step(2.5, quadId)); 254 | // 0 1 2 1 2 3 255 | float facePointId = mod(id, CUBE_POINTS_PER_FACE); 256 | float pointId = mod(facePointId - floor(facePointId / 3.0), 6.0); 257 | float a = pointId * PI * 2. / 4. + PI * 0.25; 258 | vec3 p = vec3(cos(a), 0.707106781, sin(a)) * flip; 259 | vec3 n = vec3(0, 1, 0) * flip; 260 | float lr = mod(sideId, 2.); 261 | float ud = step(2., sideId); 262 | mat4 mat = rotX(lr * PI * 0.5); 263 | mat *= rotZ(ud * PI * 0.5); 264 | position = (mat * vec4(p, 1)).xyz; 265 | normal = (mat * vec4(n, 0)).xyz; 266 | } 267 | 268 | #if 1 269 | void main() { 270 | float pointId = vertexId; 271 | 272 | vec3 pos; 273 | vec3 normal; 274 | getCubePoint(pointId, pos, normal); 275 | float cubeId = floor(pointId / 36.); 276 | float numCubes = floor(vertexCount / 36.); 277 | float down = floor(sqrt(numCubes)); 278 | float across = floor(numCubes / down); 279 | 280 | float cx = mod(cubeId, across); 281 | float cy = floor(cubeId / across); 282 | 283 | float cu = cx / (across - 1.); 284 | float cv = cy / (down - 1.); 285 | 286 | float ca = cu * 2. - 1.; 287 | float cd = cv * 2. - 1.; 288 | 289 | float tm = time * 0.1; 290 | mat4 mat = persp(radians(60.0), resolution.x / resolution.y, 0.1, 1000.0); 291 | vec3 eye = vec3(cos(tm) * 1., sin(tm * 0.9) * .1 + 0.5, sin(tm) * 1.); 292 | vec3 target = vec3(-eye.x, -1, -eye.z) * 5.5; 293 | vec3 up = vec3(0,1,0); 294 | 295 | mat *= cameraLookAt(eye, target, up); 296 | mat *= trans(vec3(ca, 0, cd) * 2.); 297 | mat *= rotX(time + abs(ca) * 5.); 298 | mat *= rotZ(time + abs(cd) * 6.); 299 | mat *= uniformScale(0.03); 300 | 301 | 302 | gl_Position = mat * vec4(pos, 1); 303 | vec3 n = normalize((mat * vec4(normal, 0)).xyz); 304 | 305 | vec3 lightDir = normalize(vec3(0.3, 0.4, -1)); 306 | 307 | float snd = texture2D(sound, vec2(mix(0.1, 0.5, abs(atan(ca, cd)) / PI), length(vec2(ca,cd))) * .5).a; 308 | 309 | float hue = abs(ca * cd) * 2.; 310 | float sat = pow(snd, 5.); 311 | float val = mix(1., 0.5, abs(cd)); 312 | vec3 color = hsv2rgb(vec3(hue, sat, val)); 313 | v_color = vec4(color * (dot(n, lightDir) * 0.5 + 0.5), pow(snd, 2.)); 314 | v_color.rgb *= v_color.a; 315 | } 316 | #endif 317 | 318 | ` 319 | }, 320 | revisionId: "2a6NsC49zwe5Z6NZ6", 321 | revisionUrl: "https://www.vertexshaderart.com/art/ZFSiQpx33DLDg9hmd/revision/2a6NsC49zwe5Z6NZ6", 322 | artUrl: "https://www.vertexshaderart.com/art/undefined" 323 | }; 324 | -------------------------------------------------------------------------------- /examples/js/index/effects/starfield.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-trailing-comma/require-trailing-comma */ 2 | /* eslint-disable no-useless-escape */ 3 | export default { 4 | _id: "pfa9757K3NJx6euhN", 5 | createdAt: "2019-02-21T11:38:22.742Z", 6 | modifiedAt: "2019-02-26T06:41:10.489Z", 7 | origId: null, 8 | name: "starfield", 9 | username: "gman", 10 | avatarUrl: "https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200", 11 | settings: { 12 | num: 100000, 13 | mode: "POINTS", 14 | sound: "", 15 | lineSize: "NATIVE", 16 | backgroundColor: [ 17 | 0, 18 | 0, 19 | 0, 20 | 1 21 | ], 22 | shader: `#define PI radians(180.0) 23 | 24 | vec3 hsv2rgb(vec3 c) { 25 | c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); 26 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 27 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 28 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 29 | } 30 | 31 | 32 | mat4 rotX(float angleInRadians) { 33 | float s = sin(angleInRadians); 34 | float c = cos(angleInRadians); 35 | 36 | return mat4( 37 | 1, 0, 0, 0, 38 | 0, c, s, 0, 39 | 0, -s, c, 0, 40 | 0, 0, 0, 1); 41 | } 42 | 43 | mat4 rotY(float angleInRadians) { 44 | float s = sin(angleInRadians); 45 | float c = cos(angleInRadians); 46 | 47 | return mat4( 48 | c, 0,-s, 0, 49 | 0, 1, 0, 0, 50 | s, 0, c, 0, 51 | 0, 0, 0, 1); 52 | } 53 | 54 | mat4 rotZ(float angleInRadians) { 55 | float s = sin(angleInRadians); 56 | float c = cos(angleInRadians); 57 | 58 | return mat4( 59 | c,-s, 0, 0, 60 | s, c, 0, 0, 61 | 0, 0, 1, 0, 62 | 0, 0, 0, 1); 63 | } 64 | 65 | mat4 trans(vec3 trans) { 66 | return mat4( 67 | 1, 0, 0, 0, 68 | 0, 1, 0, 0, 69 | 0, 0, 1, 0, 70 | trans, 1); 71 | } 72 | 73 | mat4 ident() { 74 | return mat4( 75 | 1, 0, 0, 0, 76 | 0, 1, 0, 0, 77 | 0, 0, 1, 0, 78 | 0, 0, 0, 1); 79 | } 80 | 81 | mat4 scale(vec3 s) { 82 | return mat4( 83 | s[0], 0, 0, 0, 84 | 0, s[1], 0, 0, 85 | 0, 0, s[2], 0, 86 | 0, 0, 0, 1); 87 | } 88 | 89 | mat4 uniformScale(float s) { 90 | return mat4( 91 | s, 0, 0, 0, 92 | 0, s, 0, 0, 93 | 0, 0, s, 0, 94 | 0, 0, 0, 1); 95 | } 96 | 97 | mat4 persp(float fov, float aspect, float zNear, float zFar) { 98 | float f = tan(PI * 0.5 - 0.5 * fov); 99 | float rangeInv = 1.0 / (zNear - zFar); 100 | 101 | return mat4( 102 | f / aspect, 0, 0, 0, 103 | 0, f, 0, 0, 104 | 0, 0, (zNear + zFar) * rangeInv, -1, 105 | 0, 0, zNear * zFar * rangeInv * 2., 0); 106 | } 107 | 108 | mat4 trInv(mat4 m) { 109 | mat3 i = mat3( 110 | m[0][0], m[1][0], m[2][0], 111 | m[0][1], m[1][1], m[2][1], 112 | m[0][2], m[1][2], m[2][2]); 113 | vec3 t = -i * m[3].xyz; 114 | 115 | return mat4( 116 | i[0], t[0], 117 | i[1], t[1], 118 | i[2], t[2], 119 | 0, 0, 0, 1); 120 | } 121 | 122 | mat4 transpose(mat4 m) { 123 | return mat4( 124 | m[0][0], m[1][0], m[2][0], m[3][0], 125 | m[0][1], m[1][1], m[2][1], m[3][1], 126 | m[0][2], m[1][2], m[2][2], m[3][2], 127 | m[0][3], m[1][3], m[2][3], m[3][3]); 128 | } 129 | 130 | mat4 lookAt(vec3 eye, vec3 target, vec3 up) { 131 | vec3 zAxis = normalize(eye - target); 132 | vec3 xAxis = normalize(cross(up, zAxis)); 133 | vec3 yAxis = cross(zAxis, xAxis); 134 | 135 | return mat4( 136 | xAxis, 0, 137 | yAxis, 0, 138 | zAxis, 0, 139 | eye, 1); 140 | } 141 | 142 | mat4 inverse(mat4 m) { 143 | float 144 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 145 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 146 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 147 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 148 | 149 | b00 = a00 * a11 - a01 * a10, 150 | b01 = a00 * a12 - a02 * a10, 151 | b02 = a00 * a13 - a03 * a10, 152 | b03 = a01 * a12 - a02 * a11, 153 | b04 = a01 * a13 - a03 * a11, 154 | b05 = a02 * a13 - a03 * a12, 155 | b06 = a20 * a31 - a21 * a30, 156 | b07 = a20 * a32 - a22 * a30, 157 | b08 = a20 * a33 - a23 * a30, 158 | b09 = a21 * a32 - a22 * a31, 159 | b10 = a21 * a33 - a23 * a31, 160 | b11 = a22 * a33 - a23 * a32, 161 | 162 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 163 | 164 | return mat4( 165 | a11 * b11 - a12 * b10 + a13 * b09, 166 | a02 * b10 - a01 * b11 - a03 * b09, 167 | a31 * b05 - a32 * b04 + a33 * b03, 168 | a22 * b04 - a21 * b05 - a23 * b03, 169 | a12 * b08 - a10 * b11 - a13 * b07, 170 | a00 * b11 - a02 * b08 + a03 * b07, 171 | a32 * b02 - a30 * b05 - a33 * b01, 172 | a20 * b05 - a22 * b02 + a23 * b01, 173 | a10 * b10 - a11 * b08 + a13 * b06, 174 | a01 * b08 - a00 * b10 - a03 * b06, 175 | a30 * b04 - a31 * b02 + a33 * b00, 176 | a21 * b02 - a20 * b04 - a23 * b00, 177 | a11 * b07 - a10 * b09 - a12 * b06, 178 | a00 * b09 - a01 * b07 + a02 * b06, 179 | a31 * b01 - a30 * b03 - a32 * b00, 180 | a20 * b03 - a21 * b01 + a22 * b00) / det; 181 | } 182 | 183 | mat4 cameraLookAt(vec3 eye, vec3 target, vec3 up) { 184 | #if 1 185 | return inverse(lookAt(eye, target, up)); 186 | #else 187 | vec3 zAxis = normalize(target - eye); 188 | vec3 xAxis = normalize(cross(up, zAxis)); 189 | vec3 yAxis = cross(zAxis, xAxis); 190 | 191 | return mat4( 192 | xAxis, 0, 193 | yAxis, 0, 194 | zAxis, 0, 195 | -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1); 196 | #endif 197 | 198 | } 199 | 200 | 201 | 202 | // hash function from https://www.shadertoy.com/view/4djSRW 203 | float hash(float p) { 204 | vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); 205 | p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); 206 | return fract(p2.x * p2.y * 95.4337); 207 | } 208 | 209 | // times 2 minus 1 210 | float t2m1(float v) { 211 | return v * 2. - 1.; 212 | } 213 | 214 | // times .5 plus .5 215 | float t5p5(float v) { 216 | return v * 0.5 + 0.5; 217 | } 218 | 219 | float inv(float v) { 220 | return 1. - v; 221 | } 222 | 223 | 224 | void main() { 225 | 226 | float near = 0.01; 227 | float far = 25.0; 228 | mat4 pmat = persp(radians(60.0), resolution.x / resolution.y, near, far); 229 | mat4 cmat = ident(); 230 | cmat *= rotX(mouse.y * PI); 231 | cmat *= rotY(mouse.x * -PI); 232 | mat4 vmat = inverse(cmat); 233 | 234 | vec3 pos = vec3( 235 | hash(vertexId * 0.123), 236 | hash(vertexId * 0.357), 237 | // fract(hash(vertexId * 0.531) - time * .01)) * vec3(2, 2, -40) - vec3(1, 1, -20); 238 | fract(hash(vertexId * 0.531) + time * .05)) * 2. - 1.; 239 | float d = length(pos); 240 | pos *= vec3(2); 241 | 242 | vec3 boxMin = (cmat * vec4(-1, -1, 0, 1)).xyz; 243 | vec3 boxMax = (cmat * vec4(1, 1, -20, 1)).xyz; 244 | 245 | 246 | 247 | 248 | /* 249 | 250 | +--------+--------+--------+--------+ 251 | | | | | | 252 | | |\ | | | 253 | | | \ | | | 254 | | | \ | | | 255 | +--------+--------+--------+--------+ 256 | | | \ | | | 257 | | | \ | | | 258 | | | \ | | | 259 | | | \| | | 260 | +--------+--------c--------+--------+ 261 | | | /| | | 262 | | | / | | | 263 | | | / | | | 264 | | | / | | | 265 | +--------+--------+--------+--------+ 266 | | | / | | | 267 | | | / | | | 268 | | |/ | | | 269 | | | | | | 270 | +--------+--------+--------+--------+ 271 | 272 | */ 273 | 274 | 275 | //mat[2][2] /= far; 276 | //mat[2][3] /= far; 277 | 278 | gl_Position = pmat * vmat * vec4(pos, 1); 279 | // gl_Position.z = gl_Position.z * gl_Position.w / far; 280 | 281 | 282 | /* 283 | mat4 persp(float fov, float aspect, float zNear, float zFar) { 284 | float f = tan(PI * 0.5 - 0.5 * fov); 285 | float rangeInv = 1.0 / (zNear - zFar); 286 | 287 | return mat4( 288 | f / aspect, 0, 0, 0, 289 | 0, f, 0, 0, 290 | 0, 0, (zNear + zFar) * rangeInv, -1, 291 | 0, 0, zNear * zFar * rangeInv * 2., 0); 292 | } 293 | 294 | rangeInv = 1.0 / (near - far) 295 | rangnInv = 1.0 / (0.01 - 25.0) 296 | rangeInv = 1.0 / -24.99 297 | rangeInv = -0.040016006402561027 298 | 299 | -0.1 * (0.1 + 25.0) * -0.04001 + 1 * (0.1 * 25.0 * -0.04001) * 2 = -0.000996 300 | -20 * (0.1 + 25.0) * -0.04001 + 1 * (0.1 * 25.0 * -0.04001) * 2 = 0.1988 301 | 302 | 303 | */ 304 | 305 | vec4 f = gl_Position; 306 | float depth = f.z * .5 + .5; 307 | 308 | gl_PointSize = mix(10.0, 1.0, depth); 309 | v_color = vec4(hsv2rgb(vec3(hash(vertexId * 0.237), 0.25, 1)), 1. - d); 310 | v_color.rgb *= v_color.a; 311 | // v_color = vec4(hsv2rgb(vec3(depth, 1, 1)), 1); 312 | }` 313 | }, 314 | revisionId: "oTpAmrwEQsSFL3L3y", 315 | revisionUrl: "https://www.vertexshaderart.com/art/pfa9757K3NJx6euhN/revision/oTpAmrwEQsSFL3L3y", 316 | artUrl: "https://www.vertexshaderart.com/art/undefined" 317 | }; 318 | -------------------------------------------------------------------------------- /examples/js/index/index.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../3rdParty/twgl-full.module.js'; 2 | import VSAEffect from './VSAEffect.js'; 3 | import effects from './effects.js'; 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | import GUI, { helpers, Direction, TextNumber } from '../../../src/esm.js'; /* muigui-include */ 6 | 7 | const canvas = document.querySelector('#bg'); 8 | const gl = canvas.getContext('webgl'); 9 | 10 | const elements = []; 11 | 12 | function render(time) { 13 | time *= 0.001; 14 | 15 | twgl.resizeCanvasToDisplaySize(canvas); 16 | gl.disable(gl.SCISSOR_TEST); 17 | gl.clearColor(0, 0, 0, 0); 18 | gl.clear(gl.COLOR_BUFFER_BIT); 19 | 20 | canvas.style.transform = `translateX(${window.scrollX}px) translateY(${window.scrollY}px)`; 21 | 22 | for (const {render, elem} of elements) { 23 | const rect = elem.getBoundingClientRect(); 24 | if (rect.bottom < 0 || rect.top > gl.canvas.clientHeight || 25 | rect.right < 0 || rect.left > gl.canvas.clientWidth) { 26 | continue; // it's off screen 27 | } 28 | 29 | const width = rect.right - rect.left; 30 | const height = rect.bottom - rect.top; 31 | const left = rect.left; 32 | const bottom = gl.canvas.clientHeight - rect.bottom; 33 | 34 | render(gl, { 35 | time, 36 | width, 37 | height, 38 | left, 39 | bottom, 40 | }); 41 | } 42 | requestAnimationFrame(render); 43 | } 44 | requestAnimationFrame(render); 45 | 46 | //const eff = Object.values(effects); 47 | // eslint-disable-next-line no-underscore-dangle 48 | //console.log(eff.map(e => `https://www.vertexshaderart.com/art/${e._id}`).join(' ')); 49 | 50 | const sections = { 51 | basic({uiElem}) { 52 | const gui = new GUI(uiElem); 53 | const vsaEffect = new VSAEffect(gl); 54 | const vsa = effects.discus; 55 | 56 | const settings = { 57 | numSides: 4, 58 | speed: 0, 59 | brightness: 0.1, 60 | opacity: 1, 61 | run: true, 62 | rotation: 0, 63 | split: 0.96, 64 | s1: 1, 65 | s2: 1, 66 | s3: 10, 67 | s4: 10, 68 | color1: [0.439, 0.463, 0.78], 69 | color2: [1, 1, 0], 70 | }; 71 | gui.add(settings, 'run'); 72 | gui.add(settings, 'numSides', { 73 | keyValues: { 74 | '△ triangle': 3, 75 | '□ square': 4, 76 | '⭔ pentagon': 5, 77 | '⬡ hexagon': 6, 78 | '○ circle': 48, 79 | }, 80 | }).name('shape'); 81 | //gui.add(settings, 'brightness', 0, 1); 82 | //gui.add(settings, 's1', 0, 1); 83 | //gui.add(settings, 's2', 0, 1); 84 | //gui.add(settings, 's3', 0, 50); 85 | //gui.add(settings, 's4', 0, 50); 86 | const degToRad = d => d * Math.PI / 180; 87 | const radToDeg = r => r * 180 / Math.PI; 88 | //gui.add(new Direction(settings, 'rotation', { 89 | // converters: { 90 | // to: v => { return radToDeg(v); }, 91 | // from: v => { const outV = degToRad(v); console.log(outV); return [true, outV]; }, 92 | // }, 93 | //})); 94 | gui.add(settings, 'rotation', { 95 | min: -180, 96 | max: 180, 97 | converters: { 98 | to: radToDeg, 99 | from: v => [true, degToRad(v)], 100 | }, 101 | }); 102 | gui.add(settings, 'split', 0, 1); 103 | //gui.add(settings, 'count', 1, 100, 1); 104 | gui.addColor(settings, 'color1'); 105 | gui.addColor(settings, 'color2'); 106 | 107 | vsaEffect.setSettings(vsa); 108 | 109 | let time = 0; 110 | let then = 0; 111 | return function (gl, inputs) { 112 | const deltaTime = settings.run ? (inputs.time - then) : 0; 113 | then = inputs.time; 114 | time += deltaTime; 115 | vsaEffect.render(gl, { 116 | ...inputs, 117 | ...settings, 118 | time, 119 | }, { 120 | getTime() { 121 | return time * 44100 | 0; 122 | }, 123 | getDesiredSampleRate() { 124 | return 44100; 125 | }, 126 | }, [ 127 | { 128 | frequencyBinCount: 4096, 129 | getByteFrequencyData(b) { 130 | const {s1, s2, s3, s4} = settings; 131 | for (let i = 0; i < b.length; ++i) { 132 | b[i] = (Math.sin(time * s1 + i * s3) + Math.sin(time * s2 + i * s4)) * 0.5 * 127 + 127; 133 | } 134 | }, 135 | }, 136 | ]); 137 | }; 138 | }, 139 | float({uiElem}) { 140 | const gui = new GUI(uiElem); 141 | gui.setTheme('float'); 142 | const vsaEffect = new VSAEffect(gl); 143 | const vsa = effects.rollin; 144 | 145 | const settings = { 146 | period1: 9, 147 | period2: 8.17, 148 | p1: 1, 149 | p2: 1, 150 | heightMult: 4, 151 | baseColor: [0.02, 0.396, 1], 152 | }; 153 | gui.add(settings, 'period1', 0.1, 20); 154 | gui.add(settings, 'period2', 0.1, 20); 155 | gui.add(settings, 'p1', 0.1, 20); 156 | gui.add(settings, 'p2', 0.1, 20); 157 | gui.add(settings, 'heightMult', 1, 10); 158 | gui.addColor(settings, 'baseColor'); 159 | 160 | vsaEffect.setSettings(vsa); 161 | 162 | return function (gl, inputs) { 163 | vsaEffect.render(gl, { 164 | ...inputs, 165 | ...settings, 166 | }, { 167 | getTime() { 168 | return inputs.time * 44100 | 0; 169 | }, 170 | getDesiredSampleRate() { 171 | return 44100; 172 | }, 173 | }, [ 174 | { 175 | frequencyBinCount: 4096, 176 | getByteFrequencyData(b) { 177 | for (let i = 0; i < b.length; ++i) { 178 | b[i] = Math.sin(inputs.time * 10 + i * 0.1) * 0.2 * 127 + 127; 179 | } 180 | }, 181 | }, 182 | ]); 183 | }; 184 | }, 185 | form({uiElem}) { 186 | const s = { 187 | name: "Jane Cheng", 188 | address1: "B 1, No. 5, Xuzhou R", 189 | address2: "Taipei 100218", 190 | email: "jane_c@notreally.notcom", 191 | receipt: true, 192 | currency: '$', 193 | }; 194 | 195 | const gui = new GUI(uiElem).name(''); 196 | gui.setTheme('form'); 197 | gui.add(s, 'name'); 198 | gui.add(s, 'address1'); 199 | gui.add(s, 'address2'); 200 | gui.add(s, 'email'); 201 | gui.add(s, 'receipt'); 202 | gui.add(s, 'currency', ['$', '¥', '€', '£', '₣']); 203 | gui.addButton('submit', () => {}); 204 | }, 205 | }; 206 | 207 | document.querySelectorAll('[data-section]').forEach(elem => { 208 | const uiElem = elem.querySelector('.ui'); 209 | const effectElem = elem.querySelector('.effect'); 210 | const fn = sections[elem.dataset.section]; 211 | if (!fn) { 212 | console.error(`no effect: '${elem.dataset.section}'`); 213 | return; 214 | } 215 | const render = fn({elem, uiElem, effectElem}); 216 | if (render) { 217 | elements.push({ 218 | elem: effectElem, 219 | render, 220 | }); 221 | } 222 | }); 223 | 224 | const getNextId = (() => { 225 | let nextId = 0; 226 | return function getNextId() { 227 | return `gui-${nextId++}`; 228 | }; 229 | })(); 230 | 231 | window.GUI = GUI; 232 | window.TextNumber = TextNumber; 233 | window.randElem = (arr) => arr[Math.random() * arr.length | 0]; 234 | window.helpers = helpers; 235 | 236 | function getSupportCode({logId, code}) { 237 | return ` 238 | ${code} 239 | 240 | function log(...args) { 241 | const logElem = document.querySelector('#${logId}'); 242 | logElem.className = 'log'; 243 | const lineNo = parseInt(logElem.dataset.lineNo || 1); 244 | const lines = logElem.textContent.split('\\n'); 245 | lines.push(\`\${lineNo}: \${args.join(' ')}\`); 246 | if (lines.length > 5) { 247 | lines.splice(0, lines.length - 5); 248 | } 249 | logElem.textContent = lines.join('\\n'); 250 | logElem.dataset.lineNo = lineNo + 1; 251 | } 252 | `; 253 | } 254 | 255 | document.querySelectorAll('[data-example]').forEach(elem => { 256 | const pre = elem.querySelector('pre'); 257 | const div = document.createElement('div'); 258 | const id = getNextId(); 259 | div.id = id; 260 | div.className = 'ui'; 261 | elem.appendChild(div); 262 | 263 | const logElem = document.createElement('pre'); 264 | const logId = getNextId(); 265 | logElem.id = logId; 266 | elem.appendChild(logElem); 267 | 268 | const extra = elem.dataset.extraCode || ''; 269 | const code = elem.querySelector('code').textContent 270 | .replace('GUI()', `GUI(document.querySelector('#${id}'))`) 271 | .replace(/import(.*?)'(\/.*?)'/g, `import$1'${window.location.origin}$2'`); 272 | const script = document.createElement('script'); 273 | script.type = 'module'; 274 | script.text = getSupportCode({logId, code: `${code}\n${extra}`}); 275 | pre.appendChild(script); 276 | }); 277 | 278 | /* global hljs */ 279 | document.querySelectorAll('pre>code').forEach(elem => { 280 | elem.textContent = elem.textContent.trim(); 281 | }); 282 | hljs.highlightAll(); 283 | 284 | // show min/max 285 | 286 | // show slider 287 | // show direction 288 | // show vec2 289 | // show folder 290 | // show onChange 291 | // show onChange of folder 292 | // make button function so no need for prop 293 | 294 | // save/restore? 295 | // show float 296 | // show form 297 | -------------------------------------------------------------------------------- /examples/js/logger.js: -------------------------------------------------------------------------------- 1 | export default class Logger { 2 | constructor(maxLines = 3) { 3 | let logController; 4 | 5 | const lines = []; 6 | this.log = (...args) => { 7 | lines.push(args.join(' ')); 8 | if (lines.length > maxLines) { 9 | lines.shift(); 10 | } 11 | logController.text(lines.join('\n')); 12 | }; 13 | 14 | this.setController = (controller) => { 15 | logController = controller; 16 | }; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /examples/js/long-hide.js: -------------------------------------------------------------------------------- 1 | import GUI from '../../dist/0.x/muigui.module.js'; 2 | //import {GUI} from '../../src/muigui.js'; 3 | 4 | const gui = new GUI(); 5 | 6 | function addFolder(gui, name) { 7 | const f = gui.addFolder(name); 8 | for (let i = 0; i < 50; ++i) { 9 | f.addButton(i.toString(), () => {}); 10 | } 11 | } 12 | 13 | const degToRad = deg => deg * Math.PI / 180; 14 | 15 | const settings = { 16 | translation: new Float32Array([0, 0, 0]), 17 | rotation: new Float32Array([0, 0, 0]), 18 | scale: new Float32Array([1, 1, 1]), 19 | baseRotation: degToRad(-45), 20 | }; 21 | 22 | const radToDegOptions = { min: -180, max: 180, step: 1, converters: GUI.converters.radToDeg }; 23 | const cameraRadToDegOptions = { min: -180, max: 180, step: 1, converters: GUI.converters.radToDeg }; 24 | 25 | gui.add(settings, 'baseRotation', cameraRadToDegOptions); 26 | /*const nodeLabel =*/ gui.addLabel('node:'); 27 | const trsFolder = gui.addFolder('orientation'); 28 | trsFolder.add(settings.translation, '0', -50, 50, 1).name('translation x'); 29 | trsFolder.add(settings.translation, '1', -50, 50, 1).name('translation y'); 30 | trsFolder.add(settings.translation, '2', -50, 50, 1).name('translation z'); 31 | trsFolder.add(settings.rotation, '0', radToDegOptions).name('rotation x'); 32 | trsFolder.add(settings.rotation, '1', radToDegOptions).name('rotation y'); 33 | trsFolder.add(settings.rotation, '2', radToDegOptions).name('rotation z'); 34 | trsFolder.add(settings.scale, '0', 0.001, 2).name('scale x'); 35 | trsFolder.add(settings.scale, '1', 0.001, 2).name('scale y'); 36 | trsFolder.add(settings.scale, '2', 0.001, 2).name('scale z'); 37 | 38 | addFolder(gui, 'one'); 39 | addFolder(gui, 'two'); 40 | addFolder(gui, 'three'); 41 | -------------------------------------------------------------------------------- /examples/js/model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | import * as THREE from '../3rdParty/threejs/build/three.module.js'; 3 | import {TrackballControls} from '../3rdParty/threejs/examples/jsm/controls/TrackballControls.js'; 4 | 5 | export function model(canvas) { 6 | const renderer = new THREE.WebGLRenderer({canvas, alpha: true}); 7 | 8 | const scene = new THREE.Scene(); 9 | 10 | const fov = 45; 11 | const aspect = 2; // the canvas default 12 | const near = 0.01; 13 | const far = 15; 14 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 15 | camera.position.set(0, 1, 2); 16 | camera.lookAt(0, 0, 0); 17 | scene.add(camera); 18 | 19 | const controls = new TrackballControls(camera, canvas); 20 | controls.noZoom = true; 21 | controls.noPan = true; 22 | 23 | const light = (() => { 24 | const color = 0xFFFFFF; 25 | const intensity = 1; 26 | const light = new THREE.DirectionalLight(color, intensity); 27 | light.position.set(-1, 2, 4); 28 | camera.add(light); 29 | return light; 30 | })(); 31 | 32 | const meshes = []; 33 | const material = new THREE.MeshPhongMaterial({ 34 | color: 'red', 35 | shininess: 150, 36 | flatShading: true, 37 | }); 38 | 39 | { 40 | const geometry = new THREE.TorusGeometry(0.4, 0.3, 9, 12); 41 | //const geometry = new THREE.BoxGeometry(1, 1, 1); 42 | const mesh = new THREE.Mesh(geometry, material); 43 | scene.add(mesh); 44 | meshes.push(mesh); 45 | } 46 | 47 | if (0) { 48 | const geometry = new THREE.SphereGeometry(0.8, 12, 6); 49 | const mesh = new THREE.Mesh(geometry, material); 50 | mesh.position.x = -1.5; 51 | scene.add(mesh); 52 | meshes.push(mesh); 53 | } 54 | 55 | if (0) { 56 | const geometry = new THREE.TorusGeometry(0.4, 0.3, 6, 12); 57 | const mesh = new THREE.Mesh(geometry, material); 58 | mesh.position.x = 1.5; 59 | scene.add(mesh); 60 | meshes.push(mesh); 61 | } 62 | 63 | function update(time, rect) { 64 | meshes.forEach((mesh, i) => { 65 | mesh.rotation.y = time * .1 * (i + 1); 66 | }); 67 | camera.aspect = rect.width / rect.height; 68 | camera.updateProjectionMatrix(); 69 | controls.handleResize(); 70 | controls.update(); 71 | renderer.render(scene, camera); 72 | } 73 | 74 | function resizeRendererToDisplaySize(renderer, mult = 1) { 75 | const canvas = renderer.domElement; 76 | const width = canvas.clientWidth * mult; 77 | const height = canvas.clientHeight * mult; 78 | const needResize = canvas.width !== width || canvas.height !== height; 79 | if (needResize) { 80 | renderer.setSize(width, height, false); 81 | } 82 | return needResize; 83 | } 84 | 85 | function render(time) { 86 | time *= 0.001; 87 | 88 | resizeRendererToDisplaySize(renderer, 2); 89 | 90 | const {width, height} = canvas; 91 | update(time, {width, height}); 92 | requestAnimationFrame(render); 93 | } 94 | 95 | requestAnimationFrame(render); 96 | 97 | return { 98 | camera, 99 | light, 100 | material, 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /examples/js/utils.js: -------------------------------------------------------------------------------- 1 | export function getCSSRulesBySelector(selector, styleSheet) { 2 | const rules = []; 3 | const styleSheets = styleSheet ? [styleSheet] : document.styleSheets; 4 | for (let s = 0; s < styleSheets.length; ++s) { 5 | const cssRules = styleSheets[s].cssRules; 6 | for (let i = 0; i < cssRules.length; ++i) { 7 | const r = cssRules[i]; 8 | if (r.selectorText === selector) { 9 | rules.push(r); 10 | } 11 | } 12 | } 13 | return rules; 14 | } 15 | 16 | export function resizeCanvasToDisplaySize(canvas, mult = 1) { 17 | const width = canvas.clientWidth * mult; 18 | const height = canvas.clientHeight * mult; 19 | const needResize = canvas.width !== width || canvas.height !== canvas.height; 20 | if (needResize) { 21 | canvas.width = width; 22 | canvas.height = height; 23 | } 24 | return needResize; 25 | } 26 | 27 | export const hsl = (h, s, l) => `hsl(${h * 360},${s * 100}%,${l * 100}%)`; 28 | -------------------------------------------------------------------------------- /examples/layout/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/layout/layout.js: -------------------------------------------------------------------------------- 1 | //import { 2 | // Column, 3 | // Frame, 4 | // Grid, 5 | // Row, 6 | //} from '../../src/muigui.js'; 7 | // 8 | //const elem = document.querySelector('#ui'); 9 | // 10 | // 11 | //const root = new Column(); 12 | //const topRow = root.add(new Row()); 13 | //const midRow = root.add(new Row()); 14 | //const botRow = root.add(new Row()); 15 | 16 | /* 17 | 18 | +-[Column]---- 19 | |+-[Row]------------------------ 20 | ||+-[Frame(fixed)]-++-[Frame(stretch-h)]- 21 | ||| || 22 | ||+-- 23 | | 24 | +--- 25 | 26 | 27 | topRow.add(new Frame({size: [1, 1]})); 28 | topRow.add(new Frame({size: [1, 1]})); 29 | head.add(new ) 30 | 31 | */ 32 | -------------------------------------------------------------------------------- /examples/long-hide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | muigui 7 | 8 | 13 | 14 |

muigui

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/lots-umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | muigui 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 77 | 79 | 80 | 81 |

muigui

82 |
83 |
84 |
85 |
86 | 87 | 88 | 89 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /examples/lots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | muigui 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 60 | 79 | 81 | 82 | 83 |

muigui

84 |
85 |
86 |
87 |
88 | 89 | 90 | 91 | 98 | 99 | -------------------------------------------------------------------------------- /images/muigui-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/muigui/1f28500584de7716a99cdc4b30882befbfe2a6a1/images/muigui-icon.png -------------------------------------------------------------------------------- /images/muigui-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/muigui/1f28500584de7716a99cdc4b30882befbfe2a6a1/images/muigui-screenshot.png -------------------------------------------------------------------------------- /images/muigui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/muigui/1f28500584de7716a99cdc4b30882befbfe2a6a1/images/muigui.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muigui", 3 | "version": "0.0.21", 4 | "description": "A Simple GUI", 5 | "main": "dist/0.x/muigui.js", 6 | "module": "dist/0.x/muigui.module.js", 7 | "type": "module", 8 | "scripts": { 9 | "build": "npm run build-normal", 10 | "build-ci": "npm run build && node build/prep-for-deploy.js", 11 | "build-normal": "rollup -c", 12 | "build-min": "rollup -c", 13 | "check-ci": "npm run pre-push", 14 | "eslint": "eslint \"**/*.js\"", 15 | "fix": "eslint --fix", 16 | "pre-push": "npm run eslint && npm run build && npm run test", 17 | "start": "node build/serve.js", 18 | "test": "node test/puppeteer.js", 19 | "watch": "npm run start" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/greggman/muigui.git" 24 | }, 25 | "keywords": [ 26 | "muigui", 27 | "gui", 28 | "ui" 29 | ], 30 | "author": "Gregg Tavares", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/greggman/muigui/issues" 34 | }, 35 | "files": [ 36 | "dist/**/*", 37 | "src/**/*" 38 | ], 39 | "homepage": "https://github.com/greggman/muiguiy#readme", 40 | "devDependencies": { 41 | "@rollup/plugin-node-resolve": "^15.0.1", 42 | "@rollup/plugin-terser": "^0.4.0", 43 | "@rollup/plugin-typescript": "^11.1.5", 44 | "@tsconfig/recommended": "^1.0.3", 45 | "@typescript-eslint/eslint-plugin": "^6.12.0", 46 | "@typescript-eslint/parser": "^6.12.0", 47 | "chokidar": "^3.5.3", 48 | "eslint": "^8.54.0", 49 | "eslint-plugin-html": "^7.1.0", 50 | "eslint-plugin-one-variable-per-var": "^0.0.3", 51 | "eslint-plugin-optional-comma-spacing": "0.0.4", 52 | "eslint-plugin-require-trailing-comma": "0.0.1", 53 | "puppeteer": "^23.10.4", 54 | "rollup": "^3.20.2", 55 | "servez": "^2.2.4", 56 | "tslib": "^2.6.2", 57 | "typescript": "5.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | import fs from 'fs'; 5 | 6 | const pkg = JSON.parse(fs.readFileSync('package.json', {encoding: 'utf8'})); 7 | const banner = `/* muigui@${pkg.version}, license MIT */`; 8 | 9 | const plugins = [ 10 | typescript({ tsconfig: './tsconfig.json' }), 11 | resolve({ 12 | modulesOnly: true, 13 | }), 14 | ]; 15 | 16 | export default [ 17 | { 18 | input: 'src/esm.ts', 19 | treeshake: false, 20 | plugins, 21 | output: [ 22 | { 23 | format: 'es', 24 | file: 'dist/0.x/muigui.module.js', 25 | indent: ' ', 26 | banner, 27 | sourcemap: true, 28 | }, 29 | ], 30 | }, 31 | { 32 | input: 'src/umd.js', 33 | treeshake: false, 34 | plugins, 35 | output: [ 36 | { 37 | format: 'umd', 38 | file: 'dist/0.x/muigui.js', 39 | indent: ' ', 40 | banner, 41 | name: 'GUI', 42 | sourcemap: true, 43 | }, 44 | ], 45 | }, 46 | { 47 | input: 'src/esm.ts', 48 | treeshake: false, 49 | plugins: [ 50 | ...plugins, 51 | terser(), 52 | ], 53 | output: [ 54 | { 55 | format: 'es', 56 | file: 'dist/0.x/muigui.module.min.js', 57 | indent: ' ', 58 | banner, 59 | sourcemap: true, 60 | }, 61 | ], 62 | }, 63 | { 64 | input: 'src/umd.js', 65 | treeshake: false, 66 | plugins: [ 67 | ...plugins, 68 | terser(), 69 | ], 70 | output: [ 71 | { 72 | format: 'umd', 73 | file: 'dist/0.x/muigui.min.js', 74 | indent: ' ', 75 | banner, 76 | name: 'GUI', 77 | sourcemap: true, 78 | }, 79 | ], 80 | }, 81 | ]; 82 | -------------------------------------------------------------------------------- /src/controllers/Button.js: -------------------------------------------------------------------------------- 1 | import { 2 | createElem, 3 | } from '../libs/elem.js'; 4 | import { copyExistingProperties } from '../libs/utils.js'; 5 | import Controller from './Controller.js'; 6 | 7 | export default class Button extends Controller { 8 | #object; 9 | #property; 10 | #buttonElem; 11 | #options = { 12 | name: '', 13 | }; 14 | 15 | constructor(object, property, options = {}) { 16 | super('muigui-button', ''); 17 | this.#object = object; 18 | this.#property = property; 19 | 20 | this.#buttonElem = this.addElem( 21 | createElem('button', { 22 | type: 'button', 23 | onClick: () => { 24 | this.#object[this.#property](this); 25 | }, 26 | })); 27 | this.setOptions({name: property, ...options}); 28 | } 29 | name(name) { 30 | this.#buttonElem.textContent = name; 31 | return this; 32 | } 33 | setOptions(options) { 34 | copyExistingProperties(this.#options, options); 35 | const {name} = this.#options; 36 | this.#buttonElem.textContent = name; 37 | return this; 38 | } 39 | } -------------------------------------------------------------------------------- /src/controllers/Canvas.js: -------------------------------------------------------------------------------- 1 | import ElementView from '../views/ElementView.js'; 2 | import LabelController from './LabelController.js'; 3 | 4 | // TODO: remove this? Should just be user side 5 | export default class Canvas extends LabelController { 6 | #canvasElem; 7 | 8 | constructor(name) { 9 | super('muigui-canvas', name); 10 | this.#canvasElem = this.add( 11 | new ElementView('canvas', 'muigui-canvas'), 12 | ).domElement; 13 | } 14 | get canvas() { 15 | return this.#canvasElem; 16 | } 17 | listen() { 18 | return this; 19 | } 20 | } -------------------------------------------------------------------------------- /src/controllers/Checkbox.js: -------------------------------------------------------------------------------- 1 | import CheckboxView from '../views/CheckboxView.js'; 2 | import ValueController from './ValueController.js'; 3 | 4 | export default class Checkbox extends ValueController { 5 | constructor(object, property) { 6 | super(object, property, 'muigui-checkbox'); 7 | const id = this.id; 8 | this.add(new CheckboxView(this, id)); 9 | this.updateDisplay(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/controllers/Color.js: -------------------------------------------------------------------------------- 1 | import { 2 | colorFormatConverters, 3 | guessFormat, 4 | } from '../libs/color-utils.js'; 5 | import ValueController from './ValueController.js'; 6 | import TextView from '../views/TextView.js'; 7 | import ColorView from '../views/ColorView.js'; 8 | 9 | export default class Color extends ValueController { 10 | #colorView; 11 | #textView; 12 | 13 | constructor(object, property, options = {}) { 14 | super(object, property, 'muigui-color'); 15 | const format = options.format || guessFormat(this.getValue()); 16 | const {color, text} = colorFormatConverters[format]; 17 | this.#colorView = this.add(new ColorView(this, {converters: color})); 18 | this.#textView = this.add(new TextView(this, {converters: text})); 19 | this.updateDisplay(); 20 | } 21 | setOptions(options) { 22 | const {format} = options; 23 | if (format) { 24 | const {color, text} = colorFormatConverters[format]; 25 | this.#colorView.setOptions({converters: color}); 26 | this.#textView.setOptions({converters: text}); 27 | } 28 | super.setOptions(options); 29 | return this; 30 | } 31 | } -------------------------------------------------------------------------------- /src/controllers/ColorChooser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { 3 | colorFormatConverters, 4 | guessFormat, 5 | hasAlpha, 6 | hexToUint8RGB, 7 | hslToRgbUint8, 8 | rgbUint8ToHsl, 9 | uint8RGBToHex, 10 | } from '../libs/color-utils.js'; 11 | import ColorChooserView from '../views/ColorChooserView.js'; 12 | import TextView from '../views/TextView.js'; 13 | import PopDownController from './PopDownController.js'; 14 | 15 | export default class ColorChooser extends PopDownController { 16 | #colorView; 17 | #textView; 18 | #to; 19 | 20 | constructor(object, property, options = {}) { 21 | super(object, property, 'muigui-color-chooser'); 22 | const format = options.format || guessFormat(this.getValue()); 23 | const {color, text} = colorFormatConverters[format]; 24 | this.#to = color.to; 25 | this.#textView = new TextView(this, {converters: text, alpha: hasAlpha(format)}); 26 | this.#colorView = new ColorChooserView(this, {converters: color, alpha: hasAlpha(format)}); 27 | this.addTop(this.#textView); 28 | this.addBottom(this.#colorView); 29 | // WTF! FIX! 30 | this.___setKnobHelper = true; 31 | this.updateDisplay(); 32 | } 33 | #setKnobHelper() { 34 | if (this.#to) { 35 | const hex6Or8 = this.#to(this.getValue()); 36 | const alpha = hex6Or8.length === 9 ? hex6Or8.substring(7, 9) : 'FF'; 37 | const hsl = rgbUint8ToHsl(hexToUint8RGB(hex6Or8)); 38 | hsl[2] = (hsl[2] + 50) % 100; 39 | const hex = uint8RGBToHex(hslToRgbUint8(hsl)); 40 | this.setKnobColor(`${hex6Or8.substring(0, 7)}${alpha}`, hex); 41 | } 42 | } 43 | updateDisplay() { 44 | super.updateDisplay(); 45 | if (this.___setKnobHelper) { 46 | this.#setKnobHelper(); 47 | } 48 | } 49 | setOptions(options) { 50 | super.setOptions(options); 51 | return this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/controllers/Container.js: -------------------------------------------------------------------------------- 1 | import Controller from './Controller.js'; 2 | 3 | export default class Container extends Controller { 4 | #controllers; 5 | #childDestController; 6 | 7 | constructor(className) { 8 | super(className); 9 | this.#controllers = []; 10 | this.#childDestController = this; 11 | } 12 | get children() { 13 | return this.#controllers; // should we return a copy? 14 | } 15 | get controllers() { 16 | return this.#controllers.filter(c => !(c instanceof Container)); 17 | } 18 | get folders() { 19 | return this.#controllers.filter(c => c instanceof Container); 20 | } 21 | reset(recursive = true) { 22 | for (const controller of this.#controllers) { 23 | if (!(controller instanceof Container) || recursive) { 24 | controller.reset(recursive); 25 | } 26 | } 27 | return this; 28 | } 29 | updateDisplay() { 30 | for (const controller of this.#controllers) { 31 | controller.updateDisplay(); 32 | } 33 | return this; 34 | } 35 | remove(controller) { 36 | const ndx = this.#controllers.indexOf(controller); 37 | if (ndx >= 0) { 38 | const c = this.#controllers.splice(ndx, 1); 39 | const c0 = c[0]; 40 | const elem = c0.domElement; 41 | elem.remove(); 42 | c0.setParent(null); 43 | } 44 | return this; 45 | } 46 | #addControllerImpl(controller) { 47 | this.domElement.appendChild(controller.domElement); 48 | this.#controllers.push(controller); 49 | controller.setParent(this); 50 | return controller; 51 | } 52 | addController(controller) { 53 | return this.#childDestController.#addControllerImpl(controller); 54 | } 55 | pushContainer(container) { 56 | this.addController(container); 57 | this.#childDestController = container; 58 | return container; 59 | } 60 | popContainer() { 61 | this.#childDestController = this.#childDestController.parent; 62 | return this; 63 | } 64 | listen() { 65 | this.#controllers.forEach(c => { 66 | c.listen(); 67 | }); 68 | return this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/controllers/Controller.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { removeArrayElem } from '../libs/utils.js'; 3 | import View from '../views/View.js'; 4 | 5 | export default class Controller extends View { 6 | #changeFns; 7 | #finishChangeFns; 8 | #parent; 9 | 10 | constructor(className) { 11 | super(createElem('div', {className: 'muigui-controller'})); 12 | this.#changeFns = []; 13 | this.#finishChangeFns = []; 14 | // we need the specialization to come last so it takes precedence. 15 | if (className) { 16 | this.domElement.classList.add(className); 17 | } 18 | } 19 | get parent() { 20 | return this.#parent; 21 | } 22 | setParent(parent) { 23 | this.#parent = parent; 24 | this.enable(!this.disabled()); 25 | } 26 | show(show = true) { 27 | this.domElement.classList.toggle('muigui-hide', !show); 28 | this.domElement.classList.toggle('muigui-show', show); 29 | return this; 30 | } 31 | hide() { 32 | return this.show(false); 33 | } 34 | disabled() { 35 | return !!this.domElement.closest('.muigui-disabled'); 36 | } 37 | 38 | enable(enable = true) { 39 | this.domElement.classList.toggle('muigui-disabled', !enable); 40 | 41 | // If disabled we need to set the attribute 'disabled=true' to all 42 | // input/select/button/textarea's below 43 | // 44 | // If enabled we need to set the attribute 'disabled=false' to all below 45 | // until we hit a disabled controller. 46 | // 47 | // ATM the problem is we can find the input/select/button/textarea elements 48 | // but we can't easily find which controller they belong do. 49 | // But we don't need to? We can just check up if it or parent has 50 | // '.muigui-disabled' 51 | ['input', 'button', 'select', 'textarea'].forEach(tag => { 52 | this.domElement.querySelectorAll(tag).forEach(elem => { 53 | const disabled = !!elem.closest('.muigui-disabled'); 54 | elem.disabled = disabled; 55 | }); 56 | }); 57 | 58 | return this; 59 | } 60 | disable(disable = true) { 61 | return this.enable(!disable); 62 | } 63 | onChange(fn) { 64 | this.removeChange(fn); 65 | this.#changeFns.push(fn); 66 | return this; 67 | } 68 | removeChange(fn) { 69 | removeArrayElem(this.#changeFns, fn); 70 | return this; 71 | } 72 | onFinishChange(fn) { 73 | this.removeFinishChange(fn); 74 | this.#finishChangeFns.push(fn); 75 | return this; 76 | } 77 | removeFinishChange(fn) { 78 | removeArrayElem(this.#finishChangeFns, fn); 79 | return this; 80 | } 81 | #callListeners(fns, newV) { 82 | for (const fn of fns) { 83 | fn.call(this, newV); 84 | } 85 | } 86 | emitChange(value, object, property) { 87 | this.#callListeners(this.#changeFns, value); 88 | if (this.#parent) { 89 | if (object === undefined) { 90 | this.#parent.emitChange(value); 91 | } else { 92 | this.#parent.emitChange({ 93 | object, 94 | property, 95 | value, 96 | controller: this, 97 | }); 98 | } 99 | } 100 | } 101 | emitFinalChange(value, object, property) { 102 | this.#callListeners(this.#finishChangeFns, value); 103 | if (this.#parent) { 104 | if (object === undefined) { 105 | this.#parent.emitChange(value); 106 | } else { 107 | this.#parent.emitFinalChange({ 108 | object, 109 | property, 110 | value, 111 | controller: this, 112 | }); 113 | } 114 | } 115 | } 116 | updateDisplay() { 117 | // placeholder. override 118 | } 119 | getColors() { 120 | const toCamelCase = s => s.replace(/-([a-z])/g, (m, m1) => m1.toUpperCase()); 121 | const keys = [ 122 | 'color', 123 | 'bg-color', 124 | 'value-color', 125 | 'value-bg-color', 126 | 'hover-bg-color', 127 | 'menu-bg-color', 128 | 'menu-sep-color', 129 | 'disabled-color', 130 | ]; 131 | const div = createElem('div'); 132 | this.domElement.appendChild(div); 133 | const colors = Object.fromEntries(keys.map(key => { 134 | div.style.color = `var(--${key})`; 135 | const s = getComputedStyle(div); 136 | return [toCamelCase(key), s.color]; 137 | })); 138 | div.remove(); 139 | return colors; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/controllers/Direction.js: -------------------------------------------------------------------------------- 1 | import { identity } from '../libs/conversions.js'; 2 | import DirectionView from '../views/DirectionView.js'; 3 | import NumberView from '../views/NumberView.js'; 4 | // import ValueController from './ValueController.js'; 5 | import PopDownController from './PopDownController.js'; 6 | 7 | 8 | // deg2rad 9 | // where is 0 10 | // range (0, 360), (-180, +180), (0,0) Really this is a range 11 | 12 | export default class Direction extends PopDownController { 13 | #options; 14 | constructor(object, property, options) { 15 | super(object, property, 'muigui-direction'); 16 | this.#options = options; // FIX 17 | this.addTop(new NumberView(this, 18 | identity)); 19 | this.addBottom(new DirectionView(this, options)); 20 | this.updateDisplay(); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/controllers/Divider.js: -------------------------------------------------------------------------------- 1 | import Controller from './Controller.js'; 2 | 3 | // This feels like it should be something else like 4 | // gui.addController({className: 'muigui-divider')}; 5 | export default class Divider extends Controller { 6 | constructor() { 7 | super('muigui-divider'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/controllers/Folder.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import Container from './Container.js'; 3 | 4 | export default class Folder extends Container { 5 | #labelElem; 6 | 7 | constructor(name = 'Controls', className = 'muigui-menu') { 8 | super(className); 9 | this.#labelElem = createElem('label'); 10 | this.addElem(createElem('button', { 11 | type: 'button', 12 | onClick: () => this.toggleOpen(), 13 | }, [this.#labelElem])); 14 | this.pushContainer(new Container('muigui-open-container')); 15 | this.pushContainer(new Container()); 16 | this.name(name); 17 | this.open(); 18 | } 19 | open(open = true) { 20 | this.domElement.classList.toggle('muigui-closed', !open); 21 | this.domElement.classList.toggle('muigui-open', open); 22 | return this; 23 | } 24 | close() { 25 | return this.open(false); 26 | } 27 | name(name) { 28 | this.#labelElem.textContent = name; 29 | return this; 30 | } 31 | title(title) { 32 | return this.name(title); 33 | } 34 | toggleOpen() { 35 | this.open(!this.domElement.classList.contains('muigui-open')); 36 | return this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/controllers/Label.js: -------------------------------------------------------------------------------- 1 | import Controller from './Controller.js'; 2 | 3 | // This feels like it should be something else like 4 | // gui.addDividing = new Controller() 5 | export default class Label extends Controller { 6 | constructor(text) { 7 | super('muigui-label'); 8 | this.text(text); 9 | } 10 | text(text) { 11 | this.domElement.textContent = text; 12 | return this; 13 | } 14 | } -------------------------------------------------------------------------------- /src/controllers/LabelController.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { makeId } from '../libs/ids.js'; 3 | import ValueView from '../views/ValueView.js'; 4 | import Controller from './Controller.js'; 5 | 6 | export default class LabelController extends Controller { 7 | #id; 8 | #nameElem; 9 | 10 | constructor(className = '', name = '') { 11 | super('muigui-label-controller'); 12 | this.#id = makeId(); 13 | this.#nameElem = createElem('label', {for: this.#id}); 14 | this.domElement.appendChild(this.#nameElem); 15 | this.pushSubView(new ValueView(className)); 16 | this.name(name); 17 | } 18 | get id() { 19 | return this.#id; 20 | } 21 | name(name) { 22 | if (this.#nameElem.title === this.#nameElem.textContent) { 23 | this.#nameElem.title = name; 24 | } 25 | this.#nameElem.textContent = name; 26 | return this; 27 | } 28 | tooltip(tip) { 29 | this.#nameElem.title = tip; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/controllers/PopDownController.js: -------------------------------------------------------------------------------- 1 | import ElementView from '../views/ElementView.js'; 2 | import ValueController from './ValueController.js'; 3 | import { copyExistingProperties } from '../libs/utils.js'; 4 | import { createElem } from '../libs/elem.js'; 5 | /* 6 | 7 | holder = new TabHolder 8 | tab = holder.add(new Tab("name")) 9 | tab.add(...) 10 | 11 | 12 | pc = new PopdownController 13 | top = pc.add(new Row()) 14 | top.add(new Button()); 15 | values = topRow.add(new Div()) 16 | bottom = pc.add(new Row()); 17 | 18 | 19 | 20 | pc = new PopdownController 21 | pc.addTop 22 | pc.addTop 23 | 24 | pc.addBottom 25 | 26 | 27 | */ 28 | 29 | export default class PopDownController extends ValueController { 30 | #top; 31 | #valuesView; 32 | #checkboxElem; 33 | #bottom; 34 | #options = { 35 | open: false, 36 | }; 37 | 38 | constructor(object, property, options = {}) { 39 | super(object, property, 'muigui-pop-down-controller'); 40 | /* 41 | [ValueView 42 | [[B][values]] upper row 43 | [[ visual ]] lower row 44 | ] 45 | */ 46 | this.#top = this.add(new ElementView('div', 'muigui-pop-down-top')); 47 | // this.#top.add(new CheckboxView(makeSetter(this.#options, 'open'))); 48 | const checkboxElem = this.#top.addElem(createElem('input', { 49 | type: 'checkbox', 50 | onChange: () => { 51 | this.#options.open = checkboxElem.checked; 52 | this.updateDisplay(); 53 | }, 54 | })); 55 | this.#checkboxElem = checkboxElem; 56 | this.#valuesView = this.#top.add(new ElementView('div', 'muigui-pop-down-values')); 57 | const container = new ElementView('div', 'muigui-pop-down-bottom muigui-open-container'); 58 | this.#bottom = new ElementView('div'); 59 | container.add(this.#bottom); 60 | this.add(container); 61 | this.setOptions(options); 62 | } 63 | setKnobColor(bgCssColor/*, fgCssColor*/) { 64 | if (this.#checkboxElem) { 65 | this.#checkboxElem.style = ` 66 | --range-color: ${bgCssColor}; 67 | --value-bg-color: ${bgCssColor}; 68 | `; 69 | } 70 | } 71 | updateDisplay() { 72 | super.updateDisplay(); 73 | const {open} = this.#options; 74 | this.domElement.children[1].classList.toggle('muigui-open', open); 75 | this.domElement.children[1].classList.toggle('muigui-closed', !open); 76 | } 77 | setOptions(options) { 78 | copyExistingProperties(this.#options, options); 79 | super.setOptions(options); 80 | this.updateDisplay(); 81 | } 82 | addTop(view) { 83 | return this.#valuesView.add(view); 84 | } 85 | addBottom(view) { 86 | return this.#bottom.add(view); 87 | } 88 | } -------------------------------------------------------------------------------- /src/controllers/RadioGrid.js: -------------------------------------------------------------------------------- 1 | import RadioGridView from '../views/RadioGridView.js'; 2 | import ValueController from './ValueController.js'; 3 | import { convertToKeyValues } from '../libs/key-values.js'; 4 | 5 | export default class RadioGrid extends ValueController { 6 | constructor(object, property, options) { 7 | super(object, property, 'muigui-radio-grid'); 8 | const valueIsNumber = typeof this.getValue() === 'number'; 9 | const { 10 | keyValues: keyValuesInput, 11 | cols = 3, 12 | } = options; 13 | const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); 14 | this.add(new RadioGridView(this, keyValues, cols)); 15 | this.updateDisplay(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/controllers/Range.js: -------------------------------------------------------------------------------- 1 | import ValueController from './ValueController.js'; 2 | import NumberView from '../views/NumberView.js'; 3 | import RangeView from '../views/RangeView.js'; 4 | 5 | export default class Range extends ValueController { 6 | constructor(object, property, options) { 7 | super(object, property, 'muigui-range'); 8 | this.add(new RangeView(this, options)); 9 | this.add(new NumberView(this, options)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/controllers/Select.js: -------------------------------------------------------------------------------- 1 | import SelectView from '../views/SelectView.js'; 2 | import ValueController from './ValueController.js'; 3 | import { convertToKeyValues } from '../libs/key-values.js'; 4 | 5 | export default class Select extends ValueController { 6 | constructor(object, property, options) { 7 | super(object, property, 'muigui-select'); 8 | const valueIsNumber = typeof this.getValue() === 'number'; 9 | const {keyValues: keyValuesInput} = options; 10 | const keyValues = convertToKeyValues(keyValuesInput, valueIsNumber); 11 | this.add(new SelectView(this, keyValues)); 12 | this.updateDisplay(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/controllers/Slider.js: -------------------------------------------------------------------------------- 1 | import ValueController from './ValueController.js'; 2 | import NumberView from '../views/NumberView.js'; 3 | import SliderView from '../views/SliderView.js'; 4 | 5 | export default class Slider extends ValueController { 6 | constructor(object, property, options = {}) { 7 | super(object, property, 'muigui-slider'); 8 | this.add(new SliderView(this, options)); 9 | this.add(new NumberView(this, options)); 10 | this.updateDisplay(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/controllers/TabHolder.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import Controller from './Controller.js'; 3 | 4 | export default class TabHolder extends Controller { 5 | #labelElem; 6 | 7 | constructor(name = 'Controls', className = 'muigui-tab-holder') { 8 | super(className); 9 | this.#labelElem = createElem('label'); 10 | this.addElem(createElem('button', { 11 | type: 'button', 12 | onClick: () => this.toggleOpen(), 13 | }, [this.#labelElem])); 14 | this.name(name); 15 | this.open(); 16 | } 17 | open(open = true) { 18 | this.domElement.classList.toggle('muigui-closed', !open); 19 | this.domElement.classList.toggle('muigui-open', open); 20 | return this; 21 | } 22 | close() { 23 | return this.open(false); 24 | } 25 | name(name) { 26 | this.#labelElem.textContent = name; 27 | return this; 28 | } 29 | title(title) { 30 | return this.name(title); 31 | } 32 | toggleOpen() { 33 | this.open(!this.domElement.classList.contains('muigui-open')); 34 | return this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/controllers/Text.js: -------------------------------------------------------------------------------- 1 | import TextView from '../views/TextView.js'; 2 | import ValueController from './ValueController.js'; 3 | 4 | export default class Text extends ValueController { 5 | constructor(object, property) { 6 | super(object, property, 'muigui-text'); 7 | this.add(new TextView(this)); 8 | this.updateDisplay(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/controllers/TextNumber.js: -------------------------------------------------------------------------------- 1 | 2 | import NumberView from '../views/NumberView.js'; 3 | import ValueController from './ValueController.js'; 4 | 5 | // Wanted to name this `Number` but it conflicts with 6 | // JavaScript `Number`. It most likely wouldn't be 7 | // an issue? But users might `import {Number} ...` and 8 | // things would break. 9 | export default class TextNumber extends ValueController { 10 | #textView; 11 | #step; 12 | 13 | constructor(object, property, options = {}) { 14 | super(object, property, 'muigui-text-number'); 15 | this.#textView = this.add(new NumberView(this, options)); 16 | this.updateDisplay(); 17 | } 18 | } -------------------------------------------------------------------------------- /src/controllers/ValueController.js: -------------------------------------------------------------------------------- 1 | import {addTask, removeTask} from '../libs/taskrunner.js'; 2 | import { isTypedArray } from '../libs/utils.js'; 3 | import LabelController from './LabelController.js'; 4 | 5 | export default class ValueController extends LabelController { 6 | #object; 7 | #property; 8 | #initialValue; 9 | #listening; 10 | #views; 11 | #updateFn; 12 | 13 | constructor(object, property, className = '') { 14 | super(className, property); 15 | this.#object = object; 16 | this.#property = property; 17 | this.#initialValue = this.getValue(); 18 | this.#listening = false; 19 | this.#views = []; 20 | } 21 | get initialValue() { 22 | return this.#initialValue; 23 | } 24 | get object() { 25 | return this.#object; 26 | } 27 | get property() { 28 | return this.#property; 29 | } 30 | add(view) { 31 | this.#views.push(view); 32 | super.add(view); 33 | this.updateDisplay(); 34 | return view; 35 | } 36 | #setValueImpl(v, ignoreCache) { 37 | let isDifferent = false; 38 | if (typeof v === 'object') { 39 | const dst = this.#object[this.#property]; 40 | // don't replace objects, just their values. 41 | if (Array.isArray(v) || isTypedArray(v)) { 42 | for (let i = 0; i < v.length; ++i) { 43 | isDifferent ||= dst[i] !== v[i]; 44 | dst[i] = v[i]; 45 | } 46 | } else { 47 | for (const key of Object.keys(v)) { 48 | isDifferent ||= dst[key] !== v[key]; 49 | } 50 | Object.assign(dst, v); 51 | } 52 | } else { 53 | isDifferent = this.#object[this.#property] !== v; 54 | this.#object[this.#property] = v; 55 | } 56 | this.updateDisplay(ignoreCache); 57 | if (isDifferent) { 58 | this.emitChange(this.getValue(), this.#object, this.#property); 59 | } 60 | return isDifferent; 61 | } 62 | setValue(v) { 63 | this.#setValueImpl(v); 64 | } 65 | setFinalValue(v) { 66 | const isDifferent = this.#setValueImpl(v, true); 67 | if (isDifferent) { 68 | this.emitFinalChange(this.getValue(), this.#object, this.#property); 69 | } 70 | return this; 71 | } 72 | updateDisplay(ignoreCache) { 73 | const newV = this.getValue(); 74 | for (const view of this.#views) { 75 | view.updateDisplayIfNeeded(newV, ignoreCache); 76 | } 77 | return this; 78 | } 79 | setOptions(options) { 80 | for (const view of this.#views) { 81 | view.setOptions(options); 82 | } 83 | this.updateDisplay(); 84 | return this; 85 | } 86 | getValue() { 87 | return this.#object[this.#property]; 88 | } 89 | value(v) { 90 | this.setValue(v); 91 | return this; 92 | } 93 | reset() { 94 | this.setValue(this.#initialValue); 95 | return this; 96 | } 97 | listen(listen = true) { 98 | if (!this.#updateFn) { 99 | this.#updateFn = this.updateDisplay.bind(this); 100 | } 101 | if (listen) { 102 | if (!this.#listening) { 103 | this.#listening = true; 104 | addTask(this.#updateFn); 105 | } 106 | } else { 107 | if (this.#listening) { 108 | this.#listening = false; 109 | removeTask(this.#updateFn); 110 | } 111 | } 112 | return this; 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/controllers/Vec2.js: -------------------------------------------------------------------------------- 1 | import NumberView from '../views/NumberView.js'; 2 | import Vec2View from '../views/Vec2View.js'; 3 | import PopDownController from './PopDownController.js'; 4 | import { strToNumber } from '../libs/conversions.js'; 5 | 6 | // TODO: zoom with wheel and pinch? 7 | // TODO: grid? 8 | // // options 9 | // scale: 10 | // range: number (both x and y + /) 11 | // range: array (min, max) 12 | // xRange: 13 | // deg/rad/turn 14 | 15 | export default class Vec2 extends PopDownController { 16 | constructor(object, property) { 17 | super(object, property, 'muigui-vec2'); 18 | 19 | const makeSetter = (ndx) => { 20 | return { 21 | setValue: (v) => { 22 | const newV = this.getValue(); 23 | newV[ndx] = v; 24 | this.setValue(newV); 25 | }, 26 | setFinalValue: (v) => { 27 | const newV = this.getValue(); 28 | newV[ndx] = v; 29 | this.setFinalValue(newV); 30 | }, 31 | }; 32 | }; 33 | 34 | this.addTop(new NumberView(makeSetter(0), { 35 | converters: { 36 | to: v => v[0], 37 | from: strToNumber.from, 38 | }, 39 | })); 40 | this.addTop(new NumberView(makeSetter(1), { 41 | converters: { 42 | to: v => v[1], 43 | from: strToNumber.from, 44 | }, 45 | })); 46 | this.addBottom(new Vec2View(this)); 47 | this.updateDisplay(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/controllers/create-controller.js: -------------------------------------------------------------------------------- 1 | import Button from './Button.js'; 2 | import Checkbox from './Checkbox.js'; 3 | import TextNumber from './TextNumber.js'; 4 | import Select from './Select.js'; 5 | import Range from './Range.js'; 6 | import Text from './Text.js'; 7 | 8 | // const isConversion = o => typeof o.to === 'function' && typeof o.from === 'function'; 9 | 10 | /** 11 | * possible inputs 12 | * add(o, p, min: number, max: number) 13 | * add(o, p, min: number, max: number, step: number) 14 | * add(o, p, array: [value]) 15 | * add(o, p, array: [[key, value]]) 16 | * 17 | * @param {*} object 18 | * @param {string} property 19 | * @param {...any} args 20 | * @returns {Controller} 21 | */ 22 | export function createController(object, property, ...args) { 23 | const [arg1] = args; 24 | if (Array.isArray(arg1)) { 25 | return new Select(object, property, {keyValues: arg1}); 26 | } 27 | if (arg1 && arg1.keyValues) { 28 | return new Select(object, property, {keyValues: arg1.keyValues}); 29 | } 30 | 31 | const t = typeof object[property]; 32 | switch (t) { 33 | case 'number': 34 | if (typeof args[0] === 'number' && typeof args[1] === 'number') { 35 | const min = args[0]; 36 | const max = args[1]; 37 | const step = args[2]; 38 | return new Range(object, property, {min, max, ...(step && {step})}); 39 | } 40 | return args.length === 0 41 | ? new TextNumber(object, property, ...args) 42 | : new Range(object, property, ...args); 43 | case 'boolean': 44 | return new Checkbox(object, property, ...args); 45 | case 'function': 46 | return new Button(object, property, ...args); 47 | case 'string': 48 | return new Text(object, property, ...args); 49 | case 'undefined': 50 | throw new Error(`no property named ${property}`); 51 | default: 52 | throw new Error(`unhandled type ${t} for property ${property}`); 53 | } 54 | } -------------------------------------------------------------------------------- /src/esm.ts: -------------------------------------------------------------------------------- 1 | import GUI from './muigui.js'; 2 | 3 | export { default as ColorChooser } from './controllers/ColorChooser.js'; 4 | export { default as Direction } from './controllers/Direction.js'; 5 | export { default as RadioGrid } from './controllers/RadioGrid.js'; 6 | export { default as Range } from './controllers/Range.js'; 7 | export { default as Select } from './controllers/Select.js'; 8 | export { default as Slider } from './controllers/Slider.js'; 9 | export { default as TextNumber } from './controllers/TextNumber.js'; 10 | export { default as Vec2 } from './controllers/Vec2.js'; 11 | 12 | import {graph} from './libs/graph.js'; 13 | import {monitor} from './libs/monitor.js'; 14 | 15 | export const helpers = { 16 | graph, 17 | monitor, 18 | }; 19 | 20 | export default GUI; -------------------------------------------------------------------------------- /src/layout/Column.js: -------------------------------------------------------------------------------- 1 | import Layout from './Layout.js'; 2 | 3 | export default class Column extends Layout { 4 | constructor() { 5 | super('div', 'muigui-row'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/layout/Frame.js: -------------------------------------------------------------------------------- 1 | import Layout from './Layout.js'; 2 | 3 | export default class Frame extends Layout { 4 | static css = 'foo'; 5 | constructor() { 6 | super('div', 'muigui-frame'); 7 | } 8 | static get foo() { 9 | return 'boo'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/layout/Grid.js: -------------------------------------------------------------------------------- 1 | import Layout from './Layout.js'; 2 | 3 | export default class Grid extends Layout { 4 | constructor() { 5 | super('div', 'muigui-grid'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/layout/Layout.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import View from '../views/View.js'; 3 | 4 | function showCSS(ob) { 5 | if (ob.prototype.css) { 6 | showCSS(ob.prototype); 7 | } 8 | } 9 | 10 | export default class Layout extends View { 11 | static css = 'bar'; 12 | constructor(tag, className) { 13 | super(createElem(tag, {className})); 14 | 15 | showCSS(this); 16 | } 17 | } 18 | 19 | /* 20 | class ValueController ?? { 21 | const row = this.add(new Row()); 22 | const label = row.add(new Label()); 23 | const div = row.add(new Div()); 24 | const row = div.add(new Row()); 25 | } 26 | */ 27 | 28 | /* 29 | class MyCustomThing extends ValueController { 30 | constructor(object, property, options) { 31 | const topRow = this.add(new Row()); 32 | const bottomRow = this.add(new Row()); 33 | topRow.add(new NumberView()); 34 | topRow.add(new NumberView()); 35 | topRow.add(new NumberView()); 36 | topRow.add(new NumberView()); 37 | bottomRow.add(new DirectionView()); 38 | bottomRow.add(new DirectionView()); 39 | bottomRow.add(new DirectionView()); 40 | bottomRow.add(new DirectionView()); 41 | } 42 | } 43 | new Grid([ 44 | [new 45 | ] 46 | */ -------------------------------------------------------------------------------- /src/layout/Row.js: -------------------------------------------------------------------------------- 1 | import Layout from './Layout.js'; 2 | 3 | export default class Row extends Layout { 4 | constructor() { 5 | super('div', 'muigui-row'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/libs/assert.js: -------------------------------------------------------------------------------- 1 | export function assert(truthy, msg = '') { 2 | if (!truthy) { 3 | throw new Error(msg); 4 | } 5 | } -------------------------------------------------------------------------------- /src/libs/conversions.js: -------------------------------------------------------------------------------- 1 | import { 2 | makeRangeConverters, 3 | } from './utils.js'; 4 | 5 | export const identity = { 6 | to: v => v, 7 | from: v => [true, v], 8 | }; 9 | 10 | // from: from string to value 11 | // to: from value to string 12 | export const strToNumber = { 13 | to: v => v.toString(), 14 | from: v => { 15 | const newV = parseFloat(v); 16 | return [!Number.isNaN(newV), newV]; 17 | }, 18 | }; 19 | 20 | export const converters = { 21 | radToDeg: makeRangeConverters({to: [0, 180], from: [0, Math.PI]}), 22 | }; 23 | -------------------------------------------------------------------------------- /src/libs/css-utils.js: -------------------------------------------------------------------------------- 1 | export function classNamesToStr(names) { 2 | return Array.isArray(names) ? names.filter(v => v).join(' ') : names; 3 | } -------------------------------------------------------------------------------- /src/libs/elem.js: -------------------------------------------------------------------------------- 1 | export function setElemProps(elem, attrs, children) { 2 | for (const [key, value] of Object.entries(attrs)) { 3 | if (typeof value === 'function' && key.startsWith('on')) { 4 | const eventName = key.substring(2).toLowerCase(); 5 | elem.addEventListener(eventName, value, {passive: false}); 6 | } else if (typeof value === 'object') { 7 | for (const [k, v] of Object.entries(value)) { 8 | elem[key][k] = v; 9 | } 10 | } else if (elem[key] === undefined) { 11 | elem.setAttribute(key, value); 12 | } else { 13 | elem[key] = value; 14 | } 15 | } 16 | for (const child of children) { 17 | elem.appendChild(child); 18 | } 19 | return elem; 20 | } 21 | 22 | export function createElem(tag, attrs = {}, children = []) { 23 | const elem = document.createElement(tag); 24 | setElemProps(elem, attrs, children); 25 | return elem; 26 | } 27 | 28 | export function addElem(tag, parent, attrs = {}, children = []) { 29 | const elem = createElem(tag, attrs, children); 30 | parent.appendChild(elem); 31 | return elem; 32 | } 33 | 34 | let nextId = 0; 35 | export function getNewId() { 36 | return `muigui-id-${nextId++}`; 37 | } 38 | -------------------------------------------------------------------------------- /src/libs/emitter.js: -------------------------------------------------------------------------------- 1 | import { removeArrayElem } from '../libs/utils.js'; 2 | 3 | /** 4 | * Similar to EventSource 5 | */ 6 | export default class Emitter { 7 | #listeners; 8 | #changes; 9 | #receivers; 10 | #emitting; 11 | 12 | constructor() { 13 | this.#listeners = {}; 14 | this.#changes = []; 15 | this.#receivers = []; 16 | } 17 | on(type, listener) { 18 | if (this.#emitting) { 19 | this.#changes.push(['add', type, listener]); 20 | return; 21 | } 22 | const listeners = this.#listeners[type] || []; 23 | listeners.push(listener); 24 | this.#listeners[type] = listeners; 25 | } 26 | addListener(type, listener) { 27 | return this.on(type, listener); 28 | } 29 | removeListener(type, listener) { 30 | if (this.#emitting) { 31 | this.#changes.push(['remove', type, listener]); 32 | return; 33 | } 34 | const listeners = this.#listeners[type]; 35 | if (listeners) { 36 | removeArrayElem(listeners, listener); 37 | } 38 | } 39 | propagate(receiver) { 40 | this.#receivers.push(receiver); 41 | } 42 | emit(type, ...args) { 43 | this.#emitting = true; 44 | const listeners = this.#listeners[type]; 45 | if (listeners) { 46 | for (const listener of listeners) { 47 | listener(...args); 48 | } 49 | } 50 | this.#emitting = false; 51 | while (this.#changes.length) { 52 | const [cmd, type, listener] = this.#changes.shift(); 53 | switch (cmd) { 54 | case 'add': 55 | this.on(type, listener); 56 | break; 57 | case 'remove': 58 | this.removeListener(type, listener); 59 | break; 60 | default: 61 | throw new Error('unknown type'); 62 | } 63 | } 64 | for (const receiver of this.#receivers) { 65 | receiver.emit(type, ...args); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/libs/graph.js: -------------------------------------------------------------------------------- 1 | const darkColors = { 2 | main: '#ddd', 3 | }; 4 | const lightColors = { 5 | main: '#333', 6 | }; 7 | 8 | const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)'); 9 | 10 | let colors; 11 | let isDarkMode; 12 | 13 | function update() { 14 | isDarkMode = darkMatcher.matches; 15 | colors = isDarkMode ? darkColors : lightColors; 16 | } 17 | darkMatcher.addEventListener('change', update); 18 | update(); 19 | 20 | export function graph(canvas, data, { 21 | min = -1, 22 | max = 1, 23 | interval = 16, 24 | color, 25 | }) { 26 | const ctx = canvas.getContext('2d'); 27 | 28 | function render() { 29 | const {width, height} = canvas; 30 | ctx.clearRect(0, 0, width, height); 31 | ctx.beginPath(); 32 | const range = max - min; 33 | for (let i = 0; i < data.length; ++i) { 34 | const x = i * width / data.length; 35 | const y = (data[i] - min) * height / range; 36 | ctx.lineTo(x, y); 37 | } 38 | ctx.strokeStyle = color || colors.main; 39 | ctx.stroke(); 40 | } 41 | setInterval(render, interval); 42 | } -------------------------------------------------------------------------------- /src/libs/ids.js: -------------------------------------------------------------------------------- 1 | let id = 0; 2 | 3 | export function makeId() { 4 | return `muigui-${++id}`; 5 | } 6 | -------------------------------------------------------------------------------- /src/libs/iterable-array.js: -------------------------------------------------------------------------------- 1 | import { removeArrayElem } from './utils.js'; 2 | 3 | export default class IterableArray { 4 | #arr; 5 | #added; 6 | #removedSet; 7 | #iterating; 8 | 9 | constructor() { 10 | this.#arr = []; 11 | this.#added = []; 12 | this.#removedSet = new Set(); 13 | } 14 | add(elem) { 15 | if (this.#iterating) { 16 | this.#removedSet.delete(elem); 17 | this.#added.push(elem); 18 | } else { 19 | this.#arr.push(elem); 20 | } 21 | } 22 | remove(elem) { 23 | if (this.#iterating) { 24 | removeArrayElem(this.#added, elem); 25 | this.#removedSet.add(elem); 26 | } else { 27 | removeArrayElem(this.#arr, elem); 28 | } 29 | } 30 | #process(arr, fn) { 31 | for (const elem of arr) { 32 | if (!this.#removedSet.has(elem)) { 33 | if (fn(elem) === false) { 34 | break; 35 | } 36 | } 37 | } 38 | } 39 | forEach(fn) { 40 | this.#iterating = true; 41 | this.#process(this.#arr, fn); 42 | do { 43 | if (this.#removedSet.size) { 44 | for (const elem of this.#removedSet.values()) { 45 | removeArrayElem(this.#arr, elem); 46 | } 47 | this.#removedSet.clear(); 48 | } 49 | if (this.#added.length) { 50 | const added = this.#added; 51 | this.#added = []; 52 | this.#process(added, fn); 53 | } 54 | } while (this.#added.length || this.#removedSet.size); 55 | this.#iterating = false; 56 | } 57 | } -------------------------------------------------------------------------------- /src/libs/key-values.js: -------------------------------------------------------------------------------- 1 | 2 | // 4 cases 3 | // (a) keyValues is array of arrays, each sub array is key value 4 | // (b) keyValues is array and value is number then keys = array contents, value = index 5 | // (c) keyValues is array and value is not number, key = array contents, value = array contents 6 | // (d) keyValues is object then key->value 7 | export function convertToKeyValues(keyValues, valueIsNumber) { 8 | if (Array.isArray(keyValues)) { 9 | if (Array.isArray(keyValues[0])) { 10 | // (a) keyValues is array of arrays, each sub array is key value 11 | return keyValues; 12 | } else { 13 | if (valueIsNumber) { 14 | // (b) keyValues is array and value is number then keys = array contents, value = index 15 | return keyValues.map((v, ndx) => [v, ndx]); 16 | } else { 17 | // (c) keyValues is array and value is not number, key = array contents, value = array contents 18 | return keyValues.map(v => [v, v]); 19 | } 20 | } 21 | } else { 22 | // (d) 23 | return [...Object.entries(keyValues)]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/libs/keyboard.js: -------------------------------------------------------------------------------- 1 | function noop() { 2 | } 3 | 4 | const keyDirections = { 5 | ArrowLeft: [-1, 0], 6 | ArrowRight: [1, 0], 7 | ArrowUp: [0, -1], 8 | ArrowDown: [0, 1], 9 | }; 10 | 11 | // This probably needs to be global 12 | export function addKeyboardEvents(elem, {onDown = noop, onUp = noop}) { 13 | const keyDown = function (event) { 14 | const mult = event.shiftKey ? 10 : 1; 15 | const [dx, dy] = (keyDirections[event.key] || [0, 0]).map(v => v * mult); 16 | const fn = event.type === 'keydown' ? onDown : onUp; 17 | fn({ 18 | type: event.type.substring(3), 19 | dx, 20 | dy, 21 | event, 22 | }); 23 | }; 24 | 25 | elem.addEventListener('keydown', keyDown); 26 | elem.addEventListener('keyup', keyDown); 27 | 28 | return function () { 29 | elem.removeEventListener('keydown', keyDown); 30 | elem.removeEventListener('keyup', keyDown); 31 | }; 32 | } -------------------------------------------------------------------------------- /src/libs/monitor.js: -------------------------------------------------------------------------------- 1 | export function monitor(label, object, property, {interval = 200} = {}) { 2 | setInterval(() => { 3 | label.text(JSON.stringify(object[property], null, 2)); 4 | }, interval); 5 | } 6 | -------------------------------------------------------------------------------- /src/libs/resize-helpers.js: -------------------------------------------------------------------------------- 1 | export function onResize(elem, callback) { 2 | new ResizeObserver(() => { 3 | callback({rect: elem.getBoundingClientRect(), elem}); 4 | }).observe(elem); 5 | } 6 | 7 | export function onResizeSVGNoScale(elem, hAnchor, vAnchor, callback) { 8 | onResize(elem, ({rect}) => { 9 | const {width, height} = rect; 10 | elem.setAttribute('viewBox', `-${width * hAnchor} -${height * vAnchor} ${width} ${height}`); 11 | callback({elem, rect}); 12 | }); 13 | } 14 | 15 | export function onResizeCanvas(elem, callback) { 16 | onResize(elem, ({rect}) => { 17 | const {width, height} = rect; 18 | elem.width = width; 19 | elem.height = height; 20 | callback({elem, rect}); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/libs/svg.js: -------------------------------------------------------------------------------- 1 | import { assert } from '../libs/assert.js'; 2 | 3 | function getEllipsePointForAngle(cx, cy, rx, ry, phi, theta) { 4 | const m = Math.abs(rx) * Math.cos(theta); 5 | const n = Math.abs(ry) * Math.sin(theta); 6 | 7 | return [ 8 | cx + Math.cos(phi) * m - Math.sin(phi) * n, 9 | cy + Math.sin(phi) * m + Math.cos(phi) * n, 10 | ]; 11 | } 12 | 13 | function getEndpointParameters(cx, cy, rx, ry, phi, theta, dTheta) { 14 | const [x1, y1] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta); 15 | const [x2, y2] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta + dTheta); 16 | 17 | const fa = Math.abs(dTheta) > Math.PI ? 1 : 0; 18 | const fs = dTheta > 0 ? 1 : 0; 19 | 20 | return { x1, y1, x2, y2, fa, fs }; 21 | } 22 | 23 | export function arc(cx, cy, r, start, end) { 24 | assert(Math.abs(start - end) <= Math.PI * 2); 25 | assert(start >= -Math.PI && start <= Math.PI * 2); 26 | assert(start <= end); 27 | assert(end >= -Math.PI && end <= Math.PI * 4); 28 | 29 | const { x1, y1, x2, y2, fa, fs } = getEndpointParameters(cx, cy, r, r, 0, start, end - start); 30 | return Math.abs(Math.abs(start - end) - Math.PI * 2) > Number.EPSILON 31 | ? `M${cx} ${cy} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2} L${cx} ${cy}` 32 | : `M${x1} ${y1} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2}`; 33 | } 34 | -------------------------------------------------------------------------------- /src/libs/taskrunner.js: -------------------------------------------------------------------------------- 1 | import { removeArrayElem } from './utils.js'; 2 | 3 | const tasks = []; 4 | const tasksToRemove = new Set(); 5 | 6 | let requestId; 7 | let processing; 8 | 9 | function removeTasks() { 10 | if (!tasksToRemove.size) { 11 | return; 12 | } 13 | 14 | if (processing) { 15 | queueProcessing(); 16 | return; 17 | } 18 | 19 | tasksToRemove.forEach(task => { 20 | removeArrayElem(tasks, task); 21 | }); 22 | tasksToRemove.clear(); 23 | } 24 | 25 | function processTasks() { 26 | requestId = undefined; 27 | processing = true; 28 | for (const task of tasks) { 29 | if (!tasksToRemove.has(task)) { 30 | task(); 31 | } 32 | } 33 | processing = false; 34 | removeTasks(); 35 | queueProcessing(); 36 | } 37 | 38 | function queueProcessing() { 39 | if (!requestId && tasks.length) { 40 | requestId = requestAnimationFrame(processTasks); 41 | } 42 | } 43 | 44 | export function addTask(fn) { 45 | tasks.push(fn); 46 | queueProcessing(); 47 | } 48 | 49 | export function removeTask(fn) { 50 | tasksToRemove.set(fn); 51 | 52 | const ndx = tasks.indexOf(fn); 53 | if (ndx >= 0) { 54 | tasks.splice(ndx, 1); 55 | } 56 | } -------------------------------------------------------------------------------- /src/libs/touch.js: -------------------------------------------------------------------------------- 1 | function noop() { 2 | } 3 | 4 | export function computeRelativePosition(elem, event, start) { 5 | const rect = elem.getBoundingClientRect(); 6 | const x = event.clientX - rect.left; 7 | const y = event.clientY - rect.top; 8 | const nx = x / rect.width; 9 | const ny = y / rect.height; 10 | start = start || [x, y]; 11 | const dx = x - start[0]; 12 | const dy = y - start[1]; 13 | const ndx = dx / rect.width; 14 | const ndy = dy / rect.width; 15 | return {x, y, nx, ny, dx, dy, ndx, ndy}; 16 | } 17 | 18 | export function addTouchEvents(elem, {onDown = noop, onMove = noop, onUp = noop}) { 19 | let start; 20 | const pointerMove = function (event) { 21 | const e = { 22 | type: 'move', 23 | ...computeRelativePosition(elem, event, start), 24 | }; 25 | onMove(e); 26 | }; 27 | 28 | const pointerUp = function (event) { 29 | elem.releasePointerCapture(event.pointerId); 30 | elem.removeEventListener('pointermove', pointerMove); 31 | elem.removeEventListener('pointerup', pointerUp); 32 | 33 | document.body.style.backgroundColor = ''; 34 | 35 | onUp('up'); 36 | }; 37 | 38 | const pointerDown = function (event) { 39 | elem.addEventListener('pointermove', pointerMove); 40 | elem.addEventListener('pointerup', pointerUp); 41 | elem.setPointerCapture(event.pointerId); 42 | 43 | const rel = computeRelativePosition(elem, event); 44 | start = [rel.x, rel.y]; 45 | onDown({ 46 | type: 'down', 47 | ...rel, 48 | }); 49 | }; 50 | 51 | elem.addEventListener('pointerdown', pointerDown); 52 | 53 | return function () { 54 | elem.removeEventListener('pointerdown', pointerDown); 55 | }; 56 | } -------------------------------------------------------------------------------- /src/libs/utils.js: -------------------------------------------------------------------------------- 1 | export function removeArrayElem(array, value) { 2 | const ndx = array.indexOf(value); 3 | if (ndx) { 4 | array.splice(ndx, 1); 5 | } 6 | return array; 7 | } 8 | 9 | /** 10 | * Converts an camelCase or snake_case id to "camel case" or "snake case" 11 | * @param {string} id 12 | */ 13 | const underscoreRE = /_/g; 14 | const upperLowerRE = /([A-Z])([a-z])/g; 15 | export function idToLabel(id) { 16 | return id.replace(underscoreRE, ' ') 17 | .replace(upperLowerRE, (m, m1, m2) => `${m1.toLowerCase()} ${m2}`); 18 | } 19 | 20 | export function clamp(v, min, max) { 21 | return Math.max(min, Math.min(max, v)); 22 | } 23 | 24 | export const isTypedArray = typeof SharedArrayBuffer !== 'undefined' 25 | ? function isArrayBufferOrSharedArrayBuffer(a) { 26 | return a && a.buffer && (a.buffer instanceof ArrayBuffer || a.buffer instanceof SharedArrayBuffer); 27 | } 28 | : function isArrayBuffer(a) { 29 | return a && a.buffer && a.buffer instanceof ArrayBuffer; 30 | }; 31 | 32 | export const isArrayOrTypedArray = v => Array.isArray(v) || isTypedArray(v); 33 | 34 | // Yea, I know this should be `Math.round(v / step) * step 35 | // but try step = 0.1, newV = 19.95 36 | // 37 | // I get 38 | // Math.round(19.95 / 0.1) * 0.1 39 | // 19.900000000000002 40 | // vs 41 | // Math.round(19.95 / 0.1) / (1 / 0.1) 42 | // 19.9 43 | // 44 | export const stepify = (v, from, step) => Math.round(from(v) / step) / (1 / step); 45 | 46 | export const euclideanModulo = (v, n) => ((v % n) + n) % n; 47 | export const lerp = (a, b, t) => a + (b - a) * t; 48 | export function copyExistingProperties(dst, src) { 49 | for (const key in src) { 50 | if (key in dst) { 51 | dst[key] = src[key]; 52 | } 53 | } 54 | return dst; 55 | } 56 | 57 | export const mapRange = (v, inMin, inMax, outMin, outMax) => (v - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; 58 | 59 | export const makeRangeConverters = ({from, to}) => { 60 | return { 61 | to: v => mapRange(v, ...from, ...to), 62 | from: v => [true, mapRange(v, ...to, ...from)], 63 | }; 64 | }; 65 | 66 | export const makeRangeOptions = ({from, to, step}) => { 67 | return { 68 | min: to[0], 69 | max: to[1], 70 | ...(step && {step}), 71 | converters: makeRangeConverters({from, to}), 72 | }; 73 | }; 74 | 75 | // TODO: remove an use one in conversions. Move makeRangeConverters there? 76 | export const identity = { 77 | to: v => v, 78 | from: v => [true, v], 79 | }; 80 | export function makeMinMaxPair(gui, properties, minPropName, maxPropName, options) { 81 | const { converters: { from } = identity } = options; 82 | const { min, max } = options; 83 | const guiMinRange = options.minRange || 0; 84 | const valueMinRange = from(guiMinRange)[1]; 85 | const minGui = gui 86 | .add(properties, minPropName, { 87 | ...options, 88 | min, 89 | max: max - guiMinRange, 90 | }) 91 | .onChange(v => { 92 | maxGui.setValue(Math.min(max, Math.max(v + valueMinRange, properties[maxPropName]))); 93 | }); 94 | const maxGui = gui 95 | .add(properties, maxPropName, { 96 | ...options, 97 | min: min + guiMinRange, 98 | max, 99 | }) 100 | .onChange(v => { 101 | minGui.setValue(Math.max(min, Math.min(v - valueMinRange, properties[minPropName]))); 102 | }); 103 | return [ minGui, maxGui ]; 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/libs/wheel.js: -------------------------------------------------------------------------------- 1 | export function createWheelHelper() { 2 | let wheelAccum = 0; 3 | return function (e, step, wheelScale = 5) { 4 | wheelAccum -= e.deltaY * step / wheelScale; 5 | const wheelSteps = Math.floor(Math.abs(wheelAccum) / step) * Math.sign(wheelAccum); 6 | const delta = wheelSteps * step; 7 | wheelAccum -= delta; 8 | return delta; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/muigui.js: -------------------------------------------------------------------------------- 1 | import css from './styles/muigui.css.js'; 2 | import {createElem} from './libs/elem.js'; 3 | import {createController} from './controllers/create-controller.js'; 4 | import { 5 | mapRange, 6 | makeRangeConverters, 7 | makeRangeOptions, 8 | makeMinMaxPair, 9 | } from './libs/utils.js'; 10 | import { 11 | converters 12 | } from './libs/conversions.js'; 13 | import { 14 | hasAlpha, 15 | guessFormat, 16 | } from './libs/color-utils.js'; 17 | import Canvas from './controllers/Canvas.js'; 18 | import Color from './controllers/Color.js'; 19 | import Divider from './controllers/Divider.js'; 20 | import Folder from './controllers/Folder.js'; 21 | import Label from './controllers/Label.js'; 22 | import Controller from './controllers/Controller.js'; 23 | import ColorChooser from './controllers/ColorChooser.js'; 24 | 25 | import Column from './layout/Column.js'; 26 | import Frame from './layout/Frame.js'; 27 | import Grid from './layout/Grid.js'; 28 | import Row from './layout/Row.js'; 29 | 30 | export { 31 | Column, 32 | Frame, 33 | Grid, 34 | Row, 35 | }; 36 | 37 | function camelCaseToSnakeCase(id) { 38 | return id 39 | .replace(/(.)([A-Z][a-z]+)/g, '$1_$2') 40 | .replace(/([a-z0-9])([A-Z])/g, '$1_$2') 41 | .toLowerCase(); 42 | } 43 | 44 | function prepName(name) { 45 | return camelCaseToSnakeCase(name.toString()).replaceAll('_', ' '); 46 | } 47 | 48 | export class GUIFolder extends Folder { 49 | add(object, property, ...args) { 50 | const controller = object instanceof Controller 51 | ? object 52 | : createController(object, property, ...args).name(prepName(property)); 53 | return this.addController(controller); 54 | } 55 | addCanvas(name) { 56 | return this.addController(new Canvas(name)); 57 | } 58 | addColor(object, property, options = {}) { 59 | const value = object[property]; 60 | if (hasAlpha(options.format || guessFormat(value))) { 61 | return this 62 | .addController(new ColorChooser(object, property, options)) 63 | .name(prepName(property)); 64 | } else { 65 | return this 66 | .addController(new Color(object, property, options)) 67 | .name(prepName(property)); 68 | } 69 | } 70 | addDivider() { 71 | return this.addController(new Divider()); 72 | } 73 | addFolder(name) { 74 | return this.addController(new GUIFolder(name)); 75 | } 76 | addLabel(text) { 77 | return this.addController(new Label(text)); 78 | } 79 | addButton(name, fn) { 80 | const o = {fn}; 81 | return this.add(o, 'fn').name(prepName(name)); 82 | } 83 | } 84 | 85 | class MuiguiElement extends HTMLElement { 86 | constructor() { 87 | super(); 88 | this.shadow = this.attachShadow({mode: 'open'}); 89 | } 90 | } 91 | 92 | customElements.define('muigui-element', MuiguiElement); 93 | 94 | const baseStyleSheet = new CSSStyleSheet(); 95 | //baseStyleSheet.replaceSync(css.default); 96 | const userStyleSheet = new CSSStyleSheet(); 97 | 98 | function makeStyleSheetUpdater(styleSheet) { 99 | let newCss; 100 | let newCssPromise; 101 | 102 | function updateStyle() { 103 | if (newCss && !newCssPromise) { 104 | const s = newCss; 105 | newCss = undefined; 106 | newCssPromise = styleSheet.replace(s).then(() => { 107 | newCssPromise = undefined; 108 | updateStyle(); 109 | }); 110 | } 111 | } 112 | 113 | return function updateStyleSheet(css) { 114 | newCss = css; 115 | updateStyle(); 116 | }; 117 | } 118 | 119 | const updateBaseStyle = makeStyleSheetUpdater(baseStyleSheet); 120 | const updateUserStyle = makeStyleSheetUpdater(userStyleSheet); 121 | 122 | function getTheme(name) { 123 | const { include, css: cssStr } = css.themes[name]; 124 | return `${include.map(m => css[m]).join('\n')} : css.default}\n${cssStr || ''}`; 125 | } 126 | 127 | export class GUI extends GUIFolder { 128 | static converters = converters; 129 | static mapRange = mapRange; 130 | static makeRangeConverters = makeRangeConverters; 131 | static makeRangeOptions = makeRangeOptions; 132 | static makeMinMaxPair = makeMinMaxPair; 133 | #localStyleSheet = new CSSStyleSheet(); 134 | 135 | constructor(options = {}) { 136 | super('Controls', 'muigui-root'); 137 | if (options instanceof HTMLElement) { 138 | options = {parent: options}; 139 | } 140 | const { 141 | autoPlace = true, 142 | width, 143 | title = 'Controls', 144 | } = options; 145 | let { 146 | parent, 147 | } = options; 148 | 149 | if (width) { 150 | this.domElement.style.width = /^\d+$/.test(width) ? `${width}px` : width; 151 | } 152 | if (parent === undefined && autoPlace) { 153 | parent = document.body; 154 | this.domElement.classList.add('muigui-auto-place'); 155 | } 156 | if (parent) { 157 | const muiguiElement = createElem('muigui-element'); 158 | muiguiElement.shadowRoot.adoptedStyleSheets = [this.#localStyleSheet, baseStyleSheet, userStyleSheet]; 159 | muiguiElement.shadow.appendChild(this.domElement); 160 | parent.appendChild(muiguiElement); 161 | } 162 | if (title) { 163 | this.title(title); 164 | } 165 | this.#localStyleSheet.replaceSync(css.default); 166 | this.domElement.classList.add('muigui', 'muigui-colors'); 167 | } 168 | setStyle(css) { 169 | this.#localStyleSheet.replace(css); 170 | } 171 | static setBaseStyles(css) { 172 | updateBaseStyle(css); 173 | } 174 | static getBaseStyleSheet() { 175 | return baseStyleSheet; 176 | } 177 | static setUserStyles(css) { 178 | updateUserStyle(css); 179 | } 180 | static getUserStyleSheet() { 181 | return userStyleSheet; 182 | } 183 | setTheme(name) { 184 | this.setStyle(getTheme(name)); 185 | } 186 | static setTheme(name) { 187 | GUI.setBaseStyles(getTheme(name)); 188 | } 189 | } 190 | 191 | export default GUI; 192 | -------------------------------------------------------------------------------- /src/umd.js: -------------------------------------------------------------------------------- 1 | import GUI from './muigui.js'; 2 | 3 | import ColorChooser from './controllers/ColorChooser.js'; 4 | import Direction from './controllers/Direction.js'; 5 | import RadioGrid from './controllers/RadioGrid.js'; 6 | import Range from './controllers/Range.js'; 7 | import Select from './controllers/Select.js'; 8 | import Slider from './controllers/Slider.js'; 9 | import TextNumber from './controllers/TextNumber.js'; 10 | import Vec2 from './controllers/Vec2.js'; 11 | 12 | GUI.ColorChooser = ColorChooser; 13 | GUI.Direction = Direction; 14 | GUI.RadioGrid = RadioGrid; 15 | GUI.Range = Range; 16 | GUI.Select = Select; 17 | GUI.Slider = Slider; 18 | GUI.TextNumber = TextNumber; 19 | GUI.Vec2 = Vec2; 20 | 21 | export default GUI; -------------------------------------------------------------------------------- /src/views/CheckboxView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import EditView from './EditView.js'; 3 | 4 | export default class CheckboxView extends EditView { 5 | #checkboxElem; 6 | constructor(setter, id) { 7 | const checkboxElem = createElem('input', { 8 | type: 'checkbox', 9 | id, 10 | onInput: () => { 11 | setter.setValue(checkboxElem.checked); 12 | }, 13 | onChange: () => { 14 | setter.setFinalValue(checkboxElem.checked); 15 | }, 16 | }); 17 | super(createElem('label', {}, [checkboxElem])); 18 | this.#checkboxElem = checkboxElem; 19 | } 20 | updateDisplay(v) { 21 | this.#checkboxElem.checked = v; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/ColorChooserView.js: -------------------------------------------------------------------------------- 1 | import { createElem, getNewId } from '../libs/elem.js'; 2 | import { addTouchEvents } from '../libs/touch.js'; 3 | import { identity } from '../libs/conversions.js'; 4 | import { clamp } from '../libs/utils.js'; 5 | import EditView from './EditView.js'; 6 | import { 7 | hexToFloatRGB, 8 | hexToFloatRGBA, 9 | hsv01ToRGBFloat, 10 | hsva01ToRGBAFloat, 11 | rgbFloatToHSV01, 12 | rgbaFloatToHSVA01, 13 | floatRGBToHex, 14 | floatRGBAToHex, 15 | rgbaFloatToHsla01, 16 | } from '../libs/color-utils.js'; 17 | import { copyExistingProperties } from '../libs/utils.js'; 18 | 19 | const svg = ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | `; 60 | 61 | function connectFillTargets(elem) { 62 | elem.querySelectorAll('[data-src]').forEach(srcElem => { 63 | const id = getNewId(); 64 | srcElem.id = id; 65 | elem.querySelectorAll(`[data-target=${srcElem.dataset.src}]`).forEach(targetElem => { 66 | targetElem.setAttribute('fill', `url(#${id})`); 67 | }); 68 | }); 69 | return elem; 70 | } 71 | 72 | // Was originally going to make alpha an option. Issue is 73 | // hard coded conversions? 74 | export default class ColorChooserView extends EditView { 75 | #to; 76 | #from; 77 | #satLevelElem; 78 | #circleElem; 79 | #hueUIElem; 80 | #hueElem; 81 | #hueCursorElem; 82 | #alphaUIElem; 83 | #alphaElem; 84 | #alphaCursorElem; 85 | #hsva; 86 | #skipHueUpdate; 87 | #skipSatLevelUpdate; 88 | #skipAlphaUpdate; 89 | #options = { 90 | converters: identity, 91 | alpha: false, 92 | }; 93 | #convertInternalToHex; 94 | #convertHexToInternal; 95 | 96 | constructor(setter, options) { 97 | super(createElem('div', { 98 | innerHTML: svg, 99 | className: 'muigui-no-scroll', 100 | })); 101 | this.#satLevelElem = this.domElement.children[0]; 102 | this.#hueUIElem = this.domElement.children[1]; 103 | this.#alphaUIElem = this.domElement.children[2]; 104 | connectFillTargets(this.#satLevelElem); 105 | connectFillTargets(this.#hueUIElem); 106 | connectFillTargets(this.#alphaUIElem); 107 | this.#circleElem = this.$('.muigui-color-chooser-circle'); 108 | this.#hueElem = this.$('[data-src=muigui-color-chooser-hue]'); 109 | this.#hueCursorElem = this.$('.muigui-color-chooser-hue-cursor'); 110 | this.#alphaElem = this.$('[data-src=muigui-color-chooser-alpha]'); 111 | this.#alphaCursorElem = this.$('.muigui-color-chooser-alpha-cursor'); 112 | 113 | const handleSatLevelChange = (e) => { 114 | const s = clamp(e.nx, 0, 1); 115 | const v = clamp(e.ny, 0, 1); 116 | this.#hsva[1] = s; 117 | this.#hsva[2] = (1 - v); 118 | this.#skipHueUpdate = true; 119 | this.#skipAlphaUpdate = true; 120 | const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); 121 | if (valid) { 122 | setter.setValue(newV); 123 | } 124 | }; 125 | 126 | const handleHueChange = (e) => { 127 | const h = clamp(e.nx, 0, 1); 128 | this.#hsva[0] = h; 129 | this.#skipSatLevelUpdate = true; 130 | this.#skipAlphaUpdate = true; 131 | const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); 132 | if (valid) { 133 | setter.setValue(newV); 134 | } 135 | }; 136 | 137 | const handleAlphaChange = (e) => { 138 | const a = clamp(e.nx, 0, 1); 139 | this.#hsva[3] = a; 140 | this.#skipHueUpdate = true; 141 | this.#skipSatLevelUpdate = true; 142 | const [valid, newV] = this.#from(this.#convertInternalToHex(this.#hsva)); 143 | if (valid) { 144 | setter.setValue(newV); 145 | } 146 | }; 147 | 148 | addTouchEvents(this.#satLevelElem, { 149 | onDown: handleSatLevelChange, 150 | onMove: handleSatLevelChange, 151 | }); 152 | addTouchEvents(this.#hueUIElem, { 153 | onDown: handleHueChange, 154 | onMove: handleHueChange, 155 | }); 156 | addTouchEvents(this.#alphaUIElem, { 157 | onDown: handleAlphaChange, 158 | onMove: handleAlphaChange, 159 | }); 160 | this.setOptions(options); 161 | } 162 | updateDisplay(newV) { 163 | if (!this.#hsva) { 164 | this.#hsva = this.#convertHexToInternal(this.#to(newV)); 165 | } 166 | { 167 | const [h, s, v, a = 1] = this.#convertHexToInternal(this.#to(newV)); 168 | // Don't copy the hue if it was un-computable. 169 | if (!this.#skipHueUpdate) { 170 | this.#hsva[0] = s > 0.001 && v > 0.001 ? h : this.#hsva[0]; 171 | } 172 | if (!this.#skipSatLevelUpdate) { 173 | this.#hsva[1] = s; 174 | this.#hsva[2] = v; 175 | } 176 | if (!this.#skipAlphaUpdate) { 177 | this.#hsva[3] = a; 178 | } 179 | } 180 | { 181 | const [h, s, v, a] = this.#hsva; 182 | const [hue, sat, lum] = rgbaFloatToHsla01(hsva01ToRGBAFloat(this.#hsva)); 183 | 184 | if (!this.#skipHueUpdate) { 185 | this.#hueCursorElem.setAttribute('transform', `translate(${h * 64}, 0)`); 186 | } 187 | this.#hueElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} 0% 100% / ${a})`); 188 | this.#hueElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} 100% 50% / ${a})`); 189 | if (!this.#skipAlphaUpdate) { 190 | this.#alphaCursorElem.setAttribute('transform', `translate(${a * 64}, 0)`); 191 | } 192 | this.#alphaElem.children[0].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 0)`); 193 | this.#alphaElem.children[1].setAttribute('stop-color', `hsl(${hue * 360} ${sat * 100}% ${lum * 100}% / 1)`); 194 | 195 | if (!this.#skipSatLevelUpdate) { 196 | this.#circleElem.setAttribute('cx', `${s * 64}`); 197 | this.#circleElem.setAttribute('cy', `${(1 - v) * 48}`); 198 | } 199 | } 200 | this.#skipHueUpdate = false; 201 | this.#skipSatLevelUpdate = false; 202 | this.#skipAlphaUpdate = false; 203 | } 204 | setOptions(options) { 205 | copyExistingProperties(this.#options, options); 206 | const {converters: {to, from}, alpha} = this.#options; 207 | this.#alphaUIElem.style.display = alpha ? '' : 'none'; 208 | this.#convertInternalToHex = alpha 209 | ? v => floatRGBAToHex(hsva01ToRGBAFloat(v)) 210 | : v => floatRGBToHex(hsv01ToRGBFloat(v)); 211 | this.#convertHexToInternal = alpha 212 | ? v => rgbaFloatToHSVA01(hexToFloatRGBA(v)) 213 | : v => rgbFloatToHSV01(hexToFloatRGB(v)); 214 | this.#to = to; 215 | this.#from = from; 216 | return this; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/views/ColorView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { identity } from '../libs/conversions.js'; 3 | import EditView from './EditView.js'; 4 | import { copyExistingProperties } from '../libs/utils.js'; 5 | 6 | export default class ColorView extends EditView { 7 | #to; 8 | #from; 9 | #colorElem; 10 | #skipUpdate; 11 | #options = { 12 | converters: identity, 13 | }; 14 | 15 | constructor(setter, options) { 16 | const colorElem = createElem('input', { 17 | type: 'color', 18 | onInput: () => { 19 | const [valid, newV] = this.#from(colorElem.value); 20 | if (valid) { 21 | this.#skipUpdate = true; 22 | setter.setValue(newV); 23 | } 24 | }, 25 | onChange: () => { 26 | const [valid, newV] = this.#from(colorElem.value); 27 | if (valid) { 28 | this.#skipUpdate = true; 29 | setter.setFinalValue(newV); 30 | } 31 | }, 32 | }); 33 | super(createElem('div', {}, [colorElem])); 34 | this.setOptions(options); 35 | this.#colorElem = colorElem; 36 | } 37 | updateDisplay(v) { 38 | if (!this.#skipUpdate) { 39 | this.#colorElem.value = this.#to(v); 40 | } 41 | this.#skipUpdate = false; 42 | } 43 | setOptions(options) { 44 | copyExistingProperties(this.#options, options); 45 | const {converters: {to, from}} = this.#options; 46 | this.#to = to; 47 | this.#from = from; 48 | return this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/views/DirectionView.js: -------------------------------------------------------------------------------- 1 | import { identity } from '../libs/conversions.js'; 2 | import { createElem } from '../libs/elem.js'; 3 | import { addKeyboardEvents } from '../libs/keyboard.js'; 4 | import { arc } from '../libs/svg.js'; 5 | import { addTouchEvents } from '../libs/touch.js'; 6 | import { createWheelHelper } from '../libs/wheel.js'; 7 | import { clamp, copyExistingProperties, euclideanModulo, lerp, stepify } from '../libs/utils.js'; 8 | import EditView from './EditView.js'; 9 | 10 | const svg = ` 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | `; 21 | 22 | const twoPiMod = v => euclideanModulo(v + Math.PI, Math.PI * 2) - Math.PI; 23 | 24 | export default class DirectionView extends EditView { 25 | #arrowElem; 26 | #rangeElem; 27 | #lastV; 28 | #wrap; 29 | #options = { 30 | step: 1, 31 | min: -180, 32 | max: 180, 33 | 34 | /* 35 | -------- 36 | / -π/2 \ 37 | / | \ 38 | |<- -π * | 39 | | * 0 ->| zero is down the positive X axis 40 | |<- +π * | 41 | \ | / 42 | \ π/2 / 43 | -------- 44 | */ 45 | dirMin: -Math.PI, 46 | dirMax: Math.PI, 47 | //dirMin: Math.PI * 0.5, 48 | //dirMax: Math.PI * 2.5, 49 | //dirMin: -Math.PI * 0.75, // test 10:30 to 7:30 50 | //dirMax: Math.PI * 0.75, 51 | //dirMin: Math.PI * 0.75, // test 7:30 to 10:30 52 | //dirMax: -Math.PI * 0.75, 53 | //dirMin: -Math.PI * 0.75, // test 10:30 to 1:30 54 | //dirMax: -Math.PI * 0.25, 55 | //dirMin: Math.PI * 0.25, // test 4:30 to 7:30 56 | //dirMax: Math.PI * 0.75, 57 | //dirMin: Math.PI * 0.75, // test 4:30 to 7:30 58 | //dirMax: Math.PI * 0.25, 59 | wrap: undefined, 60 | converters: identity, 61 | }; 62 | 63 | constructor(setter, options = {}) { 64 | const wheelHelper = createWheelHelper(); 65 | super(createElem('div', { 66 | className: 'muigui-direction muigui-no-scroll', 67 | innerHTML: svg, 68 | onWheel: e => { 69 | e.preventDefault(); 70 | const {min, max, step} = this.#options; 71 | const delta = wheelHelper(e, step); 72 | let tempV = this.#lastV + delta; 73 | if (this.#wrap) { 74 | tempV = euclideanModulo(tempV - min, max - min) + min; 75 | } 76 | const newV = clamp(stepify(tempV, v => v, step), min, max); 77 | setter.setValue(newV); 78 | }, 79 | })); 80 | const handleTouch = (e) => { 81 | const {min, max, step, dirMin, dirMax} = this.#options; 82 | const nx = e.nx * 2 - 1; 83 | const ny = e.ny * 2 - 1; 84 | const a = Math.atan2(ny, nx); 85 | 86 | const center = (dirMin + dirMax) / 2; 87 | 88 | const centeredAngle = twoPiMod(a - center); 89 | const centeredStart = twoPiMod(dirMin - center); 90 | const diff = dirMax - dirMin; 91 | 92 | const n = clamp((centeredAngle - centeredStart) / (diff), 0, 1); 93 | const newV = stepify(min + (max - min) * n, v => v, step); 94 | setter.setValue(newV); 95 | }; 96 | addTouchEvents(this.domElement, { 97 | onDown: handleTouch, 98 | onMove: handleTouch, 99 | }); 100 | addKeyboardEvents(this.domElement, { 101 | onDown: (e) => { 102 | const {min, max, step} = this.#options; 103 | const newV = clamp(stepify(this.#lastV + e.dx * step, v => v, step), min, max); 104 | setter.setValue(newV); 105 | }, 106 | }); 107 | this.#arrowElem = this.$('#muigui-arrow'); 108 | this.#rangeElem = this.$('#muigui-range'); 109 | this.setOptions(options); 110 | } 111 | updateDisplay(v) { 112 | this.#lastV = v; 113 | const {min, max} = this.#options; 114 | const n = (v - min) / (max - min); 115 | const angle = lerp(this.#options.dirMin, this.#options.dirMax, n); 116 | this.#arrowElem.style.transform = `rotate(${angle}rad)`; 117 | } 118 | setOptions(options) { 119 | copyExistingProperties(this.#options, options); 120 | const {dirMin, dirMax, wrap} = this.#options; 121 | this.#wrap = wrap !== undefined 122 | ? wrap 123 | : Math.abs(dirMin - dirMax) >= Math.PI * 2 - Number.EPSILON; 124 | const [min, max] = dirMin < dirMax ? [dirMin, dirMax] : [dirMax , dirMin]; 125 | this.#rangeElem.setAttribute('d', arc(0, 0, 28.87, min, max)); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/views/EditView.js: -------------------------------------------------------------------------------- 1 | import { isTypedArray } from '../libs/utils.js'; 2 | import View from './View.js'; 3 | 4 | function arraysEqual(a, b) { 5 | if (a.length !== b.length) { 6 | return false; 7 | } 8 | for (let i = 0; i < a.length; ++i) { 9 | if (a[i] !== b[i]) { 10 | return false; 11 | } 12 | } 13 | return true; 14 | } 15 | 16 | function copyArrayElementsFromTo(src, dst) { 17 | dst.length = src.length; 18 | for (let i = 0; i < src.length; ++i) { 19 | dst[i] = src[i]; 20 | } 21 | } 22 | 23 | export default class EditView extends View { 24 | #oldV; 25 | #updateCheck; 26 | 27 | #checkArrayNeedsUpdate(newV) { 28 | // It's an array, we need to compare all elements 29 | // Example, vec2, [r,g,b], ... 30 | const needUpdate = !arraysEqual(newV, this.#oldV); 31 | if (needUpdate) { 32 | copyArrayElementsFromTo(newV, this.#oldV); 33 | } 34 | return needUpdate; 35 | } 36 | 37 | #checkTypedArrayNeedsUpdate() { 38 | let once = true; 39 | return function checkTypedArrayNeedsUpdateImpl(newV) { 40 | // It's a typedarray, we need to compare all elements 41 | // Example: Float32Array([r, g, b]) 42 | let needUpdate = once; 43 | once = false; 44 | if (!needUpdate) { 45 | needUpdate = !arraysEqual(newV, this.#oldV); 46 | } 47 | return needUpdate; 48 | }; 49 | } 50 | 51 | #checkObjectNeedsUpdate(newV) { 52 | let needUpdate = false; 53 | for (const key in newV) { 54 | if (newV[key] !== this.#oldV[key]) { 55 | needUpdate = true; 56 | this.#oldV[key] = newV[key]; 57 | } 58 | } 59 | return needUpdate; 60 | } 61 | 62 | #checkValueNeedsUpdate(newV) { 63 | const needUpdate = newV !== this.#oldV; 64 | this.#oldV = newV; 65 | return needUpdate; 66 | } 67 | 68 | #getUpdateCheckForType(newV) { 69 | if (Array.isArray(newV)) { 70 | this.#oldV = []; 71 | return this.#checkArrayNeedsUpdate.bind(this); 72 | } else if (isTypedArray(newV)) { 73 | this.#oldV = new newV.constructor(newV); 74 | return this.#checkTypedArrayNeedsUpdate(this); 75 | } else if (typeof newV === 'object') { 76 | this.#oldV = {}; 77 | return this.#checkObjectNeedsUpdate.bind(this); 78 | } else { 79 | return this.#checkValueNeedsUpdate.bind(this); 80 | } 81 | } 82 | 83 | // The point of this is updating DOM elements 84 | // is slow but if we've called `listen` then 85 | // every frame we're going to try to update 86 | // things with the current value so if nothing 87 | // has changed then skip it. 88 | updateDisplayIfNeeded(newV, ignoreCache) { 89 | this.#updateCheck = this.#updateCheck || this.#getUpdateCheckForType(newV); 90 | // Note: We call #updateCheck first because it updates 91 | // the cache 92 | if (this.#updateCheck(newV) || ignoreCache) { 93 | this.updateDisplay(newV); 94 | } 95 | } 96 | setOptions(/*options*/) { 97 | // override this 98 | return this; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/views/ElementView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import View from './View.js'; 3 | 4 | export default class ElementView extends View { 5 | constructor(tag, className) { 6 | super(createElem(tag, {className})); 7 | } 8 | } -------------------------------------------------------------------------------- /src/views/GridView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import View from './View.js'; 3 | 4 | export default class GridView extends View { 5 | // FIX: should this be 'options'? 6 | constructor(cols) { 7 | super(createElem('div', { 8 | className: 'muigui-grid', 9 | })); 10 | this.cols(cols); 11 | } 12 | cols(cols) { 13 | this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; 14 | } 15 | } -------------------------------------------------------------------------------- /src/views/NumberView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { strToNumber } from '../libs/conversions.js'; 3 | import { createWheelHelper } from '../libs/wheel.js'; 4 | import { clamp, copyExistingProperties, stepify } from '../libs/utils.js'; 5 | import EditView from './EditView.js'; 6 | 7 | export default class NumberView extends EditView { 8 | #to; 9 | #from; 10 | #step; 11 | #skipUpdate; 12 | #options = { 13 | step: 0.01, 14 | converters: strToNumber, 15 | min: Number.NEGATIVE_INFINITY, 16 | max: Number.POSITIVE_INFINITY, 17 | }; 18 | 19 | constructor(setter, options) { 20 | const setValue = setter.setValue.bind(setter); 21 | const setFinalValue = setter.setFinalValue.bind(setter); 22 | const wheelHelper = createWheelHelper(); 23 | super(createElem('input', { 24 | type: 'number', 25 | onInput: () => { 26 | this.#handleInput(setValue, true); 27 | }, 28 | onChange: () => { 29 | this.#handleInput(setFinalValue, false); 30 | }, 31 | onWheel: e => { 32 | e.preventDefault(); 33 | const {min, max, step} = this.#options; 34 | const delta = wheelHelper(e, step); 35 | const v = parseFloat(this.domElement.value); 36 | const newV = clamp(stepify(v + delta, v => v, step), min, max); 37 | const [valid, outV] = this.#from(newV); 38 | if (valid) { 39 | setter.setValue(outV); 40 | } 41 | }, 42 | })); 43 | this.setOptions(options); 44 | } 45 | #handleInput(setFn, skipUpdate) { 46 | const v = parseFloat(this.domElement.value); 47 | const [valid, newV] = this.#from(v); 48 | let inRange; 49 | if (valid && !Number.isNaN(v)) { 50 | const {min, max} = this.#options; 51 | inRange = newV >= min && newV <= max; 52 | this.#skipUpdate = skipUpdate; 53 | setFn(clamp(newV, min, max)); 54 | } 55 | this.domElement.classList.toggle('muigui-invalid-value', !valid || !inRange); 56 | } 57 | updateDisplay(v) { 58 | if (!this.#skipUpdate) { 59 | this.domElement.value = stepify(v, this.#to, this.#step); 60 | } 61 | this.#skipUpdate = false; 62 | } 63 | setOptions(options) { 64 | copyExistingProperties(this.#options, options); 65 | const { 66 | step, 67 | converters: {to, from}, 68 | } = this.#options; 69 | this.#to = to; 70 | this.#from = from; 71 | this.#step = step; 72 | return this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/views/RadioGridView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { makeId } from '../libs/ids.js'; 3 | import EditView from './EditView.js'; 4 | 5 | export default class RadioGridView extends EditView { 6 | #values; 7 | 8 | constructor(setter, keyValues, cols = 3) { 9 | const values = []; 10 | const name = makeId(); 11 | super(createElem('div', {}, keyValues.map(([key, value], ndx) => { 12 | values.push(value); 13 | return createElem('label', {}, [ 14 | createElem('input', { 15 | type: 'radio', 16 | name, 17 | value: ndx, 18 | onChange: function () { 19 | if (this.checked) { 20 | setter.setFinalValue(that.#values[this.value]); 21 | } 22 | }, 23 | }), 24 | createElem('button', { 25 | type: 'button', 26 | textContent: key, 27 | onClick: function () { 28 | this.previousElementSibling.click(); 29 | }, 30 | }), 31 | ]); 32 | }))); 33 | // eslint-disable-next-line @typescript-eslint/no-this-alias 34 | const that = this; 35 | this.#values = values; 36 | this.cols(cols); 37 | } 38 | updateDisplay(v) { 39 | const ndx = this.#values.indexOf(v); 40 | for (let i = 0; i < this.domElement.children.length; ++i) { 41 | this.domElement.children[i].children[0].checked = i === ndx; 42 | } 43 | } 44 | cols(cols) { 45 | this.domElement.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/views/RangeView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { identity } from '../libs/conversions.js'; 3 | import { clamp, copyExistingProperties, stepify } from '../libs/utils.js'; 4 | import { createWheelHelper } from '../libs/wheel.js'; 5 | import EditView from './EditView.js'; 6 | 7 | export default class RangeView extends EditView { 8 | #to; 9 | #from; 10 | #step; 11 | #skipUpdate; 12 | #options = { 13 | step: 0.01, 14 | min: 0, 15 | max: 1, 16 | converters: identity, 17 | }; 18 | 19 | constructor(setter, options) { 20 | const wheelHelper = createWheelHelper(); 21 | super(createElem('input', { 22 | type: 'range', 23 | onInput: () => { 24 | this.#skipUpdate = true; 25 | const {min, max, step} = this.#options; 26 | const v = parseFloat(this.domElement.value); 27 | const newV = clamp(stepify(v, v => v, step), min, max); 28 | const [valid, validV] = this.#from(newV); 29 | if (valid) { 30 | setter.setValue(validV); 31 | } 32 | }, 33 | onChange: () => { 34 | this.#skipUpdate = true; 35 | const {min, max, step} = this.#options; 36 | const v = parseFloat(this.domElement.value); 37 | const newV = clamp(stepify(v, v => v, step), min, max); 38 | const [valid, validV] = this.#from(newV); 39 | if (valid) { 40 | setter.setFinalValue(validV); 41 | } 42 | }, 43 | onWheel: e => { 44 | e.preventDefault(); 45 | const [valid, v] = this.#from(parseFloat(this.domElement.value)); 46 | if (!valid) { 47 | return; 48 | } 49 | const {min, max, step} = this.#options; 50 | const delta = wheelHelper(e, step); 51 | const newV = clamp(stepify(v + delta, v => v, step), min, max); 52 | setter.setValue(newV); 53 | }, 54 | })); 55 | this.setOptions(options); 56 | } 57 | updateDisplay(v) { 58 | if (!this.#skipUpdate) { 59 | this.domElement.value = stepify(v, this.#to, this.#step); 60 | } 61 | this.#skipUpdate = false; 62 | } 63 | setOptions(options) { 64 | copyExistingProperties(this.#options, options); 65 | const { 66 | step, 67 | min, 68 | max, 69 | converters: {to, from}, 70 | } = this.#options; 71 | this.#to = to; 72 | this.#from = from; 73 | this.#step = step; 74 | this.domElement.step = step; 75 | this.domElement.min = min; 76 | this.domElement.max = max; 77 | return this; 78 | } 79 | } -------------------------------------------------------------------------------- /src/views/SelectView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import EditView from './EditView.js'; 3 | 4 | export default class SelectView extends EditView { 5 | #values; 6 | 7 | constructor(setter, keyValues) { 8 | const values = []; 9 | super(createElem('select', { 10 | onChange: () => { 11 | setter.setFinalValue(this.#values[this.domElement.selectedIndex]); 12 | }, 13 | }, keyValues.map(([key, value]) => { 14 | values.push(value); 15 | return createElem('option', {textContent: key}); 16 | }))); 17 | this.#values = values; 18 | } 19 | updateDisplay(v) { 20 | const ndx = this.#values.indexOf(v); 21 | this.domElement.selectedIndex = ndx; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/SliderView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { addKeyboardEvents } from '../libs/keyboard.js'; 3 | import { addTouchEvents } from '../libs/touch.js'; 4 | import { createWheelHelper } from '../libs/wheel.js'; 5 | import { onResizeSVGNoScale } from '../libs/resize-helpers.js'; 6 | import { clamp, copyExistingProperties, stepify } from '../libs/utils.js'; 7 | import EditView from './EditView.js'; 8 | 9 | const svg = ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | `; 39 | 40 | function createSVGTicks(start, end, step, min, max, height) { 41 | const p = []; 42 | if (start < min) { 43 | start += stepify(min - start, v => v, step); 44 | } 45 | end = Math.min(end, max); 46 | for (let i = start; i <= end; i += step) { 47 | p.push(`M${i} 0 l0 ${height}`); 48 | } 49 | return p.join(' '); 50 | } 51 | 52 | function createSVGNumbers(start, end, unitSize, unit, minusSize, min, max, labelFn) { 53 | const texts = []; 54 | if (start < min) { 55 | start += stepify(min - start, v => v, unitSize); 56 | } 57 | end = Math.min(end, max); 58 | const digits = Math.max(0, -Math.log10(unit)); 59 | const f = v => labelFn(v.toFixed(digits)); 60 | for (let i = start; i <= end; i += unitSize) { 61 | texts.push(`${f(i / unitSize * unit)}`); 62 | } 63 | return texts.join('\n'); 64 | } 65 | 66 | function computeSizeOfMinus(elem) { 67 | const oldHTML = elem.innerHTML; 68 | elem.innerHTML = '- '; 69 | const text = elem.querySelector('text'); 70 | const size = text.getComputedTextLength(); 71 | elem.innerHTML = oldHTML; 72 | return size; 73 | } 74 | 75 | export default class SliderView extends EditView { 76 | #svgElem; 77 | #originElem; 78 | #ticksElem; 79 | #thicksElem; 80 | #numbersElem; 81 | #leftGradElem; 82 | #rightGradElem; 83 | #width; 84 | #height; 85 | #lastV; 86 | #minusSize; 87 | #options = { 88 | min: -100, 89 | max: 100, 90 | step: 1, 91 | unit: 10, 92 | unitSize: 10, 93 | ticksPerUnit: 5, 94 | labelFn: v => v, 95 | tickHeight: 1, 96 | limits: true, 97 | thicksColor: undefined, 98 | orientation: undefined, 99 | }; 100 | 101 | constructor(setter, options) { 102 | const wheelHelper = createWheelHelper(); 103 | super(createElem('div', { 104 | innerHTML: svg, 105 | className: 'muigui-no-v-scroll', 106 | onWheel: e => { 107 | e.preventDefault(); 108 | const {min, max, step} = this.#options; 109 | const delta = wheelHelper(e, step); 110 | const newV = clamp(stepify(this.#lastV + delta, v => v, step), min, max); 111 | setter.setValue(newV); 112 | }, 113 | })); 114 | this.#svgElem = this.$('svg'); 115 | this.#originElem = this.$('#muigui-origin'); 116 | this.#ticksElem = this.$('#muigui-ticks'); 117 | this.#thicksElem = this.$('#muigui-thicks'); 118 | this.#numbersElem = this.$('#muigui-numbers'); 119 | this.#leftGradElem = this.$('#muigui-left-grad'); 120 | this.#rightGradElem = this.$('#muigui-right-grad'); 121 | this.setOptions(options); 122 | let startV; 123 | addTouchEvents(this.domElement, { 124 | onDown: () => { 125 | startV = this.#lastV; 126 | }, 127 | onMove: (e) => { 128 | const {min, max, unitSize, unit, step} = this.#options; 129 | const newV = clamp(stepify(startV - e.dx / unitSize * unit, v => v, step), min, max); 130 | setter.setValue(newV); 131 | }, 132 | }); 133 | addKeyboardEvents(this.domElement, { 134 | onDown: (e) => { 135 | const {min, max, step} = this.#options; 136 | const newV = clamp(stepify(this.#lastV + e.dx * step, v => v, step), min, max); 137 | setter.setValue(newV); 138 | }, 139 | }); 140 | onResizeSVGNoScale(this.#svgElem, 0.5, 0, ({rect: {width}}) => { 141 | this.#leftGradElem.setAttribute('x', -width / 2); 142 | this.#rightGradElem.setAttribute('x', width / 2 - 20); 143 | this.#minusSize = computeSizeOfMinus(this.#numbersElem); 144 | this.#width = width; 145 | this.#updateSlider(); 146 | }); 147 | } 148 | // |--------V--------| 149 | // . . | . . . | . . . | 150 | // 151 | #updateSlider() { 152 | // There's no size if ResizeObserver has not fired yet. 153 | if (!this.#width || this.#lastV === undefined) { 154 | return; 155 | } 156 | const { 157 | labelFn, 158 | limits, 159 | min, 160 | max, 161 | orientation, 162 | tickHeight, 163 | ticksPerUnit, 164 | unit, 165 | unitSize, 166 | thicksColor, 167 | } = this.#options; 168 | const unitsAcross = Math.ceil(this.#width / unitSize); 169 | const center = this.#lastV; 170 | const centerUnitSpace = center / unit; 171 | const startUnitSpace = Math.round(centerUnitSpace - unitsAcross); 172 | const endUnitSpace = startUnitSpace + unitsAcross * 2; 173 | const start = startUnitSpace * unitSize; 174 | const end = endUnitSpace * unitSize; 175 | const minUnitSpace = limits ? min * unitSize / unit : start; 176 | const maxUnitSpace = limits ? max * unitSize / unit : end; 177 | const height = labelFn(1) === '' ? 10 : 5; 178 | if (ticksPerUnit > 1) { 179 | this.#ticksElem.setAttribute('d', createSVGTicks(start, end, unitSize / ticksPerUnit, minUnitSpace, maxUnitSpace, height * tickHeight)); 180 | } 181 | this.#thicksElem.style.stroke = thicksColor; //setAttribute('stroke', thicksColor); 182 | this.#thicksElem.setAttribute('d', createSVGTicks(start, end, unitSize, minUnitSpace, maxUnitSpace, height)); 183 | this.#numbersElem.innerHTML = createSVGNumbers(start, end, unitSize, unit, this.#minusSize, minUnitSpace, maxUnitSpace, labelFn); 184 | this.#originElem.setAttribute('transform', `translate(${-this.#lastV * unitSize / unit} 0)`); 185 | this.#svgElem.classList.toggle('muigui-slider-up', orientation === 'up'); 186 | } 187 | updateDisplay(v) { 188 | this.#lastV = v; 189 | this.#updateSlider(); 190 | } 191 | setOptions(options) { 192 | copyExistingProperties(this.#options, options); 193 | return this; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/views/TextView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { identity } from '../libs/conversions.js'; 3 | import EditView from './EditView.js'; 4 | import { copyExistingProperties } from '../libs/utils.js'; 5 | 6 | export default class TextView extends EditView { 7 | #to; 8 | #from; 9 | #skipUpdate; 10 | #options = { 11 | converters: identity, 12 | }; 13 | 14 | constructor(setter, options) { 15 | const setValue = setter.setValue.bind(setter); 16 | const setFinalValue = setter.setFinalValue.bind(setter); 17 | super(createElem('input', { 18 | type: 'text', 19 | onInput: () => { 20 | this.#handleInput(setValue, true); 21 | }, 22 | onChange: () => { 23 | this.#handleInput(setFinalValue, false); 24 | }, 25 | })); 26 | this.setOptions(options); 27 | } 28 | #handleInput(setFn, skipUpdate) { 29 | const [valid, newV] = this.#from(this.domElement.value); 30 | if (valid) { 31 | this.#skipUpdate = skipUpdate; 32 | setFn(newV); 33 | } 34 | this.domElement.style.color = valid ? '' : 'var(--invalid-color)'; 35 | 36 | } 37 | updateDisplay(v) { 38 | if (!this.#skipUpdate) { 39 | this.domElement.value = this.#to(v); 40 | this.domElement.style.color = ''; 41 | } 42 | this.#skipUpdate = false; 43 | } 44 | setOptions(options) { 45 | copyExistingProperties(this.#options, options); 46 | const { 47 | converters: {to, from}, 48 | } = this.#options; 49 | this.#to = to; 50 | this.#from = from; 51 | return this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/views/ValueView.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import View from './View.js'; 3 | 4 | export default class ValueView extends View { 5 | constructor(className = '') { 6 | super(createElem('div', {className: 'muigui-value'})); 7 | if (className) { 8 | this.domElement.classList.add(className); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/views/Vec2View.js: -------------------------------------------------------------------------------- 1 | import { createElem } from '../libs/elem.js'; 2 | import { addTouchEvents } from '../libs/touch.js'; 3 | import { onResizeSVGNoScale } from '../libs/resize-helpers.js'; 4 | import EditView from './EditView.js'; 5 | 6 | const svg = ` 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | 16 | export default class Vec2View extends EditView { 17 | #svgElem; 18 | #arrowElem; 19 | #circleElem; 20 | #lastV = []; 21 | 22 | constructor(setter) { 23 | super(createElem('div', { 24 | innerHTML: svg, 25 | className: 'muigui-no-scroll', 26 | })); 27 | const onTouch = (e) => { 28 | const {width, height} = this.#svgElem.getBoundingClientRect(); 29 | const nx = e.nx * 2 - 1; 30 | const ny = e.ny * 2 - 1; 31 | setter.setValue([nx * width * 0.5, ny * height * 0.5]); 32 | }; 33 | addTouchEvents(this.domElement, { 34 | onDown: onTouch, 35 | onMove: onTouch, 36 | }); 37 | this.#svgElem = this.$('svg'); 38 | this.#arrowElem = this.$('#muigui-arrow'); 39 | this.#circleElem = this.$('#muigui-circle'); 40 | onResizeSVGNoScale(this.#svgElem, 0.5, 0.5, () => this.#updateDisplayImpl); 41 | } 42 | #updateDisplayImpl() { 43 | const [x, y] = this.#lastV; 44 | this.#arrowElem.setAttribute('d', `M0,0L${x},${y}`); 45 | this.#circleElem.setAttribute('transform', `translate(${x}, ${y})`); 46 | } 47 | updateDisplay(v) { 48 | this.#lastV[0] = v[0]; 49 | this.#lastV[1] = v[1]; 50 | this.#updateDisplayImpl(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/views/View.ts: -------------------------------------------------------------------------------- 1 | import { removeArrayElem } from '../libs/utils.js'; 2 | 3 | export default class View { 4 | domElement: HTMLElement; 5 | 6 | #childDestElem: HTMLElement; 7 | #views: View[] = []; 8 | 9 | constructor(elem: HTMLElement) { 10 | this.domElement = elem; 11 | this.#childDestElem = elem; 12 | } 13 | addElem(elem: HTMLElement) { 14 | this.#childDestElem.appendChild(elem); 15 | return elem; 16 | } 17 | removeElem(elem: HTMLElement) { 18 | this.#childDestElem.removeChild(elem); 19 | return elem; 20 | } 21 | pushSubElem(elem: HTMLElement) { 22 | this.#childDestElem.appendChild(elem); 23 | this.#childDestElem = elem; 24 | } 25 | popSubElem() { 26 | this.#childDestElem = this.#childDestElem.parentElement!; 27 | } 28 | add(view: View) { 29 | this.#views.push(view); 30 | this.addElem(view.domElement); 31 | return view; 32 | } 33 | remove(view: View) { 34 | this.removeElem(view.domElement); 35 | removeArrayElem(this.#views, view); 36 | return view; 37 | } 38 | pushSubView(view: View) { 39 | this.pushSubElem(view.domElement); 40 | } 41 | popSubView() { 42 | this.popSubElem(); 43 | } 44 | setOptions(options: any) { 45 | for (const view of this.#views) { 46 | view.setOptions(options); 47 | } 48 | } 49 | updateDisplayIfNeeded(newV: any, ignoreCache?: boolean) { 50 | for (const view of this.#views) { 51 | view.updateDisplayIfNeeded(newV, ignoreCache); 52 | } 53 | return this; 54 | } 55 | $(selector: string) { 56 | return this.domElement.querySelector(selector); 57 | } 58 | } -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | export const config = {}; 2 | 3 | const isArrayLike = v => Array.isArray(v) || 4 | (v.buffer instanceof ArrayBuffer && typeof v.length === 'number' && v.byteLength === 'number'); 5 | 6 | export function setConfig(options) { 7 | Object.assign(config, options); 8 | } 9 | 10 | function formatMsg(msg) { 11 | return `${msg}${msg ? ': ' : ''}`; 12 | } 13 | 14 | export function assertTruthy(actual, msg = '') { 15 | if (!actual) { 16 | throw new Error(`${formatMsg(msg)}expected: truthy, actual: ${actual}`); 17 | } 18 | } 19 | 20 | export function assertFalsy(actual, msg = '') { 21 | if (actual) { 22 | throw new Error(`${formatMsg(msg)}expected: falsy, actual: ${actual}`); 23 | } 24 | } 25 | 26 | export function assertStringMatchesRegEx(actual, regex, msg = '') { 27 | if (!regex.test(actual)) { 28 | throw new Error(`${formatMsg(msg)}expected: ${regex}, actual: ${actual}`); 29 | } 30 | } 31 | 32 | export function assertLessThan(actual, expected, msg = '') { 33 | if (actual >= expected) { 34 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be less than: ${expected}`); 35 | } 36 | } 37 | 38 | export function assertEqualApproximately(actual, expected, range, msg = '') { 39 | const diff = Math.abs(actual - expected); 40 | if (diff > range) { 41 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be less ${range} different than: ${expected}`); 42 | } 43 | } 44 | 45 | export function assertInstanceOf(actual, expectedType, msg = '') { 46 | if (!(actual instanceof expectedType)) { 47 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be of type: ${expectedType.constructor.name}`); 48 | } 49 | } 50 | 51 | export function assertIsArray(actual, msg = '') { 52 | if (!Array.isArray(actual)) { 53 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be an Array`); 54 | } 55 | } 56 | 57 | export function assertEqual(actual, expected, msg = '') { 58 | // I'm sure this is not sufficient 59 | if (actual.length && expected.length && isArrayLike(actual) && isArrayLike(expected)) { 60 | assertArrayEqual(actual, expected); 61 | } else if (actual !== expected) { 62 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`); 63 | } 64 | } 65 | 66 | export function assertStrictEqual(actual, expected, msg = '') { 67 | if (actual !== expected) { 68 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`); 69 | } 70 | } 71 | 72 | export function assertNotEqual(actual, expected, msg = '') { 73 | if (actual === expected) { 74 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`); 75 | } 76 | } 77 | 78 | export function assertStrictNotEqual(actual, expected, msg = '') { 79 | if (actual === expected) { 80 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`); 81 | } 82 | } 83 | 84 | export function assertArrayEqualApproximately(actual, expected, maxDiff = Number.EPSILON, msg = '') { 85 | if (actual.length !== expected.length) { 86 | throw new Error(`${formatMsg(msg)}expected: array.length ${expected.length} to equal actual.length: ${actual.length}`); 87 | } 88 | const errors = []; 89 | for (let i = 0; i < actual.length && errors.length < 10; ++i) { 90 | try { 91 | assertEqualApproximately(actual[i], expected[i], maxDiff); 92 | } catch (e) { 93 | errors.push(`${formatMsg(msg)}expected: expected[${i}] ${expected[i]} to equal actual[${i}]: ${actual[i]}`); 94 | } 95 | } 96 | if (errors.length > 0) { 97 | throw new Error(errors.join('\n')); 98 | } 99 | } 100 | 101 | export function assertArrayEqual(actual, expected, msg = '') { 102 | assertArrayEqualApproximately(actual, expected, 0, msg); 103 | } 104 | 105 | export function assertThrowsWith(func, expectations, msg = '') { 106 | let error = ''; 107 | if (config.throwOnError === false) { 108 | const origFn = console.error; 109 | const errors = []; 110 | console.error = function (...args) { 111 | errors.push(args.join(' ')); 112 | }; 113 | func(); 114 | console.error = origFn; 115 | if (errors.length) { 116 | error = errors.join('\n'); 117 | console.error(error); 118 | } 119 | } else { 120 | try { 121 | func(); 122 | } catch (e) { 123 | console.error(e); // eslint-disable-line 124 | error = e; 125 | } 126 | 127 | } 128 | 129 | if (config.noLint) { 130 | return; 131 | } 132 | 133 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 134 | } 135 | 136 | // check if it throws it throws with x 137 | export function assertIfThrowsItThrowsWith(func, expectations, msg = '') { 138 | let error = ''; 139 | let threw = false; 140 | if (config.throwOnError === false) { 141 | const origFn = console.error; 142 | const errors = []; 143 | console.error = function (...args) { 144 | errors.push(args.join(' ')); 145 | }; 146 | func(); 147 | console.error = origFn; 148 | if (errors.length) { 149 | error = errors.join('\n'); 150 | console.error(error); 151 | } 152 | } else { 153 | try { 154 | func(); 155 | } catch (e) { 156 | console.error(e); // eslint-disable-line 157 | error = e; 158 | threw = true; 159 | } 160 | 161 | } 162 | 163 | if (config.noLint) { 164 | return; 165 | } 166 | 167 | if (!threw) { 168 | return; 169 | } 170 | 171 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 172 | } 173 | 174 | function assertStringMatchesREs(actual, expectations, msg) { 175 | for (const expectation of expectations) { 176 | if (expectation instanceof RegExp) { 177 | if (!expectation.test(actual)) { 178 | throw new Error(`${formatMsg(msg)}expected: ${expectation}, actual: ${actual}`); 179 | } 180 | } 181 | } 182 | 183 | } 184 | export function assertWarnsWith(func, expectations, msg = '') { 185 | const warnings = []; 186 | const origWarnFn = console.warn; 187 | console.warn = function (...args) { 188 | origWarnFn.call(this, ...args); 189 | warnings.push(args.join(' ')); 190 | }; 191 | 192 | let error; 193 | try { 194 | func(); 195 | } catch (e) { 196 | error = e; 197 | } 198 | 199 | console.warn = origWarnFn; 200 | 201 | if (error) { 202 | throw error; 203 | } 204 | 205 | if (config.noLint) { 206 | return; 207 | } 208 | 209 | assertStringMatchesREs(warnings.join(' '), expectations, msg); 210 | } 211 | 212 | export default { 213 | false: assertFalsy, 214 | equal: assertEqual, 215 | matchesRegEx: assertStringMatchesRegEx, 216 | notEqual: assertNotEqual, 217 | throwsWith: assertThrowsWith, 218 | true: assertTruthy, 219 | }; -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | muigui Tests 6 | 7 | 8 | 13 | 14 | 15 |
16 |
17 | 33 | 34 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | 3 | import './tests/muigui-tests.js'; 4 | import './tests/color-utils-tests.js'; 5 | 6 | const settings = Object.fromEntries(new URLSearchParams(window.location.search).entries()); 7 | if (settings.reporter) { 8 | mocha.reporter(settings.reporter); 9 | } 10 | mocha.run((failures) => { 11 | window.testsPromiseInfo.resolve(failures); 12 | }); 13 | -------------------------------------------------------------------------------- /test/mocha-support.js: -------------------------------------------------------------------------------- 1 | export const describe = window.describe; 2 | export const it = window.it; 3 | export const before = window.before; 4 | export const after = window.after; 5 | export const beforeEach = window.beforeEach; 6 | export const afterEach = window.afterEach; 7 | 8 | -------------------------------------------------------------------------------- /test/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | white-space: pre-wrap; 138 | } 139 | 140 | #mocha .test .html-error { 141 | overflow: auto; 142 | color: black; 143 | display: block; 144 | float: left; 145 | clear: left; 146 | font: 12px/1.5 monaco, monospace; 147 | margin: 5px; 148 | padding: 15px; 149 | border: 1px solid #eee; 150 | max-width: 85%; /*(1)*/ 151 | max-width: -webkit-calc(100% - 42px); 152 | max-width: -moz-calc(100% - 42px); 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | max-height: 300px; 155 | word-wrap: break-word; 156 | border-bottom-color: #ddd; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-box-shadow: 0 1px 3px #eee; 159 | box-shadow: 0 1px 3px #eee; 160 | -webkit-border-radius: 3px; 161 | -moz-border-radius: 3px; 162 | border-radius: 3px; 163 | } 164 | 165 | #mocha .test .html-error pre.error { 166 | border: none; 167 | -webkit-border-radius: 0; 168 | -moz-border-radius: 0; 169 | border-radius: 0; 170 | -webkit-box-shadow: 0; 171 | -moz-box-shadow: 0; 172 | box-shadow: 0; 173 | padding: 0; 174 | margin: 0; 175 | margin-top: 18px; 176 | max-height: none; 177 | } 178 | 179 | /** 180 | * (1): approximate for browsers not supporting calc 181 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 182 | * ^^ seriously 183 | */ 184 | #mocha .test pre { 185 | display: block; 186 | float: left; 187 | clear: left; 188 | font: 12px/1.5 monaco, monospace; 189 | margin: 5px; 190 | padding: 15px; 191 | border: 1px solid #eee; 192 | max-width: 85%; /*(1)*/ 193 | max-width: -webkit-calc(100% - 42px); 194 | max-width: -moz-calc(100% - 42px); 195 | max-width: calc(100% - 42px); /*(2)*/ 196 | word-wrap: break-word; 197 | border-bottom-color: #ddd; 198 | -webkit-box-shadow: 0 1px 3px #eee; 199 | -moz-box-shadow: 0 1px 3px #eee; 200 | box-shadow: 0 1px 3px #eee; 201 | -webkit-border-radius: 3px; 202 | -moz-border-radius: 3px; 203 | border-radius: 3px; 204 | } 205 | 206 | #mocha .test h2 { 207 | position: relative; 208 | } 209 | 210 | #mocha .test a.replay { 211 | position: absolute; 212 | top: 3px; 213 | right: 0; 214 | text-decoration: none; 215 | vertical-align: middle; 216 | display: block; 217 | width: 15px; 218 | height: 15px; 219 | line-height: 15px; 220 | text-align: center; 221 | background: #eee; 222 | font-size: 15px; 223 | -webkit-border-radius: 15px; 224 | -moz-border-radius: 15px; 225 | border-radius: 15px; 226 | -webkit-transition:opacity 200ms; 227 | -moz-transition:opacity 200ms; 228 | -o-transition:opacity 200ms; 229 | transition: opacity 200ms; 230 | opacity: 0.3; 231 | color: #888; 232 | } 233 | 234 | #mocha .test:hover a.replay { 235 | opacity: 1; 236 | } 237 | 238 | #mocha-report.pass .test.fail { 239 | display: none; 240 | } 241 | 242 | #mocha-report.fail .test.pass { 243 | display: none; 244 | } 245 | 246 | #mocha-report.pending .test.pass, 247 | #mocha-report.pending .test.fail { 248 | display: none; 249 | } 250 | #mocha-report.pending .test.pass.pending { 251 | display: block; 252 | } 253 | 254 | #mocha-error { 255 | color: #c00; 256 | font-size: 1.5em; 257 | font-weight: 100; 258 | letter-spacing: 1px; 259 | } 260 | 261 | #mocha-stats { 262 | position: fixed; 263 | top: 15px; 264 | right: 10px; 265 | font-size: 12px; 266 | margin: 0; 267 | color: #888; 268 | z-index: 1; 269 | } 270 | 271 | #mocha-stats .progress { 272 | float: right; 273 | padding-top: 0; 274 | 275 | /** 276 | * Set safe initial values, so mochas .progress does not inherit these 277 | * properties from Bootstrap .progress (which causes .progress height to 278 | * equal line height set in Bootstrap). 279 | */ 280 | height: auto; 281 | -webkit-box-shadow: none; 282 | -moz-box-shadow: none; 283 | box-shadow: none; 284 | background-color: initial; 285 | } 286 | 287 | #mocha-stats em { 288 | color: black; 289 | } 290 | 291 | #mocha-stats a { 292 | text-decoration: none; 293 | color: inherit; 294 | } 295 | 296 | #mocha-stats a:hover { 297 | border-bottom: 1px solid #eee; 298 | } 299 | 300 | #mocha-stats li { 301 | display: inline-block; 302 | margin: 0 5px; 303 | list-style: none; 304 | padding-top: 11px; 305 | } 306 | 307 | #mocha-stats canvas { 308 | width: 40px; 309 | height: 40px; 310 | } 311 | 312 | #mocha code .comment { color: #ddd; } 313 | #mocha code .init { color: #2f6fad; } 314 | #mocha code .string { color: #5890ad; } 315 | #mocha code .keyword { color: #8a6343; } 316 | #mocha code .number { color: #2f6fad; } 317 | 318 | @media screen and (max-device-width: 480px) { 319 | #mocha { 320 | margin: 60px 0px; 321 | } 322 | 323 | #mocha #stats { 324 | position: absolute; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /test/puppeteer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import puppeteer from 'puppeteer'; 4 | import path from 'path'; 5 | import express from 'express'; 6 | import url from 'url'; 7 | const app = express(); 8 | const port = 3000; 9 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); // eslint-disable-line 10 | 11 | app.use(express.static(path.dirname(__dirname))); 12 | const server = app.listen(port, () => { 13 | console.log(`Example app listening on port ${port}!`); 14 | test(port); 15 | }); 16 | 17 | function makePromiseInfo() { 18 | const info = {}; 19 | const promise = new Promise((resolve, reject) => { 20 | Object.assign(info, {resolve, reject}); 21 | }); 22 | info.promise = promise; 23 | return info; 24 | } 25 | 26 | 27 | async function test(port) { 28 | const browser = await puppeteer.launch({ 29 | headless: 'new', 30 | args: [ 31 | //'--enable-unsafe-webgpu', 32 | //'--enable-webgpu-developer-features', 33 | //'--use-angle=swiftshader', 34 | '--user-agent=puppeteer', 35 | '--no-sandbox', 36 | '--disable-setuid-sandbox', 37 | ], 38 | }); 39 | const page = await browser.newPage(); 40 | 41 | page.on('console', async e => { 42 | const args = await Promise.all(e.args().map(a => a.jsonValue())); 43 | console.log(...args); 44 | }); 45 | 46 | let totalFailures = 0; 47 | let waitingPromiseInfo; 48 | 49 | // Get the "viewport" of the page, as reported by the page. 50 | page.on('domcontentloaded', async () => { 51 | const failures = await page.evaluate(() => { 52 | return window.testsPromiseInfo.promise; 53 | }); 54 | 55 | totalFailures += failures; 56 | 57 | waitingPromiseInfo.resolve(); 58 | }); 59 | 60 | const urls = [ 61 | `http://localhost:${port}/test/index.html?reporter=spec`, 62 | ]; 63 | 64 | for (const url of urls) { 65 | waitingPromiseInfo = makePromiseInfo(); 66 | await page.goto(url); 67 | await waitingPromiseInfo.promise; 68 | } 69 | 70 | await browser.close(); 71 | server.close(); 72 | 73 | process.exit(totalFailures ? 1 : 0); // eslint-disable-line 74 | } 75 | -------------------------------------------------------------------------------- /test/tests/color-utils-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | import { 3 | assertArrayEqualApproximately, 4 | assertEqual, 5 | } from '../assert.js'; 6 | import { 7 | colorFormatConverters, 8 | guessFormat, 9 | hsv01ToRGBFloat, 10 | rgbFloatToHSV01, 11 | } from '../../src/libs/color-utils.js'; 12 | 13 | describe('color-utils tests', () => { 14 | 15 | it('guesses the correct color format', () => { 16 | assertEqual(guessFormat('#569AEF'), 'hex6'); 17 | assertEqual(guessFormat('EF569A'), 'hex6-no-hash'); 18 | assertEqual(guessFormat('#F88'), 'hex3'); 19 | assertEqual(guessFormat('8F8'), 'hex3-no-hash'); 20 | assertEqual(guessFormat('rgb(170,68,240)'), 'css-rgb'); 21 | assertEqual(guessFormat('hsl(170,68%,80%)'), 'css-hsl'); 22 | // can no longer guess numbers as they could alpha or not 23 | //assertEqual(guessFormat(0xFEA956), 'uint32-rgb'); 24 | assertEqual(guessFormat([255, 192, 255]), 'float-rgb'); // we can't really distinguish between [u8, u8, u8] and [float, float, float] 25 | assertEqual(guessFormat([0.2, 0.9, 0.5]), 'float-rgb'); 26 | assertEqual(guessFormat(new Float32Array([0.2, 0.9, 0.5])), 'float-rgb'); 27 | assertEqual(guessFormat(new Uint8Array([20, 90, 50])), 'uint8-rgb'); 28 | assertEqual(guessFormat({r: 0, g: 0, b: 1}), 'object-rgb'); 29 | }); 30 | 31 | it('converts to/from css-rgb', () => { 32 | const { 33 | color: {from: fromHex, to: toHex}, 34 | text: {from: fromStr, to: toStr}, 35 | } = colorFormatConverters['css-rgb']; 36 | assertEqual(fromHex('#123456'), [true, 'rgb(18, 52, 86)']); 37 | assertEqual(toHex('rgb(86, 52, 18)'), '#563412'); 38 | assertEqual(fromStr('rgb(1,22,33)'), [true, 'rgb(1, 22, 33)']); 39 | assertEqual(toStr('rgb(111,22,33)'), 'rgb(111, 22, 33)'); 40 | }); 41 | 42 | it('converts to/from css-hsl', () => { 43 | const { 44 | color: {from: fromHex, to: toHex}, 45 | text: {from: fromStr, to: toStr}, 46 | } = colorFormatConverters['css-hsl']; 47 | assertEqual(fromHex('#eed4c9'), [true, 'hsl(18, 52%, 86%)']); 48 | assertEqual(toHex('hsl(86, 52%, 18%)'), '#314616'); 49 | assertEqual(fromStr('hsl(1,22%,33%)'), [true, 'hsl(1, 22%, 33%)']); 50 | assertEqual(toStr('hsl(111,22%,33%)'), 'hsl(111, 22%, 33%)'); 51 | assertEqual(fromHex('#eeeeee'), [true, 'hsl(0, 0%, 93%)']); 52 | }); 53 | 54 | it('converts to/from hex6-no-hash', () => { 55 | const { 56 | color: {from: fromHex, to: toHex}, 57 | text: {from: fromStr, to: toStr}, 58 | } = colorFormatConverters['hex6-no-hash']; 59 | assertEqual(fromHex('#123456'), [true, '123456']); 60 | assertEqual(toHex('123456'), '#123456'); 61 | assertEqual(fromStr(' 123456 '), [true, '123456']); 62 | assertEqual(toStr('123456'), '123456'); 63 | }); 64 | 65 | it('converts to/from hex3', () => { 66 | const { 67 | color: {from: fromHex, to: toHex}, 68 | text: {from: fromStr, to: toStr}, 69 | } = colorFormatConverters['hex3']; 70 | assertEqual(fromHex('#123456'), [true, '#123456']); 71 | assertEqual(fromHex('#223355'), [true, '#235']); 72 | assertEqual(toHex('#123'), '#112233'); 73 | assertEqual(fromStr(' #123 '), [true, '#123']); 74 | assertEqual(toStr('#456'), '#456'); 75 | }); 76 | 77 | it('converts to/from hex6', () => { 78 | const { 79 | color: {from: fromHex, to: toHex}, 80 | text: {from: fromStr, to: toStr}, 81 | } = colorFormatConverters['hex6']; 82 | assertEqual(fromHex('#123456'), [true, '#123456']); 83 | assertEqual(fromHex('#223355'), [true, '#223355']); 84 | assertEqual(toHex('#123456'), '#123456'); 85 | assertEqual(fromStr(' #123456 '), [true, '#123456']); 86 | assertEqual(fromStr(' #123456f '), [false, '#123456f']); 87 | assertEqual(toStr('#456789'), '#456789'); 88 | }); 89 | 90 | it('converts to/from float-rgb', () => { 91 | const { 92 | color: {from: fromHex, to: toHex}, 93 | text: {from: fromStr, to: toStr}, 94 | } = colorFormatConverters['float-rgb']; 95 | assertEqual(fromHex('#123456'), [true, [0.071, 0.204, 0.337]]); 96 | assertEqual(toHex([0.337, 0.204, 0.071]), '#563412'); 97 | assertEqual(fromStr('0.10 , 12.2301, 1.00'), [true, [0.1, 12.23, 1]]); 98 | assertEqual(fromStr('0.10 , 12.2301, 1.00f'), [false, [0.1, 12.23, 1]]); 99 | assertEqual(toStr([0.12, 0.34, 5.67]), '0.12, 0.34, 5.67'); 100 | assertEqual(toStr(new Float32Array([0.2, 0.9, 0.5])), '0.2, 0.9, 0.5'); 101 | }); 102 | 103 | it('converts from/too hsv01/rgbFloat', () => { 104 | assertArrayEqualApproximately(rgbFloatToHSV01([1, 0, 0]), [0, 1, 1], 0.001); 105 | assertArrayEqualApproximately(rgbFloatToHSV01([0, 1, 0]), [1 / 3, 1, 1], 0.001); 106 | assertArrayEqualApproximately(rgbFloatToHSV01([0, 0, 1]), [2 / 3, 1, 1], 0.001); 107 | assertArrayEqualApproximately(hsv01ToRGBFloat([0, 1, 1]), [1, 0, 0], 0.0); 108 | assertArrayEqualApproximately(hsv01ToRGBFloat([1 / 3, 1, 1]), [0, 1, 0], 0.0); 109 | assertArrayEqualApproximately(hsv01ToRGBFloat([2 / 3, 1, 1]), [0, 0, 1], 0.00001); 110 | assertArrayEqualApproximately(hsv01ToRGBFloat([1 / 6, 1, 1]), [1, 1, 0], 0.0); 111 | assertArrayEqualApproximately(hsv01ToRGBFloat([3 / 6, 1, 1]), [0, 1, 1], 0.0); 112 | assertArrayEqualApproximately(hsv01ToRGBFloat([5 / 6, 1, 1]), [1, 0, 1], 0.00001); 113 | }); 114 | 115 | }); -------------------------------------------------------------------------------- /test/tests/muigui-tests.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from '../mocha-support.js'; 2 | 3 | describe('muigui tests', () => { 4 | 5 | it('test muigui', () => { 6 | }); 7 | 8 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "NodeNext", 6 | "outDir": "dist/0.x", 7 | "moduleResolution": "NodeNext", 8 | "declaration": true, 9 | "allowJs": true, 10 | }, 11 | "include": [ 12 | "src/**/*.ts", 13 | "src/**/*.js", 14 | "build/**/*.js", 15 | "examples/**/*.js", 16 | "test/**/*.js", 17 | ".eslintrc.cjs", 18 | "*.js", 19 | ], 20 | "exclude": [ 21 | "examples/3rdParty/**/*.js", 22 | "test/mocha.js", 23 | "out/**/*.js", 24 | "build/out/**/", 25 | ], 26 | } --------------------------------------------------------------------------------