├── .editorconfig ├── .gitignore ├── .nojekyll ├── .npmignore ├── CHANGELOG.md ├── GUIControl.js ├── LICENSE.md ├── README.md ├── examples ├── all-controls.js ├── assets │ ├── noise.png │ ├── palette-hsl.png │ ├── palette.jpg │ ├── pex.png │ ├── pisa │ │ ├── pisa.txt │ │ ├── pisa_negx.jpg │ │ ├── pisa_negy.jpg │ │ ├── pisa_negz.jpg │ │ ├── pisa_posx.jpg │ │ ├── pisa_posy.jpg │ │ └── pisa_posz.jpg │ ├── plask.png │ └── rainbow.jpg ├── canvas-2d.js ├── debug.js ├── options.js ├── pex-context.js └── theme.js ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── renderers ├── CanvasRenderer.js ├── DebugRenderer.js ├── PexContextRenderer.js └── index.js ├── screenshot.png ├── shaders ├── chunks │ ├── encode-decode.glsl.js │ ├── gamma.glsl.js │ └── rgbm.glsl.js ├── main.vert.js ├── texture-2d.frag.js └── texture-cube.frag.js ├── theme.js ├── types.js └── web_modules ├── _chunks ├── _commonjsHelpers-BFTU3MAI.js ├── avec3-CX_9gCVx.js ├── mat4-DkKmQtAV.js ├── polyfills-BwRuO6W0.js ├── ray-bLshgWN6.js └── vec3-DW1VLBq6.js ├── es-module-shims.js ├── es-module-shims ├── debug.js ├── typescript-transform.js └── wasm.js ├── import-map.json ├── pex-cam.js ├── pex-color.js ├── pex-context.js ├── pex-geom.js ├── pex-io.js ├── pex-math.js └── primitive-geometry.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /types 4 | /lib 5 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/.nojekyll -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /web_modules 2 | /examples 3 | /docs 4 | /coverage 5 | /test 6 | /.github 7 | screenshot.* 8 | index.html 9 | tsconfig.json 10 | .editorconfig 11 | .nojekyll 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## [3.1.3](https://github.com/pex-gl/pex-gui/compare/v3.1.2...v3.1.3) (2025-04-11) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * add fallback font to theme ([be96f71](https://github.com/pex-gl/pex-gui/commit/be96f715ea9f755631ea69a86604480484b537be)), closes [#44](https://github.com/pex-gl/pex-gui/issues/44) 11 | 12 | 13 | 14 | ## [3.1.2](https://github.com/pex-gl/pex-gui/compare/v3.1.1...v3.1.2) (2025-03-24) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * ensure graph values don't overflow render width ([570dcaf](https://github.com/pex-gl/pex-gui/commit/570dcaf35149b356b5a8438864cf876a5b7c9e50)) 20 | 21 | 22 | 23 | ## [3.1.1](https://github.com/pex-gl/pex-gui/compare/v3.1.0...v3.1.1) (2024-11-26) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * allow update interval of 0 for stats and graph controls ([154220f](https://github.com/pex-gl/pex-gui/commit/154220ffec80027e4ed88d16f416ce098fed5b9a)) 29 | 30 | 31 | 32 | # [3.1.0](https://github.com/pex-gl/pex-gui/compare/v3.0.1...v3.1.0) (2024-11-22) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * check for nullish when rendering graph control value ([6e5c7bb](https://github.com/pex-gl/pex-gui/commit/6e5c7bb0c946312075de03e35120224163707412)) 38 | * use round instead of floor in fps graph redraw ([1373b6b](https://github.com/pex-gl/pex-gui/commit/1373b6b894539396262e67e978d32c8ad9f1ba05)) 39 | 40 | 41 | ### Features 42 | 43 | * add format support to graph controls ([506553a](https://github.com/pex-gl/pex-gui/commit/506553aaa04bf526adf4e6c13a3ceba96de59ec8)) 44 | * handle multiline and empty string title in control stats ([285e223](https://github.com/pex-gl/pex-gui/commit/285e223447a4f3e6fde422f990939576809de54a)) 45 | 46 | 47 | 48 | ## [3.0.1](https://github.com/pex-gl/pex-gui/compare/v3.0.0...v3.0.1) (2024-07-09) 49 | 50 | 51 | 52 | # [3.0.0](https://github.com/pex-gl/pex-gui/compare/v3.0.0-alpha.3...v3.0.0) (2024-02-05) 53 | 54 | 55 | 56 | # [3.0.0-alpha.3](https://github.com/pex-gl/pex-gui/compare/v3.0.0-alpha.2...v3.0.0-alpha.3) (2023-02-27) 57 | 58 | 59 | ### Features 60 | 61 | * add title for gui.addStats ([68e7f5f](https://github.com/pex-gl/pex-gui/commit/68e7f5f251da1c1afb265873e0e5255b983985ff)) 62 | 63 | 64 | 65 | # [3.0.0-alpha.2](https://github.com/pex-gl/pex-gui/compare/v3.0.0-alpha.1...v3.0.0-alpha.2) (2022-09-09) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * don't assume pixel ratio from pex-context ([b8a6d9f](https://github.com/pex-gl/pex-gui/commit/b8a6d9f3b502643a1a189e509113510886928c3d)) 71 | 72 | 73 | ### Features 74 | 75 | * make gui render at the size regardless of the canvas size or pixel ratio unless it overflows the canvas viewport ([17eab56](https://github.com/pex-gl/pex-gui/commit/17eab565705b031fc96826dee434a60b46534e08)) 76 | 77 | 78 | 79 | # [3.0.0-alpha.1](https://github.com/pex-gl/pex-gui/compare/v3.0.0-alpha.0...v3.0.0-alpha.1) (2022-07-26) 80 | 81 | 82 | 83 | # [3.0.0-alpha.0](https://github.com/pex-gl/pex-gui/compare/v2.4.0...v3.0.0-alpha.0) (2022-07-26) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * add missing GUIControl.setPosition ([b9bf584](https://github.com/pex-gl/pex-gui/commit/b9bf5840fecd0144a6b8639ffcbd0a940bbcd4f6)) 89 | * allow hardcoded GUIControl position x/y ([278c787](https://github.com/pex-gl/pex-gui/commit/278c7870a9f5b5c5c0f08e824102dfe135860333)) 90 | * handle nullish options in addParam ([fbb6599](https://github.com/pex-gl/pex-gui/commit/fbb6599eadbe2647b5f1e6cc4c7f9f86c794e9c9)) 91 | 92 | 93 | ### Code Refactoring 94 | 95 | * use ES modules ([ca261fa](https://github.com/pex-gl/pex-gui/commit/ca261faa2568dad81ca8c0467da26339df0063e8)) 96 | 97 | 98 | ### Features 99 | 100 | * add flipY option for 2D textures ([368dabb](https://github.com/pex-gl/pex-gui/commit/368dabbe60645bcc3ec55f0789bf8b82a55b2c7f)), closes [#38](https://github.com/pex-gl/pex-gui/issues/38) 101 | * allow gui.pixelRatio to be updated ([e9169db](https://github.com/pex-gl/pex-gui/commit/e9169db7f0c46fda741eeedbfd2b00eee2a64e28)) 102 | * allow scale and responsive as options ([e27ca9c](https://github.com/pex-gl/pex-gui/commit/e27ca9ccc53cdd4dc86241cdfa5ca4a7088fef15)) 103 | * move to pointer events ([e64c6d1](https://github.com/pex-gl/pex-gui/commit/e64c6d12d1742be7a88885559b96c6c41438b55f)) 104 | * update pex-color to latest api ([9faa62a](https://github.com/pex-gl/pex-gui/commit/9faa62af0e5e48b7c54c1d3db7a52b451dd972ee)) 105 | 106 | 107 | ### BREAKING CHANGES 108 | 109 | * switch to type module 110 | -------------------------------------------------------------------------------- /GUIControl.js: -------------------------------------------------------------------------------- 1 | import { fromHSL, toHSL } from "pex-color"; 2 | 3 | class GUIControl { 4 | constructor(options) { 5 | Object.assign(this, options); 6 | } 7 | 8 | setPosition(x, y) { 9 | this.x = x; 10 | this.y = y; 11 | } 12 | 13 | getNormalizedValue(idx) { 14 | if (!this.contextObject) return 0; 15 | 16 | let val = this.contextObject[this.attributeName]; 17 | if ( 18 | this.options && 19 | this.options.min !== undefined && 20 | this.options.max !== undefined 21 | ) { 22 | if (this.type === "multislider") { 23 | val = 24 | (val[idx] - this.options.min) / (this.options.max - this.options.min); 25 | } else if (this.type === "color") { 26 | const hsl = toHSL(val); 27 | if (idx === 0) val = hsl[0]; 28 | if (idx === 1) val = hsl[1]; 29 | if (idx === 2) val = hsl[2]; 30 | if (idx === 3) val = val[3]; 31 | } else { 32 | val = (val - this.options.min) / (this.options.max - this.options.min); 33 | } 34 | } 35 | return val; 36 | } 37 | 38 | setNormalizedValue(val, idx) { 39 | if (!this.contextObject) return; 40 | 41 | if ( 42 | this.options && 43 | this.options.min !== undefined && 44 | this.options.max !== undefined 45 | ) { 46 | if (this.type === "multislider") { 47 | const a = this.contextObject[this.attributeName]; 48 | if (idx >= a.length) { 49 | return; 50 | } 51 | a[idx] = this.options.min + val * (this.options.max - this.options.min); 52 | val = a; 53 | } else if (this.type === "color") { 54 | const c = this.contextObject[this.attributeName]; 55 | if (idx === 3) { 56 | c[3] = val; 57 | } else { 58 | const hsl = toHSL(c); 59 | if (idx === 0) hsl[0] = val; 60 | if (idx === 1) hsl[1] = val; 61 | if (idx === 2) hsl[2] = val; 62 | fromHSL(c, ...hsl); 63 | } 64 | 65 | val = c; 66 | } else { 67 | val = this.options.min + val * (this.options.max - this.options.min); 68 | } 69 | if (this.options && this.options.step) { 70 | val = val - (val % this.options.step); 71 | } 72 | } 73 | this.contextObject[this.attributeName] = val; 74 | } 75 | 76 | getSerializedValue() { 77 | return this.contextObject ? this.contextObject[this.attributeName] : ""; 78 | } 79 | 80 | setSerializedValue(value) { 81 | if (this.type === "slider") { 82 | this.contextObject[this.attributeName] = value; 83 | } else if (this.type === "multislider") { 84 | this.contextObject[this.attributeName] = value; 85 | } else if (this.type === "color") { 86 | this.contextObject[this.attributeName].r = value.r; 87 | this.contextObject[this.attributeName].g = value.g; 88 | this.contextObject[this.attributeName].b = value.b; 89 | this.contextObject[this.attributeName].a = value.a; 90 | } else if (this.type === "toggle") { 91 | this.contextObject[this.attributeName] = value; 92 | } else if (this.type === "radiolist") { 93 | this.contextObject[this.attributeName] = value; 94 | } 95 | } 96 | 97 | getValue() { 98 | if (this.type === "slider") { 99 | return this.contextObject[this.attributeName]; 100 | } else if (this.type === "multislider") { 101 | return this.contextObject[this.attributeName]; 102 | } else if (this.type === "color") { 103 | return this.contextObject[this.attributeName]; 104 | } else if (this.type === "toggle") { 105 | return this.contextObject[this.attributeName]; 106 | } else { 107 | return 0; 108 | } 109 | } 110 | 111 | getStrValue() { 112 | if (this.type === "slider") { 113 | const str = `${this.contextObject[this.attributeName]}`; 114 | let dotPos = str.indexOf(".") + 1; 115 | 116 | if (dotPos === 0) return `${str}.0`; 117 | 118 | while (str.charAt(dotPos) === "0") { 119 | dotPos++; 120 | } 121 | return str.substr(0, dotPos + 2); 122 | } else if (this.type === "color") { 123 | return this.options.alpha ? "HSLA" : "HSL"; 124 | } else if (this.type === "toggle") { 125 | return this.contextObject[this.attributeName]; 126 | } else { 127 | return ""; 128 | } 129 | } 130 | } 131 | 132 | export default GUIControl; 133 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Marcin Ignac 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pex-gui 2 | 3 | [![npm version](https://img.shields.io/npm/v/pex-gui)](https://www.npmjs.com/package/pex-gui) 4 | [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://www.npmjs.com/package/pex-gui) 5 | [![npm minzipped size](https://img.shields.io/bundlephobia/minzip/pex-gui)](https://bundlephobia.com/package/pex-gui) 6 | [![dependencies](https://img.shields.io/librariesio/release/npm/pex-gui)](https://github.com/pex-gl/pex-gui/blob/main/package.json) 7 | [![types](https://img.shields.io/npm/types/pex-gui)](https://github.com/microsoft/TypeScript) 8 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fa6673.svg)](https://conventionalcommits.org) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-Prettier-f8bc45.svg?logo=prettier)](https://github.com/prettier/prettier) 10 | [![linted with eslint](https://img.shields.io/badge/linted_with-ES_Lint-4B32C3.svg?logo=eslint)](https://github.com/eslint/eslint) 11 | [![license](https://img.shields.io/github/license/pex-gl/pex-gui)](https://github.com/pex-gl/pex-gui/blob/main/LICENSE.md) 12 | 13 | GUI controls for [PEX](https://pex.gl). 14 | 15 | ![](https://raw.githubusercontent.com/pex-gl/pex-gui/main/screenshot.png) 16 | 17 | ## Installation 18 | 19 | ```bash 20 | npm install pex-gui 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | import createGUI from "pex-gui"; 27 | import createContext from "pex-context"; 28 | import { loadImage } from "pex-io"; 29 | 30 | const ctx = createContext({ pixelRatio: 2 }); 31 | const gui = createGUI(ctx); 32 | 33 | const res = await load({ 34 | palette: { image: `examples/assets/palette.jpg` }, 35 | paletteHsl: { image: `examples/assets/palette-hsl.png` }, 36 | plask: { image: `examples/assets/plask.png` }, 37 | pex: { image: `examples/assets/pex.png` }, 38 | noise: { image: `examples/assets/noise.png` }, 39 | posx: { image: `examples/assets/pisa/pisa_posx.jpg` }, 40 | negx: { image: `examples/assets/pisa/pisa_negx.jpg` }, 41 | posy: { image: `examples/assets/pisa/pisa_posy.jpg` }, 42 | negy: { image: `examples/assets/pisa/pisa_negy.jpg` }, 43 | posz: { image: `examples/assets/pisa/pisa_posz.jpg` }, 44 | negz: { image: `examples/assets/pisa/pisa_negz.jpg` }, 45 | }); 46 | 47 | const images = [res.plask, res.pex, res.noise]; 48 | 49 | const State = { 50 | currentRadioListChoice: 0, 51 | radioListChoices: ["Choice 1", "Choice 2", "Choice 3"].map((name, value) => ({ 52 | name, 53 | value, 54 | })), 55 | checkboxValue: false, 56 | message: "Message", 57 | range: 0, 58 | position: [2, 0], 59 | rgb: [0.92, 0.2, 0.2], 60 | rgba: [0.2, 0.92, 0.2, 1.0], 61 | palette: Float32Array.of(0.2, 0.2, 0.92, 1.0), 62 | paletteHsl: Float32Array.of(0.92, 0.2, 0.92, 1.0), 63 | cubeTexture: ctx.textureCube({ 64 | data: [res.posx, res.negx, res.posy, res.negy, res.posz, res.negz], 65 | width: 64, 66 | height: 64, 67 | }), 68 | currentTexture: 0, 69 | textures: images.map((image) => 70 | ctx.texture2D({ 71 | data: image, 72 | width: image.width, 73 | height: image.height, 74 | flipY: true, 75 | wrap: ctx.Wrap.Repeat, 76 | encoding: ctx.Encoding.SRGB, 77 | mipmap: true, 78 | min: ctx.Filter.LinearMipmapLinear, 79 | aniso: 16, 80 | }) 81 | ), 82 | }; 83 | 84 | // Controls 85 | gui.addTab("Controls"); 86 | gui.addColumn("Inputs"); 87 | gui.addLabel("Special Parameters"); 88 | gui.addLabel("Multiline\nLabel"); 89 | gui.addButton("Button", () => { 90 | console.log("Called back"); 91 | }); 92 | gui.addRadioList( 93 | "Radio list", 94 | State, 95 | "currentRadioListChoice", 96 | State.radioListChoices 97 | ); 98 | 99 | gui.addSeparator(); 100 | gui.addLabel("Smart Parameters"); 101 | gui.addParam("Checkbox", State, "checkboxValue"); 102 | gui.addParam("Text message", State, "message", {}, (value) => { 103 | console.log(value); 104 | }); 105 | gui.addParam("Slider", State, "range", { 106 | min: -Math.PI / 2, 107 | max: Math.PI / 2, 108 | }); 109 | gui.addParam("Multi Slider", State, "position", { 110 | min: 0, 111 | max: 10, 112 | }); 113 | 114 | gui.addColumn("Colors"); 115 | gui.addParam("Color", State, "rgb", { 116 | type: "color", 117 | }); 118 | gui.addParam("Color alpha", State, "rgba", { 119 | type: "color", 120 | alpha: true, 121 | }); 122 | gui.addParam("Palette", State, "palette", { 123 | type: "color", 124 | palette: res.palette, 125 | }); 126 | gui.addParam("Palette HSL", State, "paletteHsl", { 127 | type: "color", 128 | palette: res.paletteHsl, 129 | }); 130 | 131 | gui.addColumn("Textures"); 132 | gui.addTexture2D("Single", State.textures[0]); 133 | gui.addTexture2DList( 134 | "List", 135 | State, 136 | "currentTexture", 137 | State.textures.map((texture, value) => ({ 138 | texture, 139 | value, 140 | })) 141 | ); 142 | gui.addTextureCube("Cube", State.cubeTexture, { level: 2 }); 143 | 144 | gui.addColumn("Graphs"); 145 | gui.addGraph("Sin", { 146 | interval: 500, 147 | t: 0, 148 | update(item) { 149 | item.options.t += 0.01; 150 | }, 151 | redraw(item) { 152 | item.values.push(+Math.sin(item.options.t).toFixed(3)); 153 | }, 154 | }); 155 | gui.addFPSMeeter(); 156 | gui.addHeader("Stats"); 157 | gui.addStats(); 158 | gui.addStats("Object stats", { 159 | update(item) { 160 | Object.assign(item.stats, { 161 | r: State.rgb[0], 162 | g: State.rgb[1], 163 | b: State.rgb[2], 164 | }); 165 | }, 166 | }); 167 | ``` 168 | 169 | ## API 170 | 171 | 172 | 173 | ## Modules 174 | 175 |
176 |
pex-gui
177 |
178 |
179 | 180 | ## Classes 181 | 182 |
183 |
GUI
184 |

GUI controls for PEX.

185 |
186 |
187 | 188 | ## Typedefs 189 | 190 |
191 |
GUIControlOptions : object
192 |
193 |
GUIOptions : object
194 |
195 |
ctx : module:pex-context~ctx
196 |
197 |
198 | 199 | 200 | 201 | ## pex-gui 202 | 203 | **Summary**: Export a factory function for creating a GUI instance. 204 | 205 | 206 | ### pex-gui.default(ctx, opts) ⇒ [GUI](#GUI) 207 | 208 | **Kind**: static method of [pex-gui](#module_pex-gui) 209 | 210 | | Param | Type | 211 | | ----- | ----------------------------------------------------------------- | 212 | | ctx | [ctx](#ctx) \| CanvasRenderingContext2D | 213 | | opts | [GUIOptions](#GUIOptions) | 214 | 215 | 216 | 217 | ## GUI 218 | 219 | GUI controls for PEX. 220 | 221 | **Kind**: global class 222 | **Properties** 223 | 224 | | Name | Type | Default | Description | 225 | | --------- | -------------------- | ----------------- | ----------------------------------------------- | 226 | | [enabled] | boolean | true | Enable/disable pointer interaction and drawing. | 227 | 228 | - [GUI](#GUI) 229 | - [new GUI(ctx, opts)](#new_GUI_new) 230 | - [.addTab(title, contextObject, attributeName, [options], onChange)](#GUI+addTab) ⇒ GUIControl 231 | - [.addColumn(title, [width])](#GUI+addColumn) ⇒ GUIControl 232 | - [.addHeader(title)](#GUI+addHeader) ⇒ GUIControl 233 | - [.addSeparator()](#GUI+addSeparator) ⇒ GUIControl 234 | - [.addLabel(title)](#GUI+addLabel) ⇒ GUIControl 235 | - [.addParam(title, contextObject, attributeName, [options], onChange)](#GUI+addParam) ⇒ GUIControl 236 | - [.addButton(title, onClick)](#GUI+addButton) ⇒ GUIControl 237 | - [.addRadioList(title, contextObject, attributeName, items, onChange)](#GUI+addRadioList) ⇒ GUIControl 238 | - [.addTexture2DList(title, contextObject, attributeName, items, [itemsPerRow], onChange)](#GUI+addTexture2DList) ⇒ GUIControl 239 | - [.addTexture2D(title, texture, options)](#GUI+addTexture2D) ⇒ GUIControl 240 | - [.addTextureCube(title, texture, options)](#GUI+addTextureCube) ⇒ GUIControl 241 | - [.addGraph(title, options)](#GUI+addGraph) ⇒ GUIControl 242 | - [.addFPSMeeter()](#GUI+addFPSMeeter) ⇒ GUIControl 243 | - [.addStats(title, [options])](#GUI+addStats) ⇒ GUIControl 244 | - [.draw()](#GUI+draw) 245 | - [.serialize()](#GUI+serialize) ⇒ object 246 | - [.deserialize(data)](#GUI+deserialize) 247 | - [.dispose()](#GUI+dispose) 248 | 249 | 250 | 251 | ### new GUI(ctx, opts) 252 | 253 | Creates an instance of GUI. 254 | 255 | | Param | Type | 256 | | ----- | ----------------------------------------------------------------- | 257 | | ctx | [ctx](#ctx) \| CanvasRenderingContext2D | 258 | | opts | [GUIOptions](#GUIOptions) | 259 | 260 | 261 | 262 | ### guI.addTab(title, contextObject, attributeName, [options], onChange) ⇒ GUIControl 263 | 264 | Add a tab control. 265 | 266 | **Kind**: instance method of [GUI](#GUI) 267 | 268 | | Param | Type | Default | 269 | | ------------- | ---------------------------------------------------- | --------------- | 270 | | title | string | | 271 | | contextObject | object | | 272 | | attributeName | string | | 273 | | [options] | [GUIControlOptions](#GUIControlOptions) | {} | 274 | | onChange | function | | 275 | 276 | 277 | 278 | ### guI.addColumn(title, [width]) ⇒ GUIControl 279 | 280 | Add a column control with a header. 281 | 282 | **Kind**: instance method of [GUI](#GUI) 283 | 284 | | Param | Type | Default | 285 | | ------- | ------------------- | ----------------------------------- | 286 | | title | string | | 287 | | [width] | number | this.theme.columnWidth | 288 | 289 | 290 | 291 | ### guI.addHeader(title) ⇒ GUIControl 292 | 293 | Add a header control. 294 | 295 | **Kind**: instance method of [GUI](#GUI) 296 | 297 | | Param | Type | 298 | | ----- | ------------------- | 299 | | title | string | 300 | 301 | 302 | 303 | ### guI.addSeparator() ⇒ GUIControl 304 | 305 | Add some breathing space between controls. 306 | 307 | **Kind**: instance method of [GUI](#GUI) 308 | 309 | 310 | ### guI.addLabel(title) ⇒ GUIControl 311 | 312 | Add a text label. Can be multiple line. 313 | 314 | **Kind**: instance method of [GUI](#GUI) 315 | 316 | | Param | Type | 317 | | ----- | ------------------- | 318 | | title | string | 319 | 320 | **Example** 321 | 322 | ```js 323 | gui.addLabel("Multiline\nLabel"); 324 | ``` 325 | 326 | 327 | 328 | ### guI.addParam(title, contextObject, attributeName, [options], onChange) ⇒ GUIControl 329 | 330 | Add a generic parameter control. 331 | 332 | **Kind**: instance method of [GUI](#GUI) 333 | 334 | | Param | Type | Default | 335 | | ------------- | ---------------------------------------------------- | --------------- | 336 | | title | string | | 337 | | contextObject | object | | 338 | | attributeName | string | | 339 | | [options] | [GUIControlOptions](#GUIControlOptions) | {} | 340 | | onChange | function | | 341 | 342 | **Example** 343 | 344 | ```js 345 | gui.addParam("Checkbox", State, "rotate"); 346 | 347 | gui.addParam("Text message", State, "text", {}, function (value) { 348 | console.log(value); 349 | }); 350 | 351 | gui.addParam("Slider", State, "range", { 352 | min: -Math.PI / 2, 353 | max: Math.PI / 2, 354 | }); 355 | 356 | gui.addParam("Multi Slider", State, "position", { 357 | min: 0, 358 | max: 10, 359 | }); 360 | 361 | gui.addParam("Color [RGBA]", State, "color"); 362 | 363 | gui.addParam("Texture", State, "texture"); 364 | gui.addParam("Texture Cube", State, "textureCube"); 365 | ``` 366 | 367 | 368 | 369 | ### guI.addButton(title, onClick) ⇒ GUIControl 370 | 371 | Add a clickable button. 372 | 373 | **Kind**: instance method of [GUI](#GUI) 374 | 375 | | Param | Type | 376 | | ------- | --------------------- | 377 | | title | string | 378 | | onClick | function | 379 | 380 | **Example** 381 | 382 | ```js 383 | gui.addButton("Button", () => { 384 | console.log("Called back"); 385 | }); 386 | ``` 387 | 388 | 389 | 390 | ### guI.addRadioList(title, contextObject, attributeName, items, onChange) ⇒ GUIControl 391 | 392 | Add a radio list with options. 393 | 394 | **Kind**: instance method of [GUI](#GUI) 395 | 396 | | Param | Type | 397 | | ------------- | -------------------------------------------------------- | 398 | | title | string | 399 | | contextObject | object | 400 | | attributeName | string | 401 | | items | Array.<{name: string, value: number}> | 402 | | onChange | function | 403 | 404 | **Example** 405 | 406 | ```js 407 | gui.addRadioList( 408 | "Radio list", 409 | State, 410 | "currentRadioListChoice", 411 | ["Choice 1", "Choice 2", "Choice 3"].map((name, value) => ({ 412 | name, 413 | value, 414 | })), 415 | ); 416 | ``` 417 | 418 | 419 | 420 | ### guI.addTexture2DList(title, contextObject, attributeName, items, [itemsPerRow], onChange) ⇒ GUIControl 421 | 422 | Add a texture visualiser and selector for multiple textures (from pex-context) or images. 423 | 424 | **Kind**: instance method of [GUI](#GUI) 425 | 426 | | Param | Type | Default | 427 | | ------------- | ---------------------------------------------------------------------------------------------------- | -------------- | 428 | | title | string | | 429 | | contextObject | object | | 430 | | attributeName | string | | 431 | | items | Array.<{texture: (module:pex-context~texture\|CanvasImageSource), value: number}> | | 432 | | [itemsPerRow] | number | 4 | 433 | | onChange | function | | 434 | 435 | **Example** 436 | 437 | ```js 438 | gui.addTexture2DList("List", State, "currentTexture", textures.map((texture, value) = > ({ texture, value }))); 439 | ``` 440 | 441 | 442 | 443 | ### guI.addTexture2D(title, texture, options) ⇒ GUIControl 444 | 445 | Add a texture (from pex-context) or image visualiser. 446 | Notes: texture cannot be updated once created. 447 | 448 | **Kind**: instance method of [GUI](#GUI) 449 | 450 | | Param | Type | 451 | | ------- | ------------------------------------------------------------------------- | 452 | | title | string | 453 | | texture | module:pex-context~texture \| CanvasImageSource | 454 | | options | [GUIControlOptions](#GUIControlOptions) | 455 | 456 | **Example** 457 | 458 | ```js 459 | gui.addTexture2D("Single", image); 460 | ``` 461 | 462 | 463 | 464 | ### guI.addTextureCube(title, texture, options) ⇒ GUIControl 465 | 466 | Add a cube texture visualiser (from pex-context). 467 | Notes: texture cannot be updated once created. 468 | 469 | **Kind**: instance method of [GUI](#GUI) 470 | 471 | | Param | Type | 472 | | ------- | ------------------------------------------- | 473 | | title | string | 474 | | texture | module:pex-context~textureCube | 475 | | options | Object | 476 | 477 | **Example** 478 | 479 | ```js 480 | gui.addTextureCube("Cube", State.cubeTexture, { level: 2 }); 481 | ``` 482 | 483 | 484 | 485 | ### guI.addGraph(title, options) ⇒ GUIControl 486 | 487 | Add a XY graph visualiser from the control values. 488 | 489 | **Kind**: instance method of [GUI](#GUI) 490 | 491 | | Param | Type | 492 | | ------- | ---------------------------------------------------- | 493 | | title | string | 494 | | options | [GUIControlOptions](#GUIControlOptions) | 495 | 496 | **Example** 497 | 498 | ```js 499 | gui.addGraph("Sin", { 500 | interval: 500, 501 | t: 0, 502 | update(item) { 503 | item.options.t += 0.01; 504 | }, 505 | redraw(item) { 506 | item.values.push(+Math.sin(item.options.t).toFixed(3)); 507 | }, 508 | }); 509 | ``` 510 | 511 | 512 | 513 | ### guI.addFPSMeeter() ⇒ GUIControl 514 | 515 | Add a FPS counter. Need "gui.draw()" to be called on frame. 516 | 517 | **Kind**: instance method of [GUI](#GUI) 518 | 519 | 520 | ### guI.addStats(title, [options]) ⇒ GUIControl 521 | 522 | Add an updatable object stats visualiser. 523 | 524 | **Kind**: instance method of [GUI](#GUI) 525 | 526 | | Param | Type | Description | 527 | | --------- | ------------------- | ------------------------------------------------------------ | 528 | | title | string | | 529 | | [options] | object | An object with an update() function to update control.stats. | 530 | 531 | 532 | 533 | ### guI.draw() 534 | 535 | Renders the GUI. Should be called at the end of the frame. 536 | 537 | **Kind**: instance method of [GUI](#GUI) 538 | 539 | 540 | ### guI.serialize() ⇒ object 541 | 542 | Retrieve a serialized value of the current GUI's state. 543 | 544 | **Kind**: instance method of [GUI](#GUI) 545 | 546 | 547 | ### guI.deserialize(data) 548 | 549 | Deserialize a previously serialized data state GUI's state. 550 | 551 | **Kind**: instance method of [GUI](#GUI) 552 | 553 | | Param | Type | 554 | | ----- | ------------------- | 555 | | data | object | 556 | 557 | 558 | 559 | ### guI.dispose() 560 | 561 | Remove events listeners, empty list of controls and dispose of the gui's resources. 562 | 563 | **Kind**: instance method of [GUI](#GUI) 564 | 565 | 566 | ## GUIControlOptions : object 567 | 568 | **Kind**: global typedef 569 | **Properties** 570 | 571 | | Name | Type | Default | Description | 572 | | ------------ | ------------------------------ | -------------- | ----------------------------------------------------------------------------------- | 573 | | [min] | number | 0 | | 574 | | [max] | number | 0 | | 575 | | [type] | "color" | | Interpret an array as color. | 576 | | [colorSpace] | string | | Display a color as values of a pex-color color space. | 577 | | [alpha] | boolean | | Add a 4th slider for colors. | 578 | | [palette] | HTMLImageElement | | Draw a palette image as color picker. | 579 | | [flipEnvMap] | boolean | | Should be 1 for dynamic cubemaps and -1 for cubemaps from file with X axis flipped. | 580 | | [flipY] | boolean | | Flip texture 2D vertically. | 581 | | [level] | number | | Level of detail for cube textures. | 582 | 583 | 584 | 585 | ## GUIOptions : object 586 | 587 | **Kind**: global typedef 588 | **Properties** 589 | 590 | | Name | Type | Default | Description | 591 | | ------------ | -------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- | 592 | | [pixelRatio] | boolean | window.devicePixelRatio | | 593 | | [theme] | boolean | {} | See [theme file](https://github.com/pex-gl/pex-gui/blob/main/theme.js) for all options. | 594 | | [scale] | number | 1 | | 595 | | [responsive] | boolean | true | Adapts to canvas dimension. | 596 | 597 | 598 | 599 | ## ctx : module:pex-context~ctx 600 | 601 | **Kind**: global typedef 602 | 603 | 604 | 605 | ## License 606 | 607 | MIT. See [license file](https://github.com/pex-gl/pex-gui/blob/main/LICENSE.md). 608 | -------------------------------------------------------------------------------- /examples/all-controls.js: -------------------------------------------------------------------------------- 1 | import { load } from "pex-io"; 2 | 3 | export default async function addAllControls(gui, ctx) { 4 | const res = await load({ 5 | palette: { image: `examples/assets/palette.jpg` }, 6 | paletteHsl: { image: `examples/assets/palette-hsl.png` }, 7 | plask: { image: `examples/assets/plask.png` }, 8 | pex: { image: `examples/assets/pex.png` }, 9 | noise: { image: `examples/assets/noise.png` }, 10 | posx: { image: `examples/assets/pisa/pisa_posx.jpg` }, 11 | negx: { image: `examples/assets/pisa/pisa_negx.jpg` }, 12 | posy: { image: `examples/assets/pisa/pisa_posy.jpg` }, 13 | negy: { image: `examples/assets/pisa/pisa_negy.jpg` }, 14 | posz: { image: `examples/assets/pisa/pisa_posz.jpg` }, 15 | negz: { image: `examples/assets/pisa/pisa_negz.jpg` }, 16 | }); 17 | 18 | const isPexGl = ctx.gl; 19 | 20 | const images = [res.plask, res.pex, res.noise]; 21 | 22 | const State = { 23 | currentRadioListChoice: 0, 24 | radioListChoices: ["Choice 1", "Choice 2", "Choice 3"].map( 25 | (name, value) => ({ 26 | name, 27 | value, 28 | }), 29 | ), 30 | checkboxValue: false, 31 | message: "Message", 32 | range: 0, 33 | position: [2, 0], 34 | rgb: [0.92, 0.2, 0.2], 35 | rgba: [0.2, 0.92, 0.2, 1.0], 36 | palette: Float32Array.of(0.2, 0.2, 0.92, 1.0), 37 | paletteHsl: Float32Array.of(0.92, 0.2, 0.92, 1.0), 38 | cubeTexture: isPexGl 39 | ? ctx.textureCube({ 40 | data: [res.posx, res.negx, res.posy, res.negy, res.posz, res.negz], 41 | width: 64, 42 | height: 64, 43 | }) 44 | : null, 45 | currentTexture: 0, 46 | textures: isPexGl 47 | ? images.map((image) => 48 | ctx.texture2D({ 49 | data: image, 50 | width: image.width, 51 | height: image.height, 52 | flipY: true, 53 | wrap: ctx.Wrap.Repeat, 54 | encoding: ctx.Encoding.SRGB, 55 | mipmap: true, 56 | min: ctx.Filter.LinearMipmapLinear, 57 | aniso: 16, 58 | }), 59 | ) 60 | : images, 61 | }; 62 | 63 | // Controls 64 | gui.addTab("Controls"); 65 | gui.addColumn("Inputs"); 66 | gui.addLabel("Special Parameters"); 67 | gui.addLabel("Multiline\nLabel"); 68 | gui.addButton("Button", () => { 69 | console.log("Called back"); 70 | }); 71 | gui.addRadioList( 72 | "Radio list", 73 | State, 74 | "currentRadioListChoice", 75 | State.radioListChoices, 76 | ); 77 | 78 | gui.addSeparator(); 79 | gui.addLabel("Smart Parameters"); 80 | gui.addParam("Checkbox", State, "checkboxValue"); 81 | gui.addParam("Text message", State, "message", {}, (value) => { 82 | console.log(value); 83 | }); 84 | gui.addParam("Slider", State, "range", { 85 | min: -Math.PI / 2, 86 | max: Math.PI / 2, 87 | }); 88 | gui.addParam("Multi Slider", State, "position", { 89 | min: 0, 90 | max: 10, 91 | }); 92 | 93 | gui.addColumn("Colors"); 94 | gui.addParam("Color", State, "rgb", { 95 | type: "color", 96 | }); 97 | gui.addParam("Color alpha", State, "rgba", { 98 | type: "color", 99 | alpha: true, 100 | }); 101 | gui.addParam("Palette", State, "palette", { 102 | type: "color", 103 | palette: res.palette, 104 | }); 105 | gui.addParam("Palette HSL", State, "paletteHsl", { 106 | type: "color", 107 | palette: res.paletteHsl, 108 | }); 109 | 110 | gui.addColumn("Textures"); 111 | gui.addTexture2D("Single", State.textures[0]); // or gui.addParam("Single", State, "texture"); 112 | gui.addTexture2D("Single flipped", State.textures[0], { flipY: true }); 113 | gui.addTexture2DList( 114 | "List", 115 | State, 116 | "currentTexture", 117 | State.textures.map((texture, value) => ({ 118 | texture, 119 | value, 120 | })), 121 | ); 122 | if (isPexGl) gui.addTextureCube("Cube", State.cubeTexture, { level: 2 }); // gui.addParam("Cube", State, "cubeTexture", { level: 2 }); 123 | 124 | gui.addColumn("Graphs"); 125 | gui.addGraph("Sin", { 126 | interval: 500, 127 | t: 0, 128 | update(item) { 129 | item.options.t += 0.01; 130 | }, 131 | redraw(item) { 132 | item.values.push(Math.sin(item.options.t)); 133 | }, 134 | format: (value) => value?.toFixed(3) || "", 135 | }); 136 | gui.addFPSMeeter(); 137 | gui.addHeader("Stats"); 138 | gui.addStats(); 139 | gui.addStats("Object stats", { 140 | update(item) { 141 | Object.assign(item.stats, { 142 | r: State.rgb[0], 143 | g: State.rgb[1], 144 | b: State.rgb[2], 145 | }); 146 | }, 147 | }); 148 | 149 | return { State, res }; 150 | } 151 | -------------------------------------------------------------------------------- /examples/assets/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/noise.png -------------------------------------------------------------------------------- /examples/assets/palette-hsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/palette-hsl.png -------------------------------------------------------------------------------- /examples/assets/palette.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/palette.jpg -------------------------------------------------------------------------------- /examples/assets/pex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pex.png -------------------------------------------------------------------------------- /examples/assets/pisa/pisa.txt: -------------------------------------------------------------------------------- 1 | http://gl.ict.usc.edu/Data/HighResProbes/ 2 | 3 | Google Maps location 4 | https://www.google.com/maps/@43.7223952,10.3982855,3a,75y,94.86t/data=!3m6!1e1!3m4!1sMbQ_X8y3hq31NwsRCx7T7Q!2e0!7i13312!8i6656 -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_negx.jpg -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_negy.jpg -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_negz.jpg -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_posx.jpg -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_posy.jpg -------------------------------------------------------------------------------- /examples/assets/pisa/pisa_posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/pisa/pisa_posz.jpg -------------------------------------------------------------------------------- /examples/assets/plask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/plask.png -------------------------------------------------------------------------------- /examples/assets/rainbow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/examples/assets/rainbow.jpg -------------------------------------------------------------------------------- /examples/canvas-2d.js: -------------------------------------------------------------------------------- 1 | import { toHex } from "pex-color"; 2 | import createGUI from "../index.js"; 3 | import addAllControls from "./all-controls.js"; 4 | 5 | const canvas = document.createElement("canvas"); 6 | 7 | const ctx = canvas.getContext("2d"); 8 | document.querySelector("main").appendChild(canvas); 9 | 10 | const gui = createGUI(ctx); 11 | 12 | window.gui = gui; 13 | 14 | const { State } = await addAllControls(gui, ctx); 15 | 16 | window.State = State; 17 | 18 | requestAnimationFrame(function frame() { 19 | const W = ctx.canvas.width; 20 | const H = ctx.canvas.height; 21 | ctx.clearRect(0, 0, W, H); 22 | ctx.fillStyle = "#333"; 23 | ctx.fillRect(0, 0, W, H); 24 | 25 | ctx.fillStyle = toHex(State.rgb); 26 | ctx.fillRect(W * 0.5 - 100, H * 0.5 - 100, 200, 200); 27 | gui.draw(); 28 | requestAnimationFrame(frame); 29 | }); 30 | 31 | const onResize = () => { 32 | const W = window.innerWidth; 33 | const H = window.innerHeight; 34 | canvas.width = W * devicePixelRatio; 35 | canvas.height = H * devicePixelRatio; 36 | canvas.style.width = `${W}px`; 37 | canvas.style.height = `${H}px`; 38 | }; 39 | 40 | window.addEventListener("resize", onResize); 41 | 42 | onResize(); 43 | -------------------------------------------------------------------------------- /examples/debug.js: -------------------------------------------------------------------------------- 1 | import createGUI, { DEFAULT_THEME, Renderers } from "../index.js"; 2 | import addAllControls from "./all-controls.js"; 3 | 4 | const canvas = document.createElement("canvas"); 5 | 6 | const ctx = canvas.getContext("2d"); 7 | document.querySelector("main").appendChild(canvas); 8 | 9 | const gui = createGUI(ctx, { 10 | renderer: new Renderers.DebugRenderer({ 11 | width: canvas.width / 3, 12 | height: canvas.height / 3, 13 | theme: DEFAULT_THEME, 14 | }), 15 | }); 16 | 17 | await addAllControls(gui, ctx); 18 | 19 | requestAnimationFrame(function frame() { 20 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 21 | ctx.fillStyle = "#333"; 22 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 23 | gui.draw(); 24 | requestAnimationFrame(frame); 25 | }); 26 | 27 | const onResize = () => { 28 | const W = window.innerWidth; 29 | const H = window.innerHeight; 30 | canvas.width = W * devicePixelRatio; 31 | canvas.height = H * devicePixelRatio; 32 | canvas.style.width = `${W}px`; 33 | canvas.style.height = `${H}px`; 34 | }; 35 | 36 | window.addEventListener("resize", onResize); 37 | 38 | onResize(); 39 | -------------------------------------------------------------------------------- /examples/options.js: -------------------------------------------------------------------------------- 1 | import createGUI from "../index.js"; 2 | import addAllControls from "./all-controls.js"; 3 | 4 | const guis = new Map(); 5 | 6 | Object.assign(document.querySelector("main").style, { 7 | position: "relative", 8 | padding: "0 20px", 9 | }); 10 | 11 | const DEFAULT_WIDTH = 800; 12 | const DEFAULT_HEIGHT = 600; 13 | const mainElement = document.querySelector("main"); 14 | 15 | // Helpers 16 | const addSeparator = () => 17 | mainElement.appendChild(document.createElement("hr")); 18 | 19 | const addButton = (title, cb) => { 20 | const buttonElement = document.createElement("button"); 21 | buttonElement.innerText = title; 22 | buttonElement.addEventListener("click", cb); 23 | mainElement.appendChild(buttonElement); 24 | }; 25 | const addText = (title, tagName = "h3") => { 26 | const titleElement = document.createElement(tagName); 27 | titleElement.innerHTML = title; 28 | mainElement.appendChild(titleElement); 29 | }; 30 | 31 | const addGUI = async ( 32 | title, 33 | options = {}, 34 | width = DEFAULT_WIDTH, 35 | height = DEFAULT_HEIGHT, 36 | pixelRatio = devicePixelRatio 37 | ) => { 38 | addText(title, "p"); 39 | 40 | const canvas = document.createElement("canvas"); 41 | mainElement.appendChild(canvas); 42 | 43 | const ctx = canvas.getContext("2d"); 44 | canvas.width = width * pixelRatio; 45 | canvas.height = height * pixelRatio; 46 | canvas.style.width = `${width}px`; 47 | canvas.style.height = `${height}px`; 48 | 49 | const gui = createGUI(ctx, options); 50 | guis.set(ctx, gui); 51 | 52 | await addAllControls(gui, ctx); 53 | }; 54 | 55 | // Content 56 | addText("Change all canvas dimensions (canvas.width and canvas.height)", "h3"); 57 | addButton("Divide all by two", () => { 58 | Array.from(guis.values()).forEach((ctx) => { 59 | ctx.canvas.width /= 2; 60 | ctx.canvas.height /= 2; 61 | }); 62 | }); 63 | addButton("Multiply all by two", () => { 64 | Array.from(guis.values()).forEach((ctx) => { 65 | ctx.canvas.width *= 2; 66 | ctx.canvas.height *= 2; 67 | }); 68 | }); 69 | 70 | addSeparator(); 71 | addText( 72 | "Default render at the same size regardless of the canvas size or pixel ratio unless it overflows the canvas viewport" 73 | ); 74 | await addGUI( 75 | "Default ({ responsive: true, scale: 1, pixelRatio: devicePixelRatio })" 76 | ); 77 | await addGUI( 78 | "Default with canvas at double size", 79 | undefined, 80 | DEFAULT_WIDTH * 2, 81 | DEFAULT_HEIGHT * 2 82 | ); 83 | await addGUI( 84 | "Default with canvas at half size", 85 | undefined, 86 | DEFAULT_WIDTH / 2, 87 | DEFAULT_HEIGHT / 2 88 | ); 89 | await addGUI( 90 | "Default with canvas dimensions at multiplied by a ratio of 1", 91 | undefined, 92 | undefined, 93 | undefined, 94 | 1 95 | ); 96 | await addGUI( 97 | "Default with canvas dimensions at multiplied by a ratio of 0.5", 98 | undefined, 99 | undefined, 100 | undefined, 101 | 0.5 102 | ); 103 | 104 | addSeparator(); 105 | addText("How big is the GUI"); 106 | 107 | await addGUI("Hardcoded scale at 2", { scale: 2 }); 108 | await addGUI("Hardcoded scale at 0.5", { scale: 0.5 }); 109 | 110 | addSeparator(); 111 | addText("How sharp things are"); 112 | 113 | await addGUI("Renderer at pixelRatio 1", { pixelRatio: 1 }); 114 | await addGUI("Renderer at pixelRatio 0.5", { pixelRatio: 0.5 }); 115 | 116 | addSeparator(); 117 | addText("No responsive doesn't change rendering size"); 118 | 119 | await addGUI("No responsive", { responsive: false }); 120 | await addGUI("No responsive and renderer at pixelRatio 0.5", { 121 | responsive: false, 122 | pixelRatio: 0.5, 123 | }); 124 | 125 | addSeparator(); 126 | addText("No responsive on small canvas will make the content overflow"); 127 | 128 | await addGUI( 129 | "No responsive overflowing", 130 | { responsive: false }, 131 | DEFAULT_WIDTH / 2, 132 | DEFAULT_HEIGHT / 2 133 | ); 134 | await addGUI( 135 | "No responsive overflowing and renderer at pixelRatio 0.5", 136 | { 137 | responsive: false, 138 | pixelRatio: 0.5, 139 | }, 140 | DEFAULT_WIDTH / 2, 141 | DEFAULT_HEIGHT / 2 142 | ); 143 | 144 | addSeparator(); 145 | addText("Scale can compensate for non-responsive GUI"); 146 | 147 | await addGUI( 148 | "No responsive, hardcoded scale at 0.5", 149 | { responsive: false, scale: 0.5 }, 150 | DEFAULT_WIDTH / 2, 151 | DEFAULT_HEIGHT / 2 152 | ); 153 | await addGUI( 154 | "No responsive, hardcoded scale at 0.5 and renderer at pixelRatio 0.5", 155 | { 156 | responsive: false, 157 | scale: 0.5, 158 | pixelRatio: 0.5, 159 | }, 160 | DEFAULT_WIDTH / 2, 161 | DEFAULT_HEIGHT / 2 162 | ); 163 | 164 | requestAnimationFrame(function frame() { 165 | Array.from(guis.entries()).forEach(([ctx, gui]) => { 166 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 167 | ctx.fillStyle = "#333"; 168 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 169 | gui.draw(); 170 | }); 171 | requestAnimationFrame(frame); 172 | }); 173 | -------------------------------------------------------------------------------- /examples/pex-context.js: -------------------------------------------------------------------------------- 1 | import createContext from "pex-context"; 2 | import { perspective as createCamera, orbiter as createOrbiter } from "pex-cam"; 3 | import { mat4, quat } from "pex-math"; 4 | import { torus as createTorus, cube as createCube } from "primitive-geometry"; 5 | 6 | import createGUI from "../index.js"; 7 | 8 | import allControls from "./all-controls.js"; 9 | 10 | const ExampleState = { 11 | scale: 1, 12 | rotate: false, 13 | time: 0, 14 | size: [1, 0.2], 15 | rotation: [0, 0, 0], 16 | bgColor: [0.2, 0.2, 0.2, 1.0], 17 | currentGeometry: 0, 18 | geometries: [], 19 | }; 20 | 21 | const vert = /* glsl */ ` 22 | attribute vec2 aTexCoord; 23 | attribute vec3 aPosition; 24 | uniform mat4 uProjectionMatrix; 25 | uniform mat4 uViewMatrix; 26 | uniform mat4 uModelMatrix; 27 | varying vec2 vTexCoord; 28 | void main() { 29 | vTexCoord = aTexCoord; 30 | gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0); 31 | } 32 | `; 33 | 34 | const frag = /* glsl */ ` 35 | #ifdef GL_ES 36 | precision mediump float; 37 | #endif 38 | 39 | varying vec2 vTexCoord; 40 | uniform sampler2D uTexture; 41 | uniform vec2 uRepeat; 42 | void main() { 43 | gl_FragColor = texture2D(uTexture, vTexCoord * uRepeat); 44 | }`; 45 | 46 | const ctx = createContext({ pixelRatio: devicePixelRatio }); 47 | 48 | const gui = createGUI(ctx); 49 | 50 | const camera = createCamera({ 51 | fov: Math.PI / 4, 52 | aspect: ctx.gl.drawingBufferWidth / ctx.gl.drawingBufferHeight, 53 | near: 0.1, 54 | far: 100, 55 | position: [3, 3, -3], 56 | target: [0, 0, 0], 57 | }); 58 | 59 | const orbiter = createOrbiter({ camera }); 60 | 61 | const { State, res } = await allControls(gui, ctx); 62 | 63 | // Example 64 | gui.addTab("Example"); 65 | gui.addColumn("Settings"); 66 | gui.addParam("Scale", ExampleState, "scale", { min: 0.1, max: 2 }); 67 | gui.addParam("Rotate camera", ExampleState, "rotate"); 68 | gui.addParam("Rotation", ExampleState, "rotation", { 69 | min: -Math.PI / 2, 70 | max: Math.PI / 2, 71 | }); 72 | gui.addHeader("Color"); 73 | gui.addParam("BG Color [RGBA]", ExampleState, "bgColor"); 74 | gui.addParam("BG Color [HSB]", ExampleState, "bgColor", { 75 | type: "color", 76 | palette: res.paletteHsl, 77 | }); 78 | 79 | gui.addColumn("Geometry"); 80 | gui.addRadioList( 81 | "Type", 82 | ExampleState, 83 | "currentGeometry", 84 | ExampleState.geometries 85 | ); 86 | gui.addParam("Torus Size", ExampleState, "size", { min: 0.1, max: 2 }, () => { 87 | const torus = createTorus({ 88 | minorRadius: ExampleState.size[1], 89 | radius: ExampleState.size[0], 90 | }); 91 | ctx.update(ExampleState.geometries[1].attributes.aPosition, { 92 | data: torus.positions, 93 | }); 94 | }); 95 | 96 | gui.addColumn("Texture"); 97 | gui.addTexture2D("Default", State.textures[1]); 98 | gui.addTexture2DList( 99 | "Default", 100 | State, 101 | "currentTexture", 102 | State.textures.map((tex, index) => { 103 | return { texture: tex, value: index }; 104 | }) 105 | ); 106 | 107 | const clearCmd = { 108 | pass: ctx.pass({ 109 | clearColor: ExampleState.bgColor, 110 | clearDepth: 1, 111 | }), 112 | }; 113 | 114 | const cube = createCube(); 115 | ExampleState.geometries.push({ 116 | name: "Cube", 117 | value: 0, 118 | attributes: { 119 | aPosition: ctx.vertexBuffer(cube.positions), 120 | aTexCoord: ctx.vertexBuffer(cube.uvs), 121 | }, 122 | indices: ctx.indexBuffer(cube.cells), 123 | }); 124 | 125 | const torus = createTorus(); 126 | ExampleState.geometries.push({ 127 | name: "Torus", 128 | value: 1, 129 | attributes: { 130 | aPosition: ctx.vertexBuffer(torus.positions), 131 | aTexCoord: ctx.vertexBuffer(torus.uvs), 132 | }, 133 | indices: ctx.indexBuffer(torus.cells), 134 | }); 135 | 136 | const modelMatrix = mat4.create(); 137 | const rotationQuat = quat.create(); 138 | 139 | const drawCmd = { 140 | name: "drawCmd", 141 | pipeline: ctx.pipeline({ 142 | vert, 143 | frag, 144 | depthTest: true, 145 | }), 146 | uniforms: { 147 | uProjectionMatrix: camera.projectionMatrix, 148 | uViewMatrix: camera.viewMatrix, 149 | uModelMatrix: modelMatrix, 150 | uRepeat: [1, 1], 151 | }, 152 | }; 153 | 154 | let frameNumber = 0; 155 | ctx.frame(() => { 156 | ctx.debug(frameNumber++ === 10); 157 | ctx.submit(clearCmd); 158 | 159 | quat.fromEuler(rotationQuat, ExampleState.rotation); 160 | mat4.fromQuat(modelMatrix, rotationQuat); 161 | mat4.scale(modelMatrix, [ 162 | ExampleState.scale, 163 | ExampleState.scale, 164 | ExampleState.scale, 165 | ]); 166 | 167 | if (ExampleState.rotate) { 168 | ExampleState.time += 1 / 60; 169 | camera.set({ 170 | position: [ 171 | Math.cos(ExampleState.time * Math.PI) * 5, 172 | Math.sin(ExampleState.time * 0.5) * 3, 173 | Math.sin(ExampleState.time * Math.PI) * 5, 174 | ], 175 | }); 176 | orbiter.set({ camera }); 177 | } 178 | 179 | if (State.textures.length > 0) { 180 | ctx.submit(drawCmd, { 181 | attributes: 182 | ExampleState.geometries[ExampleState.currentGeometry].attributes, 183 | indices: ExampleState.geometries[ExampleState.currentGeometry].indices, 184 | uniforms: { 185 | uTexture: State.textures[State.currentTexture], 186 | }, 187 | }); 188 | } 189 | 190 | gui.draw(); 191 | }); 192 | 193 | const onResize = () => { 194 | const W = window.innerWidth; 195 | const H = window.innerHeight; 196 | ctx.set({ 197 | width: W, 198 | height: H, 199 | }); 200 | camera.set({ 201 | aspect: W / H, 202 | }); 203 | }; 204 | 205 | window.addEventListener("resize", onResize); 206 | 207 | onResize(); 208 | -------------------------------------------------------------------------------- /examples/theme.js: -------------------------------------------------------------------------------- 1 | import createGUI from "../index.js"; 2 | import addAllControls from "./all-controls.js"; 3 | 4 | const canvas = document.createElement("canvas"); 5 | canvas.width = window.innerWidth * devicePixelRatio; 6 | canvas.height = window.innerHeight * devicePixelRatio; 7 | canvas.style.width = `${window.innerWidth}px`; 8 | canvas.style.height = `${window.innerHeight}px`; 9 | 10 | const ctx = canvas.getContext("2d"); 11 | document.querySelector("main").appendChild(canvas); 12 | 13 | const gui = createGUI(ctx, { 14 | theme: { 15 | fontFamily: "Courier New", 16 | fontSize: 12, 17 | capHeight: 0.115, 18 | 19 | leftOffset: 0, 20 | topOffset: 0, 21 | 22 | columnWidth: 200, 23 | 24 | tabHeight: 26, 25 | headerSize: 22, 26 | 27 | titleHeight: 24, 28 | itemHeight: 30, 29 | graphHeight: 60, 30 | 31 | padding: 1, 32 | textPadding: 1, 33 | accent: "rgba(255, 0, 0, 1)", 34 | tabColorActive: "rgba(255, 255, 255, 1)", 35 | tabBackgroundActive: "rgba(0, 0, 255, 1)", 36 | }, 37 | }); 38 | 39 | gui.scale = 1.2; 40 | gui.x = 100; 41 | gui.y = 100; 42 | 43 | await addAllControls(gui, ctx); 44 | 45 | requestAnimationFrame(function frame() { 46 | ctx.fillStyle = "#333"; 47 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 48 | gui.draw(); 49 | requestAnimationFrame(frame); 50 | }); 51 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pex-gui by pex-gl (https://github.com/pex-gl) 8 | 56 | 57 | 58 |
59 | 69 | 70 | 71 | 72 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pex-gui", 3 | "version": "3.1.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "pex-gui", 9 | "version": "3.1.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "pex-color": "^2.2.0", 13 | "pex-geom": "^3.0.2", 14 | "pex-math": "^4.1.1" 15 | }, 16 | "devDependencies": { 17 | "es-module-shims": "2.0.10", 18 | "pex-cam": "^3.0.0-alpha.1", 19 | "pex-context": "^3.2.0", 20 | "pex-io": "^3.0.2", 21 | "primitive-geometry": "^2.10.1" 22 | }, 23 | "engines": { 24 | "node": ">=22.0.0", 25 | "npm": ">=10.5.1", 26 | "snowdev": ">=2.3.x" 27 | } 28 | }, 29 | "node_modules/es-module-shims": { 30 | "version": "2.0.10", 31 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-2.0.10.tgz", 32 | "integrity": "sha512-Ub1xUe9v97lKsSnTMfCJvnX6dSmJ3nRVWHpH1szP0RqmLliiBGQiUXLPkz1b0uh6LnPjUdTtND1YjBT+VIuozw==", 33 | "dev": true, 34 | "license": "MIT" 35 | }, 36 | "node_modules/interpolate-angle": { 37 | "version": "1.0.2", 38 | "resolved": "https://registry.npmjs.org/interpolate-angle/-/interpolate-angle-1.0.2.tgz", 39 | "integrity": "sha1-q6mSFyJy4ucJOMlTC0HcNcV+5do=", 40 | "dev": true, 41 | "dependencies": { 42 | "lerp": "^1.0.3" 43 | } 44 | }, 45 | "node_modules/latlon-to-xyz": { 46 | "version": "1.0.1", 47 | "resolved": "https://registry.npmjs.org/latlon-to-xyz/-/latlon-to-xyz-1.0.1.tgz", 48 | "integrity": "sha512-OF65aE60Y9aMOibxWFC2Vl7EN2BOtMKabm0t97+VaVgR9LoStZHbNNMc8dS/LG6iE1BpEXmnoH85wWr1drp/Tw==", 49 | "dev": true 50 | }, 51 | "node_modules/lerp": { 52 | "version": "1.0.3", 53 | "resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz", 54 | "integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24=", 55 | "dev": true 56 | }, 57 | "node_modules/mouse-event-offset": { 58 | "version": "3.0.2", 59 | "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", 60 | "integrity": "sha1-39hqbiSMa6jK1TuQXVA3ogY+mYQ=", 61 | "dev": true 62 | }, 63 | "node_modules/pex-cam": { 64 | "version": "3.0.0-alpha.1", 65 | "resolved": "https://registry.npmjs.org/pex-cam/-/pex-cam-3.0.0-alpha.1.tgz", 66 | "integrity": "sha512-mVDMzz5cyGA2em7k/fq9Yq9inO1hRputz4poeiXKdzydUh+EHXVvx3gzNyeXez2BTfV0TLpuoCgoLYLc5b4Ifw==", 67 | "dev": true, 68 | "dependencies": { 69 | "interpolate-angle": "^1.0.2", 70 | "latlon-to-xyz": "^1.0.1", 71 | "mouse-event-offset": "^3.0.2", 72 | "pex-geom": "^3.0.0-alpha.0", 73 | "pex-math": "^4.0.0-alpha.1", 74 | "xyz-to-latlon": "^1.0.2" 75 | }, 76 | "engines": { 77 | "node": ">=16.0.0", 78 | "npm": ">=7.0.0" 79 | } 80 | }, 81 | "node_modules/pex-color": { 82 | "version": "2.2.0", 83 | "resolved": "https://registry.npmjs.org/pex-color/-/pex-color-2.2.0.tgz", 84 | "integrity": "sha512-eZgxo99JIvcTTrmvgXwRJgGNdkDTIgs03m2y1Q/aMA2TVD9fK2tyCetVQmcbAlwt94cmg5go59gzNjjnPF/01A==", 85 | "license": "MIT", 86 | "engines": { 87 | "node": ">=22.0.0", 88 | "npm": ">=10.5.1", 89 | "snowdev": ">=2.2.x" 90 | } 91 | }, 92 | "node_modules/pex-context": { 93 | "version": "3.2.0", 94 | "resolved": "https://registry.npmjs.org/pex-context/-/pex-context-3.2.0.tgz", 95 | "integrity": "sha512-yM7XS0SCbWWM/QbonhNnwVxD58ZDjcOJ1VwONeJGj0kFoMHkJskoAFSJH3i4fGAEte6YiHt361LX9D/waP1IAw==", 96 | "dev": true, 97 | "license": "MIT", 98 | "dependencies": { 99 | "pex-gl": "^3.0.2" 100 | }, 101 | "engines": { 102 | "node": ">=22.0.0", 103 | "npm": ">=10.5.1", 104 | "snowdev": ">=2.3.x" 105 | } 106 | }, 107 | "node_modules/pex-geom": { 108 | "version": "3.0.2", 109 | "resolved": "https://registry.npmjs.org/pex-geom/-/pex-geom-3.0.2.tgz", 110 | "integrity": "sha512-jLx3ISMMrVuDouPvkmN1hLTqeJE/45otdNVIuoMHKxcz7LZb8k9+XuV7VmsAPA+/TySJyvO+gVvFopUVVG40jg==", 111 | "license": "MIT", 112 | "dependencies": { 113 | "pex-math": "^4.1.1" 114 | }, 115 | "engines": { 116 | "node": ">=22.0.0", 117 | "npm": ">=10.5.1", 118 | "snowdev": ">=2.2.x" 119 | } 120 | }, 121 | "node_modules/pex-gl": { 122 | "version": "3.0.2", 123 | "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.2.tgz", 124 | "integrity": "sha512-q0MEUXyRlXEa7A/QGQn7GvrkLf+Yc+tRpTRFW0gmbG1RnJpKC0U504WYaB9O47OoMmy9LlKGCaolP2cJJUw5gQ==", 125 | "dev": true, 126 | "license": "MIT", 127 | "engines": { 128 | "node": ">=22.0.0", 129 | "npm": ">=10.5.1", 130 | "snowdev": ">=2.2.x" 131 | } 132 | }, 133 | "node_modules/pex-io": { 134 | "version": "3.0.2", 135 | "resolved": "https://registry.npmjs.org/pex-io/-/pex-io-3.0.2.tgz", 136 | "integrity": "sha512-9lVsogyCEVqUZ/qQ2Yv1Yr6ATRMpgsuUF4UGsOA6vKVkekOHCFlQ71B6m3zQll5HgZmPUw8QM9yrQkcmjE05EA==", 137 | "dev": true, 138 | "license": "MIT", 139 | "engines": { 140 | "node": ">=22.0.0", 141 | "npm": ">=10.5.1", 142 | "snowdev": ">=2.2.x" 143 | } 144 | }, 145 | "node_modules/pex-math": { 146 | "version": "4.1.1", 147 | "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-4.1.1.tgz", 148 | "integrity": "sha512-jhFPZfgdhUczx8EJsV7PwFTDjvhDMhNiabtK+rOLsfkWl33LNV97hUYB0URlMiTf9UR4nNpPcTLuha96aniyQg==", 149 | "license": "MIT", 150 | "engines": { 151 | "node": ">=22.0.0", 152 | "npm": ">=10.5.1", 153 | "snowdev": ">=2.2.x" 154 | } 155 | }, 156 | "node_modules/primitive-geometry": { 157 | "version": "2.10.1", 158 | "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.10.1.tgz", 159 | "integrity": "sha512-nfWIrVugpis6O9f/itBAo0qS69/VBGyZtxyyCpjgWeOWshAUAkmr/hEl80AlSK4WFnaKhdratnKCxhaq8ImPUQ==", 160 | "dev": true, 161 | "funding": [ 162 | { 163 | "type": "individual", 164 | "url": "https://paypal.me/dmnsgn" 165 | }, 166 | { 167 | "type": "individual", 168 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 169 | } 170 | ], 171 | "license": "MIT", 172 | "engines": { 173 | "node": ">=15.0.0", 174 | "npm": ">=7.0.0" 175 | } 176 | }, 177 | "node_modules/xyz-to-latlon": { 178 | "version": "1.0.2", 179 | "resolved": "https://registry.npmjs.org/xyz-to-latlon/-/xyz-to-latlon-1.0.2.tgz", 180 | "integrity": "sha512-iTQe5HIzAE0r2L2u2abs17s/SekCCUAlIHzYnHHo10aR0B1iKRSGye33z7a52IMJoUUMTpPwNUTNIy+OhZVRIg==", 181 | "dev": true 182 | } 183 | }, 184 | "dependencies": { 185 | "es-module-shims": { 186 | "version": "2.0.10", 187 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-2.0.10.tgz", 188 | "integrity": "sha512-Ub1xUe9v97lKsSnTMfCJvnX6dSmJ3nRVWHpH1szP0RqmLliiBGQiUXLPkz1b0uh6LnPjUdTtND1YjBT+VIuozw==", 189 | "dev": true 190 | }, 191 | "interpolate-angle": { 192 | "version": "1.0.2", 193 | "resolved": "https://registry.npmjs.org/interpolate-angle/-/interpolate-angle-1.0.2.tgz", 194 | "integrity": "sha1-q6mSFyJy4ucJOMlTC0HcNcV+5do=", 195 | "dev": true, 196 | "requires": { 197 | "lerp": "^1.0.3" 198 | } 199 | }, 200 | "latlon-to-xyz": { 201 | "version": "1.0.1", 202 | "resolved": "https://registry.npmjs.org/latlon-to-xyz/-/latlon-to-xyz-1.0.1.tgz", 203 | "integrity": "sha512-OF65aE60Y9aMOibxWFC2Vl7EN2BOtMKabm0t97+VaVgR9LoStZHbNNMc8dS/LG6iE1BpEXmnoH85wWr1drp/Tw==", 204 | "dev": true 205 | }, 206 | "lerp": { 207 | "version": "1.0.3", 208 | "resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz", 209 | "integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24=", 210 | "dev": true 211 | }, 212 | "mouse-event-offset": { 213 | "version": "3.0.2", 214 | "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", 215 | "integrity": "sha1-39hqbiSMa6jK1TuQXVA3ogY+mYQ=", 216 | "dev": true 217 | }, 218 | "pex-cam": { 219 | "version": "3.0.0-alpha.1", 220 | "resolved": "https://registry.npmjs.org/pex-cam/-/pex-cam-3.0.0-alpha.1.tgz", 221 | "integrity": "sha512-mVDMzz5cyGA2em7k/fq9Yq9inO1hRputz4poeiXKdzydUh+EHXVvx3gzNyeXez2BTfV0TLpuoCgoLYLc5b4Ifw==", 222 | "dev": true, 223 | "requires": { 224 | "interpolate-angle": "^1.0.2", 225 | "latlon-to-xyz": "^1.0.1", 226 | "mouse-event-offset": "^3.0.2", 227 | "pex-geom": "^3.0.0-alpha.0", 228 | "pex-math": "^4.0.0-alpha.1", 229 | "xyz-to-latlon": "^1.0.2" 230 | } 231 | }, 232 | "pex-color": { 233 | "version": "2.2.0", 234 | "resolved": "https://registry.npmjs.org/pex-color/-/pex-color-2.2.0.tgz", 235 | "integrity": "sha512-eZgxo99JIvcTTrmvgXwRJgGNdkDTIgs03m2y1Q/aMA2TVD9fK2tyCetVQmcbAlwt94cmg5go59gzNjjnPF/01A==" 236 | }, 237 | "pex-context": { 238 | "version": "3.2.0", 239 | "resolved": "https://registry.npmjs.org/pex-context/-/pex-context-3.2.0.tgz", 240 | "integrity": "sha512-yM7XS0SCbWWM/QbonhNnwVxD58ZDjcOJ1VwONeJGj0kFoMHkJskoAFSJH3i4fGAEte6YiHt361LX9D/waP1IAw==", 241 | "dev": true, 242 | "requires": { 243 | "pex-gl": "^3.0.2" 244 | } 245 | }, 246 | "pex-geom": { 247 | "version": "3.0.2", 248 | "resolved": "https://registry.npmjs.org/pex-geom/-/pex-geom-3.0.2.tgz", 249 | "integrity": "sha512-jLx3ISMMrVuDouPvkmN1hLTqeJE/45otdNVIuoMHKxcz7LZb8k9+XuV7VmsAPA+/TySJyvO+gVvFopUVVG40jg==", 250 | "requires": { 251 | "pex-math": "^4.1.1" 252 | } 253 | }, 254 | "pex-gl": { 255 | "version": "3.0.2", 256 | "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.2.tgz", 257 | "integrity": "sha512-q0MEUXyRlXEa7A/QGQn7GvrkLf+Yc+tRpTRFW0gmbG1RnJpKC0U504WYaB9O47OoMmy9LlKGCaolP2cJJUw5gQ==", 258 | "dev": true 259 | }, 260 | "pex-io": { 261 | "version": "3.0.2", 262 | "resolved": "https://registry.npmjs.org/pex-io/-/pex-io-3.0.2.tgz", 263 | "integrity": "sha512-9lVsogyCEVqUZ/qQ2Yv1Yr6ATRMpgsuUF4UGsOA6vKVkekOHCFlQ71B6m3zQll5HgZmPUw8QM9yrQkcmjE05EA==", 264 | "dev": true 265 | }, 266 | "pex-math": { 267 | "version": "4.1.1", 268 | "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-4.1.1.tgz", 269 | "integrity": "sha512-jhFPZfgdhUczx8EJsV7PwFTDjvhDMhNiabtK+rOLsfkWl33LNV97hUYB0URlMiTf9UR4nNpPcTLuha96aniyQg==" 270 | }, 271 | "primitive-geometry": { 272 | "version": "2.10.1", 273 | "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.10.1.tgz", 274 | "integrity": "sha512-nfWIrVugpis6O9f/itBAo0qS69/VBGyZtxyyCpjgWeOWshAUAkmr/hEl80AlSK4WFnaKhdratnKCxhaq8ImPUQ==", 275 | "dev": true 276 | }, 277 | "xyz-to-latlon": { 278 | "version": "1.0.2", 279 | "resolved": "https://registry.npmjs.org/xyz-to-latlon/-/xyz-to-latlon-1.0.2.tgz", 280 | "integrity": "sha512-iTQe5HIzAE0r2L2u2abs17s/SekCCUAlIHzYnHHo10aR0B1iKRSGye33z7a52IMJoUUMTpPwNUTNIy+OhZVRIg==", 281 | "dev": true 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pex-gui", 3 | "version": "3.1.3", 4 | "description": "GUI controls for PEX.", 5 | "keywords": [ 6 | "pex", 7 | "gui", 8 | "webgl", 9 | "3d", 10 | "graphics", 11 | "ui", 12 | "controls" 13 | ], 14 | "homepage": "https://github.com/pex-gl/pex-gui", 15 | "bugs": "https://github.com/pex-gl/pex-gui/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/pex-gl/pex-gui.git" 19 | }, 20 | "license": "MIT", 21 | "author": "Marcin Ignac (http://marcinignac.com)", 22 | "contributors": [ 23 | "Damien Seguin (https://github.com/dmnsgn)" 24 | ], 25 | "type": "module", 26 | "exports": { 27 | ".": { 28 | "types": "./types/index.d.ts", 29 | "default": "./index.js" 30 | } 31 | }, 32 | "main": "index.js", 33 | "types": "types/index.d.ts", 34 | "scripts": { 35 | "build": "snowdev build", 36 | "dev": "snowdev dev", 37 | "release": "snowdev release" 38 | }, 39 | "dependencies": { 40 | "pex-color": "^2.2.0", 41 | "pex-geom": "^3.0.2", 42 | "pex-math": "^4.1.1" 43 | }, 44 | "devDependencies": { 45 | "es-module-shims": "2.0.10", 46 | "pex-cam": "^3.0.0-alpha.1", 47 | "pex-context": "^3.2.0", 48 | "pex-io": "^3.0.2", 49 | "primitive-geometry": "^2.10.1" 50 | }, 51 | "engines": { 52 | "node": ">=22.0.0", 53 | "npm": ">=10.5.1", 54 | "snowdev": ">=2.3.x" 55 | }, 56 | "snowdev": { 57 | "files": "{*.js,*(renderers|shaders)/**/*.js}" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /renderers/CanvasRenderer.js: -------------------------------------------------------------------------------- 1 | import { toHex } from "pex-color"; 2 | import { utils } from "pex-math"; 3 | 4 | function rectSet4(a, x, y, w, h) { 5 | a[0][0] = x; 6 | a[0][1] = y; 7 | a[1][0] = x + w; 8 | a[1][1] = y + h; 9 | return a; 10 | } 11 | 12 | function makePaletteImage(item, w, img) { 13 | const canvas = document.createElement("canvas"); 14 | canvas.width = w; 15 | canvas.height = (w * img.height) / img.width; 16 | const ctx = canvas.getContext("2d"); 17 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 18 | item.options.paletteImage = canvas; 19 | item.options.paletteImage.data = ctx.getImageData( 20 | 0, 21 | 0, 22 | canvas.width, 23 | canvas.height, 24 | ).data; 25 | item.options.paletteImage.aspectRatio = canvas.height / canvas.width; 26 | item.dirty = true; 27 | } 28 | 29 | class CanvasRenderer { 30 | constructor({ width, height, pixelRatio = devicePixelRatio, theme }) { 31 | this.pixelRatio = pixelRatio; 32 | this.theme = theme; 33 | 34 | this.canvas = document.createElement("canvas"); 35 | this.canvas.width = width * this.pixelRatio; 36 | this.canvas.height = height * this.pixelRatio; 37 | this.ctx = this.canvas.getContext("2d"); 38 | this.dirty = true; 39 | } 40 | 41 | draw(items) { 42 | this.dirty = false; 43 | 44 | const { 45 | fontFamily, 46 | fontSize, 47 | capHeight, 48 | leftOffset, 49 | topOffset, 50 | columnWidth, 51 | tabHeight, 52 | headerSize, 53 | titleHeight, 54 | itemHeight, 55 | graphHeight, 56 | padding, 57 | textPadding, 58 | } = this.theme; 59 | 60 | const sliderHeight = 0.7 * itemHeight; 61 | const buttonHeight = 1.2 * itemHeight; 62 | 63 | const ctx = this.ctx; 64 | 65 | ctx.save(); 66 | ctx.scale(this.pixelRatio, this.pixelRatio); 67 | ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 68 | 69 | const fontCapOffset = capHeight * fontSize; 70 | ctx.font = `${fontSize}px ${fontFamily}`; 71 | ctx.textBaseline = "middle"; 72 | 73 | let dx = leftOffset; 74 | let dy = topOffset; 75 | let w = columnWidth; 76 | const gap = padding; 77 | 78 | let cellSize = 0; 79 | let numRows = 0; 80 | let columnIndex = 0; 81 | const tabs = items.filter(({ type }) => type === "tab"); 82 | const defaultDy = tabs.length 83 | ? topOffset + tabHeight + padding * 3 84 | : topOffset; 85 | 86 | tabs.forEach((tab) => { 87 | ctx.fillStyle = this.theme.background; 88 | ctx.fillRect(dx, dy, w, tabHeight + padding * 2); 89 | 90 | ctx.fillStyle = tab.current 91 | ? this.theme.tabBackgroundActive 92 | : this.theme.tabBackground; 93 | 94 | const x = dx + padding; 95 | const y = dy + padding; 96 | const width = w - padding * 2; 97 | ctx.fillRect(x, y, width, tabHeight); 98 | 99 | if (!tab.current) { 100 | ctx.fillStyle = this.theme.background; 101 | ctx.fillRect(x, dy + tabHeight + padding / 2, width, padding / 2); 102 | } 103 | 104 | ctx.fillStyle = tab.current 105 | ? this.theme.tabColorActive 106 | : this.theme.tabColor; 107 | ctx.fillText( 108 | tab.title, 109 | x + textPadding, 110 | y + tabHeight / 2 + fontCapOffset, 111 | ); 112 | 113 | rectSet4(tab.activeArea, x, y, width, tabHeight); 114 | 115 | dx += w + gap; 116 | }); 117 | 118 | dx = leftOffset; 119 | 120 | let maxWidth = 0; 121 | let maxHeight = 0; 122 | let needInitialDy = true; 123 | for (let i = 0; i < items.length; i++) { 124 | const item = items[i]; 125 | 126 | if (Number.isFinite(item.x)) dx = item.x; 127 | if (Number.isFinite(item.y)) dy = item.y; 128 | 129 | let eh = itemHeight; 130 | if (item.type === "tab") continue; 131 | 132 | if (tabs.length > 0) { 133 | const prevTabs = items.filter( 134 | ({ type }, index) => index < i && type === "tab", 135 | ); 136 | const parentTab = prevTabs[prevTabs.length - 1]; 137 | if (parentTab && !parentTab.current) { 138 | continue; 139 | } else { 140 | if (needInitialDy && item.type !== "column") { 141 | needInitialDy = false; 142 | dy += tabHeight; 143 | } 144 | } 145 | needInitialDy = false; 146 | } 147 | 148 | const x = dx + padding; 149 | const width = w - padding * 2; 150 | const textY = titleHeight / 2 + fontCapOffset; 151 | 152 | // Compute item height 153 | if (item.type === "column") { 154 | dx = leftOffset + columnIndex * (w + gap); 155 | dy = defaultDy; 156 | w = item.width; 157 | columnIndex++; 158 | continue; 159 | } else if (item.type === "slider") { 160 | eh = titleHeight + sliderHeight; 161 | } else if (item.type === "toggle") { 162 | eh = padding + itemHeight; 163 | } else if (item.type === "multislider") { 164 | const numSliders = item.getValue().length; 165 | eh = 166 | titleHeight + numSliders * sliderHeight + (numSliders - 1) * padding; 167 | } else if (item.type === "color") { 168 | const numSliders = item.options.alpha ? 4 : 3; 169 | const sliderGap = item.options.paletteImage ? 0 : 1; 170 | eh = 171 | titleHeight + 172 | numSliders * sliderHeight + 173 | (numSliders - sliderGap) * padding; 174 | if (item.options.paletteImage) { 175 | eh += width * item.options.paletteImage.aspectRatio; 176 | } 177 | } else if (item.type === "button") { 178 | eh = padding + buttonHeight; 179 | } else if (item.type === "texture2D") { 180 | eh = titleHeight + (item.texture.height * width) / item.texture.width; 181 | } else if (item.type === "textureCube") { 182 | eh = titleHeight + width / 2; 183 | } else if (item.type === "radiolist") { 184 | eh = 185 | titleHeight + 186 | item.items.length * itemHeight + 187 | (item.items.length - 1) * padding * 2; 188 | } else if (item.type === "texturelist") { 189 | cellSize = Math.floor(width / item.itemsPerRow); 190 | numRows = Math.ceil(item.items.length / item.itemsPerRow); 191 | eh = titleHeight + numRows * cellSize; 192 | } else if (item.type === "header") { 193 | eh = padding + headerSize; 194 | } else if (item.type === "text") { 195 | eh = titleHeight + buttonHeight; 196 | } else if (item.type === "graph") { 197 | eh = titleHeight + graphHeight; 198 | } else if (item.type === "stats") { 199 | eh = 200 | Object.values(item.stats) 201 | .map((value) => String(value).split("\n").length) 202 | .reduce((a, b) => a + b, 0) * titleHeight; 203 | if (item.title !== "") eh += titleHeight; 204 | } else if (item.type === "label") { 205 | eh = item.title.split("\n").length * titleHeight; 206 | } 207 | 208 | const needsPadding = !["column", "label"].includes(item.type); 209 | 210 | // Draw background 211 | if (item.type === "separator") { 212 | eh /= 2; 213 | } else { 214 | ctx.fillStyle = this.theme.background; 215 | ctx.fillRect(dx, dy, w, eh + (needsPadding ? padding : 0)); 216 | } 217 | 218 | // Draw item 219 | if (item.type === "slider") { 220 | const y = dy + titleHeight; 221 | const height = eh - titleHeight; 222 | 223 | ctx.fillStyle = this.theme.color; 224 | ctx.fillText( 225 | `${item.title}: ${item.getStrValue()}`, 226 | x + textPadding, 227 | dy + textY, 228 | ); 229 | 230 | ctx.fillStyle = this.theme.input; 231 | ctx.fillRect(x, y, width, height); 232 | 233 | ctx.fillStyle = this.theme.accent; 234 | ctx.fillRect(x, y, width * item.getNormalizedValue(), height); 235 | 236 | rectSet4(item.activeArea, x, y, width, height); 237 | } else if (item.type === "multislider" || item.type === "color") { 238 | const isColor = item.type === "color"; 239 | const y = dy + titleHeight; 240 | const height = eh - titleHeight; 241 | const numSliders = isColor 242 | ? item.options.alpha 243 | ? 4 244 | : 3 245 | : item.getValue().length; 246 | 247 | ctx.fillStyle = this.theme.color; 248 | ctx.fillText( 249 | `${item.title}: ${item.getStrValue()}`, 250 | x + textPadding, 251 | dy + textY, 252 | ); 253 | 254 | for (let j = 0; j < numSliders; j++) { 255 | const sliderY = y + j * (sliderHeight + padding); 256 | ctx.fillStyle = this.theme.input; 257 | ctx.fillRect(x, sliderY, width, sliderHeight); 258 | 259 | ctx.fillStyle = this.theme.accent; 260 | ctx.fillRect( 261 | x, 262 | sliderY, 263 | width * item.getNormalizedValue(j), 264 | sliderHeight, 265 | ); 266 | } 267 | 268 | if (isColor) { 269 | const sqSize = titleHeight * 0.6; 270 | 271 | ctx.fillStyle = toHex(item.contextObject[item.attributeName]); 272 | ctx.fillRect( 273 | dx + w - sqSize - padding, 274 | dy + titleHeight * 0.2, 275 | sqSize, 276 | sqSize, 277 | ); 278 | 279 | if (item.options?.palette && !item.options.paletteImage) { 280 | if (item.options.palette.width) { 281 | makePaletteImage(item, w, item.options.palette); 282 | } else { 283 | const img = new Image(); 284 | img.onload = () => { 285 | makePaletteImage(item, w, img); 286 | }; 287 | img.src = item.options.palette; 288 | } 289 | } 290 | 291 | if (item.options.paletteImage) { 292 | ctx.drawImage( 293 | item.options.paletteImage, 294 | x, 295 | y + (sliderHeight + padding) * numSliders, 296 | width, 297 | width * item.options.paletteImage.aspectRatio, 298 | ); 299 | } 300 | } 301 | 302 | rectSet4(item.activeArea, x, y, width, height); 303 | } else if (item.type === "button") { 304 | const y = dy + padding; 305 | const height = buttonHeight; 306 | 307 | ctx.fillStyle = item.active ? this.theme.accent : this.theme.input; 308 | ctx.fillRect(x, y, width, height); 309 | 310 | ctx.fillStyle = item.active ? this.theme.input : this.theme.color; 311 | ctx.fillText( 312 | item.title, 313 | x + textPadding * 2, 314 | y + height / 2 + fontCapOffset, 315 | ); 316 | 317 | rectSet4(item.activeArea, x, y, width, height); 318 | } else if (item.type === "toggle") { 319 | const y = dy + padding; 320 | const height = itemHeight; 321 | ctx.fillStyle = item.contextObject[item.attributeName] 322 | ? this.theme.accent 323 | : this.theme.input; 324 | ctx.fillRect(x, y, height, height); 325 | 326 | ctx.fillStyle = this.theme.color; 327 | ctx.fillText( 328 | item.title, 329 | x + itemHeight + textPadding * 2, 330 | dy + padding + itemHeight / 2 + fontCapOffset, 331 | ); 332 | 333 | rectSet4(item.activeArea, x, y, height, height); 334 | } else if (item.type === "radiolist") { 335 | const y = dy + titleHeight; 336 | const height = 337 | item.items.length * itemHeight + 338 | (item.items.length - 1) * 2 * padding; 339 | 340 | ctx.fillStyle = this.theme.color; 341 | ctx.fillText(item.title, x + textPadding, dy + textY); 342 | 343 | for (let j = 0; j < item.items.length; j++) { 344 | const i = item.items[j]; 345 | const radioY = j * (itemHeight + padding * 2); 346 | 347 | ctx.fillStyle = 348 | item.contextObject[item.attributeName] === i.value 349 | ? this.theme.accent 350 | : this.theme.input; 351 | ctx.fillRect(x, y + radioY, itemHeight, itemHeight); 352 | 353 | ctx.fillStyle = this.theme.color; 354 | ctx.fillText( 355 | i.name, 356 | x + itemHeight + textPadding * 2, 357 | titleHeight + radioY + dy + itemHeight / 2 + fontCapOffset, 358 | ); 359 | } 360 | 361 | rectSet4(item.activeArea, x, y, itemHeight, height); 362 | } else if (item.type === "texturelist") { 363 | const y = dy + titleHeight; 364 | ctx.fillStyle = this.theme.color; 365 | ctx.fillText(item.title, x + textPadding, dy + textY); 366 | 367 | for (let j = 0; j < item.items.length; j++) { 368 | const col = j % item.itemsPerRow; 369 | const row = Math.floor(j / item.itemsPerRow); 370 | const itemX = x + col * cellSize; 371 | const itemY = dy + titleHeight + row * cellSize; 372 | let shrink = 0; 373 | if (item.items[j].value === item.contextObject[item.attributeName]) { 374 | ctx.fillStyle = "none"; 375 | ctx.strokeStyle = this.theme.accent; 376 | ctx.lineWidth = padding; 377 | ctx.strokeRect( 378 | itemX + padding * 0.5, 379 | itemY + padding * 0.5, 380 | cellSize - 1 - padding, 381 | cellSize - 1 - padding, 382 | ); 383 | ctx.lineWidth = 1; 384 | shrink = padding; 385 | } 386 | if (!item.items[j].activeArea) { 387 | item.items[j].activeArea = [ 388 | [0, 0], 389 | [0, 0], 390 | ]; 391 | } 392 | rectSet4( 393 | item.items[j].activeArea, 394 | itemX + shrink, 395 | itemY + shrink, 396 | cellSize - 1 - 2 * shrink, 397 | cellSize - 1 - 2 * shrink, 398 | ); 399 | } 400 | 401 | rectSet4(item.activeArea, x, y, width, cellSize * numRows); 402 | } else if (item.type === "texture2D") { 403 | const y = dy + titleHeight; 404 | const height = eh - titleHeight; 405 | 406 | ctx.fillStyle = this.theme.color; 407 | ctx.fillText(item.title, x + textPadding, dy + textY); 408 | 409 | rectSet4(item.activeArea, x, y, width, height); 410 | } else if (item.type === "textureCube") { 411 | const y = dy + titleHeight; 412 | const height = eh - titleHeight; 413 | ctx.fillStyle = this.theme.color; 414 | ctx.fillText(item.title, x + textPadding, dy + textY); 415 | 416 | rectSet4(item.activeArea, x, y, width, height); 417 | } else if (item.type === "header") { 418 | ctx.fillStyle = this.theme.headerBackground; 419 | ctx.fillRect(x, dy + padding, width, eh - padding); 420 | 421 | ctx.fillStyle = this.theme.headerColor; 422 | ctx.fillText( 423 | item.title, 424 | x + textPadding, 425 | dy + padding + headerSize / 2 + fontCapOffset, 426 | ); 427 | } else if (item.type === "text") { 428 | const y = dy + titleHeight; 429 | const height = eh - titleHeight; 430 | 431 | ctx.fillStyle = this.theme.color; 432 | ctx.fillText(item.title, x + textPadding, dy + textY); 433 | 434 | ctx.fillStyle = this.theme.input; 435 | ctx.fillRect( 436 | x, 437 | y, 438 | item.activeArea[1][0] - item.activeArea[0][0], 439 | item.activeArea[1][1] - item.activeArea[0][1], 440 | ); 441 | 442 | ctx.fillStyle = this.theme.color; 443 | ctx.fillText( 444 | item.contextObject[item.attributeName], 445 | x + textPadding * 2, 446 | y + buttonHeight / 2 + fontCapOffset, 447 | ); 448 | if (item.focus) { 449 | ctx.strokeStyle = this.theme.accent; 450 | ctx.strokeRect( 451 | item.activeArea[0][0] - 0.5, 452 | item.activeArea[0][1] - 0.5, 453 | item.activeArea[1][0] - item.activeArea[0][0], 454 | item.activeArea[1][1] - item.activeArea[0][1], 455 | ); 456 | } 457 | 458 | rectSet4(item.activeArea, x, y, width, height); 459 | } else if (item.type === "graph") { 460 | const y = dy + titleHeight; 461 | const height = eh - titleHeight; 462 | 463 | if (item.values.length > width) item.values = item.values.slice(-width); 464 | if (item.values.length) { 465 | item.max = item.options.max ?? Math.max(...item.values); 466 | } 467 | if (item.values.length) { 468 | item.min = item.options.min ?? Math.min(...item.values); 469 | } 470 | 471 | ctx.fillStyle = this.theme.graphBackground; 472 | ctx.fillRect(x, y, width, height); 473 | 474 | ctx.strokeStyle = this.theme.background; 475 | ctx.beginPath(); 476 | ctx.moveTo(x, y + padding); 477 | ctx.lineTo(x + width, y + padding); 478 | ctx.closePath(); 479 | ctx.stroke(); 480 | 481 | ctx.beginPath(); 482 | ctx.moveTo(x, y + height - padding); 483 | ctx.lineTo(x + width, y + height - padding); 484 | ctx.closePath(); 485 | ctx.stroke(); 486 | 487 | ctx.fillStyle = this.theme.color; 488 | ctx.save(); 489 | ctx.font = `${fontSize * 0.5}px ${fontFamily}`; 490 | ctx.textAlign = "right"; 491 | const textX = x + width - padding; 492 | if (item.max !== undefined) { 493 | ctx.fillText(item.options.format(item.max), textX, y + padding * 2.5); 494 | } 495 | if (item.min !== undefined) { 496 | ctx.fillText( 497 | item.options.format(item.min), 498 | textX, 499 | y + height - padding * 2.5, 500 | ); 501 | } 502 | ctx.restore(); 503 | 504 | ctx.strokeStyle = this.theme.color; 505 | ctx.beginPath(); 506 | for (let j = 0; j < item.values.length; j++) { 507 | const v = utils.remap(item.values[j], item.min, item.max, 0, 1); 508 | ctx[j === 0 ? "moveTo" : "lineTo"]( 509 | x + j, 510 | y + height - v * (height - padding * 2) - padding, 511 | ); 512 | } 513 | ctx.stroke(); 514 | 515 | ctx.fillText( 516 | `${item.title}: ${item.options.format(item.values[item.values.length - 1])}`, 517 | x + textPadding, 518 | dy + textY, 519 | ); 520 | } else if (item.type === "stats") { 521 | ctx.fillStyle = this.theme.color; 522 | 523 | let lineIndex = 0; 524 | if (item.title) { 525 | ctx.fillText(item.title, x + textPadding, dy + textY); 526 | lineIndex++; 527 | } 528 | 529 | for (let [name, value] of Object.entries(item.stats)) { 530 | const lines = String(value).split("\n"); 531 | 532 | for (let j = 0; j < lines.length; j++) { 533 | const lineValue = lines[j]; 534 | 535 | ctx.fillText( 536 | j === 0 ? `${name}: ${lineValue}` : lineValue, 537 | x + textPadding * 2, 538 | dy + textY + titleHeight * lineIndex, 539 | ); 540 | lineIndex++; 541 | } 542 | } 543 | } else if (item.type === "label") { 544 | ctx.fillStyle = this.theme.color; 545 | const lines = item.title.split("\n"); 546 | for (let i = 0; i < lines.length; i++) { 547 | ctx.fillText(lines[i], x + textPadding, dy + textY + titleHeight * i); 548 | } 549 | } else if (item.type === "separator") { 550 | // Nothing to draw, just increase the gap. 551 | } else { 552 | ctx.fillStyle = this.theme.color; 553 | ctx.fillText(item.title, x + textPadding, dy + textY); 554 | } 555 | dy += eh + (needsPadding ? padding : 0) + gap; 556 | maxWidth = Math.max(maxWidth, dx + w + leftOffset); 557 | maxHeight = Math.max(maxHeight, dy + topOffset); 558 | } 559 | 560 | this.afterDraw(); 561 | ctx.restore(); 562 | 563 | maxWidth = Math.max(maxWidth, tabs.length * (w + gap)); 564 | 565 | if (maxWidth && maxHeight) { 566 | maxWidth = (maxWidth * this.pixelRatio) | 0; 567 | maxHeight = (maxHeight * this.pixelRatio) | 0; 568 | if (this.canvas.width !== maxWidth) { 569 | this.canvas.width = maxWidth; 570 | this.dirty = true; 571 | } 572 | if (this.canvas.height !== maxHeight) { 573 | this.canvas.height = maxHeight; 574 | this.dirty = true; 575 | } 576 | if (this.dirty) { 577 | this.draw(items); 578 | } 579 | } 580 | } 581 | 582 | afterDraw() {} 583 | 584 | getTexture() { 585 | return this.canvas; 586 | } 587 | 588 | dispose() { 589 | this.canvas.remove(); 590 | } 591 | } 592 | 593 | export default CanvasRenderer; 594 | -------------------------------------------------------------------------------- /renderers/DebugRenderer.js: -------------------------------------------------------------------------------- 1 | import { rect } from "pex-geom"; 2 | 3 | import CanvasRenderer from "./CanvasRenderer.js"; 4 | 5 | class DebugRenderer extends CanvasRenderer { 6 | constructor(opts) { 7 | super(opts); 8 | } 9 | 10 | draw(items) { 11 | this.items = items; 12 | super.draw(items); 13 | } 14 | 15 | afterDraw() { 16 | this.items.forEach((item) => { 17 | this.ctx.strokeStyle = "rgba(255, 0, 0, 0.5)"; 18 | this.ctx.strokeRect( 19 | item.activeArea[0][0], 20 | item.activeArea[0][1], 21 | rect.width(item.activeArea), 22 | rect.height(item.activeArea), 23 | ); 24 | }); 25 | } 26 | } 27 | 28 | export default DebugRenderer; 29 | -------------------------------------------------------------------------------- /renderers/PexContextRenderer.js: -------------------------------------------------------------------------------- 1 | import CanvasRenderer from "./CanvasRenderer.js"; 2 | 3 | class PexContextRenderer extends CanvasRenderer { 4 | #ctx; 5 | 6 | constructor(opts) { 7 | super(opts); 8 | 9 | const { ctx } = opts; 10 | 11 | this.#ctx = ctx; 12 | 13 | this.rendererTexture = ctx.texture2D({ 14 | width: opts[0], 15 | height: opts[1], 16 | pixelFormat: ctx.PixelFormat.RGBA8, 17 | encoding: ctx.Encoding.SRGB, 18 | }); 19 | } 20 | 21 | draw(items) { 22 | super.draw(items); 23 | } 24 | 25 | afterDraw() { 26 | this.#ctx.update(this.rendererTexture, { 27 | data: this.canvas, 28 | width: this.canvas.width, 29 | height: this.canvas.height, 30 | flipY: true, 31 | }); 32 | } 33 | 34 | getTexture() { 35 | return this.rendererTexture; 36 | } 37 | 38 | dispose() { 39 | super.dispose(); 40 | 41 | this.#ctx.dispose(this.rendererTexture); 42 | } 43 | } 44 | 45 | export default PexContextRenderer; 46 | -------------------------------------------------------------------------------- /renderers/index.js: -------------------------------------------------------------------------------- 1 | export { default as CanvasRenderer } from "./CanvasRenderer.js"; 2 | export { default as PexContextRenderer } from "./PexContextRenderer.js"; 3 | export { default as DebugRenderer } from "./DebugRenderer.js"; 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pex-gl/pex-gui/93ee94a82801cd78cc36077dc3e0fd194223a592/screenshot.png -------------------------------------------------------------------------------- /shaders/chunks/encode-decode.glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | #define LINEAR 1 3 | #define GAMMA 2 4 | #define SRGB 3 5 | #define RGBM 4 6 | 7 | vec4 decode(vec4 pixel, int encoding) { 8 | if (encoding == LINEAR) return pixel; 9 | if (encoding == GAMMA) return toLinear(pixel); 10 | if (encoding == SRGB) return toLinear(pixel); 11 | if (encoding == RGBM) return vec4(decodeRGBM(pixel), 1.0); 12 | return pixel; 13 | } 14 | 15 | vec4 encode(vec4 pixel, int encoding) { 16 | if (encoding == LINEAR) return pixel; 17 | if (encoding == GAMMA) return toGamma(pixel); 18 | if (encoding == SRGB) return toGamma(pixel); 19 | if (encoding == RGBM) return encodeRGBM(pixel.rgb); 20 | return pixel; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /shaders/chunks/gamma.glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | const float gamma = 2.2; 3 | 4 | // Linear 5 | float toLinear(float v) { 6 | return pow(v, gamma); 7 | } 8 | 9 | vec2 toLinear(vec2 v) { 10 | return pow(v, vec2(gamma)); 11 | } 12 | 13 | vec3 toLinear(vec3 v) { 14 | return pow(v, vec3(gamma)); 15 | } 16 | 17 | vec4 toLinear(vec4 v) { 18 | return vec4(toLinear(v.rgb), v.a); 19 | } 20 | 21 | // Gamma 22 | float toGamma(float v) { 23 | return pow(v, 1.0 / gamma); 24 | } 25 | 26 | vec2 toGamma(vec2 v) { 27 | return pow(v, vec2(1.0 / gamma)); 28 | } 29 | 30 | vec3 toGamma(vec3 v) { 31 | return pow(v, vec3(1.0 / gamma)); 32 | } 33 | 34 | vec4 toGamma(vec4 v) { 35 | return vec4(toGamma(v.rgb), v.a); 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /shaders/chunks/rgbm.glsl.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | vec3 decodeRGBM (vec4 rgbm) { 3 | vec3 r = rgbm.rgb * (7.0 * rgbm.a); 4 | return r * r; 5 | } 6 | 7 | vec4 encodeRGBM (vec3 rgb_0) { 8 | vec4 r; 9 | r.xyz = (1.0 / 7.0) * sqrt(rgb_0); 10 | r.a = max(max(r.x, r.y), r.z); 11 | r.a = clamp(r.a, 1.0 / 255.0, 1.0); 12 | r.a = ceil(r.a * 255.0) / 255.0; 13 | r.xyz /= r.a; 14 | return r; 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /shaders/main.vert.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */ ` 2 | attribute vec2 aPosition; 3 | attribute vec2 aTexCoord0; 4 | 5 | uniform vec4 uViewport; 6 | uniform vec4 uRect; 7 | 8 | varying vec2 vTexCoord0; 9 | 10 | void main() { 11 | vTexCoord0 = vec2(aTexCoord0.x, 1.0 - aTexCoord0.y); 12 | vec2 vertexPos = aPosition * 0.5 + 0.5; 13 | 14 | vec2 pos = vec2(0.0, 0.0); // window pos 15 | vec2 windowSize = vec2(uViewport.z - uViewport.x, uViewport.w - uViewport.y); 16 | pos.x = uRect.x / windowSize.x + vertexPos.x * (uRect.z - uRect.x) / windowSize.x; 17 | pos.y = uRect.y / windowSize.y + vertexPos.y * (uRect.w - uRect.y) / windowSize.y; 18 | pos.y = 1.0 - pos.y; 19 | pos = pos * 2.0 - 1.0; 20 | 21 | gl_Position = vec4(pos, 0.0, 1.0); 22 | }`; 23 | -------------------------------------------------------------------------------- /shaders/texture-2d.frag.js: -------------------------------------------------------------------------------- 1 | import GAMMA from "./chunks/gamma.glsl.js"; 2 | import RGBM from "./chunks/rgbm.glsl.js"; 3 | import DECODE_ENCODE from "./chunks/encode-decode.glsl.js"; 4 | 5 | export default /* glsl */ `#version 100 6 | precision highp float; 7 | 8 | ${GAMMA} 9 | ${RGBM} 10 | ${DECODE_ENCODE} 11 | 12 | uniform sampler2D uTexture; 13 | uniform int uTextureEncoding; 14 | varying vec2 vTexCoord0; 15 | 16 | void main() { 17 | vec4 color = texture2D(uTexture, vTexCoord0); 18 | color = decode(color, uTextureEncoding); 19 | // if LINEAR || RGBM then tonemap 20 | if (uTextureEncoding == LINEAR || uTextureEncoding == RGBM) { 21 | color.rgb = color.rgb / (color.rgb + 1.0); 22 | } 23 | gl_FragColor = encode(color, 2); // to gamma 24 | }`; 25 | -------------------------------------------------------------------------------- /shaders/texture-cube.frag.js: -------------------------------------------------------------------------------- 1 | import GAMMA from "./chunks/gamma.glsl.js"; 2 | import RGBM from "./chunks/rgbm.glsl.js"; 3 | import DECODE_ENCODE from "./chunks/encode-decode.glsl.js"; 4 | 5 | export default /* glsl */ `#version 100 6 | precision highp float; 7 | 8 | ${GAMMA} 9 | ${RGBM} 10 | ${DECODE_ENCODE} 11 | 12 | const float PI = 3.1415926; 13 | 14 | varying vec2 vTexCoord0; 15 | 16 | uniform samplerCube uTexture; 17 | uniform int uTextureEncoding; 18 | uniform float uLevel; 19 | uniform float uFlipEnvMap; 20 | 21 | void main() { 22 | float theta = PI * (vTexCoord0.x * 2.0); 23 | float phi = PI * (1.0 - vTexCoord0.y); 24 | 25 | float x = sin(phi) * sin(theta); 26 | float y = -cos(phi); 27 | float z = -sin(phi) * cos(theta); 28 | 29 | vec3 N = normalize(vec3(uFlipEnvMap * x, y, z)); 30 | vec4 color = textureCube(uTexture, N, uLevel); 31 | color = decode(color, uTextureEncoding); 32 | // if LINEAR || RGBM then tonemap 33 | if (uTextureEncoding == LINEAR || uTextureEncoding == RGBM) { 34 | color.rgb = color.rgb / (color.rgb + 1.0); 35 | } 36 | gl_FragColor = encode(color, 2); // to gamma 37 | }`; 38 | -------------------------------------------------------------------------------- /theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fontFamily: `"Monaco", monospace`, 3 | fontSize: 10, 4 | capHeight: 0.09, 5 | 6 | leftOffset: 10, 7 | topOffset: 10, 8 | 9 | columnWidth: 160, 10 | 11 | tabHeight: 22, 12 | headerSize: 18, 13 | 14 | titleHeight: 20, 15 | itemHeight: 16, 16 | graphHeight: 40, 17 | 18 | padding: 3, 19 | textPadding: 2, 20 | 21 | color: "rgba(255, 255, 255, 1)", 22 | background: "rgba(0, 0, 0, 0.56)", 23 | input: "rgba(150, 150, 150, 1)", 24 | accent: "rgba(255, 255, 0, 1)", 25 | 26 | tabBackground: "rgba(75, 75, 75, 1)", 27 | tabColor: "rgba(175, 175, 175, 1)", 28 | tabBackgroundActive: "rgba(46, 204, 113, 1)", 29 | tabColorActive: "rgba(0, 0, 0, 1)", 30 | 31 | headerBackground: "rgba(255, 255, 255, 1)", 32 | headerColor: "rgba(0, 0, 0, 1)", 33 | 34 | graphBackground: "rgba(50, 50, 50, 1)", 35 | }; 36 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {object} GUIControlOptions 3 | * @property {number} [min=0] 4 | * @property {number} [max=0] 5 | * @property {"color"} [type] Interpret an array as color. 6 | * @property {string} [colorSpace] Display a color as values of a pex-color color space. 7 | * @property {boolean} [alpha] Add a 4th slider for colors. 8 | * @property {HTMLImageElement} [palette] Draw a palette image as color picker. 9 | * @property {boolean} [flipEnvMap] Should be 1 for dynamic cubemaps and -1 for cubemaps from file with X axis flipped. 10 | * @property {boolean} [flipY] Flip texture 2D vertically. 11 | * @property {number} [level] Level of detail for cube textures. 12 | */ 13 | /** 14 | * @typedef {object} GUIOptions 15 | * @property {boolean} [pixelRatio=window.devicePixelRatio] 16 | * @property {boolean} [theme={}] See [theme file]{@link https://github.com/pex-gl/pex-gui/blob/main/theme.js} for all options. 17 | * @property {number} [scale=1] 18 | * @property {boolean} [responsive=true] Adapts to canvas dimension. 19 | */ 20 | 21 | /** @typedef {import("pex-context").ctx} ctx */ 22 | 23 | export {}; 24 | -------------------------------------------------------------------------------- /web_modules/_chunks/_commonjsHelpers-BFTU3MAI.js: -------------------------------------------------------------------------------- 1 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 2 | 3 | function getDefaultExportFromCjs (x) { 4 | return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 5 | } 6 | 7 | export { commonjsGlobal as c, getDefaultExportFromCjs as g }; 8 | -------------------------------------------------------------------------------- /web_modules/_chunks/avec3-CX_9gCVx.js: -------------------------------------------------------------------------------- 1 | import { h as create } from './vec3-DW1VLBq6.js'; 2 | 3 | const TEMP_VEC3 = create(); 4 | /** 5 | * Sets a vector components. 6 | * @param {import("./types.js").avec3} a 7 | * @param {number} i 8 | * @param {number} x 9 | * @param {number} y 10 | * @param {number} z 11 | */ function set3(a, i, x, y, z) { 12 | a[i * 3] = x; 13 | a[i * 3 + 1] = y; 14 | a[i * 3 + 2] = z; 15 | } 16 | /** 17 | * Sets a vector to another vector. 18 | * @param {import("./types.js").avec3} a 19 | * @param {number} i 20 | * @param {import("./types.js").avec3} b 21 | * @param {number} j 22 | */ function set(a, i, b, j) { 23 | a[i * 3] = b[j * 3]; 24 | a[i * 3 + 1] = b[j * 3 + 1]; 25 | a[i * 3 + 2] = b[j * 3 + 2]; 26 | } 27 | /** 28 | * Compares two vectors. 29 | * @param {import("./types.js").avec3} a 30 | * @param {number} i 31 | * @param {import("./types.js").avec3} b 32 | * @param {number} j 33 | * @returns {boolean} 34 | */ function equals(a, i, b, j) { 35 | return a[i * 3] === b[j * 3] && a[i * 3 + 1] === b[j * 3 + 1] && a[i * 3 + 2] === b[j * 3 + 2]; 36 | } 37 | /** 38 | * Adds a vector to another. 39 | * @param {import("./types.js").avec3} a 40 | * @param {number} i 41 | * @param {import("./types.js").avec3} b 42 | * @param {number} j 43 | */ function add(a, i, b, j) { 44 | a[i * 3] += b[j * 3]; 45 | a[i * 3 + 1] += b[j * 3 + 1]; 46 | a[i * 3 + 2] += b[j * 3 + 2]; 47 | } 48 | /** 49 | * Subtracts a vector from another. 50 | * @param {import("./types.js").avec3} a 51 | * @param {number} i 52 | * @param {import("./types.js").avec3} b 53 | * @param {number} j 54 | */ function sub(a, i, b, j) { 55 | a[i * 3] -= b[j * 3]; 56 | a[i * 3 + 1] -= b[j * 3 + 1]; 57 | a[i * 3 + 2] -= b[j * 3 + 2]; 58 | } 59 | /** 60 | * Scales a vector by a number. 61 | * @param {import("./types.js").avec3} a 62 | * @param {number} i 63 | * @param {number} s 64 | */ function scale(a, i, s) { 65 | a[i * 3] *= s; 66 | a[i * 3 + 1] *= s; 67 | a[i * 3 + 2] *= s; 68 | } 69 | /** 70 | * Adds two vectors after scaling the second one. 71 | * @param {import("./types.js").avec3} a 72 | * @param {number} i 73 | * @param {import("./types.js").avec3} b 74 | * @param {number} j 75 | * @param {number} s 76 | */ function addScaled(a, i, b, j, s) { 77 | a[i * 3] += b[j * 3] * s; 78 | a[i * 3 + 1] += b[j * 3 + 1] * s; 79 | a[i * 3 + 2] += b[j * 3 + 2] * s; 80 | } 81 | /** 82 | * Multiplies a vector by a matrix. 83 | * @param {import("./types.js").avec3} a 84 | * @param {number} i 85 | * @param {import("./types.js").amat4} m 86 | * @param {number} j 87 | */ function multMat4(a, i, m, j) { 88 | const x = a[i * 3]; 89 | const y = a[i * 3 + 1]; 90 | const z = a[i * 3 + 2]; 91 | a[i * 3] = m[j * 16] * x + m[j * 16 + 4] * y + m[j * 16 + 8] * z + m[j * 16 + 12]; 92 | a[i * 3 + 1] = m[j * 16 + 1] * x + m[j * 16 + 5] * y + m[j * 16 + 9] * z + m[j * 16 + 13]; 93 | a[i * 3 + 2] = m[j * 16 + 2] * x + m[j * 16 + 6] * y + m[j * 16 + 10] * z + m[j * 16 + 14]; 94 | } 95 | /** 96 | * Multiplies a vector by a quaternion. 97 | * @param {import("./types.js").avec3} a 98 | * @param {number} i 99 | * @param {import("./types.js").aquat} q 100 | * @param {number} j 101 | */ function multQuat(a, i, q, j) { 102 | const x = a[i * 3]; 103 | const y = a[i * 3 + 1]; 104 | const z = a[i * 3 + 2]; 105 | const qx = q[j * 4]; 106 | const qy = q[j * 4 + 1]; 107 | const qz = q[j * 4 + 2]; 108 | const qw = q[j * 4 + 3]; 109 | const ix = qw * x + qy * z - qz * y; 110 | const iy = qw * y + qz * x - qx * z; 111 | const iz = qw * z + qx * y - qy * x; 112 | const iw = -qx * x - qy * y - qz * z; 113 | a[i * 3] = ix * qw + iw * -qx + iy * -qz - iz * -qy; 114 | a[i * 3 + 1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; 115 | a[i * 3 + 2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; 116 | } 117 | /** 118 | * Calculates the dot product of two vectors. 119 | * @param {import("./types.js").avec3} a 120 | * @param {number} i 121 | * @param {import("./types.js").avec3} b 122 | * @param {number} j 123 | * @returns {number} 124 | */ function dot(a, i, b, j) { 125 | return a[i * 3] * b[j * 3] + a[i * 3 + 1] * b[j * 3 + 1] + a[i * 3 + 2] * b[j * 3 + 2]; 126 | } 127 | /** 128 | * Calculates the cross product of two vectors. 129 | * @param {import("./types.js").avec3} a 130 | * @param {number} i 131 | * @param {import("./types.js").avec3} b 132 | * @param {number} j 133 | */ function cross(a, i, b, j) { 134 | const x = a[i * 3]; 135 | const y = a[i * 3 + 1]; 136 | const z = a[i * 3 + 2]; 137 | const vx = b[j * 3]; 138 | const vy = b[j * 3 + 1]; 139 | const vz = b[j * 3 + 2]; 140 | a[i * 3] = y * vz - vy * z; 141 | a[i * 3 + 1] = z * vx - vz * x; 142 | a[i * 3 + 2] = x * vy - vx * y; 143 | } 144 | /** 145 | * Calculates the length of a vector. 146 | * @param {import("./types.js").avec3} a 147 | * @param {number} i 148 | * @returns {number} 149 | */ function length(a, i) { 150 | const x = a[i * 3]; 151 | const y = a[i * 3 + 1]; 152 | const z = a[i * 3 + 2]; 153 | return Math.sqrt(x * x + y * y + z * z); 154 | } 155 | /** 156 | * Calculates the squared length of a vector. 157 | * @param {import("./types.js").avec3} a 158 | * @param {number} i 159 | * @returns {number} 160 | */ function lengthSq(a, i) { 161 | const x = a[i * 3]; 162 | const y = a[i * 3 + 1]; 163 | const z = a[i * 3 + 2]; 164 | return x * x + y * y + z * z; 165 | } 166 | /** 167 | * Normalises a vector. 168 | * @param {import("./types.js").avec3} a 169 | * @param {number} i 170 | */ function normalize(a, i) { 171 | const lenSq = a[i * 3] * a[i * 3] + a[i * 3 + 1] * a[i * 3 + 1] + a[i * 3 + 2] * a[i * 3 + 2]; 172 | if (lenSq > 0) { 173 | const len = Math.sqrt(lenSq); 174 | a[i * 3] /= len; 175 | a[i * 3 + 1] /= len; 176 | a[i * 3 + 2] /= len; 177 | } 178 | } 179 | /** 180 | * Calculates the distance between two vectors. 181 | * @param {import("./types.js").avec3} a 182 | * @param {number} i 183 | * @param {import("./types.js").avec3} b 184 | * @param {number} j 185 | * @returns {number} 186 | */ function distance(a, i, b, j) { 187 | const dx = b[j * 3] - a[i * 3]; 188 | const dy = b[j * 3 + 1] - a[i * 3 + 1]; 189 | const dz = b[j * 3 + 2] - a[i * 3 + 2]; 190 | return Math.sqrt(dx * dx + dy * dy + dz * dz); 191 | } 192 | /** 193 | * Calculates the squared distance between two vectors. 194 | * @param {import("./types.js").avec3} a 195 | * @param {number} i 196 | * @param {import("./types.js").avec3} b 197 | * @param {number} j 198 | * @returns {number} 199 | */ function distanceSq(a, i, b, j) { 200 | const dx = b[j * 3] - a[i * 3]; 201 | const dy = b[j * 3 + 1] - a[i * 3 + 1]; 202 | const dz = b[j * 3 + 2] - a[i * 3 + 2]; 203 | return dx * dx + dy * dy + dz * dz; 204 | } 205 | /** 206 | * Limits a vector to a length. 207 | * @param {import("./types.js").avec3} a 208 | * @param {number} i 209 | * @param {number} len 210 | */ function limit(a, i, len) { 211 | const x = a[i * 3]; 212 | const y = a[i * 3 + 1]; 213 | const z = a[i * 3 + 2]; 214 | const dsq = x * x + y * y + z * z; 215 | const lsq = len * len; 216 | if (lsq > 0 && dsq > lsq) { 217 | const nd = len / Math.sqrt(dsq); 218 | a[i * 3] *= nd; 219 | a[i * 3 + 1] *= nd; 220 | a[i * 3 + 2] *= nd; 221 | } 222 | } 223 | /** 224 | * Linearly interpolates between two vectors. 225 | * @param {import("./types.js").avec3} a 226 | * @param {number} i 227 | * @param {import("./types.js").avec3} b 228 | * @param {number} j 229 | * @param {number} t 230 | */ function lerp(a, i, b, j, t) { 231 | const x = a[i * 3]; 232 | const y = a[i * 3 + 1]; 233 | const z = a[i * 3 + 2]; 234 | a[i * 3] = x + (b[j * 3] - x) * t; 235 | a[i * 3 + 1] = y + (b[j * 3 + 1] - y) * t; 236 | a[i * 3 + 2] = z + (b[j * 3 + 2] - z) * t; 237 | } 238 | /** 239 | * Executes a function once for each array element. 240 | * @param {import("./types.js").avec3} a 241 | * @param {import("./types.js").iterativeCallback} callbackFn 242 | */ function forEach(a, callbackFn) { 243 | for(let i = 0; i < a.length / 3; i++){ 244 | TEMP_VEC3[0] = a[i * 3]; 245 | TEMP_VEC3[1] = a[i * 3 + 1]; 246 | TEMP_VEC3[2] = a[i * 3 + 2]; 247 | callbackFn(TEMP_VEC3, i, a); 248 | a[i * 3] = TEMP_VEC3[0]; 249 | a[i * 3 + 1] = TEMP_VEC3[1]; 250 | a[i * 3 + 2] = TEMP_VEC3[2]; 251 | } 252 | } 253 | /** 254 | * Creates a new array populated with the results of calling a provided function on every element in the calling array. 255 | * @param {import("./types.js").avec3} a 256 | * @param {import("./types.js").iterativeCallback} callbackFn 257 | * @returns {import("./types.js").avec3} 258 | */ function map(a, callbackFn) { 259 | const b = new a.constructor(a.length); 260 | const element = new a.constructor(3); 261 | for(let i = 0; i < a.length / 3; i++){ 262 | element[0] = a[i * 3]; 263 | element[1] = a[i * 3 + 1]; 264 | element[2] = a[i * 3 + 2]; 265 | const returnValue = callbackFn(element, i, a); 266 | b[i * 3] = returnValue[0]; 267 | b[i * 3 + 1] = returnValue[1]; 268 | b[i * 3 + 2] = returnValue[2]; 269 | } 270 | return b; 271 | } 272 | /** 273 | * Prints a vector to a string. 274 | * @param {import("./types.js").avec3} a 275 | * @param {number} i 276 | * @param {number} [precision=4] 277 | * @returns {string} 278 | */ function toString(a, i, precision) { 279 | if (precision === undefined) precision = 4; 280 | const scale = 10 ** precision; 281 | // prettier-ignore 282 | return `[${Math.floor(a[i * 3] * scale) / scale}, ${Math.floor(a[i * 3 + 1] * scale) / scale}, ${Math.floor(a[i * 3 + 2] * scale) / scale}]`; 283 | } 284 | 285 | var avec3 = /*#__PURE__*/Object.freeze({ 286 | __proto__: null, 287 | add: add, 288 | addScaled: addScaled, 289 | cross: cross, 290 | distance: distance, 291 | distanceSq: distanceSq, 292 | dot: dot, 293 | equals: equals, 294 | forEach: forEach, 295 | length: length, 296 | lengthSq: lengthSq, 297 | lerp: lerp, 298 | limit: limit, 299 | map: map, 300 | multMat4: multMat4, 301 | multQuat: multQuat, 302 | normalize: normalize, 303 | scale: scale, 304 | set: set, 305 | set3: set3, 306 | sub: sub, 307 | toString: toString 308 | }); 309 | 310 | export { avec3 as a, set3 as s }; 311 | -------------------------------------------------------------------------------- /web_modules/_chunks/mat4-DkKmQtAV.js: -------------------------------------------------------------------------------- 1 | /** @module utils */ /** 2 | * @constant {number} 3 | */ const EPSILON = 0.000001; 4 | /** 5 | * @constant {import("./types.js").vec3} 6 | */ const Y_UP = Object.freeze([ 7 | 0, 8 | 1, 9 | 0 10 | ]); 11 | /** 12 | * Linear interpolation between two numbers. 13 | * @param {number} a 14 | * @param {number} b 15 | * @param {number} t 16 | * @returns {number} 17 | */ function lerp(a, b, t) { 18 | return a + (b - a) * t; 19 | } 20 | /** 21 | * Clamps a number between two numbers. 22 | * @param {number} n 23 | * @param {number} min 24 | * @param {number} max 25 | * @returns {number} 26 | */ function clamp(n, min, max) { 27 | return Math.max(min, Math.min(n, max)); 28 | } 29 | /** 30 | * Smooth Hermite interpolation between 0 and 1 31 | * @param {number} n 32 | * @param {number} min 33 | * @param {number} max 34 | * @returns {number} 35 | */ function smoothstep(n, min, max) { 36 | n = clamp((n - min) / (max - min), 0, 1); 37 | return n * n * (3 - 2 * n); 38 | } 39 | /** 40 | * Maps a number from one range to another. 41 | * @param {number} n 42 | * @param {number} inStart 43 | * @param {number} inEnd 44 | * @param {number} outStart 45 | * @param {number} outEnd 46 | * @returns {number} 47 | */ function remap(n, inStart, inEnd, outStart, outEnd) { 48 | return outStart + (outEnd - outStart) * (n - inStart) / (inEnd - inStart); 49 | } 50 | /** 51 | * @deprecated Use "remap()" 52 | * @ignore 53 | */ function map() { 54 | for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ 55 | args[_key] = arguments[_key]; 56 | } 57 | return remap(...args); 58 | } 59 | /** 60 | * Transforms degrees into radians. 61 | * @param {import("./types.js").Degrees} degrees 62 | * @returns {import("./types.js").Radians} 63 | */ function toRadians(degrees) { 64 | return degrees * Math.PI / 180; 65 | } 66 | /** 67 | * Transforms radians into degrees. 68 | * @param {import("./types.js").Radians} radians 69 | * @returns {import("./types.js").Degrees} 70 | */ function toDegrees(radians) { 71 | return radians * 180 / Math.PI; 72 | } 73 | /** 74 | * Check if a number is a power of two 75 | * @param {number} a 76 | * @returns {boolean} 77 | */ function isPowerOfTwo(a) { 78 | return (a & a - 1) === 0; 79 | } 80 | /** 81 | * Returns the next highest power of two. 82 | * @param {number} n 83 | * @returns {number} 84 | */ function nextPowerOfTwo(n) { 85 | if (n === 0) return 1; 86 | n--; 87 | n |= n >> 1; 88 | n |= n >> 2; 89 | n |= n >> 4; 90 | n |= n >> 8; 91 | n |= n >> 16; 92 | return n + 1; 93 | } 94 | /** 95 | * Returns the previous power of two. 96 | * @param {number} n 97 | * @returns {number} 98 | */ function prevPowerOfTwo(n) { 99 | if (n <= 1) return 0; 100 | return nextPowerOfTwo(n) / 2; 101 | } 102 | 103 | var utils = /*#__PURE__*/Object.freeze({ 104 | __proto__: null, 105 | EPSILON: EPSILON, 106 | Y_UP: Y_UP, 107 | clamp: clamp, 108 | isPowerOfTwo: isPowerOfTwo, 109 | lerp: lerp, 110 | map: map, 111 | nextPowerOfTwo: nextPowerOfTwo, 112 | prevPowerOfTwo: prevPowerOfTwo, 113 | remap: remap, 114 | smoothstep: smoothstep, 115 | toDegrees: toDegrees, 116 | toRadians: toRadians 117 | }); 118 | 119 | /** 120 | * Returns a 4x4 identity matrix. 121 | * 122 | * Row major memory layout: 123 | * 124 | * ``` 125 | * 0 1 2 3 126 | * 4 5 6 7 127 | * 8 9 10 11 128 | * 12 13 14 15 129 | * ``` 130 | * 131 | * Equivalent to the column major OpenGL spec: 132 | * 133 | * ``` 134 | * 0 4 8 12 135 | * 1 5 9 13 136 | * 2 6 10 14 137 | * 3 7 11 15 138 | * 139 | * m00 m10 m20 m30 140 | * m01 m11 m21 m31 141 | * m02 m12 m22 m32 142 | * m03 m13 m23 m33 143 | * ``` 144 | * @returns {import("./types.js").mat4} 145 | */ function create() { 146 | // prettier-ignore 147 | return [ 148 | 1, 149 | 0, 150 | 0, 151 | 0, 152 | 0, 153 | 1, 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 1, 159 | 0, 160 | 0, 161 | 0, 162 | 0, 163 | 1 164 | ]; 165 | } 166 | /** 167 | * Sets a matrix to the identity matrix. 168 | * @param {import("./types.js").mat4} a 169 | * @returns {import("./types.js").mat4} 170 | */ function identity(a) { 171 | a[0] = a[5] = a[10] = a[15] = 1; 172 | a[1] = a[2] = a[3] = a[4] = a[6] = a[7] = a[8] = a[9] = a[11] = a[12] = a[13] = a[14] = 0; 173 | return a; 174 | } 175 | /** 176 | * Returns a copy of a matrix. 177 | * @param {import("./types.js").mat4} a 178 | * @returns {import("./types.js").mat4} 179 | */ function copy(a) { 180 | return a.slice(); 181 | } 182 | /** 183 | * Sets a matrix from another matrix. 184 | * @param {import("./types.js").mat4} a 185 | * @param {import("./types.js").mat4} b 186 | * @returns {import("./types.js").mat4} 187 | */ function set(a, b) { 188 | a[0] = b[0]; 189 | a[1] = b[1]; 190 | a[2] = b[2]; 191 | a[3] = b[3]; 192 | a[4] = b[4]; 193 | a[5] = b[5]; 194 | a[6] = b[6]; 195 | a[7] = b[7]; 196 | a[8] = b[8]; 197 | a[9] = b[9]; 198 | a[10] = b[10]; 199 | a[11] = b[11]; 200 | a[12] = b[12]; 201 | a[13] = b[13]; 202 | a[14] = b[14]; 203 | a[15] = b[15]; 204 | return a; 205 | } 206 | /** 207 | * Compares two matrices. 208 | * @param {import("./types.js").mat4} a 209 | * @param {import("./types.js").mat4} b 210 | * @returns {boolean} 211 | */ function equals(a, b) { 212 | return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3] && a[4] === b[4] && a[5] === b[5] && a[6] === b[6] && a[7] === b[7] && a[8] === b[8] && a[9] === b[9] && a[10] === b[10] && a[11] === b[11] && a[12] === b[12] && a[13] === b[13] && a[14] === b[14] && a[15] === b[15]; 213 | } 214 | /** 215 | * Multiplies two matrices. 216 | * @param {import("./types.js").mat4} a 217 | * @param {import("./types.js").mat4} b 218 | * @returns {import("./types.js").mat4} 219 | */ function mult(a, b) { 220 | const a00 = a[0]; 221 | const a01 = a[1]; 222 | const a02 = a[2]; 223 | const a03 = a[3]; 224 | const a10 = a[4]; 225 | const a11 = a[5]; 226 | const a12 = a[6]; 227 | const a13 = a[7]; 228 | const a20 = a[8]; 229 | const a21 = a[9]; 230 | const a22 = a[10]; 231 | const a23 = a[11]; 232 | const a30 = a[12]; 233 | const a31 = a[13]; 234 | const a32 = a[14]; 235 | const a33 = a[15]; 236 | let b0 = b[0]; 237 | let b1 = b[1]; 238 | let b2 = b[2]; 239 | let b3 = b[3]; 240 | a[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 241 | a[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 242 | a[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 243 | a[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 244 | b0 = b[4]; 245 | b1 = b[5]; 246 | b2 = b[6]; 247 | b3 = b[7]; 248 | a[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 249 | a[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 250 | a[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 251 | a[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 252 | b0 = b[8]; 253 | b1 = b[9]; 254 | b2 = b[10]; 255 | b3 = b[11]; 256 | a[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 257 | a[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 258 | a[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 259 | a[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 260 | b0 = b[12]; 261 | b1 = b[13]; 262 | b2 = b[14]; 263 | b3 = b[15]; 264 | a[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 265 | a[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 266 | a[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 267 | a[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 268 | return a; 269 | } 270 | /** 271 | * Inverts a matrix. 272 | * @param {import("./types.js").mat4} a 273 | * @returns {import("./types.js").mat4} 274 | */ function invert(a) { 275 | const a00 = a[0]; 276 | const a10 = a[1]; 277 | const a20 = a[2]; 278 | const a30 = a[3]; 279 | const a01 = a[4]; 280 | const a11 = a[5]; 281 | const a21 = a[6]; 282 | const a31 = a[7]; 283 | const a02 = a[8]; 284 | const a12 = a[9]; 285 | const a22 = a[10]; 286 | const a32 = a[11]; 287 | const a03 = a[12]; 288 | const a13 = a[13]; 289 | const a23 = a[14]; 290 | const a33 = a[15]; 291 | const a22a33 = a22 * a33; 292 | const a32a23 = a32 * a23; 293 | const a21a33 = a21 * a33; 294 | const a31a23 = a31 * a23; 295 | const a21a32 = a21 * a32; 296 | const a31a22 = a31 * a22; 297 | const a20a33 = a20 * a33; 298 | const a30a23 = a30 * a23; 299 | const a20a32 = a20 * a32; 300 | const a30a22 = a30 * a22; 301 | const a20a31 = a20 * a31; 302 | const a30a21 = a30 * a21; 303 | const a00a11 = a00 * a11; 304 | const a01a10 = a01 * a10; 305 | const a02a10 = a02 * a10; 306 | const a03a10 = a03 * a10; 307 | const a12a33 = a12 * a33; 308 | const a32a13 = a32 * a13; 309 | const a00a31 = a00 * a31; 310 | const a03a11 = a03 * a11; 311 | const a12a23 = a12 * a23; 312 | const a22a13 = a22 * a13; 313 | const a02a11 = a02 * a11; 314 | const a21a13 = a21 * a13; 315 | const a21a12 = a21 * a12; 316 | const a02a30 = a02 * a30; 317 | const a03a30 = a03 * a30; 318 | const a02a20 = a02 * a20; 319 | const a03a20 = a03 * a20; 320 | const a01a30 = a01 * a30; 321 | const a01a20 = a01 * a20; 322 | a[0] = a11 * a22a33 - a11 * a32a23 - a12 * a21a33 + a12 * a31a23 + a13 * a21a32 - a13 * a31a22; 323 | a[4] = -a01 * a22a33 + a01 * a32a23 + a02 * a21a33 - a02 * a31a23 - a03 * a21a32 + a03 * a31a22; 324 | a[8] = a01 * a12a33 - a01 * a32a13 - a02a11 * a33 + a02 * a31 * a13 + a03a11 * a32 - a03 * a31 * a12; 325 | a[12] = -a01 * a12a23 + a01 * a22a13 + a02a11 * a23 - a02 * a21a13 - a03a11 * a22 + a03 * a21a12; 326 | a[1] = -a10 * a22a33 + a10 * a32a23 + a12 * a20a33 - a12 * a30a23 - a13 * a20a32 + a13 * a30a22; 327 | a[5] = a00 * a22a33 - a00 * a32a23 - a02 * a20a33 + a02 * a30a23 + a03 * a20a32 - a03 * a30a22; 328 | a[9] = -a00 * a12a33 + a00 * a32a13 + a02a10 * a33 - a02a30 * a13 - a03a10 * a32 + a03a30 * a12; 329 | a[13] = a00 * a12a23 - a00 * a22a13 - a02a10 * a23 + a02a20 * a13 + a03a10 * a22 - a03a20 * a12; 330 | a[2] = a10 * a21a33 - a10 * a31a23 - a11 * a20a33 + a11 * a30a23 + a13 * a20a31 - a13 * a30a21; 331 | a[6] = -a00 * a21a33 + a00 * a31a23 + a01 * a20a33 - a01 * a30a23 - a03 * a20a31 + a03 * a30a21; 332 | a[10] = a00a11 * a33 - a00a31 * a13 - a01a10 * a33 + a01a30 * a13 + a03a10 * a31 - a03a30 * a11; 333 | a[14] = -a00a11 * a23 + a00 * a21a13 + a01a10 * a23 - a01a20 * a13 - a03a10 * a21 + a03a20 * a11; 334 | a[3] = -a10 * a21a32 + a10 * a31a22 + a11 * a20a32 - a11 * a30a22 - a12 * a20a31 + a12 * a30a21; 335 | a[7] = a00 * a21a32 - a00 * a31a22 - a01 * a20a32 + a01 * a30a22 + a02 * a20a31 - a02 * a30a21; 336 | a[11] = -a00a11 * a32 + a00a31 * a12 + a01a10 * a32 - a01a30 * a12 - a02a10 * a31 + a02a30 * a11; 337 | a[15] = a00a11 * a22 - a00 * a21a12 - a01a10 * a22 + a01a20 * a12 + a02a10 * a21 - a02a20 * a11; 338 | let det = a00 * a[0] + a10 * a[4] + a20 * a[8] + a30 * a[12]; 339 | if (!det) return null; 340 | det = 1 / det; 341 | a[0] *= det; 342 | a[1] *= det; 343 | a[2] *= det; 344 | a[3] *= det; 345 | a[4] *= det; 346 | a[5] *= det; 347 | a[6] *= det; 348 | a[7] *= det; 349 | a[8] *= det; 350 | a[9] *= det; 351 | a[10] *= det; 352 | a[11] *= det; 353 | a[12] *= det; 354 | a[13] *= det; 355 | a[14] *= det; 356 | a[15] *= det; 357 | return a; 358 | } 359 | /** 360 | * Transposes a matrix. 361 | * @param {import("./types.js").mat4} a 362 | * @returns {import("./types.js").mat4} 363 | */ function transpose(a) { 364 | const a01 = a[1]; 365 | const a02 = a[2]; 366 | const a03 = a[3]; 367 | const a12 = a[6]; 368 | const a13 = a[7]; 369 | const a20 = a[8]; 370 | const a21 = a[9]; 371 | const a23 = a[11]; 372 | const a30 = a[12]; 373 | const a31 = a[13]; 374 | const a32 = a[14]; 375 | // 1st row - keeping a00 376 | a[1] = a[4]; 377 | a[2] = a20; 378 | a[3] = a30; 379 | // 2nd row - keeping a11 380 | a[4] = a01; 381 | a[6] = a21; 382 | a[7] = a31; 383 | // 3rd row - keeping a22 384 | a[8] = a02; 385 | a[9] = a12; 386 | a[11] = a32; 387 | // 4th row - keeping a33 388 | a[12] = a03; 389 | a[13] = a13; 390 | a[14] = a23; 391 | return a; 392 | } 393 | /** 394 | * Translates a matrix by a vector. 395 | * @param {import("./types.js").mat4} a 396 | * @param {import("./types.js").vec3} v 397 | * @returns {import("./types.js").mat4} 398 | */ function translate(a, param) { 399 | let [x, y, z] = param; 400 | a[12] += a[0] * x + a[4] * y + a[8] * z; 401 | a[13] += a[1] * x + a[5] * y + a[9] * z; 402 | a[14] += a[2] * x + a[6] * y + a[10] * z; 403 | a[15] += a[3] * x + a[7] * y + a[11] * z; 404 | return a; 405 | } 406 | /** 407 | * Rotates a matrix by an angle at an axis. 408 | * @param {import("./types.js").mat4} a 409 | * @param {import("./types.js").Radians} r 410 | * @param {import("./types.js").vec3} v 411 | * @returns {import("./types.js").mat4} 412 | */ function rotate(a, r, param) { 413 | let [x, y, z] = param; 414 | let len = Math.sqrt(x * x + y * y + z * z); 415 | if (len < EPSILON) { 416 | return null; 417 | } 418 | len = 1 / len; 419 | x *= len; 420 | y *= len; 421 | z *= len; 422 | const s = Math.sin(r); 423 | const c = Math.cos(r); 424 | const t = 1 - c; 425 | const a00 = a[0]; 426 | const a01 = a[1]; 427 | const a02 = a[2]; 428 | const a03 = a[3]; 429 | const a10 = a[4]; 430 | const a11 = a[5]; 431 | const a12 = a[6]; 432 | const a13 = a[7]; 433 | const a20 = a[8]; 434 | const a21 = a[9]; 435 | const a22 = a[10]; 436 | const a23 = a[11]; 437 | const b00 = x * x * t + c; 438 | const b01 = y * x * t + z * s; 439 | const b02 = z * x * t - y * s; 440 | const b10 = x * y * t - z * s; 441 | const b11 = y * y * t + c; 442 | const b12 = z * y * t + x * s; 443 | const b20 = x * z * t + y * s; 444 | const b21 = y * z * t - x * s; 445 | const b22 = z * z * t + c; 446 | a[0] = a00 * b00 + a10 * b01 + a20 * b02; 447 | a[1] = a01 * b00 + a11 * b01 + a21 * b02; 448 | a[2] = a02 * b00 + a12 * b01 + a22 * b02; 449 | a[3] = a03 * b00 + a13 * b01 + a23 * b02; 450 | a[4] = a00 * b10 + a10 * b11 + a20 * b12; 451 | a[5] = a01 * b10 + a11 * b11 + a21 * b12; 452 | a[6] = a02 * b10 + a12 * b11 + a22 * b12; 453 | a[7] = a03 * b10 + a13 * b11 + a23 * b12; 454 | a[8] = a00 * b20 + a10 * b21 + a20 * b22; 455 | a[9] = a01 * b20 + a11 * b21 + a21 * b22; 456 | a[10] = a02 * b20 + a12 * b21 + a22 * b22; 457 | a[11] = a03 * b20 + a13 * b21 + a23 * b22; 458 | return a; 459 | } 460 | /** 461 | * Scales a matrix by a vector. 462 | * @param {import("./types.js").mat4} a 463 | * @param {import("./types.js").vec3} v 464 | * @returns {import("./types.js").mat4} 465 | */ function scale(a, param) { 466 | let [x, y, z] = param; 467 | a[0] *= x; 468 | a[1] *= x; 469 | a[2] *= x; 470 | a[3] *= x; 471 | a[4] *= y; 472 | a[5] *= y; 473 | a[6] *= y; 474 | a[7] *= y; 475 | a[8] *= z; 476 | a[9] *= z; 477 | a[10] *= z; 478 | a[11] *= z; 479 | return a; 480 | } 481 | /** 482 | * Sets a matrix to a quaternion. 483 | * @param {import("./types.js").mat4} a 484 | * @param {import("./types.js").quat} q 485 | * @returns {import("./types.js").mat4} 486 | */ function fromQuat(a, q) { 487 | const x = q[0]; 488 | const y = q[1]; 489 | const z = q[2]; 490 | const w = q[3]; 491 | const x2 = x + x; 492 | const y2 = y + y; 493 | const z2 = z + z; 494 | const xx = x * x2; 495 | const xy = x * y2; 496 | const xz = x * z2; 497 | const yy = y * y2; 498 | const yz = y * z2; 499 | const zz = z * z2; 500 | const wx = w * x2; 501 | const wy = w * y2; 502 | const wz = w * z2; 503 | a[0] = 1 - (yy + zz); 504 | a[4] = xy - wz; 505 | a[8] = xz + wy; 506 | a[1] = xy + wz; 507 | a[5] = 1 - (xx + zz); 508 | a[9] = yz - wx; 509 | a[2] = xz - wy; 510 | a[6] = yz + wx; 511 | a[10] = 1 - (xx + yy); 512 | a[3] = a[7] = a[11] = a[12] = a[13] = a[14] = 0; 513 | a[15] = 1; 514 | return a; 515 | } 516 | /** 517 | * Sets a matrix to the TRS matrix. 518 | * @param {import("./types.js").mat4} a 519 | * @param {import("./types.js").vec3} v 520 | * @param {import("./types.js").quat} q 521 | * @param {import("./types.js").vec3} s 522 | * @returns {import("./types.js").mat4} 523 | */ function fromTranslationRotationScale(a, v, q, s) { 524 | // const TEMP_0 = create(); 525 | // identity(a); 526 | // translate(a, translation); 527 | // mult(a, fromQuat(TEMP_0, rotation)); 528 | // scale(a, scaling); 529 | const x = q[0]; 530 | const y = q[1]; 531 | const z = q[2]; 532 | const w = q[3]; 533 | const x2 = x + x; 534 | const y2 = y + y; 535 | const z2 = z + z; 536 | const xx = x * x2; 537 | const xy = x * y2; 538 | const xz = x * z2; 539 | const yy = y * y2; 540 | const yz = y * z2; 541 | const zz = z * z2; 542 | const wx = w * x2; 543 | const wy = w * y2; 544 | const wz = w * z2; 545 | a[3] = a[7] = a[11] = 0; 546 | a[15] = 1; 547 | a[0] = (1 - (yy + zz)) * s[0]; 548 | a[1] = (xy + wz) * s[0]; 549 | a[2] = (xz - wy) * s[0]; 550 | a[4] = (xy - wz) * s[1]; 551 | a[5] = (1 - (xx + zz)) * s[1]; 552 | a[6] = (yz + wx) * s[1]; 553 | a[8] = (xz + wy) * s[2]; 554 | a[9] = (yz - wx) * s[2]; 555 | a[10] = (1 - (xx + yy)) * s[2]; 556 | a[12] = v[0]; 557 | a[13] = v[1]; 558 | a[14] = v[2]; 559 | return a; 560 | } 561 | /** 562 | * Sets a 4x4 matrix to a 3x3 matrix. 563 | * @param {import("./types.js").mat4} a 564 | * @param {import("./types.js").mat3} b 565 | * @returns {import("./types.js").mat4} 566 | */ function fromMat3(a, b) { 567 | a[0] = b[0]; 568 | a[1] = b[1]; 569 | a[2] = b[2]; 570 | a[4] = b[3]; 571 | a[5] = b[4]; 572 | a[6] = b[5]; 573 | a[8] = b[6]; 574 | a[9] = b[7]; 575 | a[10] = b[8]; 576 | a[3] = a[7] = a[11] = a[12] = a[13] = a[14] = 0; 577 | a[15] = 1; 578 | return a; 579 | } 580 | /** 581 | * Creates a frustum matrix. 582 | * @param {import("./types.js").mat4} a 583 | * @param {number} left 584 | * @param {number} right 585 | * @param {number} bottom 586 | * @param {number} top 587 | * @param {number} near 588 | * @param {number} far 589 | * @returns {import("./types.js").mat4} 590 | */ function frustum(a, left, right, bottom, top, near, far) { 591 | const rl = 1 / (right - left); 592 | const tb = 1 / (top - bottom); 593 | const nf = 1 / (near - far); 594 | const near2 = near * 2; 595 | a[0] = near2 * rl; 596 | a[1] = a[2] = 0; 597 | a[3] = 0; 598 | a[4] = 0; 599 | a[5] = near2 * tb; 600 | a[6] = 0; 601 | a[7] = 0; 602 | a[8] = (right + left) * rl; 603 | a[9] = (top + bottom) * tb; 604 | a[10] = (far + near) * nf; 605 | a[11] = -1; 606 | a[12] = 0; 607 | a[13] = 0; 608 | a[14] = far * near2 * nf; 609 | a[15] = 0; 610 | return a; 611 | } 612 | /** 613 | * Creates a perspective matrix. 614 | * @param {import("./types.js").mat4} a 615 | * @param {import("./types.js").Radians} fovy 616 | * @param {number} aspectRatio 617 | * @param {number} near 618 | * @param {number} far 619 | * @returns {import("./types.js").mat4} 620 | */ function perspective(a, fovy, aspectRatio, near, far) { 621 | const f = 1 / Math.tan(fovy / 2); 622 | const nf = 1 / (near - far); 623 | a[1] = a[2] = a[3] = a[4] = a[6] = a[7] = a[8] = a[9] = a[12] = a[13] = a[15] = 0; 624 | a[0] = f / aspectRatio; 625 | a[5] = f; 626 | a[10] = (far + near) * nf; 627 | a[11] = -1; 628 | a[14] = 2 * far * near * nf; 629 | return a; 630 | } 631 | /** 632 | * Creates an orthographic matrix. 633 | * @param {import("./types.js").mat4} a 634 | * @param {number} left 635 | * @param {number} right 636 | * @param {number} bottom 637 | * @param {number} top 638 | * @param {number} near 639 | * @param {number} far 640 | * @returns {import("./types.js").mat4} 641 | */ function ortho(a, left, right, bottom, top, near, far) { 642 | const lr = left - right; 643 | const bt = bottom - top; 644 | const nf = near - far; 645 | a[1] = a[2] = a[3] = a[4] = a[6] = a[7] = a[8] = a[9] = a[11] = 0; 646 | a[0] = -2 / lr; 647 | a[5] = -2 / bt; 648 | a[10] = 2 / nf; 649 | a[12] = (left + right) / lr; 650 | a[13] = (top + bottom) / bt; 651 | a[14] = (far + near) / nf; 652 | a[15] = 1; 653 | return a; 654 | } 655 | /** 656 | * Calculates a lookAt matrix from position, target and up vectors. 657 | * @param {import("./types.js").mat4} a 658 | * @param {import("./types.js").vec3} from 659 | * @param {import("./types.js").vec3} to 660 | * @param {import("./types.js").vec3} [up=Y_UP] 661 | * @returns {import("./types.js").mat4} 662 | */ function lookAt(a, param, param1, param2) { 663 | let [eyex, eyey, eyez] = param; 664 | let [targetx, targety, targetz] = param1; 665 | let [upx, upy, upz] = param2 === undefined ? Y_UP : param2; 666 | if (Math.abs(eyex - targetx) < EPSILON && Math.abs(eyey - targety) < EPSILON && Math.abs(eyez - targetz) < EPSILON) { 667 | return identity(a); 668 | } 669 | let z0 = eyex - targetx; 670 | let z1 = eyey - targety; 671 | let z2 = eyez - targetz; 672 | let len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); 673 | z0 *= len; 674 | z1 *= len; 675 | z2 *= len; 676 | let x0 = upy * z2 - upz * z1; 677 | let x1 = upz * z0 - upx * z2; 678 | let x2 = upx * z1 - upy * z0; 679 | len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); 680 | if (len) { 681 | len = 1 / len; 682 | x0 *= len; 683 | x1 *= len; 684 | x2 *= len; 685 | } 686 | let y0 = z1 * x2 - z2 * x1; 687 | let y1 = z2 * x0 - z0 * x2; 688 | let y2 = z0 * x1 - z1 * x0; 689 | len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); 690 | if (len) { 691 | len = 1 / len; 692 | x0 *= len; 693 | x1 *= len; 694 | x2 *= len; 695 | } 696 | a[0] = x0; 697 | a[1] = y0; 698 | a[2] = z0; 699 | a[3] = 0; 700 | a[4] = x1; 701 | a[5] = y1; 702 | a[6] = z1; 703 | a[7] = 0; 704 | a[8] = x2; 705 | a[9] = y2; 706 | a[10] = z2; 707 | a[11] = 0; 708 | a[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); 709 | a[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); 710 | a[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); 711 | a[15] = 1; 712 | return a; 713 | } 714 | /** 715 | * Sets a matrix from a direction. 716 | * Note: we assume +Z facing models. 717 | * @param {import("./types.js").mat4} a 718 | * @param {import("./types.js").vec3} direction 719 | * @param {import("./types.js").vec3} [up=Y_UP] 720 | * @returns {import("./types.js").mat4} 721 | */ function fromDirection(a, param, param1) { 722 | let [z0, z1, z2] = param; 723 | let [upx, upy, upz] = param1 === undefined ? Y_UP : param1; 724 | let len = z0 * z0 + z1 * z1 + z2 * z2; 725 | if (len > 0) { 726 | len = 1 / Math.sqrt(len); 727 | z0 *= len; 728 | z1 *= len; 729 | z2 *= len; 730 | } 731 | let x0 = upy * z2 - upz * z1; 732 | let x1 = upz * z0 - upx * z2; 733 | let x2 = upx * z1 - upy * z0; 734 | len = x0 * x0 + x1 * x1 + x2 * x2; 735 | if (len > 0) { 736 | len = 1 / Math.sqrt(len); 737 | x0 *= len; 738 | x1 *= len; 739 | x2 *= len; 740 | } 741 | upx = z1 * x2 - z2 * x1; 742 | upy = z2 * x0 - z0 * x2; 743 | upz = z0 * x1 - z1 * x0; 744 | len = upx * upx + upy * upy + upz * upz; 745 | if (len > 0) { 746 | len = 1 / Math.sqrt(len); 747 | upx *= len; 748 | upy *= len; 749 | upz *= len; 750 | } 751 | a[0] = x0; 752 | a[1] = x1; 753 | a[2] = x2; 754 | a[3] = 0; 755 | a[4] = upx; 756 | a[5] = upy; 757 | a[6] = upz; 758 | a[7] = 0; 759 | a[8] = z0; 760 | a[9] = z1; 761 | a[10] = z2; 762 | a[11] = 0; 763 | a[12] = 0; 764 | a[13] = 0; 765 | a[14] = 0; 766 | a[15] = 1; 767 | return a; 768 | } 769 | /** 770 | * Sets a matrix from a point to another. 771 | * @param {import("./types.js").mat4} a 772 | * @param {import("./types.js").vec3} from 773 | * @param {import("./types.js").vec3} to 774 | * @param {import("./types.js").vec3} [up=Y_UP] 775 | * @returns {import("./types.js").mat4} 776 | */ function fromPointToPoint(a, param, param1, up) { 777 | let [fromX, fromY, fromZ] = param; 778 | let [toX, toY, toZ] = param1; 779 | return fromDirection(a, [ 780 | toX - fromX, 781 | toY - fromY, 782 | toZ - fromZ 783 | ], up); 784 | } 785 | 786 | var mat4 = /*#__PURE__*/Object.freeze({ 787 | __proto__: null, 788 | copy: copy, 789 | create: create, 790 | equals: equals, 791 | fromDirection: fromDirection, 792 | fromMat3: fromMat3, 793 | fromPointToPoint: fromPointToPoint, 794 | fromQuat: fromQuat, 795 | fromTranslationRotationScale: fromTranslationRotationScale, 796 | frustum: frustum, 797 | identity: identity, 798 | invert: invert, 799 | lookAt: lookAt, 800 | mult: mult, 801 | ortho: ortho, 802 | perspective: perspective, 803 | rotate: rotate, 804 | scale: scale, 805 | set: set, 806 | translate: translate, 807 | transpose: transpose 808 | }); 809 | 810 | export { EPSILON as E, clamp as a, toRadians as b, create as c, lerp as d, fromDirection as e, frustum as f, fromPointToPoint as g, invert as i, lookAt as l, mat4 as m, ortho as o, perspective as p, set as s, toDegrees as t, utils as u }; 811 | -------------------------------------------------------------------------------- /web_modules/_chunks/ray-bLshgWN6.js: -------------------------------------------------------------------------------- 1 | import { f as set, g as dot, s as sub, b as add, a as scale, j as cross, l as length, t as toString$1, h as create$1 } from './vec3-DW1VLBq6.js'; 2 | 3 | /** 4 | * Enum for different intersections values 5 | * @readonly 6 | * @enum {number} 7 | */ const Intersections = Object.freeze({ 8 | Intersect: 1, 9 | NoIntersect: 0, 10 | SamePlane: -1, 11 | Parallel: -2, 12 | TriangleDegenerate: -2 13 | }); 14 | const TEMP_0 = create$1(); 15 | const TEMP_1 = create$1(); 16 | const TEMP_2 = create$1(); 17 | const TEMP_3 = create$1(); 18 | const TEMP_4 = create$1(); 19 | const TEMP_5 = create$1(); 20 | const TEMP_6 = create$1(); 21 | const TEMP_7 = create$1(); 22 | const EPSILON = 1e-6; 23 | /** 24 | * Creates a new ray 25 | * @returns {import("./types.js").ray} 26 | */ function create() { 27 | return [ 28 | [ 29 | 0, 30 | 0, 31 | 0 32 | ], 33 | [ 34 | 0, 35 | 0, 36 | 1 37 | ] 38 | ]; 39 | } 40 | /** 41 | * Determines if a ray intersect a plane and set intersection point 42 | * @see {@link https://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm} 43 | * @param {import("./types.js").ray} ray 44 | * @param {import("./types.js").plane} plane 45 | * @param {import("./types.js").vec3} out 46 | * @returns {number} 47 | */ function hitTestPlane(param, param1, out) { 48 | let [origin, direction] = param; 49 | let [point, normal] = param1; 50 | if (out === undefined) out = create$1(); 51 | set(TEMP_0, origin); 52 | set(TEMP_1, direction); 53 | const dotDirectionNormal = dot(TEMP_1, normal); 54 | if (dotDirectionNormal === 0) return Intersections.SamePlane; 55 | set(TEMP_2, point); 56 | const t = dot(sub(TEMP_2, TEMP_0), normal) / dotDirectionNormal; 57 | if (t < 0) return Intersections.Parallel; 58 | set(out, add(TEMP_0, scale(TEMP_1, t))); 59 | return Intersections.Intersect; 60 | } 61 | /** 62 | * Determines if a ray intersect a triangle and set intersection point 63 | * @see {@link http://geomalgorithms.com/a06-_intersect-2.html#intersect3D_RayTriangle()} 64 | * @param {import("./types.js").ray} ray 65 | * @param {import("./types.js").triangle} triangle 66 | * @param {import("./types.js").vec3} out 67 | * @returns {number} 68 | */ function hitTestTriangle(param, param1, out) { 69 | let [origin, direction] = param; 70 | let [p0, p1, p2] = param1; 71 | if (out === undefined) out = create$1(); 72 | // get triangle edge vectors and plane normal 73 | const u = sub(set(TEMP_0, p1), p0); 74 | const v = sub(set(TEMP_1, p2), p0); 75 | const n = cross(set(TEMP_2, u), v); 76 | if (length(n) < EPSILON) return Intersections.TriangleDegenerate; 77 | // ray vectors 78 | const w0 = sub(set(TEMP_3, origin), p0); 79 | // params to calc ray-plane intersect 80 | const a = -dot(n, w0); 81 | const b = dot(n, direction); 82 | if (Math.abs(b) < EPSILON) { 83 | if (a === 0) return Intersections.SamePlane; 84 | return Intersections.NoIntersect; 85 | } 86 | // get intersect point of ray with triangle plane 87 | const r = a / b; 88 | // ray goes away from triangle 89 | if (r < -1e-6) return Intersections.NoIntersect; 90 | // for a segment, also test if (r > 1.0) => no intersect 91 | // intersect point of ray and plane 92 | const I = add(set(TEMP_4, origin), scale(set(TEMP_5, direction), r)); 93 | const uu = dot(u, u); 94 | const uv = dot(u, v); 95 | const vv = dot(v, v); 96 | const w = sub(set(TEMP_6, I), p0); 97 | const wu = dot(w, u); 98 | const wv = dot(w, v); 99 | const D = uv * uv - uu * vv; 100 | // get and test parametric coords 101 | const s = (uv * wv - vv * wu) / D; 102 | if (s < -1e-6 || s > 1.0 + EPSILON) return Intersections.NoIntersect; 103 | const t = (uv * wu - uu * wv) / D; 104 | if (t < -1e-6 || s + t > 1.0 + EPSILON) return Intersections.NoIntersect; 105 | set(out, u); 106 | scale(out, s); 107 | add(out, scale(set(TEMP_7, v), t)); 108 | add(out, p0); 109 | return Intersections.Intersect; 110 | } 111 | /** 112 | * Determines if a ray intersect an AABB bounding box 113 | * @see {@link http://gamedev.stackexchange.com/questions/18436/most-efficient-aabb-vs-ray-collision-algorithms} 114 | * @param {import("./types.js").ray} ray 115 | * @param {import("./types.js").aabb} aabb 116 | * @returns {boolean} 117 | */ function hitTestAABB(param, aabb) { 118 | let [origin, direction] = param; 119 | const dirFracx = 1.0 / direction[0]; 120 | const dirFracy = 1.0 / direction[1]; 121 | const dirFracz = 1.0 / direction[2]; 122 | const min = aabb[0]; 123 | const max = aabb[1]; 124 | const minx = min[0]; 125 | const miny = min[1]; 126 | const minz = min[2]; 127 | const maxx = max[0]; 128 | const maxy = max[1]; 129 | const maxz = max[2]; 130 | const t1 = (minx - origin[0]) * dirFracx; 131 | const t2 = (maxx - origin[0]) * dirFracx; 132 | const t3 = (miny - origin[1]) * dirFracy; 133 | const t4 = (maxy - origin[1]) * dirFracy; 134 | const t5 = (minz - origin[2]) * dirFracz; 135 | const t6 = (maxz - origin[2]) * dirFracz; 136 | const tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); 137 | const tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); 138 | return !(tmax < 0 || tmin > tmax); 139 | } 140 | /** 141 | * Alias for {@link hitTestAABB} 142 | * @function 143 | */ const intersectsAABB = hitTestAABB; 144 | /** 145 | * Prints a plane to a string. 146 | * @param {import("./types.js").ray} a 147 | * @param {number} [precision=4] 148 | * @returns {string} 149 | */ function toString(a, precision) { 150 | if (precision === undefined) precision = 4; 151 | // prettier-ignore 152 | return `[${toString$1(a[0], precision)}, ${toString$1(a[1], precision)}]`; 153 | } 154 | 155 | var ray = /*#__PURE__*/Object.freeze({ 156 | __proto__: null, 157 | Intersections: Intersections, 158 | create: create, 159 | hitTestAABB: hitTestAABB, 160 | hitTestPlane: hitTestPlane, 161 | hitTestTriangle: hitTestTriangle, 162 | intersectsAABB: intersectsAABB, 163 | toString: toString 164 | }); 165 | 166 | export { hitTestPlane as h, ray as r }; 167 | -------------------------------------------------------------------------------- /web_modules/_chunks/vec3-DW1VLBq6.js: -------------------------------------------------------------------------------- 1 | /** @module vec2 */ /** 2 | * Returns a new vec2 at 0, 0. 3 | * @returns {import("./types.js").vec2} 4 | */ function create$1() { 5 | return [ 6 | 0, 7 | 0 8 | ]; 9 | } 10 | /** 11 | * Returns a copy of a vector. 12 | * @param {import("./types.js").vec2} a 13 | * @returns {import("./types.js").vec2} 14 | */ function copy$1(a) { 15 | return a.slice(); 16 | } 17 | /** 18 | * Sets a vector to another vector. 19 | * @param {import("./types.js").vec2} a 20 | * @param {import("./types.js").vec2} b 21 | * @returns {import("./types.js").vec2} 22 | */ function set$1(a, b) { 23 | a[0] = b[0]; 24 | a[1] = b[1]; 25 | return a; 26 | } 27 | /** 28 | * Compares two vectors. 29 | * @param {import("./types.js").vec2} a 30 | * @param {import("./types.js").vec2} b 31 | * @returns {boolean} 32 | */ function equals$1(a, b) { 33 | return a[0] === b[0] && a[1] === b[1]; 34 | } 35 | /** 36 | * Add a vector to another. 37 | * @param {import("./types.js").vec2} a 38 | * @param {import("./types.js").vec2} b 39 | * @returns {import("./types.js").vec2} 40 | */ function add$1(a, b) { 41 | a[0] += b[0]; 42 | a[1] += b[1]; 43 | return a; 44 | } 45 | /** 46 | * Subtracts a vector from another. 47 | * @param {import("./types.js").vec2} a 48 | * @param {import("./types.js").vec2} b 49 | * @returns {import("./types.js").vec2} 50 | */ function sub$1(a, b) { 51 | a[0] -= b[0]; 52 | a[1] -= b[1]; 53 | return a; 54 | } 55 | /** 56 | * Scales a vector by a number. 57 | * @param {import("./types.js").vec2} a 58 | * @param {number} s 59 | * @returns {import("./types.js").vec2} 60 | */ function scale$1(a, s) { 61 | a[0] *= s; 62 | a[1] *= s; 63 | return a; 64 | } 65 | /** 66 | * Adds two vectors after scaling the second one. 67 | * @param {import("./types.js").vec2} a 68 | * @param {import("./types.js").vec2} b 69 | * @param {number} s 70 | * @returns {import("./types.js").vec2} 71 | */ function addScaled$1(a, b, s) { 72 | a[0] += b[0] * s; 73 | a[1] += b[1] * s; 74 | return a; 75 | } 76 | /** 77 | * Calculates the dot product of two vectors. 78 | * @param {import("./types.js").vec2} a 79 | * @param {import("./types.js").vec2} b 80 | * @returns {number} 81 | */ function dot$1(a, b) { 82 | return a[0] * b[0] + a[1] * b[1]; 83 | } 84 | /** 85 | * Calculates the length of a vector. 86 | * @param {import("./types.js").vec2} a 87 | * @returns {number} 88 | */ function length$1(a) { 89 | const x = a[0]; 90 | const y = a[1]; 91 | return Math.sqrt(x * x + y * y); 92 | } 93 | /** 94 | * Calculates the squared length of a vector. 95 | * @param {import("./types.js").vec2} a 96 | * @returns {number} 97 | */ function lengthSq$1(a) { 98 | const x = a[0]; 99 | const y = a[1]; 100 | return x * x + y * y; 101 | } 102 | /** 103 | * Normalises a vector. 104 | * @param {import("./types.js").vec2} a 105 | * @returns {import("./types.js").vec2} 106 | */ function normalize$1(a) { 107 | const x = a[0]; 108 | const y = a[1]; 109 | let l = Math.sqrt(x * x + y * y); 110 | l = 1 / (l || 1); 111 | a[0] *= l; 112 | a[1] *= l; 113 | return a; 114 | } 115 | /** 116 | * Calculates the distance between two vectors. 117 | * @param {import("./types.js").vec2} a 118 | * @param {import("./types.js").vec2} b 119 | * @returns {number} 120 | */ function distance$1(a, b) { 121 | const dx = b[0] - a[0]; 122 | const dy = b[1] - a[1]; 123 | return Math.sqrt(dx * dx + dy * dy); 124 | } 125 | /** 126 | * Calculates the squared distance between two vectors. 127 | * @param {import("./types.js").vec2} a 128 | * @param {import("./types.js").vec2} b 129 | * @returns {number} 130 | */ function distanceSq$1(a, b) { 131 | const dx = b[0] - a[0]; 132 | const dy = b[1] - a[1]; 133 | return dx * dx + dy * dy; 134 | } 135 | /** 136 | * Limits a vector to a length. 137 | * @param {import("./types.js").vec2} a 138 | * @param {number} len 139 | * @returns {import("./types.js").vec2} 140 | */ function limit$1(a, len) { 141 | const x = a[0]; 142 | const y = a[1]; 143 | const dsq = x * x + y * y; 144 | const lsq = len * len; 145 | if (lsq > 0 && dsq > lsq) { 146 | const nd = len / Math.sqrt(dsq); 147 | a[0] *= nd; 148 | a[1] *= nd; 149 | } 150 | return a; 151 | } 152 | /** 153 | * Linearly interpolates between two vectors. 154 | * @param {import("./types.js").vec2} a 155 | * @param {import("./types.js").vec2} b 156 | * @param {number} t 157 | * @returns {import("./types.js").vec2} 158 | */ function lerp$1(a, b, t) { 159 | const x = a[0]; 160 | const y = a[1]; 161 | a[0] = x + (b[0] - x) * t; 162 | a[1] = y + (b[1] - y) * t; 163 | return a; 164 | } 165 | /** 166 | * Prints a vector to a string. 167 | * @param {import("./types.js").vec2} a 168 | * @param {number} [precision=4] 169 | * @returns {string} 170 | */ function toString$1(a, precision) { 171 | if (precision === undefined) precision = 4; 172 | const scale = 10 ** precision; 173 | // prettier-ignore 174 | return `[${Math.floor(a[0] * scale) / scale}, ${Math.floor(a[1] * scale) / scale}]`; 175 | } 176 | 177 | var vec2 = /*#__PURE__*/Object.freeze({ 178 | __proto__: null, 179 | add: add$1, 180 | addScaled: addScaled$1, 181 | copy: copy$1, 182 | create: create$1, 183 | distance: distance$1, 184 | distanceSq: distanceSq$1, 185 | dot: dot$1, 186 | equals: equals$1, 187 | length: length$1, 188 | lengthSq: lengthSq$1, 189 | lerp: lerp$1, 190 | limit: limit$1, 191 | normalize: normalize$1, 192 | scale: scale$1, 193 | set: set$1, 194 | sub: sub$1, 195 | toString: toString$1 196 | }); 197 | 198 | /** @module vec3 */ /** 199 | * Returns a new vec3 at 0, 0, 0. 200 | * @returns {import("./types.js").vec3} 201 | */ function create() { 202 | return [ 203 | 0, 204 | 0, 205 | 0 206 | ]; 207 | } 208 | /** 209 | * Returns a copy of a vector. 210 | * @param {import("./types.js").vec3} a 211 | * @returns {import("./types.js").vec3} 212 | */ function copy(a) { 213 | return a.slice(); 214 | } 215 | /** 216 | * Sets a vector to another vector. 217 | * @param {import("./types.js").vec3} a 218 | * @param {import("./types.js").vec3} b 219 | * @returns {import("./types.js").vec3} 220 | */ function set(a, b) { 221 | a[0] = b[0]; 222 | a[1] = b[1]; 223 | a[2] = b[2]; 224 | return a; 225 | } 226 | /** 227 | * Compares two vectors. 228 | * @param {import("./types.js").vec3} a 229 | * @param {import("./types.js").vec3} b 230 | * @returns {boolean} 231 | */ function equals(a, b) { 232 | return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; 233 | } 234 | /** 235 | * Adds a vector to another. 236 | * @param {import("./types.js").vec3} a 237 | * @param {import("./types.js").vec3} b 238 | * @returns {import("./types.js").vec3} 239 | */ function add(a, b) { 240 | a[0] += b[0]; 241 | a[1] += b[1]; 242 | a[2] += b[2]; 243 | return a; 244 | } 245 | /** 246 | * Subtracts a vector from another. 247 | * @param {import("./types.js").vec3} a 248 | * @param {import("./types.js").vec3} b 249 | * @returns {import("./types.js").vec3} 250 | */ function sub(a, b) { 251 | a[0] -= b[0]; 252 | a[1] -= b[1]; 253 | a[2] -= b[2]; 254 | return a; 255 | } 256 | /** 257 | * Scales a vector by a number. 258 | * @param {import("./types.js").vec3} a 259 | * @param {number} s 260 | * @returns {import("./types.js").vec3} 261 | */ function scale(a, s) { 262 | a[0] *= s; 263 | a[1] *= s; 264 | a[2] *= s; 265 | return a; 266 | } 267 | /** 268 | * Adds two vectors after scaling the second one. 269 | * @param {import("./types.js").vec3} a 270 | * @param {import("./types.js").vec3} b 271 | * @param {number} s 272 | * @returns {import("./types.js").vec3} 273 | */ function addScaled(a, b, s) { 274 | a[0] += b[0] * s; 275 | a[1] += b[1] * s; 276 | a[2] += b[2] * s; 277 | return a; 278 | } 279 | /** 280 | * Multiplies a vector by a matrix. 281 | * @param {import("./types.js").vec3} a 282 | * @param {import("./types.js").mat4} m 283 | * @returns {import("./types.js").vec3} 284 | */ function multMat4(a, m) { 285 | const x = a[0]; 286 | const y = a[1]; 287 | const z = a[2]; 288 | a[0] = m[0] * x + m[4] * y + m[8] * z + m[12]; 289 | a[1] = m[1] * x + m[5] * y + m[9] * z + m[13]; 290 | a[2] = m[2] * x + m[6] * y + m[10] * z + m[14]; 291 | return a; 292 | } 293 | /** 294 | * Multiplies a vector by a quaternion. 295 | * @param {import("./types.js").vec3} a 296 | * @param {import("./types.js").quat} q 297 | * @returns {import("./types.js").vec3} 298 | */ function multQuat(a, q) { 299 | const x = a[0]; 300 | const y = a[1]; 301 | const z = a[2]; 302 | const qx = q[0]; 303 | const qy = q[1]; 304 | const qz = q[2]; 305 | const qw = q[3]; 306 | const ix = qw * x + qy * z - qz * y; 307 | const iy = qw * y + qz * x - qx * z; 308 | const iz = qw * z + qx * y - qy * x; 309 | const iw = -qx * x - qy * y - qz * z; 310 | a[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; 311 | a[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; 312 | a[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; 313 | return a; 314 | } 315 | /** 316 | * Calculates the dot product of two vectors. 317 | * @param {import("./types.js").vec3} a 318 | * @param {import("./types.js").vec3} b 319 | * @returns {number} 320 | */ function dot(a, b) { 321 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 322 | } 323 | /** 324 | * Calculates the cross product of two vectors. 325 | * @param {import("./types.js").vec3} a 326 | * @param {import("./types.js").vec3} b 327 | * @returns {import("./types.js").vec3} 328 | */ function cross(a, b) { 329 | const x = a[0]; 330 | const y = a[1]; 331 | const z = a[2]; 332 | const vx = b[0]; 333 | const vy = b[1]; 334 | const vz = b[2]; 335 | a[0] = y * vz - vy * z; 336 | a[1] = z * vx - vz * x; 337 | a[2] = x * vy - vx * y; 338 | return a; 339 | } 340 | /** 341 | * Calculates the length of a vector. 342 | * @param {import("./types.js").vec3} a 343 | * @returns {number} 344 | */ function length(a) { 345 | const x = a[0]; 346 | const y = a[1]; 347 | const z = a[2]; 348 | return Math.sqrt(x * x + y * y + z * z); 349 | } 350 | /** 351 | * Calculates the squared length of a vector. 352 | * @param {import("./types.js").vec3} a 353 | * @returns {number} 354 | */ function lengthSq(a) { 355 | const x = a[0]; 356 | const y = a[1]; 357 | const z = a[2]; 358 | return x * x + y * y + z * z; 359 | } 360 | /** 361 | * Normalises a vector. 362 | * @param {import("./types.js").vec3} a 363 | * @returns {import("./types.js").vec3} 364 | */ function normalize(a) { 365 | const x = a[0]; 366 | const y = a[1]; 367 | const z = a[2]; 368 | let l = Math.sqrt(x * x + y * y + z * z); 369 | l = 1 / (l || 1); 370 | a[0] *= l; 371 | a[1] *= l; 372 | a[2] *= l; 373 | return a; 374 | } 375 | /** 376 | * Calculates the distance between two vectors. 377 | * @param {import("./types.js").vec3} a 378 | * @param {import("./types.js").vec3} b 379 | * @returns {number} 380 | */ function distance(a, b) { 381 | const dx = b[0] - a[0]; 382 | const dy = b[1] - a[1]; 383 | const dz = b[2] - a[2]; 384 | return Math.sqrt(dx * dx + dy * dy + dz * dz); 385 | } 386 | /** 387 | * Calculates the squared distance between two vectors. 388 | * @param {import("./types.js").vec3} a 389 | * @param {import("./types.js").vec3} b 390 | * @returns {number} 391 | */ function distanceSq(a, b) { 392 | const dx = b[0] - a[0]; 393 | const dy = b[1] - a[1]; 394 | const dz = b[2] - a[2]; 395 | return dx * dx + dy * dy + dz * dz; 396 | } 397 | /** 398 | * Limits a vector to a length. 399 | * @param {import("./types.js").vec3} a 400 | * @param {number} len 401 | * @returns {import("./types.js").vec3} 402 | */ function limit(a, len) { 403 | const x = a[0]; 404 | const y = a[1]; 405 | const z = a[2]; 406 | const dsq = x * x + y * y + z * z; 407 | const lsq = len * len; 408 | if (lsq > 0 && dsq > lsq) { 409 | const nd = len / Math.sqrt(dsq); 410 | a[0] *= nd; 411 | a[1] *= nd; 412 | a[2] *= nd; 413 | } 414 | return a; 415 | } 416 | /** 417 | * Linearly interpolates between two vectors. 418 | * @param {import("./types.js").vec3} a 419 | * @param {import("./types.js").vec3} b 420 | * @param {number} t 421 | * @returns {import("./types.js").vec3} 422 | */ function lerp(a, b, t) { 423 | const x = a[0]; 424 | const y = a[1]; 425 | const z = a[2]; 426 | a[0] = x + (b[0] - x) * t; 427 | a[1] = y + (b[1] - y) * t; 428 | a[2] = z + (b[2] - z) * t; 429 | return a; 430 | } 431 | /** 432 | * Prints a vector to a string. 433 | * @param {import("./types.js").vec3} a 434 | * @param {number} [precision=4] 435 | * @returns {string} 436 | */ function toString(a, precision) { 437 | if (precision === undefined) precision = 4; 438 | const scale = 10 ** precision; 439 | // prettier-ignore 440 | return `[${Math.floor(a[0] * scale) / scale}, ${Math.floor(a[1] * scale) / scale}, ${Math.floor(a[2] * scale) / scale}]`; 441 | } 442 | 443 | var vec3 = /*#__PURE__*/Object.freeze({ 444 | __proto__: null, 445 | add: add, 446 | addScaled: addScaled, 447 | copy: copy, 448 | create: create, 449 | cross: cross, 450 | distance: distance, 451 | distanceSq: distanceSq, 452 | dot: dot, 453 | equals: equals, 454 | length: length, 455 | lengthSq: lengthSq, 456 | lerp: lerp, 457 | limit: limit, 458 | multMat4: multMat4, 459 | multQuat: multQuat, 460 | normalize: normalize, 461 | scale: scale, 462 | set: set, 463 | sub: sub, 464 | toString: toString 465 | }); 466 | 467 | export { scale as a, add as b, copy as c, distance as d, distance$1 as e, set as f, dot as g, create as h, toString$1 as i, cross as j, create$1 as k, length as l, multMat4 as m, normalize as n, vec3 as o, sub as s, toString as t, vec2 as v }; 468 | -------------------------------------------------------------------------------- /web_modules/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "es-module-shims": "./es-module-shims.js", 4 | "es-module-shims/debug": "./es-module-shims/debug.js", 5 | "es-module-shims/wasm": "./es-module-shims/wasm.js", 6 | "es-module-shims/typescript-transform": "./es-module-shims/typescript-transform.js", 7 | "pex-cam": "./pex-cam.js", 8 | "pex-color": "./pex-color.js", 9 | "pex-context": "./pex-context.js", 10 | "pex-geom": "./pex-geom.js", 11 | "pex-io": "./pex-io.js", 12 | "pex-math": "./pex-math.js", 13 | "primitive-geometry": "./primitive-geometry.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web_modules/pex-cam.js: -------------------------------------------------------------------------------- 1 | import { c as create, l as lookAt, s as set, i as invert, f as frustum, p as perspective$1, o as ortho, a as clamp, t as toDegrees, b as toRadians, d as lerp$2 } from './_chunks/mat4-DkKmQtAV.js'; 2 | import { n as normalize, m as multMat4, s as sub, c as copy, d as distance, a as scale, b as add, l as length, e as distance$1, f as set$1 } from './_chunks/vec3-DW1VLBq6.js'; 3 | import { g as getDefaultExportFromCjs } from './_chunks/_commonjsHelpers-BFTU3MAI.js'; 4 | import { h as hitTestPlane } from './_chunks/ray-bLshgWN6.js'; 5 | 6 | /** 7 | * An interface for cameras to extend 8 | */ class Camera { 9 | // Static getter to get different mat for each instances 10 | static get DEFAULT_OPTIONS() { 11 | return { 12 | projectionMatrix: create(), 13 | invViewMatrix: create(), 14 | viewMatrix: create(), 15 | position: [ 16 | 0, 17 | 0, 18 | 3 19 | ], 20 | target: [ 21 | 0, 22 | 0, 23 | 0 24 | ], 25 | up: [ 26 | 0, 27 | 1, 28 | 0 29 | ], 30 | aspect: 1, 31 | near: 0.1, 32 | far: 100, 33 | view: null 34 | }; 35 | } 36 | /** 37 | * Update the camera 38 | * @param {import("./types.js").CameraOptions} opts 39 | */ set(opts) { 40 | Object.assign(this, opts); 41 | if (opts.position || opts.target || opts.up) { 42 | lookAt(this.viewMatrix, this.position, this.target, this.up); 43 | set(this.invViewMatrix, this.viewMatrix); 44 | invert(this.invViewMatrix); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * A class to create a perspective camera 51 | * @extends Camera 52 | */ class PerspectiveCamera extends Camera { 53 | static get DEFAULT_OPTIONS() { 54 | return { 55 | fov: Math.PI / 3 56 | }; 57 | } 58 | /** 59 | * Create an instance of PerspectiveCamera 60 | * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts 61 | */ constructor(opts = {}){ 62 | super(); 63 | this.set({ 64 | ...Camera.DEFAULT_OPTIONS, 65 | ...PerspectiveCamera.DEFAULT_OPTIONS, 66 | ...opts 67 | }); 68 | } 69 | /** 70 | * Update the camera 71 | * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts 72 | */ set(opts) { 73 | super.set(opts); 74 | if (opts.fov || opts.aspect || opts.near || opts.far || opts.view) { 75 | if (this.view) { 76 | const aspectRatio = this.view.totalSize[0] / this.view.totalSize[1]; 77 | const top = Math.tan(this.fov * 0.5) * this.near; 78 | const bottom = -top; 79 | const left = aspectRatio * bottom; 80 | const right = aspectRatio * top; 81 | const width = Math.abs(right - left); 82 | const height = Math.abs(top - bottom); 83 | const widthNormalized = width / this.view.totalSize[0]; 84 | const heightNormalized = height / this.view.totalSize[1]; 85 | const l = left + this.view.offset[0] * widthNormalized; 86 | const r = left + (this.view.offset[0] + this.view.size[0]) * widthNormalized; 87 | const b = top - (this.view.offset[1] + this.view.size[1]) * heightNormalized; 88 | const t = top - this.view.offset[1] * heightNormalized; 89 | frustum(this.projectionMatrix, l, r, b, t, this.near, this.far); 90 | } else { 91 | perspective$1(this.projectionMatrix, this.fov, this.aspect, this.near, this.far); 92 | } 93 | } 94 | } 95 | /** 96 | * Create a picking ray in view (camera) coordinates 97 | * @param {number} x mouse x 98 | * @param {number} y mouse y 99 | * @param {number} windowWidth 100 | * @param {number} windowHeight 101 | * @returns {import("pex-geom").ray} 102 | */ getViewRay(x, y, windowWidth, windowHeight) { 103 | if (this.view) { 104 | x += this.view.offset[0]; 105 | y += this.view.offset[1]; 106 | windowWidth = this.view.totalSize[0]; 107 | windowHeight = this.view.totalSize[1]; 108 | } 109 | let nx = 2 * x / windowWidth - 1; 110 | let ny = 1 - 2 * y / windowHeight; 111 | const hNear = 2 * Math.tan(this.fov / 2) * this.near; 112 | const wNear = hNear * this.aspect; 113 | nx *= wNear * 0.5; 114 | ny *= hNear * 0.5; 115 | // [origin, direction] 116 | return [ 117 | [ 118 | 0, 119 | 0, 120 | 0 121 | ], 122 | normalize([ 123 | nx, 124 | ny, 125 | -this.near 126 | ]) 127 | ]; 128 | } 129 | /** 130 | * Create a picking ray in world coordinates 131 | * @param {number} x 132 | * @param {number} y 133 | * @param {number} windowWidth 134 | * @param {number} windowHeight 135 | * @returns {import("pex-geom").ray} 136 | */ getWorldRay(x, y, windowWidth, windowHeight) { 137 | let ray = this.getViewRay(x, y, windowWidth, windowHeight); 138 | const origin = ray[0]; 139 | const direction = ray[1]; 140 | multMat4(origin, this.invViewMatrix); 141 | // this is correct as origin is [0, 0, 0] so direction is also a point 142 | multMat4(direction, this.invViewMatrix); 143 | // TODO: is this necessary? 144 | normalize(sub(direction, origin)); 145 | return ray; 146 | } 147 | } 148 | 149 | /** 150 | * A class to create an orthographic camera 151 | * @extends Camera 152 | */ class OrthographicCamera extends Camera { 153 | static get DEFAULT_OPTIONS() { 154 | return { 155 | left: -1, 156 | right: 1, 157 | bottom: -1, 158 | top: 1, 159 | zoom: 1 160 | }; 161 | } 162 | /** 163 | * Create an instance of PerspectiveCamera 164 | * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts 165 | */ constructor(opts = {}){ 166 | super(); 167 | this.set({ 168 | ...Camera.DEFAULT_OPTIONS, 169 | ...OrthographicCamera.DEFAULT_OPTIONS, 170 | ...opts 171 | }); 172 | } 173 | /** 174 | * Update the camera 175 | * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts 176 | */ set(opts) { 177 | super.set(opts); 178 | if (opts.left || opts.right || opts.bottom || opts.top || opts.zoom || opts.near || opts.far || opts.view) { 179 | const dx = (this.right - this.left) / (2 / this.zoom); 180 | const dy = (this.top - this.bottom) / (2 / this.zoom); 181 | const cx = (this.right + this.left) / 2; 182 | const cy = (this.top + this.bottom) / 2; 183 | let left = cx - dx; 184 | let right = cx + dx; 185 | let top = cy + dy; 186 | let bottom = cy - dy; 187 | if (this.view) { 188 | const zoomW = 1 / this.zoom / (this.view.size[0] / this.view.totalSize[0]); 189 | const zoomH = 1 / this.zoom / (this.view.size[1] / this.view.totalSize[1]); 190 | const scaleW = (this.right - this.left) / this.view.size[0]; 191 | const scaleH = (this.top - this.bottom) / this.view.size[1]; 192 | left += scaleW * (this.view.offset[0] / zoomW); 193 | right = left + scaleW * (this.view.size[0] / zoomW); 194 | top -= scaleH * (this.view.offset[1] / zoomH); 195 | bottom = top - scaleH * (this.view.size[1] / zoomH); 196 | } 197 | ortho(this.projectionMatrix, left, right, bottom, top, this.near, this.far); 198 | } 199 | } 200 | getViewRay(x, y, windowWidth, windowHeight) { 201 | if (this.view) { 202 | x += this.view.offset[0]; 203 | y += this.view.offset[1]; 204 | windowWidth = this.view.totalSize[0]; 205 | windowHeight = this.view.totalSize[1]; 206 | } 207 | // [origin, direction] 208 | return [ 209 | [ 210 | 0, 211 | 0, 212 | 0 213 | ], 214 | normalize([ 215 | x * (this.right - this.left) / this.zoom / windowWidth, 216 | (1 - y) * (this.top - this.bottom) / this.zoom / windowHeight, 217 | -this.near 218 | ]) 219 | ]; 220 | } 221 | } 222 | 223 | function lerp$1(v0, v1, t) { 224 | return v0 * (1 - t) + v1 * t; 225 | } 226 | var lerp_1 = lerp$1; 227 | 228 | var lerp = lerp_1; 229 | var PI = Math.PI; 230 | var TWO_PI = Math.PI * 2; 231 | function interpolateAngle(fromAngle, toAngle, t) { 232 | fromAngle = (fromAngle + TWO_PI) % TWO_PI; 233 | toAngle = (toAngle + TWO_PI) % TWO_PI; 234 | var diff = Math.abs(fromAngle - toAngle); 235 | if (diff < PI) { 236 | return lerp(fromAngle, toAngle, t); 237 | } else { 238 | if (fromAngle > toAngle) { 239 | fromAngle = fromAngle - TWO_PI; 240 | return lerp(fromAngle, toAngle, t); 241 | } else if (toAngle > fromAngle) { 242 | toAngle = toAngle - TWO_PI; 243 | return lerp(fromAngle, toAngle, t); 244 | } 245 | } 246 | } 247 | var interpolateAngle_1 = interpolateAngle; 248 | var interpolateAngle$1 = /*@__PURE__*/ getDefaultExportFromCjs(interpolateAngle_1); 249 | 250 | function latLonToXyz(lat, lon, out) { 251 | out = out || [ 252 | 0, 253 | 0, 254 | 0 255 | ]; 256 | const phi = (lon + 90) / 180 * Math.PI; 257 | const theta = (90 - lat) / 180 * Math.PI; 258 | out[0] = Math.sin(theta) * Math.sin(phi); 259 | out[1] = Math.cos(theta); 260 | out[2] = Math.sin(theta) * Math.cos(phi); 261 | return out; 262 | } 263 | var latlonToXyz = latLonToXyz; 264 | var latLonToXyz$1 = /*@__PURE__*/ getDefaultExportFromCjs(latlonToXyz); 265 | 266 | function xyzToLatLon(normalizedPosition, out) { 267 | out = out || [ 268 | 0, 269 | 0 270 | ]; 271 | out[0] = 90 - Math.acos(normalizedPosition[1]) / Math.PI * 180; 272 | out[1] = -Math.atan2(normalizedPosition[2], normalizedPosition[0]) / Math.PI * 180; 273 | return out; 274 | } 275 | var xyzToLatlon = xyzToLatLon; 276 | var xyzToLatLon$1 = /*@__PURE__*/ getDefaultExportFromCjs(xyzToLatlon); 277 | 278 | var rootPosition = { 279 | left: 0, 280 | top: 0 281 | }; 282 | var mouseEventOffset_1 = mouseEventOffset; 283 | function mouseEventOffset(ev, target, out) { 284 | target = target || ev.currentTarget || ev.srcElement; 285 | if (!Array.isArray(out)) { 286 | out = [ 287 | 0, 288 | 0 289 | ]; 290 | } 291 | var cx = ev.clientX || 0; 292 | var cy = ev.clientY || 0; 293 | var rect = getBoundingClientOffset(target); 294 | out[0] = cx - rect.left; 295 | out[1] = cy - rect.top; 296 | return out; 297 | } 298 | function getBoundingClientOffset(element) { 299 | if (element === window || element === document || element === document.body) { 300 | return rootPosition; 301 | } else { 302 | return element.getBoundingClientRect(); 303 | } 304 | } 305 | var eventOffset = /*@__PURE__*/ getDefaultExportFromCjs(mouseEventOffset_1); 306 | 307 | /** 308 | * Camera controls to orbit around a target 309 | */ class OrbiterControls { 310 | static get DEFAULT_OPTIONS() { 311 | return { 312 | element: document, 313 | easing: 0.1, 314 | zoom: true, 315 | pan: true, 316 | drag: true, 317 | minDistance: 0.01, 318 | maxDistance: Infinity, 319 | minLat: -89.5, 320 | maxLat: 89.5, 321 | minLon: -Infinity, 322 | maxLon: Infinity, 323 | panSlowdown: 4, 324 | zoomSlowdown: 400, 325 | dragSlowdown: 4, 326 | autoUpdate: true 327 | }; 328 | } 329 | get domElement() { 330 | return this.element === document ? this.element.body : this.element; 331 | } 332 | /** 333 | * Create an instance of OrbiterControls 334 | * @param {import("./types.js").OrbiterControlsOptions} opts 335 | */ constructor(opts){ 336 | // Internals 337 | // Set initially by .set 338 | this.lat = null; // Y 339 | this.lon = null; // XZ 340 | this.currentLat = null; 341 | this.currentLon = null; 342 | this.distance = null; 343 | this.currentDistance = null; 344 | // Updated by user interaction 345 | this.panning = false; 346 | this.dragging = false; 347 | this.zooming = false; 348 | this.width = 0; 349 | this.height = 0; 350 | this.zoomTouchDistance = null; 351 | this.panPlane = null; 352 | this.clickTarget = [ 353 | 0, 354 | 0, 355 | 0 356 | ]; 357 | this.clickPosWorld = [ 358 | 0, 359 | 0, 360 | 0 361 | ]; 362 | this.clickPosPlane = [ 363 | 0, 364 | 0, 365 | 0 366 | ]; 367 | this.dragPos = [ 368 | 0, 369 | 0, 370 | 0 371 | ]; 372 | this.dragPosWorld = [ 373 | 0, 374 | 0, 375 | 0 376 | ]; 377 | this.dragPosPlane = [ 378 | 0, 379 | 0, 380 | 0 381 | ]; 382 | // TODO: add ability to set lat/lng instead of position/target 383 | this.set({ 384 | ...OrbiterControls.DEFAULT_OPTIONS, 385 | ...opts 386 | }); 387 | this.setup(); 388 | } 389 | /** 390 | * Update the control 391 | * @param {import("./types.js").OrbiterOptions} opts 392 | */ set(opts) { 393 | Object.assign(this, opts); 394 | if (opts.camera) { 395 | const latLon = xyzToLatLon$1(normalize(sub(copy(opts.camera.position), opts.camera.target))); 396 | const distance$1 = opts.distance || distance(opts.camera.position, opts.camera.target); 397 | this.lat = latLon[0]; 398 | this.lon = latLon[1]; 399 | this.currentLat = this.lat; 400 | this.currentLon = this.lon; 401 | this.distance = distance$1; 402 | this.currentDistance = this.distance; 403 | } 404 | if (Object.getOwnPropertyDescriptor(opts, "autoUpdate")) { 405 | if (this.autoUpdate) { 406 | const self = this; 407 | this.rafHandle = requestAnimationFrame(function tick() { 408 | self.updateCamera(); 409 | if (self.autoUpdate) self.rafHandle = requestAnimationFrame(tick); 410 | }); 411 | } else if (this.rafHandle) { 412 | cancelAnimationFrame(this.rafHandle); 413 | } 414 | } 415 | } 416 | updateCamera() { 417 | // instad of rotating the object we want to move camera around it 418 | if (!this.camera) return; 419 | const position = this.camera.position; 420 | const target = this.camera.target; 421 | this.lat = clamp(this.lat, this.minLat, this.maxLat); 422 | if (this.minLon !== -Infinity && this.maxLon !== Infinity) { 423 | this.lon = clamp(this.lon, this.minLon, this.maxLon) % 360; 424 | } 425 | this.currentLat = toDegrees(interpolateAngle$1((toRadians(this.currentLat) + 2 * Math.PI) % (2 * Math.PI), (toRadians(this.lat) + 2 * Math.PI) % (2 * Math.PI), this.easing)); 426 | this.currentLon += (this.lon - this.currentLon) * this.easing; 427 | this.currentDistance = lerp$2(this.currentDistance, this.distance, this.easing); 428 | // Set position from lat/lon 429 | latLonToXyz$1(this.currentLat, this.currentLon, position); 430 | // Move position according to distance and target 431 | scale(position, this.currentDistance); 432 | add(position, target); 433 | if (this.camera.zoom) this.camera.set({ 434 | zoom: length(position) 435 | }); 436 | this.camera.set({ 437 | position 438 | }); 439 | } 440 | updateWindowSize() { 441 | const width = this.domElement.clientWidth || this.domElement.innerWidth; 442 | const height = this.domElement.clientHeight || this.domElement.innerHeight; 443 | if (width !== this.width) this.width = width; 444 | if (height !== this.height) this.height = height; 445 | } 446 | handleDragStart(position) { 447 | this.dragging = true; 448 | this.dragPos = position; 449 | } 450 | handlePanZoomStart(touch0, touch1) { 451 | this.dragging = false; 452 | if (this.zoom && touch1) { 453 | this.zooming = true; 454 | this.zoomTouchDistance = distance$1(touch1, touch0); 455 | } 456 | const camera = this.camera; 457 | if (this.pan && camera) { 458 | this.panning = true; 459 | this.updateWindowSize(); 460 | // TODO: use dragPos? 461 | const clickPosWindow = touch1 ? [ 462 | (touch0[0] + touch1[0]) * 0.5, 463 | (touch0[1] + touch1[1]) * 0.5 464 | ] : touch0; 465 | set$1(this.clickTarget, camera.target); 466 | const targetInViewSpace = multMat4(copy(this.clickTarget), camera.viewMatrix); 467 | this.panPlane = [ 468 | targetInViewSpace, 469 | [ 470 | 0, 471 | 0, 472 | 1 473 | ] 474 | ]; 475 | hitTestPlane(camera.getViewRay(clickPosWindow[0], clickPosWindow[1], this.width, this.height), this.panPlane, this.clickPosPlane); 476 | } 477 | } 478 | handleDragMove(position) { 479 | const dx = position[0] - this.dragPos[0]; 480 | const dy = position[1] - this.dragPos[1]; 481 | this.lat += dy / this.dragSlowdown; 482 | this.lon -= dx / this.dragSlowdown; 483 | this.dragPos = position; 484 | } 485 | handlePanZoomMove(touch0, touch1) { 486 | if (this.zoom && touch1) { 487 | const distance = distance$1(touch1, touch0); 488 | this.handleZoom(this.zoomTouchDistance - distance); 489 | this.zoomTouchDistance = distance; 490 | } 491 | const camera = this.camera; 492 | if (this.pan && camera && this.panPlane) { 493 | const dragPosWindow = touch1 ? [ 494 | (touch0[0] + touch1[0]) * 0.5, 495 | (touch0[1] + touch1[1]) * 0.5 496 | ] : touch0; 497 | hitTestPlane(camera.getViewRay(dragPosWindow[0], dragPosWindow[1], this.width, this.height), this.panPlane, this.dragPosPlane); 498 | multMat4(set$1(this.clickPosWorld, this.clickPosPlane), camera.invViewMatrix); 499 | multMat4(set$1(this.dragPosWorld, this.dragPosPlane), camera.invViewMatrix); 500 | const diffWorld = sub(copy(this.dragPosWorld), this.clickPosWorld); 501 | camera.set({ 502 | distance: this.distance, 503 | target: sub(copy(this.clickTarget), diffWorld) 504 | }); 505 | } 506 | } 507 | handleZoom(dy) { 508 | this.distance *= 1 + dy / this.zoomSlowdown; 509 | this.distance = clamp(this.distance, this.minDistance, this.maxDistance); 510 | } 511 | handleEnd() { 512 | this.dragging = false; 513 | this.panning = false; 514 | this.zooming = false; 515 | this.panPlane = null; 516 | } 517 | setup() { 518 | this.onPointerDown = (event)=>{ 519 | const pan = event.ctrlKey || event.metaKey || event.shiftKey || event.touches && event.touches.length === 2; 520 | const touch0 = eventOffset(event.touches ? event.touches[0] : event, this.domElement); 521 | if (this.drag && !pan) { 522 | this.handleDragStart(touch0); 523 | } else if ((this.pan || this.zoom) && pan) { 524 | const touch1 = event.touches && eventOffset(event.touches[1], this.domElement); 525 | this.handlePanZoomStart(touch0, touch1); 526 | } 527 | }; 528 | this.onPointerMove = (event)=>{ 529 | const touch0 = eventOffset(event.touches ? event.touches[0] : event, this.domElement); 530 | if (this.dragging) { 531 | this.handleDragMove(touch0); 532 | } else if (this.panning || this.zooming) { 533 | if (event.touches && !event.touches[1]) return; 534 | const touch1 = event.touches && eventOffset(event.touches[1], this.domElement); 535 | this.handlePanZoomMove(touch0, touch1); 536 | } 537 | }; 538 | this.onPointerUp = ()=>{ 539 | this.handleEnd(); 540 | }; 541 | this.onTouchStart = (event)=>{ 542 | event.preventDefault(); 543 | if (event.touches.length <= 2) this.onPointerDown(event); 544 | }; 545 | this.onTouchMove = (event)=>{ 546 | !!event.cancelable && event.preventDefault(); 547 | if (event.touches.length <= 2) this.onPointerMove(event); 548 | }; 549 | this.onWheel = (event)=>{ 550 | if (!this.zoom) return; 551 | event.preventDefault(); 552 | this.handleZoom(event.deltaY); 553 | }; 554 | this.element.addEventListener("pointerdown", this.onPointerDown); 555 | this.element.addEventListener("wheel", this.onWheel, { 556 | passive: false 557 | }); 558 | document.addEventListener("pointermove", this.onPointerMove); 559 | document.addEventListener("pointerup", this.onPointerUp); 560 | this.domElement.style.touchAction = "none"; 561 | } 562 | /** 563 | * Remove all event listeners 564 | */ dispose() { 565 | if (this.rafHandle) cancelAnimationFrame(this.rafHandle); 566 | this.element.removeEventListener("pointerdown", this.onPointerDown); 567 | this.element.removeEventListener("wheel", this.onWheel); 568 | document.removeEventListener("pointermove", this.onPointerMove); 569 | document.removeEventListener("pointerup", this.onPointerUp); 570 | } 571 | } 572 | 573 | /** 574 | * Factory function for perspective camera 575 | * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts 576 | * @returns {PerspectiveCamera} 577 | */ const perspective = (opts)=>new PerspectiveCamera(opts); 578 | /** 579 | * Factory function for orthographic camera 580 | * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts 581 | * @returns {OrthographicCamera} 582 | */ const orthographic = (opts)=>new OrthographicCamera(opts); 583 | /** 584 | * Factory function for orbiter controls 585 | * @param {import("./types.js").OrbiterControlsOptions} opts 586 | * @returns {OrbiterControls} 587 | */ const orbiter = (opts)=>new OrbiterControls(opts); 588 | 589 | export { orbiter, orthographic, perspective }; 590 | -------------------------------------------------------------------------------- /web_modules/pex-geom.js: -------------------------------------------------------------------------------- 1 | import { t as toString$3, f as set$2, s as sub, n as normalize, g as dot, h as create$3, i as toString$4 } from './_chunks/vec3-DW1VLBq6.js'; 2 | import { s as set3 } from './_chunks/avec3-CX_9gCVx.js'; 3 | export { r as ray } from './_chunks/ray-bLshgWN6.js'; 4 | 5 | /** 6 | * Creates a new bounding box. 7 | * @returns {import("./types.js").aabb} 8 | */ function create$2() { 9 | // [min, max] 10 | return [ 11 | [ 12 | Infinity, 13 | Infinity, 14 | Infinity 15 | ], 16 | [ 17 | -Infinity, 18 | -Infinity, 19 | -Infinity 20 | ] 21 | ]; 22 | } 23 | /** 24 | * Reset a bounding box. 25 | * @param {import("./types.js").aabb} a 26 | * @returns {import("./types.js").rect} 27 | */ function empty$1(a) { 28 | a[0][0] = Infinity; 29 | a[0][1] = Infinity; 30 | a[0][2] = Infinity; 31 | a[1][0] = -Infinity; 32 | a[1][1] = -Infinity; 33 | a[1][2] = -Infinity; 34 | return a; 35 | } 36 | /** 37 | * Copies a bounding box. 38 | * @param {import("./types.js").aabb} a 39 | * @returns {import("./types.js").aabb} 40 | */ function copy$1(a) { 41 | return [ 42 | a[0].slice(), 43 | a[1].slice() 44 | ]; 45 | } 46 | /** 47 | * Sets a bounding box to another. 48 | * @param {import("./types.js").aabb} a 49 | * @param {import("./types.js").aabb} b 50 | * @returns {import("./types.js").aabb} 51 | */ function set$1(a, b) { 52 | a[0][0] = b[0][0]; 53 | a[0][1] = b[0][1]; 54 | a[0][2] = b[0][2]; 55 | a[1][0] = b[1][0]; 56 | a[1][1] = b[1][1]; 57 | a[1][2] = b[1][2]; 58 | return a; 59 | } 60 | /** 61 | * Checks if a bounding box is empty. 62 | * @param {import("./types.js").aabb} a 63 | * @returns {boolean} 64 | */ function isEmpty$1(a) { 65 | return a[0][0] > a[1][0] || a[0][1] > a[1][1] || a[0][2] > a[1][2]; 66 | } 67 | /** 68 | * Updates a bounding box from a list of points. 69 | * @param {import("./types.js").aabb} a 70 | * @param {import("./types.js").vec3[] | import("./types.js").TypedArray} points 71 | * @returns {import("./types.js").aabb} 72 | */ function fromPoints$1(a, points) { 73 | const isFlatArray = !points[0]?.length; 74 | const l = points.length / (isFlatArray ? 3 : 1); 75 | for(let i = 0; i < l; i++){ 76 | if (isFlatArray) { 77 | includePoint$1(a, points, i * 3); 78 | } else { 79 | includePoint$1(a, points[i]); 80 | } 81 | } 82 | return a; 83 | } 84 | /** 85 | * Returns a list of 8 points from a bounding box. 86 | * @param {import("./types.js").aabb} a 87 | * @param {import("./types.js").vec3[]} [points] 88 | * @returns {import("./types.js").vec3[]} 89 | */ function getCorners$1(a, points) { 90 | if (points === undefined) points = Array.from({ 91 | length: 8 92 | }, ()=>[]); 93 | set3(points[0], 0, a[0][0], a[0][1], a[0][2]); 94 | set3(points[1], 0, a[1][0], a[0][1], a[0][2]); 95 | set3(points[2], 0, a[1][0], a[0][1], a[1][2]); 96 | set3(points[3], 0, a[0][0], a[0][1], a[1][2]); 97 | set3(points[4], 0, a[0][0], a[1][1], a[0][2]); 98 | set3(points[5], 0, a[1][0], a[1][1], a[0][2]); 99 | set3(points[6], 0, a[1][0], a[1][1], a[1][2]); 100 | set3(points[7], 0, a[0][0], a[1][1], a[1][2]); 101 | return points; 102 | } 103 | /** 104 | * Returns the center of a bounding box. 105 | * @param {import("./types.js").aabb} a 106 | * @param {import("./types.js").vec3} out 107 | * @returns {import("./types.js").vec3} 108 | */ function center$1(a, out) { 109 | if (out === undefined) out = [ 110 | 0, 111 | 0, 112 | 0 113 | ]; 114 | out[0] = (a[0][0] + a[1][0]) / 2; 115 | out[1] = (a[0][1] + a[1][1]) / 2; 116 | out[2] = (a[0][2] + a[1][2]) / 2; 117 | return out; 118 | } 119 | /** 120 | * Returns the size of a bounding box. 121 | * @param {import("./types.js").aabb} a 122 | * @param {import("./types.js").vec3} out 123 | * @returns {import("./types.js").vec3} 124 | */ function size$1(a, out) { 125 | if (out === undefined) out = [ 126 | 0, 127 | 0, 128 | 0 129 | ]; 130 | out[0] = Math.abs(a[1][0] - a[0][0]); 131 | out[1] = Math.abs(a[1][1] - a[0][1]); 132 | out[2] = Math.abs(a[1][2] - a[0][2]); 133 | return out; 134 | } 135 | /** 136 | * Checks if a point is inside a bounding box. 137 | * @param {import("./types.js").aabb} a 138 | * @param {import("./types.js").vec3} p 139 | * @returns {boolean} 140 | */ function containsPoint$1(a, param) { 141 | let [x, y, z] = param; 142 | return x >= a[0][0] && x <= a[1][0] && y >= a[0][1] && y <= a[1][1] && z >= a[0][2] && z <= a[1][2]; 143 | } 144 | /** 145 | * Includes a bounding box in another. 146 | * @param {import("./types.js").aabb} a 147 | * @param {import("./types.js").aabb} b 148 | * @returns {import("./types.js").aabb} 149 | */ function includeAABB(a, b) { 150 | if (isEmpty$1(a)) { 151 | set$1(a, b); 152 | } else if (isEmpty$1(b)) ; else { 153 | a[0][0] = Math.min(a[0][0], b[0][0]); 154 | a[0][1] = Math.min(a[0][1], b[0][1]); 155 | a[0][2] = Math.min(a[0][2], b[0][2]); 156 | a[1][0] = Math.max(a[1][0], b[1][0]); 157 | a[1][1] = Math.max(a[1][1], b[1][1]); 158 | a[1][2] = Math.max(a[1][2], b[1][2]); 159 | } 160 | return a; 161 | } 162 | /** 163 | * Includes a point in a bounding box. 164 | * @param {import("./types.js").aabb} a 165 | * @param {import("./types.js").vec3} p 166 | * @param {number} [i=0] offset in the point array 167 | * @returns {import("./types.js").vec3} 168 | */ function includePoint$1(a, p, i) { 169 | if (i === undefined) i = 0; 170 | a[0][0] = Math.min(a[0][0], p[i + 0]); 171 | a[0][1] = Math.min(a[0][1], p[i + 1]); 172 | a[0][2] = Math.min(a[0][2], p[i + 2]); 173 | a[1][0] = Math.max(a[1][0], p[i + 0]); 174 | a[1][1] = Math.max(a[1][1], p[i + 1]); 175 | a[1][2] = Math.max(a[1][2], p[i + 2]); 176 | return a; 177 | } 178 | /** 179 | * Prints a bounding box to a string. 180 | * @param {import("./types.js").aabb} a 181 | * @param {number} [precision=4] 182 | * @returns {string} 183 | */ function toString$2(a, precision) { 184 | if (precision === undefined) precision = 4; 185 | // prettier-ignore 186 | return `[${toString$3(a[0], precision)}, ${toString$3(a[1], precision)}]`; 187 | } 188 | 189 | var aabb = /*#__PURE__*/Object.freeze({ 190 | __proto__: null, 191 | center: center$1, 192 | containsPoint: containsPoint$1, 193 | copy: copy$1, 194 | create: create$2, 195 | empty: empty$1, 196 | fromPoints: fromPoints$1, 197 | getCorners: getCorners$1, 198 | includeAABB: includeAABB, 199 | includePoint: includePoint$1, 200 | isEmpty: isEmpty$1, 201 | set: set$1, 202 | size: size$1, 203 | toString: toString$2 204 | }); 205 | 206 | /** 207 | * Enum for different side values 208 | * @readonly 209 | * @enum {number} 210 | */ const Side = Object.freeze({ 211 | OnPlane: 0, 212 | Same: -1, 213 | Opposite: 1 214 | }); 215 | const TEMP_0 = create$3(); 216 | /** 217 | * Creates a new plane 218 | * @returns {import("./types.js").plane} 219 | */ function create$1() { 220 | return [ 221 | [ 222 | 0, 223 | 0, 224 | 0 225 | ], 226 | [ 227 | 0, 228 | 1, 229 | 0 230 | ] 231 | ]; 232 | } 233 | /** 234 | * Returns on which side a point is. 235 | * @param {import("./types.js").plane} plane 236 | * @param {import("./types.js").vec3} point 237 | * @returns {number} 238 | */ function side(param, point) { 239 | let [planePoint, planeNormal] = param; 240 | set$2(TEMP_0, planePoint); 241 | sub(TEMP_0, point); 242 | normalize(TEMP_0); 243 | const dot$1 = dot(TEMP_0, planeNormal); 244 | if (dot$1 > 0) return Side.Opposite; 245 | if (dot$1 < 0) return Side.Same; 246 | return Side.OnPlane; 247 | } 248 | /** 249 | * Prints a plane to a string. 250 | * @param {import("./types.js").plane} a 251 | * @param {number} [precision=4] 252 | * @returns {string} 253 | */ function toString$1(a, precision) { 254 | if (precision === undefined) precision = 4; 255 | // prettier-ignore 256 | return `[${toString$3(a[0], precision)}, ${toString$3(a[1], precision)}]`; 257 | } 258 | 259 | var plane = /*#__PURE__*/Object.freeze({ 260 | __proto__: null, 261 | Side: Side, 262 | create: create$1, 263 | side: side, 264 | toString: toString$1 265 | }); 266 | 267 | /** 268 | * Creates a new rectangle. 269 | * @returns {import("./types.js").rect} 270 | */ function create() { 271 | return [ 272 | [ 273 | Infinity, 274 | Infinity 275 | ], 276 | [ 277 | -Infinity, 278 | -Infinity 279 | ] 280 | ]; 281 | } 282 | /** 283 | * Reset a rectangle. 284 | * @param {import("./types.js").rect} a 285 | * @returns {import("./types.js").rect} 286 | */ function empty(a) { 287 | a[0][0] = a[0][1] = Infinity; 288 | a[1][0] = a[1][1] = -Infinity; 289 | return a; 290 | } 291 | /** 292 | * Copies a rectangle. 293 | * @param {import("./types.js").rect} a 294 | * @returns {import("./types.js").rect} 295 | */ function copy(a) { 296 | return [ 297 | a[0].slice(), 298 | a[1].slice() 299 | ]; 300 | } 301 | /** 302 | * Sets a rectangle to another. 303 | * @param {import("./types.js").rect} a 304 | * @param {import("./types.js").rect} b 305 | * @returns {import("./types.js").rect} 306 | */ function set(a, b) { 307 | a[0][0] = b[0][0]; 308 | a[0][1] = b[0][1]; 309 | a[1][0] = b[1][0]; 310 | a[1][1] = b[1][1]; 311 | return a; 312 | } 313 | /** 314 | * Checks if a rectangle is empty. 315 | * @param {import("./types.js").rect} a 316 | * @returns {boolean} 317 | */ function isEmpty(a) { 318 | return a[0][0] > a[1][0] || a[0][1] > a[1][1]; 319 | } 320 | /** 321 | * Updates a rectangle from a list of points. 322 | * @param {import("./types.js").rect} a 323 | * @param {import("./types.js").vec2[] | import("./types.js").TypedArray} points 324 | * @returns {import("./types.js").rect} 325 | */ function fromPoints(a, points) { 326 | const isTypedArray = !Array.isArray(points); 327 | for(let i = 0; i < points.length / (isTypedArray ? 2 : 1); i++){ 328 | includePoint(a, isTypedArray ? points.slice(i * 2) : points[i]); 329 | } 330 | return a; 331 | } 332 | /** 333 | * Returns a list of 4 points from a rectangle. 334 | * @param {import("./types.js").rect} a 335 | * @param {import("./types.js").vec2[]} points 336 | * @returns {import("./types.js").vec2[]} 337 | */ function getCorners(a, points) { 338 | if (points === undefined) points = []; 339 | points[0] = a[0].slice(); 340 | points[1] = [ 341 | a[0][1], 342 | a[1][0] 343 | ]; 344 | points[2] = a[1].slice(); 345 | points[3] = [ 346 | a[1][0], 347 | a[0][1] 348 | ]; 349 | return points; 350 | } 351 | /** 352 | * Scales a rectangle. 353 | * @param {import("./types.js").rect} a 354 | * @param {number} n 355 | * @returns {import("./types.js").rect} 356 | */ function scale(a, n) { 357 | a[0][0] *= n; 358 | a[0][1] *= n; 359 | a[1][0] *= n; 360 | a[1][1] *= n; 361 | return a; 362 | } 363 | /** 364 | * Sets the size of a rectangle using width and height. 365 | * @param {import("./types.js").rect} a 366 | * @param {import("./types.js").vec2} size 367 | * @returns {import("./types.js").rect} 368 | */ function setSize(a, size) { 369 | a[1][0] = a[0][0] + size[0]; 370 | a[1][1] = a[0][1] + size[1]; 371 | return a; 372 | } 373 | /** 374 | * Returns the size of a rectangle. 375 | * @param {import("./types.js").rect} a 376 | * @param {import("./types.js").vec2} out 377 | * @returns {import("./types.js").vec2} 378 | */ function size(a, out) { 379 | if (out === undefined) out = []; 380 | out[0] = width(a); 381 | out[1] = height(a); 382 | return out; 383 | } 384 | /** 385 | * Returns the width of a rectangle. 386 | * @param {import("./types.js").rect} a 387 | * @returns {number} 388 | */ function width(a) { 389 | return a[1][0] - a[0][0]; 390 | } 391 | /** 392 | * Returns the height of a rectangle. 393 | * @param {import("./types.js").rect} a 394 | * @returns {number} 395 | */ function height(a) { 396 | return a[1][1] - a[0][1]; 397 | } 398 | /** 399 | * Returns the aspect ratio of a rectangle. 400 | * @param {import("./types.js").rect} a 401 | * @returns {number} 402 | */ function aspectRatio(a) { 403 | return width(a) / height(a); 404 | } 405 | /** 406 | * Sets the position of a rectangle. 407 | * @param {import("./types.js").rect} a 408 | * @param {import("./types.js").vec2} p 409 | * @returns {import("./types.js").rect} 410 | */ function setPosition(a, param) { 411 | let [x, y] = param; 412 | const w = width(a); 413 | const h = height(a); 414 | a[0][0] = x; 415 | a[0][1] = y; 416 | a[1][0] = x + w; 417 | a[1][1] = y + h; 418 | return a; 419 | } 420 | /** 421 | * Returns the center of a rectangle. 422 | * @param {import("./types.js").rect} a 423 | * @param {import("./types.js").vec2} out 424 | * @returns {import("./types.js").rect} 425 | */ function center(a, out) { 426 | if (out === undefined) out = []; 427 | out[0] = a[0][0] + width(a) * 0.5; 428 | out[1] = a[0][1] + height(a) * 0.5; 429 | return out; 430 | } 431 | /** 432 | * Checks if a point is inside a rectangle. 433 | * @param {import("./types.js").rect} a 434 | * @param {import("./types.js").vec2} p 435 | * @returns {boolean} 436 | */ function containsPoint(a, param) { 437 | let [x, y] = param; 438 | return x >= a[0][0] && x <= a[1][0] && y >= a[0][1] && y <= a[1][1]; 439 | } 440 | /** 441 | * Checks if a rectangle is inside another rectangle. 442 | * @param {import("./types.js").rect} a 443 | * @param {import("./types.js").rect} b 444 | * @returns {boolean} 445 | */ function containsRect(a, b) { 446 | return containsPoint(a, b[0]) && containsPoint(a, b[1]); 447 | } 448 | /** 449 | * Includes a point in a rectangle. 450 | * @param {import("./types.js").rect} a 451 | * @param {import("./types.js").vec2} p 452 | * @returns {import("./types.js").rect} 453 | */ function includePoint(a, param) { 454 | let [x, y] = param; 455 | const minx = a[0][0]; 456 | const miny = a[0][1]; 457 | const maxx = a[1][0]; 458 | const maxy = a[1][1]; 459 | a[0][0] = minx > x ? x : minx; 460 | a[0][1] = miny > y ? y : miny; 461 | a[1][0] = maxx < x ? x : maxx; 462 | a[1][1] = maxy < y ? y : maxy; 463 | return a; 464 | } 465 | /** 466 | * Includes a rectangle in another rectangle. 467 | * @param {import("./types.js").rect} a 468 | * @param {import("./types.js").rect} b 469 | * @returns {import("./types.js").rect} 470 | */ function includeRect(a, b) { 471 | includePoint(a, b[0]); 472 | includePoint(a, b[1]); 473 | return a; 474 | } 475 | /** 476 | * Maps a point into the dimensions of a rectangle. 477 | * @param {import("./types.js").rect} a 478 | * @param {import("./types.js").vec2} p 479 | * @returns {import("./types.js").vec2} 480 | */ function mapPoint(a, p) { 481 | const minx = a[0][0]; 482 | const miny = a[0][1]; 483 | const maxx = a[1][0]; 484 | const maxy = a[1][1]; 485 | p[0] = Math.max(minx, Math.min(p[0], maxx)) - minx; 486 | p[1] = Math.max(miny, Math.min(p[1], maxy)) - miny; 487 | return p; 488 | } 489 | /** 490 | * Clamps a point into the dimensions of a rectangle. 491 | * @param {import("./types.js").rect} a 492 | * @param {import("./types.js").vec2} p 493 | * @returns {import("./types.js").vec2} 494 | */ function clampPoint(a, p) { 495 | const minx = a[0][0]; 496 | const miny = a[0][1]; 497 | const maxx = a[1][0]; 498 | const maxy = a[1][1]; 499 | p[0] = Math.max(minx, Math.min(p[0], maxx)); 500 | p[1] = Math.max(miny, Math.min(p[1], maxy)); 501 | return p; 502 | } 503 | /** 504 | * Prints a rect to a string. 505 | * @param {import("./types.js").rect} a 506 | * @param {number} [precision=4] 507 | * @returns {string} 508 | */ function toString(a, precision) { 509 | if (precision === undefined) precision = 4; 510 | // prettier-ignore 511 | return `[${toString$4(a[0], precision)}, ${toString$4(a[1], precision)}]`; 512 | } 513 | 514 | var rect = /*#__PURE__*/Object.freeze({ 515 | __proto__: null, 516 | aspectRatio: aspectRatio, 517 | center: center, 518 | clampPoint: clampPoint, 519 | containsPoint: containsPoint, 520 | containsRect: containsRect, 521 | copy: copy, 522 | create: create, 523 | empty: empty, 524 | fromPoints: fromPoints, 525 | getCorners: getCorners, 526 | height: height, 527 | includePoint: includePoint, 528 | includeRect: includeRect, 529 | isEmpty: isEmpty, 530 | mapPoint: mapPoint, 531 | scale: scale, 532 | set: set, 533 | setPosition: setPosition, 534 | setSize: setSize, 535 | size: size, 536 | toString: toString, 537 | width: width 538 | }); 539 | 540 | export { aabb, plane, rect }; 541 | -------------------------------------------------------------------------------- /web_modules/pex-io.js: -------------------------------------------------------------------------------- 1 | /** @module pex-io */ const ok = async (response)=>response.ok ? response : Promise.reject(new Error(`GET ${response.url} ${response.status} (${response.statusText})`)); 2 | /** 3 | * Load an item and parse the Response as text. 4 | * @function 5 | * @param {RequestInfo} url 6 | * @param {RequestInit} options 7 | * @returns {Promise} 8 | */ const loadText = async (url, options)=>{ 9 | if (options === undefined) options = {}; 10 | return await (await ok(await fetch(url, options))).text(); 11 | }; 12 | /** 13 | * Load an item and parse the Response as json. 14 | * @function 15 | * @param {RequestInfo} url 16 | * @param {RequestInit} options 17 | * @returns {Promise} 18 | */ const loadJson = async (url, options)=>{ 19 | if (options === undefined) options = {}; 20 | return await (await ok(await fetch(url, options))).json(); 21 | }; 22 | /** 23 | * Load an item and parse the Response as arrayBuffer. 24 | * @function 25 | * @param {RequestInfo} url 26 | * @param {RequestInit} options 27 | * @returns {Promise} 28 | */ const loadArrayBuffer = async (url, options)=>{ 29 | if (options === undefined) options = {}; 30 | return await (await ok(await fetch(url, options))).arrayBuffer(); 31 | }; 32 | /** 33 | * Load an item and parse the Response as blob. 34 | * @function 35 | * @param {RequestInfo} url 36 | * @param {RequestInit} options 37 | * @returns {Promise} 38 | */ const loadBlob = async (url, options)=>{ 39 | if (options === undefined) options = {}; 40 | return await (await ok(await fetch(url, options))).blob(); 41 | }; 42 | /** 43 | * Load an item, parse the Response as blob and create a HTML Image. 44 | * @function 45 | * @param {string | import("./types.js").ImageOptions} urlOrOpts 46 | * @param {RequestInit} options 47 | * @returns {Promise} 48 | */ const loadImage = async (urlOrOpts, options)=>{ 49 | if (options === undefined) options = {}; 50 | const img = new Image(); 51 | let src = urlOrOpts; 52 | if (urlOrOpts.url) { 53 | const { url, ...rest } = urlOrOpts; 54 | src = url; 55 | try { 56 | Object.assign(img, rest); 57 | } catch (error) { 58 | return Promise.reject(new Error(error)); 59 | } 60 | } 61 | const data = await loadBlob(src, options); 62 | return await new Promise((resolve, reject)=>{ 63 | img.addEventListener("load", function load() { 64 | img.removeEventListener("load", load); 65 | resolve(img); 66 | }); 67 | img.addEventListener("error", function error() { 68 | img.removeEventListener("error", error); 69 | reject(img); 70 | }); 71 | img.src = URL.createObjectURL(data); 72 | }); 73 | }; 74 | /** 75 | * @private 76 | */ const LOADERS_MAP = { 77 | text: loadText, 78 | json: loadJson, 79 | image: loadImage, 80 | blob: loadBlob, 81 | arrayBuffer: loadArrayBuffer 82 | }; 83 | const LOADERS_MAP_KEYS = Object.keys(LOADERS_MAP); 84 | /** 85 | * Loads resources from a named map. 86 | * @function 87 | * @param {Object.} resources 88 | * @returns {Promise>} 89 | * @example 90 | * const resources = { 91 | * hello: { text: "assets/hello.txt" }, 92 | * data: { json: "assets/data.json" }, 93 | * img: { image: "assets/tex.jpg" }, 94 | * blob: { blob: "assets/blob" }, 95 | * hdrImg: { arrayBuffer: "assets/tex.hdr", options: { mode: "no-cors" } }, 96 | * }; 97 | * 98 | * const res = await io.load(resources); 99 | * res.hello; // => string 100 | * res.data; // => Object 101 | * res.img; // => HTMLImageElement 102 | * res.blob; // => Blob 103 | * res.hdrImg; // => ArrayBuffer 104 | */ const load = (resources)=>{ 105 | const names = Object.keys(resources); 106 | return Promise.allSettled(names.map(async (name)=>{ 107 | const res = resources[name]; 108 | const loader = LOADERS_MAP_KEYS.find((loader)=>res[loader]); 109 | if (loader) return await LOADERS_MAP[loader](res[loader], res.options); 110 | return Promise.reject(new Error(`io.load: unknown resource type "${Object.keys(res)}". 111 | Resource needs one of ${LOADERS_MAP_KEYS.join("|")} set to an url.`)); 112 | })).then((values)=>Object.fromEntries(Array.from(values.map((v)=>v.value || v.reason), (v, i)=>[ 113 | names[i], 114 | v 115 | ]))); 116 | }; 117 | 118 | export { load, loadArrayBuffer, loadBlob, loadImage, loadJson, loadText }; 119 | --------------------------------------------------------------------------------