├── .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 |
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 |
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 |
34 |
49 |
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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------