├── .editorconfig ├── .gitignore ├── .nojekyll ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── examples ├── elliptical-mapping.js ├── elliptical-mapping │ ├── all.jpg │ ├── concentric.png │ ├── cornerificTapered2.png │ ├── elliptical-mapping.md │ ├── elliptical.png │ ├── fgSquircular.png │ ├── lamé.png │ ├── nonAxial2Pinch.png │ ├── nonAxialHalfPinch.png │ ├── radial.png │ ├── rectangular.png │ ├── squelched.png │ ├── squelchedHorizontal.png │ ├── squelchedVertical.png │ ├── tapered4.png │ ├── threeSquircular.png │ └── twoSquircular.png ├── index.js ├── mode │ ├── 00-texture.png │ ├── 01-normal.png │ ├── 02-flat-shaded.png │ ├── 03-uv.png │ ├── 04-wireframe.png │ └── 05-bbox.png ├── render.js └── uv.jpg ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── screenshot.gif ├── src ├── annulus.js ├── box.js ├── capsule.js ├── circle.js ├── cone.js ├── cube.js ├── cylinder.js ├── disc.js ├── ellipse.js ├── ellipsoid.js ├── icosahedron.js ├── icosphere.js ├── mappings.js ├── plane.js ├── quad.js ├── reuleux.js ├── rounded-cube.js ├── rounded-rectangle.js ├── sphere.js ├── squircle.js ├── stadium.js ├── superellipse.js ├── tetrahedron.js ├── torus.js └── utils.js ├── types.js └── web_modules ├── _chunks ├── _commonjsHelpers-BFTU3MAI.js ├── polyfills-BwRuO6W0.js └── vec2-BUEubpJQ.js ├── async-preloader.js ├── cameras.js ├── es-module-shims.js ├── es-module-shims ├── debug.js ├── typescript-transform.js └── wasm.js ├── gl-matrix.js ├── import-map.json ├── pex-context.js ├── pex-geom.js ├── tweakpane.js └── typed-array-interleave.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/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/.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 | # [2.11.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.10.1...v2.11.0) (2025-05-24) 6 | 7 | 8 | ### Features 9 | 10 | * add mergeCentroid option for ellipse, disc, reuleux, squircle and superellipse ([e590786](https://github.com/dmnsgn/primitive-geometry/commit/e5907861c48aa6aa39d53b792b468bda8fb297ac)) 11 | * add polar uv mapping + add safer division ([fa8e850](https://github.com/dmnsgn/primitive-geometry/commit/fa8e850633e42351ede778f34fd28c81a9fb42a7)) 12 | * use ellipse for annulus + add support for elliptical annulus ([e8b2640](https://github.com/dmnsgn/primitive-geometry/commit/e8b2640d53025abb1c95a1ca1d32b90ea1dab5b6)) 13 | 14 | 15 | 16 | ## [2.10.1](https://github.com/dmnsgn/primitive-geometry/compare/v2.10.0...v2.10.1) (2024-06-20) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * Update types export in package.json ([4c18ff1](https://github.com/dmnsgn/primitive-geometry/commit/4c18ff141e398ea276919408b9f5d94d73780d41)), closes [#18](https://github.com/dmnsgn/primitive-geometry/issues/18) 22 | 23 | 24 | 25 | # [2.10.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.9.1...v2.10.0) (2024-02-04) 26 | 27 | 28 | ### Features 29 | 30 | * use 3D positions for circle ([5bb9e3b](https://github.com/dmnsgn/primitive-geometry/commit/5bb9e3b68b4de45ceec0864465a0edf4fffe1d7e)), closes [#15](https://github.com/dmnsgn/primitive-geometry/issues/15) 31 | 32 | 33 | 34 | ## [2.9.1](https://github.com/dmnsgn/primitive-geometry/compare/v2.9.0...v2.9.1) (2022-10-26) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * ccw triangle order for rounded rectangle edges ([c04dc96](https://github.com/dmnsgn/primitive-geometry/commit/c04dc9698eba752b10d73c438a1daeadb474228d)) 40 | 41 | 42 | 43 | # [2.9.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.8.0...v2.9.0) (2022-09-22) 44 | 45 | 46 | ### Features 47 | 48 | * add phi/thetaOffset ([1ea22da](https://github.com/dmnsgn/primitive-geometry/commit/1ea22da7b69384c0d5fc32d9613d68d8a1a4c2ec)) 49 | * add rounded rectangle and stadium ([0dd9d9e](https://github.com/dmnsgn/primitive-geometry/commit/0dd9d9ed70194d1e78f85df6db8096589fd626df)) 50 | 51 | 52 | 53 | # [2.8.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.7.0...v2.8.0) (2022-09-14) 54 | 55 | 56 | ### Features 57 | 58 | * add elliptical geometries (ellipse, superellipse, squircle, reuleux) ([ba5b72f](https://github.com/dmnsgn/primitive-geometry/commit/ba5b72f6fed5748cde1dc62b8b9580f33f481cc1)) 59 | 60 | 61 | 62 | # [2.7.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.6.0...v2.7.0) (2022-06-15) 63 | 64 | 65 | ### Features 66 | 67 | * **plane:** add quads option ([3b9cb02](https://github.com/dmnsgn/primitive-geometry/commit/3b9cb02db95882faceb20049a3edbd9ceeec776a)), closes [#11](https://github.com/dmnsgn/primitive-geometry/issues/11) 68 | 69 | 70 | 71 | # [2.6.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.5.0...v2.6.0) (2022-04-15) 72 | 73 | 74 | ### Features 75 | 76 | * **capsule:** add roundSegments ([4a0ca75](https://github.com/dmnsgn/primitive-geometry/commit/4a0ca7586c1426169d63bec62f466647df424e9c)) 77 | * **rounded-cube:** add roundSegments and edgeSegments ([98741b9](https://github.com/dmnsgn/primitive-geometry/commit/98741b90e5660e1555e7c998265f5b88badda19f)) 78 | 79 | 80 | 81 | # [2.5.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.4.0...v2.5.0) (2022-04-11) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **plane:** hardcode normals from direction ([18e2423](https://github.com/dmnsgn/primitive-geometry/commit/18e2423beb133c84ab35372dfc50388477bb5d49)) 87 | * **torus:** default radius and minorSegments ([14c5256](https://github.com/dmnsgn/primitive-geometry/commit/14c5256d36b125bc116b2f8e7c91de93723e3d23)) 88 | 89 | 90 | ### Features 91 | 92 | * add disc and annulus ([6328e9f](https://github.com/dmnsgn/primitive-geometry/commit/6328e9ffbe82955f8302e49b7d1ba67769f21f08)) 93 | * **quad:** make normal face z ([246f7de](https://github.com/dmnsgn/primitive-geometry/commit/246f7de131db1463c829c4bbbcb901b2ea892c82)) 94 | 95 | 96 | ### Performance Improvements 97 | 98 | * move iterator in increment expression + remove assignment to 0 (default with TypedArrays) ([d0dbfbc](https://github.com/dmnsgn/primitive-geometry/commit/d0dbfbc7c8acd19b1c1b032e7f7dfa3412ea78fd)) 99 | * **circle:** extract t value for cos/sin ([5d1c7dd](https://github.com/dmnsgn/primitive-geometry/commit/5d1c7dd8f6e55ec76b7c5b02ec6f938924120045)) 100 | 101 | 102 | 103 | # [2.4.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.3.0...v2.4.0) (2022-04-06) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * **capsule:** wrong cell order ([0aa7efb](https://github.com/dmnsgn/primitive-geometry/commit/0aa7efb8c5e1f6e00c9a7c21d465c4048e8d983d)) 109 | 110 | 111 | ### Features 112 | 113 | * add phi/theta for all geometries ([b3afdf8](https://github.com/dmnsgn/primitive-geometry/commit/b3afdf8e320b0056f7f1274e908cac97d78df632)) 114 | 115 | 116 | 117 | # [2.3.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.2.0...v2.3.0) (2022-04-06) 118 | 119 | 120 | ### Features 121 | 122 | * add tetrahedron + add icosahedron ([cd341d2](https://github.com/dmnsgn/primitive-geometry/commit/cd341d203274cec347242f58d373f69501caee13)) 123 | * **cylinder:** add capBaseSegments option ([92dc090](https://github.com/dmnsgn/primitive-geometry/commit/92dc090880ab1de82f0d68c5becddb10b90ab22e)) 124 | * add plane direction + refactor computePlane for use in both plane and cube ([a5392c3](https://github.com/dmnsgn/primitive-geometry/commit/a5392c30de99db8e2190d041451c9c963d1bd549)) 125 | * **circle:** add theta and closed options ([cf34b04](https://github.com/dmnsgn/primitive-geometry/commit/cf34b047c837efb6514b74da9848dab7d97c8eca)) 126 | 127 | 128 | 129 | # [2.2.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.1.0...v2.2.0) (2021-10-02) 130 | 131 | 132 | ### Features 133 | 134 | * add exports field to package.json ([09700e2](https://github.com/dmnsgn/primitive-geometry/commit/09700e218efe30b29e5d8b3bf71a44864adf2f32)) 135 | 136 | 137 | 138 | # [2.1.0](https://github.com/dmnsgn/primitive-geometry/compare/v2.0.1...v2.1.0) (2021-08-16) 139 | 140 | 141 | ### Features 142 | 143 | * add arguments check ([797003b](https://github.com/dmnsgn/primitive-geometry/commit/797003bee9de62b8f7a03ccec5d34a0730605f1b)), closes [#10](https://github.com/dmnsgn/primitive-geometry/issues/10) 144 | 145 | 146 | 147 | ## [2.0.1](https://github.com/dmnsgn/primitive-geometry/compare/v2.0.0...v2.0.1) (2021-06-15) 148 | 149 | 150 | 151 | # [2.0.0](https://github.com/dmnsgn/primitive-geometry/compare/v1.2.1...v2.0.0) (2021-04-27) 152 | 153 | 154 | ### Code Refactoring 155 | 156 | * use ES modules and move to typed arrays ([d6f2aed](https://github.com/dmnsgn/primitive-geometry/commit/d6f2aedf1805b8506e2baf1ffc4190e6952158c5)) 157 | 158 | 159 | ### BREAKING CHANGES 160 | 161 | * switch to type module and move to typed arrays 162 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Damien Seguin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/elliptical-mapping.js: -------------------------------------------------------------------------------- 1 | import * as Primitives from "../index.js"; 2 | 3 | import { setGeometries, pane, controls, CONFIG } from "./render.js"; 4 | 5 | const params = new URLSearchParams(window.location.search); 6 | 7 | const update = (options) => { 8 | setGeometries( 9 | params.has("screenshot") 10 | ? [Primitives.disc({ segments: 128, innerSegments: 128, ...options })] 11 | : [ 12 | Primitives.ellipse(options), 13 | Primitives.disc(options), 14 | Primitives.superellipse(options), 15 | null, 16 | Primitives.annulus(options), 17 | // Elliptical annulus 18 | Primitives.annulus({ sy: 0.5, options }), 19 | null, 20 | Primitives.squircle(options), 21 | Primitives.reuleux(options), 22 | null, 23 | Primitives.superellipse({ 24 | m: 4, 25 | sx: 1, 26 | sy: 1, 27 | ...options, 28 | }), // Lamé special quartic (Squircle) 29 | Primitives.superellipse({ 30 | m: 4, 31 | sx: 1, 32 | sy: 0.5, 33 | ...options, 34 | }), // Rectellipse 35 | Primitives.superellipse({ m: 2 / 3, sy: 1, ...options }), // Astroid 36 | null, 37 | Primitives.superellipse({ m: 1, sy: 1, ...options }), // Diamond 38 | Primitives.superellipse({ 39 | m: 5 / 2, 40 | sx: 6 / 6, 41 | sy: 5 / 6, 42 | ...options, 43 | }), // Piet Hein 6 / 5 44 | Primitives.superellipse({ 45 | m: 5 / 2, 46 | sx: 3 / 3, 47 | sy: 2 / 3, 48 | ...options, 49 | }), // Piet Hein 6 / 5 50 | ], 51 | ); 52 | }; 53 | 54 | const mappingOptions = Object.keys(Primitives.mappings); 55 | if (params.has("screenshot")) window.screenshotItems = mappingOptions; 56 | 57 | CONFIG.mapping = ""; 58 | CONFIG.theta = Primitives.utils.TAU; 59 | CONFIG.thetaOffset = 0; 60 | CONFIG.mergeCentroid = false; 61 | 62 | const getGeometryOptions = () => ({ 63 | // segments: 4, 64 | // innerSegments: 4, 65 | mapping: Primitives.mappings[CONFIG.mapping], 66 | theta: CONFIG.theta, 67 | thetaOffset: CONFIG.thetaOffset, 68 | mergeCentroid: CONFIG.mergeCentroid, 69 | }); 70 | 71 | pane.addBlade({ view: "separator" }); 72 | pane 73 | .addBinding(CONFIG, "mapping", { 74 | options: [ 75 | { text: "", value: "" }, 76 | ...mappingOptions.map((value) => ({ 77 | text: value, 78 | value, 79 | })), 80 | ], 81 | }) 82 | .on("change", (event) => { 83 | update(getGeometryOptions()); 84 | if (params.has("screenshot")) { 85 | window.dispatchEvent(new CustomEvent("screenshot")); 86 | } 87 | }); 88 | pane 89 | .addBinding(CONFIG, "theta", { 90 | min: 0, 91 | max: Primitives.utils.TAU, 92 | }) 93 | .on("change", () => { 94 | update(getGeometryOptions()); 95 | }); 96 | pane 97 | .addBinding(CONFIG, "thetaOffset", { 98 | min: 0, 99 | max: Primitives.utils.TAU, 100 | }) 101 | .on("change", () => { 102 | update(getGeometryOptions()); 103 | }); 104 | pane.addBinding(CONFIG, "mergeCentroid").on("change", () => { 105 | update(getGeometryOptions()); 106 | }); 107 | 108 | CONFIG.cycleMapping = false; 109 | pane.addBinding(CONFIG, "cycleMapping"); 110 | 111 | CONFIG.mapping = params.get("mapping"); 112 | if (params.has("screenshot")) { 113 | CONFIG.axes = false; 114 | document.querySelector("main h1").innerHTML = ``; 115 | update(); 116 | pane.refresh(); 117 | pane.dispose(); 118 | 119 | controls.damping = 0; 120 | controls.sphericalTarget = [0, Math.PI / 2, 1.5]; 121 | controls.update(); 122 | controls.damping = 0.9; 123 | } else { 124 | update(); 125 | pane.refresh(); 126 | } 127 | 128 | setInterval(() => { 129 | if (CONFIG.cycleMapping) { 130 | CONFIG.mapping = 131 | mappingOptions[ 132 | (mappingOptions.findIndex((m) => m === CONFIG.mapping) + 1) % 133 | mappingOptions.length 134 | ]; 135 | 136 | pane.refresh(); 137 | } 138 | }, 2000); 139 | -------------------------------------------------------------------------------- /examples/elliptical-mapping/all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/all.jpg -------------------------------------------------------------------------------- /examples/elliptical-mapping/concentric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/concentric.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/cornerificTapered2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/cornerificTapered2.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/elliptical-mapping.md: -------------------------------------------------------------------------------- 1 | # Elliptical mapping 2 | 3 | Based on the paper [Elliptification of Rectangular Imagery](https://arxiv.org/pdf/1709.07875.pdf) from Chamberlain Fong, I have implemented the mappings that can be used for the following geometries UVs generation: ellipse, disc, superellipse, squircle, annulus and reuleux geometries. 4 | 5 | ## All Mappings 6 | 7 | ![](./all.jpg) 8 | 9 | ## Rectangular 10 | 11 | ![Rectangular](./rectangular.png) 12 | 13 | ## Radial 14 | 15 | ![Radial](./radial.png) 16 | 17 | ## Concentric 18 | 19 | ![Concentric](./concentric.png) 20 | 21 | ## Lamé 22 | 23 | ![Lamé](./lamé.png) 24 | 25 | ## Elliptical 26 | 27 | ![Elliptical](./elliptical.png) 28 | 29 | ## Fg Squircular 30 | 31 | ![Fg Squircular](./fgSquircular.png) 32 | 33 | ## Two Squircular 34 | 35 | ![Two Squircular](./twoSquircular.png) 36 | 37 | ## Three Squircular 38 | 39 | ![Three Squircular](./threeSquircular.png) 40 | 41 | ## Cornerific Tapered2 42 | 43 | ![Cornerific Tapered2](./cornerificTapered2.png) 44 | 45 | ## Tapered4 46 | 47 | ![Tapered4](./tapered4.png) 48 | 49 | ## Non Axial2 Pinch 50 | 51 | ![Non Axial2 Pinch](./nonAxial2Pinch.png) 52 | 53 | ## Non Axial Half Pinch 54 | 55 | ![Non Axial Half Pinch](./nonAxialHalfPinch.png) 56 | 57 | ## Squelched 58 | 59 | ![Squelched](./squelched.png) 60 | 61 | ## Squelched Vertical 62 | 63 | ![Squelched Vertical](./squelchedVertical.png) 64 | 65 | ## Squelched Horizontal 66 | 67 | ![Squelched Horizontal](./squelchedHorizontal.png) 68 | -------------------------------------------------------------------------------- /examples/elliptical-mapping/elliptical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/elliptical.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/fgSquircular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/fgSquircular.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/lamé.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/lamé.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/nonAxial2Pinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/nonAxial2Pinch.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/nonAxialHalfPinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/nonAxialHalfPinch.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/radial.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/rectangular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/rectangular.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/squelched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/squelched.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/squelchedHorizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/squelchedHorizontal.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/squelchedVertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/squelchedVertical.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/tapered4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/tapered4.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/threeSquircular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/threeSquircular.png -------------------------------------------------------------------------------- /examples/elliptical-mapping/twoSquircular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/elliptical-mapping/twoSquircular.png -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import * as Primitives from "../index.js"; 2 | 3 | import { modeOptions, setGeometries, computeEdges } from "./render.js"; 4 | 5 | const params = new URLSearchParams(window.location.search); 6 | 7 | // I don't like performances, just give me the biggest you've got 8 | // Primitives.utils.setTypedArrayType(Uint32Array); 9 | 10 | const box = Primitives.box(); 11 | box.edges = computeEdges(box.positions, box.cells, 4); 12 | 13 | const quadsPlane = Primitives.plane({ nx: 10, quads: true }); 14 | quadsPlane.edges = computeEdges(quadsPlane.positions, quadsPlane.cells, 4); 15 | quadsPlane.quads = true; 16 | 17 | const circle = Primitives.circle({ closed: true }); 18 | circle.edges = circle.cells; 19 | 20 | // Box and plane of quads are rendered as lines 21 | const geometries = 22 | params.has("geometry") && Primitives[params.get("geometry")] 23 | ? [Primitives[params.get("geometry")]()] 24 | : [ 25 | box, 26 | circle, 27 | quadsPlane, 28 | Primitives.quad(), 29 | null, 30 | Primitives.plane(), 31 | Primitives.roundedRectangle(), 32 | Primitives.stadium(), 33 | null, 34 | Primitives.ellipse(), 35 | Primitives.disc(), 36 | Primitives.superellipse(), 37 | Primitives.squircle(), 38 | Primitives.annulus(), 39 | Primitives.reuleux(), 40 | null, 41 | Primitives.cube(), 42 | Primitives.roundedCube(), 43 | null, 44 | Primitives.sphere(), 45 | Primitives.icosphere(), 46 | Primitives.ellipsoid(), 47 | null, 48 | Primitives.cylinder(), 49 | Primitives.cone(), 50 | Primitives.capsule(), 51 | Primitives.torus(), 52 | null, 53 | Primitives.tetrahedron(), 54 | Primitives.icosahedron(), 55 | ]; 56 | 57 | setGeometries(geometries); 58 | 59 | if (params.has("screenshot")) { 60 | window.screenshotItems = [...modeOptions, "bbox"]; 61 | window.dispatchEvent(new CustomEvent("screenshot")); 62 | } 63 | -------------------------------------------------------------------------------- /examples/mode/00-texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/00-texture.png -------------------------------------------------------------------------------- /examples/mode/01-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/01-normal.png -------------------------------------------------------------------------------- /examples/mode/02-flat-shaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/02-flat-shaded.png -------------------------------------------------------------------------------- /examples/mode/03-uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/03-uv.png -------------------------------------------------------------------------------- /examples/mode/04-wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/04-wireframe.png -------------------------------------------------------------------------------- /examples/mode/05-bbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/mode/05-bbox.png -------------------------------------------------------------------------------- /examples/render.js: -------------------------------------------------------------------------------- 1 | import * as Primitives from "../index.js"; 2 | 3 | import { mat3, mat4 } from "gl-matrix"; 4 | import createContext from "pex-context"; 5 | import { aabb } from "pex-geom"; 6 | import AsyncPreloader from "async-preloader"; 7 | import typedArrayInterleave from "typed-array-interleave"; 8 | import { PerspectiveCamera, Controls } from "cameras"; 9 | import { Pane } from "tweakpane"; 10 | 11 | const params = new URLSearchParams(window.location.search); 12 | 13 | // Setup 14 | const canvas = document.createElement("canvas"); 15 | document.querySelector("main").appendChild(canvas); 16 | const ctx = createContext({ 17 | canvas, 18 | pixelRatio: devicePixelRatio, 19 | }); 20 | 21 | const camera = new PerspectiveCamera({ 22 | fov: Math.PI / 4, 23 | near: 0.1, 24 | far: 100, 25 | viewport: [0, 0, window.innerWidth, window.innerHeight], 26 | }); 27 | const controls = new Controls({ 28 | ...(params.has("geometry") && Primitives[params.get("geometry")] 29 | ? { 30 | position: [0, 0, 2], 31 | } 32 | : { 33 | phi: Math.PI / 3, 34 | theta: Math.PI / 4, 35 | distance: 15 * (window.innerHeight / window.innerWidth), 36 | }), 37 | element: ctx.gl.canvas, 38 | camera, 39 | distanceBounds: [1, 100], 40 | }); 41 | controls.updatePosition(); 42 | controls.target = [0, 0, 0]; 43 | 44 | // GUI 45 | const modeOptions = ["texture", "normal", "flat-shaded", "uv", "wireframe"]; 46 | 47 | const CONFIG = { 48 | mode: params.get("mode") || "texture", 49 | cycle: false, 50 | axes: true, 51 | bbox: params.get("mode") === "bbox", 52 | normals: true, 53 | }; 54 | const pane = new Pane(); 55 | pane.addBinding(CONFIG, "mode", { 56 | options: modeOptions.map((value) => ({ 57 | text: value.toUpperCase(), 58 | value, 59 | })), 60 | }); 61 | pane.addBinding(CONFIG, "cycle"); 62 | pane.addBinding(CONFIG, "bbox"); 63 | pane.addBinding(CONFIG, "normals"); 64 | 65 | setInterval(() => { 66 | if (CONFIG.cycle) { 67 | CONFIG.mode = 68 | modeOptions[ 69 | (modeOptions.findIndex((m) => m === CONFIG.mode) + 1) % 70 | modeOptions.length 71 | ]; 72 | pane.refresh(); 73 | } 74 | }, 2000); 75 | 76 | // Assets 77 | const colorMap = ctx.texture2D({ 78 | data: await AsyncPreloader.loadImage({ src: "examples/uv.jpg" }), 79 | flipY: true, 80 | wrap: ctx.Wrap.Repeat, 81 | }); 82 | 83 | // Loop 84 | const clearCmd = { 85 | pass: ctx.pass({ 86 | clearColor: [0, 0, 0, 1], 87 | clearDepth: 1, 88 | }), 89 | }; 90 | 91 | const drawCmd = { 92 | pipeline: ctx.pipeline({ 93 | depthTest: true, 94 | cullFace: true, 95 | vert: /* glsl */ `#version 300 es 96 | precision mediump float; 97 | 98 | uniform mat4 uProjectionMatrix; 99 | uniform mat4 uModelMatrix; 100 | uniform mat4 uViewMatrix; 101 | uniform mat4 uInverseViewMatrix; 102 | uniform mat3 uNormalMatrix; 103 | 104 | in vec3 aPosition; 105 | in vec2 aUv; 106 | in vec3 aNormal; 107 | 108 | out vec3 vPositionWorld; 109 | out vec3 vPositionView; 110 | out vec3 vNormalView; 111 | out vec3 vNormal; 112 | out vec3 vNormalWorld; 113 | out vec2 vUv; 114 | 115 | void main() { 116 | vNormal = aNormal; 117 | vUv = aUv; 118 | 119 | vPositionWorld = (uModelMatrix * vec4(aPosition, 1.0)).xyz; 120 | vPositionView = (uViewMatrix * vec4(vPositionWorld, 1.0)).xyz; 121 | 122 | vNormalView = uNormalMatrix * aNormal; 123 | vNormalWorld = normalize((uInverseViewMatrix * vec4(vNormalView, 0.0)).xyz); 124 | 125 | gl_Position = uProjectionMatrix * vec4(vPositionView, 1.0); 126 | }`, 127 | frag: /* glsl */ `#version 300 es 128 | precision mediump float; 129 | 130 | uniform sampler2D uColorMap; 131 | uniform float uMode; 132 | 133 | in vec3 vPositionWorld; 134 | in vec3 vPositionView; 135 | in vec3 vNormal; 136 | in vec2 vUv; 137 | 138 | out vec4 fragColor; 139 | 140 | void main () { 141 | if (uMode == 0.0) fragColor = texture(uColorMap, vUv); 142 | if (uMode == 1.0) fragColor = vec4(vNormal * 0.5 + 0.5, 1.0); 143 | if (uMode == 2.0) { 144 | vec3 fdx = vec3(dFdx(vPositionWorld.x), dFdx(vPositionWorld.y), dFdx(vPositionWorld.z)); 145 | vec3 fdy = vec3(dFdy(vPositionWorld.x), dFdy(vPositionWorld.y), dFdy(vPositionWorld.z)); 146 | vec3 normal = normalize(cross(fdx, fdy)); 147 | fragColor = vec4(normal * 0.5 + 0.5, 1.0); 148 | } 149 | if (uMode == 3.0) fragColor = vec4(vUv.xy, 0.0, 1.0); 150 | if (vUv.x > 1.0 || vUv.x < 0.0 || vUv.y > 1.0 || vUv.y < 0.0) fragColor.a = 0.1; 151 | }`, 152 | }), 153 | uniforms: { 154 | uColorMap: colorMap, 155 | }, 156 | }; 157 | const drawLinesCmd = { 158 | pipeline: ctx.pipeline({ 159 | depthTest: true, 160 | primitive: ctx.Primitive.Lines, 161 | blend: true, 162 | blendSrcRGBFactor: ctx.BlendFactor.SrcAlpha, 163 | blendSrcAlphaFactor: ctx.BlendFactor.One, 164 | blendDstRGBFactor: ctx.BlendFactor.OneMinusSrcAlpha, 165 | blendDstAlphaFactor: ctx.BlendFactor.One, 166 | vert: /* glsl */ `#version 300 es 167 | precision mediump float; 168 | 169 | uniform mat4 uProjectionMatrix; 170 | uniform mat4 uModelMatrix; 171 | uniform mat4 uViewMatrix; 172 | 173 | in vec3 aPosition; 174 | in vec3 aColor; 175 | 176 | out vec3 vPositionWorld; 177 | out vec3 vPositionView; 178 | out vec3 vColor; 179 | 180 | void main() { 181 | vPositionWorld = (uModelMatrix * vec4(aPosition, 1.0)).xyz; 182 | vPositionView = (uViewMatrix * vec4(vPositionWorld, 1.0)).xyz; 183 | vColor = aColor; 184 | 185 | gl_Position = uProjectionMatrix * vec4(vPositionView, 1.0); 186 | }`, 187 | frag: /* glsl */ `#version 300 es 188 | precision mediump float; 189 | 190 | uniform float uOpacity; 191 | 192 | in vec3 vColor; 193 | 194 | out vec4 fragColor; 195 | 196 | void main () { 197 | fragColor = vec4(vColor, uOpacity); 198 | }`, 199 | }), 200 | uniforms: { uOpacity: 1 }, 201 | }; 202 | 203 | const bboxCells = ctx.indexBuffer( 204 | // prettier-ignore 205 | Uint8Array.of( 206 | 0, 1, 1, 2, 2, 3, 3, 0, 207 | 4, 5, 5, 6, 6, 7, 7, 4, 208 | 0, 4, 1, 5, 2, 6, 3, 7 209 | ), 210 | ); 211 | const unitBox = Primitives.box(); 212 | unitBox.edges = computeEdges(unitBox.positions, unitBox.cells, 4); 213 | 214 | const drawAxesCmd = { 215 | ...drawLinesCmd, 216 | attributes: { 217 | aPosition: ctx.vertexBuffer( 218 | // prettier-ignore 219 | Float32Array.of( 220 | 0, 0, 0, 221 | 1, 0, 0, 222 | 0, 0, 0, 223 | 0, 1, 0, 224 | 0, 0, 0, 225 | 0, 0, 1, 226 | ), 227 | ), 228 | aColor: ctx.vertexBuffer( 229 | // prettier-ignore 230 | Float32Array.of( 231 | 1, 0, 0, 232 | 1, 0.5, 0.5, 233 | 0, 1, 0, 234 | 0.5, 1, 0.5, 235 | 0, 0, 1, 236 | 0.5, 0.5, 1, 237 | ), 238 | ), 239 | }, 240 | indices: ctx.indexBuffer(Uint8Array.of(0, 1, 2, 3, 4, 5)), 241 | uniforms: { uModelMatrix: mat4.create() }, 242 | }; 243 | 244 | // Events 245 | const onResize = () => { 246 | const width = window.innerWidth; 247 | const height = window.innerHeight; 248 | ctx.set({ width, height }); 249 | 250 | canvas.width = width * devicePixelRatio; 251 | canvas.height = height * devicePixelRatio; 252 | 253 | camera.aspect = width / height; 254 | camera.updateProjectionMatrix(); 255 | }; 256 | window.addEventListener("resize", onResize); 257 | onResize(); 258 | 259 | const inverseModelViewMatrix = mat4.create(); 260 | 261 | let meshes = []; 262 | 263 | // Render 264 | ctx.frame(() => { 265 | controls.update(); 266 | camera.position = controls.position; 267 | camera.target = controls.target; 268 | camera.update(); 269 | 270 | ctx.submit(clearCmd); 271 | 272 | if (CONFIG.axes) { 273 | ctx.submit(drawAxesCmd, { 274 | uniforms: { 275 | uProjectionMatrix: camera.projectionMatrix, 276 | uViewMatrix: camera.viewMatrix, 277 | }, 278 | }); 279 | } 280 | 281 | meshes.filter(Boolean).forEach((mesh) => { 282 | mat4.fromRotationTranslationScale( 283 | mesh.modelMatrix, 284 | mesh.rotation, 285 | mesh.translation, 286 | mesh.scale, 287 | ); 288 | 289 | mat4.multiply(mesh.modelViewMatrix, camera.viewMatrix, mesh.modelMatrix); 290 | 291 | mat4.invert(inverseModelViewMatrix, mesh.modelViewMatrix); 292 | mat3.fromMat4(mesh.normalMatrix, inverseModelViewMatrix); 293 | mat3.transpose(mesh.normalMatrix, mesh.normalMatrix); 294 | 295 | const isLine = 296 | !mesh.geometry.normals || mesh.quads || CONFIG.mode === "wireframe"; 297 | ctx.submit(!isLine ? drawCmd : drawLinesCmd, { 298 | attributes: mesh.attributes, 299 | indices: isLine ? mesh.edges : mesh.indices, 300 | uniforms: { 301 | uMode: modeOptions.findIndex((o) => o === CONFIG.mode), 302 | uProjectionMatrix: camera.projectionMatrix, 303 | uViewMatrix: camera.viewMatrix, 304 | uInverseViewMatrix: camera.inverseViewMatrix, 305 | uNormalMatrix: mesh.normalMatrix, 306 | uModelMatrix: mesh.modelMatrix, 307 | }, 308 | }); 309 | 310 | if (CONFIG.bbox) { 311 | unitBox.positionsBuffer ||= ctx.vertexBuffer(unitBox.positions); 312 | unitBox.colorsBuffer ||= ctx.vertexBuffer(unitBox.positions.map(() => 1)); 313 | unitBox.indicesBuffer ||= ctx.indexBuffer(unitBox.edges); 314 | ctx.submit(drawLinesCmd, { 315 | attributes: { 316 | aPosition: unitBox.positionsBuffer, 317 | aColor: unitBox.colorsBuffer, 318 | }, 319 | indices: unitBox.indicesBuffer, 320 | uniforms: { 321 | uOpacity: 0.2, 322 | uMode: modeOptions.findIndex((o) => o === CONFIG.mode), 323 | uProjectionMatrix: camera.projectionMatrix, 324 | uViewMatrix: camera.viewMatrix, 325 | uInverseViewMatrix: camera.inverseViewMatrix, 326 | uNormalMatrix: mesh.normalMatrix, 327 | uModelMatrix: mesh.modelMatrix, 328 | }, 329 | }); 330 | 331 | mesh.bboxPositions ||= ctx.vertexBuffer(mesh.bbox); 332 | mesh.bboxColors ||= ctx.vertexBuffer(mesh.bbox.map((p) => p * 0.5 + 0.5)); 333 | 334 | ctx.submit(drawLinesCmd, { 335 | attributes: { 336 | aPosition: mesh.bboxPositions, 337 | aColor: mesh.bboxColors, 338 | }, 339 | indices: bboxCells, 340 | uniforms: { 341 | uMode: modeOptions.findIndex((o) => o === CONFIG.mode), 342 | uProjectionMatrix: camera.projectionMatrix, 343 | uViewMatrix: camera.viewMatrix, 344 | uInverseViewMatrix: camera.inverseViewMatrix, 345 | uNormalMatrix: mesh.normalMatrix, 346 | uModelMatrix: mesh.modelMatrix, 347 | }, 348 | }); 349 | } 350 | 351 | if (CONFIG.normals && mesh.geometry.normals && !mesh.quads) { 352 | mesh.normalsAttributes ||= { 353 | aPosition: ctx.vertexBuffer( 354 | typedArrayInterleave( 355 | Float32Array, 356 | [3, 3], 357 | mesh.geometry.positions, 358 | mesh.geometry.positions.map( 359 | (p, i) => p + mesh.geometry.normals[i] * 0.1, 360 | ), 361 | ), 362 | ), 363 | aColor: ctx.vertexBuffer( 364 | typedArrayInterleave( 365 | Float32Array, 366 | [3, 3], 367 | mesh.geometry.normals.map((p) => p * 0.5 + 0.5), 368 | mesh.geometry.normals.map((p) => p * 0.5 + 0.5), 369 | ), 370 | ), 371 | }; 372 | 373 | mesh.normalsIndices ||= ctx.indexBuffer( 374 | new Uint32Array((mesh.geometry.positions.length / 3) * 2).map( 375 | (_, i) => i, 376 | ), 377 | ); 378 | 379 | ctx.submit(drawLinesCmd, { 380 | attributes: mesh.normalsAttributes, 381 | indices: mesh.normalsIndices, 382 | uniforms: { 383 | uMode: modeOptions.findIndex((o) => o === CONFIG.mode), 384 | uProjectionMatrix: camera.projectionMatrix, 385 | uViewMatrix: camera.viewMatrix, 386 | uInverseViewMatrix: camera.inverseViewMatrix, 387 | uNormalMatrix: mesh.normalMatrix, 388 | uModelMatrix: mesh.modelMatrix, 389 | }, 390 | }); 391 | } 392 | 393 | ctx.submit(!isLine ? drawCmd : drawLinesCmd, { 394 | attributes: mesh.attributes, 395 | indices: isLine ? mesh.edges : mesh.indices, 396 | uniforms: { 397 | uMode: modeOptions.findIndex((o) => o === CONFIG.mode), 398 | uProjectionMatrix: camera.projectionMatrix, 399 | uViewMatrix: camera.viewMatrix, 400 | uInverseViewMatrix: camera.inverseViewMatrix, 401 | uNormalMatrix: mesh.normalMatrix, 402 | uModelMatrix: mesh.modelMatrix, 403 | }, 404 | }); 405 | }); 406 | }); 407 | 408 | function computeEdges(positions, cells, stride = 3) { 409 | const edges = new (Primitives.utils.getCellsTypedArray(positions.length / 3))( 410 | cells.length * 2, 411 | ); 412 | 413 | let cellIndex = 0; 414 | 415 | for (let i = 0; i < cells.length; i += stride) { 416 | for (let j = 0; j < stride; j++) { 417 | const a = cells[i + j]; 418 | const b = cells[i + ((j + 1) % stride)]; 419 | edges[cellIndex] = Math.min(a, b); 420 | edges[cellIndex + 1] = Math.max(a, b); 421 | cellIndex += 2; 422 | } 423 | } 424 | return edges; 425 | } 426 | 427 | const setGeometries = (geometries) => { 428 | console.table(geometries); 429 | 430 | // Create the meshes for rendering 431 | meshes = geometries.map( 432 | (geometry) => 433 | geometry && { 434 | modelMatrix: mat4.create(), 435 | modelViewMatrix: mat4.create(), 436 | normalMatrix: mat3.create(), 437 | rotation: [0, 0, 0, 1], 438 | translation: [0, 0, 0], 439 | scale: [1, 1, 1], 440 | geometry, 441 | quads: geometry.quads, 442 | bbox: aabb 443 | .getCorners( 444 | aabb.fromPoints( 445 | aabb.create(), 446 | Array.from( 447 | { length: geometry.positions.length / 3 }, 448 | (_, index) => 449 | geometry.positions.slice(index * 3, index * 3 + 3), 450 | ), 451 | ), 452 | ) 453 | .flat(), 454 | edges: ctx.indexBuffer( 455 | geometry.edges || computeEdges(geometry.positions, geometry.cells), 456 | ), 457 | attributes: geometry.normals 458 | ? { 459 | aPosition: ctx.vertexBuffer(geometry.positions), 460 | aNormal: ctx.vertexBuffer(geometry.normals), 461 | aUv: ctx.vertexBuffer(geometry.uvs), 462 | aColor: ctx.vertexBuffer( 463 | geometry.normals.map((p) => p * 0.5 + 0.5), 464 | ), 465 | } 466 | : { 467 | aPosition: ctx.vertexBuffer(geometry.positions), 468 | aColor: ctx.vertexBuffer( 469 | geometry.positions.map((p) => p * 0.5 + 0.5), 470 | ), 471 | }, 472 | indices: ctx.indexBuffer(geometry.cells), 473 | }, 474 | ); 475 | console.log(meshes); 476 | 477 | // Position them 478 | const offset = 1.5; 479 | const { gridSize } = meshes.reduce( 480 | (current, mesh) => { 481 | if (mesh) { 482 | current.count++; 483 | } else { 484 | current.count = 0; 485 | } 486 | current.gridSize = Math.max(current.gridSize, current.count); 487 | return current; 488 | }, 489 | { gridSize: 0, count: 0 }, 490 | ); 491 | 492 | const halfSize = (gridSize - 1) * 0.5; 493 | let i = 0; 494 | meshes.forEach((mesh) => { 495 | if (!mesh) { 496 | if (i % gridSize !== 0) i += gridSize - (i % gridSize); 497 | return; 498 | } 499 | mesh.translation = [ 500 | (i % gridSize) * offset - halfSize * offset, 501 | 0, 502 | ~~(i / gridSize) * offset, 503 | ]; 504 | i++; 505 | }); 506 | const halfGridSize = meshes.at(-1).translation[2] * 0.5; 507 | meshes.forEach((mesh) => mesh && (mesh.translation[2] -= halfGridSize)); 508 | }; 509 | 510 | export { 511 | CONFIG, 512 | setGeometries, 513 | computeEdges, 514 | modeOptions, 515 | pane, 516 | controls, 517 | camera, 518 | }; 519 | -------------------------------------------------------------------------------- /examples/uv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/examples/uv.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | primitive-geometry by Damien Seguin (https://github.com/dmnsgn) 9 | 10 | 37 | 38 | 39 |
40 |

primitive-geometry

41 |
42 | 43 | 44 | 45 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Re-export all geometries, UV mappings functions and utils. 3 | * @module index 4 | */ 5 | 6 | export { default as box } from "./src/box.js"; 7 | export { default as circle } from "./src/circle.js"; 8 | 9 | export { default as quad } from "./src/quad.js"; 10 | export { default as plane } from "./src/plane.js"; 11 | export { default as roundedRectangle } from "./src/rounded-rectangle.js"; 12 | export { default as stadium } from "./src/stadium.js"; 13 | 14 | export { default as ellipse } from "./src/ellipse.js"; 15 | export { default as disc } from "./src/disc.js"; 16 | export { default as superellipse } from "./src/superellipse.js"; 17 | export { default as squircle } from "./src/squircle.js"; 18 | export { default as annulus } from "./src/annulus.js"; 19 | export { default as reuleux } from "./src/reuleux.js"; 20 | 21 | export { default as cube } from "./src/cube.js"; 22 | export { default as roundedCube } from "./src/rounded-cube.js"; 23 | 24 | export { default as sphere } from "./src/sphere.js"; 25 | export { default as icosphere } from "./src/icosphere.js"; 26 | export { default as ellipsoid } from "./src/ellipsoid.js"; 27 | 28 | export { default as cylinder } from "./src/cylinder.js"; 29 | export { default as cone } from "./src/cone.js"; 30 | export { default as capsule } from "./src/capsule.js"; 31 | export { default as torus } from "./src/torus.js"; 32 | 33 | export { default as tetrahedron } from "./src/tetrahedron.js"; 34 | export { default as icosahedron } from "./src/icosahedron.js"; 35 | 36 | export * as mappings from "./src/mappings.js"; 37 | 38 | export * as utils from "./src/utils.js"; 39 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primitive-geometry", 3 | "version": "2.11.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "primitive-geometry", 9 | "version": "2.11.0", 10 | "funding": [ 11 | { 12 | "type": "individual", 13 | "url": "https://paypal.me/dmnsgn" 14 | }, 15 | { 16 | "type": "individual", 17 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 18 | } 19 | ], 20 | "license": "MIT", 21 | "devDependencies": { 22 | "async-preloader": "^8.0.4", 23 | "cameras": "^3.1.1", 24 | "es-module-shims": "^2.5.1", 25 | "gl-matrix": "^3.4.3", 26 | "pex-context": "^3.2.0", 27 | "pex-geom": "^3.0.2", 28 | "tweakpane": "^4.0.5", 29 | "typed-array-interleave": "^2.0.1" 30 | }, 31 | "engines": { 32 | "node": ">=22.0.0", 33 | "npm": ">=10.5.1", 34 | "snowdev": ">=2.3.x" 35 | } 36 | }, 37 | "node_modules/async-preloader": { 38 | "version": "8.0.4", 39 | "resolved": "https://registry.npmjs.org/async-preloader/-/async-preloader-8.0.4.tgz", 40 | "integrity": "sha512-7RXL6f/aFQ4/sVZP3WXW9VroKb2QmDEHfJPRyZXllDXp349IoPrFqHvc+L1u4+sk9Asr6U1WA2xVRYTbqwtJxw==", 41 | "dev": true, 42 | "funding": [ 43 | { 44 | "type": "individual", 45 | "url": "https://paypal.me/dmnsgn" 46 | }, 47 | { 48 | "type": "individual", 49 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 50 | } 51 | ], 52 | "license": "MIT", 53 | "dependencies": { 54 | "fontfaceobserver-es": "^3.3.3", 55 | "tslib": "^2.6.3" 56 | }, 57 | "engines": { 58 | "node": ">=22.0.0", 59 | "npm": ">=10.5.1", 60 | "snowdev": ">=2.2.x" 61 | } 62 | }, 63 | "node_modules/cameras": { 64 | "version": "3.1.1", 65 | "resolved": "https://registry.npmjs.org/cameras/-/cameras-3.1.1.tgz", 66 | "integrity": "sha512-hm8z4IIMgz8ygz6PGVb9l+29qBuXHahzlw2CNsVKq8i09RnylhsrlLwoCZRuik3vbl0i1w2hJFfS0OBVh1WDNQ==", 67 | "dev": true, 68 | "funding": [ 69 | { 70 | "type": "individual", 71 | "url": "https://paypal.me/dmnsgn" 72 | }, 73 | { 74 | "type": "individual", 75 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 76 | } 77 | ], 78 | "dependencies": { 79 | "clamp": "^1.0.1", 80 | "gl-matrix": "^3.3.0", 81 | "normalize-wheel": "^1.0.1" 82 | }, 83 | "engines": { 84 | "node": ">=15.0.0", 85 | "npm": ">=7.0.0" 86 | } 87 | }, 88 | "node_modules/clamp": { 89 | "version": "1.0.1", 90 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", 91 | "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=", 92 | "dev": true 93 | }, 94 | "node_modules/es-module-shims": { 95 | "version": "2.5.1", 96 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-2.5.1.tgz", 97 | "integrity": "sha512-rx57z9aYi2XWJggkIoR0knHT2yr9kQ1vyXkva+oMNlplJr46Aj/1CI6c+GHGxEH6ljzoEhtAYxk080U5NpUpmg==", 98 | "dev": true, 99 | "license": "MIT" 100 | }, 101 | "node_modules/fontfaceobserver-es": { 102 | "version": "3.3.3", 103 | "resolved": "https://registry.npmjs.org/fontfaceobserver-es/-/fontfaceobserver-es-3.3.3.tgz", 104 | "integrity": "sha512-1EyweL1ryy0koZWk80Kr0UgWq9KhbOl2cD6x1O8BtFu0gNqCWIXK1lVFSUb37DI5FMn1m86I6X7N1JRBy8wVfw==", 105 | "dev": true 106 | }, 107 | "node_modules/gl-matrix": { 108 | "version": "3.4.3", 109 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 110 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", 111 | "dev": true 112 | }, 113 | "node_modules/normalize-wheel": { 114 | "version": "1.0.1", 115 | "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", 116 | "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=", 117 | "dev": true 118 | }, 119 | "node_modules/pex-context": { 120 | "version": "3.2.0", 121 | "resolved": "https://registry.npmjs.org/pex-context/-/pex-context-3.2.0.tgz", 122 | "integrity": "sha512-yM7XS0SCbWWM/QbonhNnwVxD58ZDjcOJ1VwONeJGj0kFoMHkJskoAFSJH3i4fGAEte6YiHt361LX9D/waP1IAw==", 123 | "dev": true, 124 | "license": "MIT", 125 | "dependencies": { 126 | "pex-gl": "^3.0.2" 127 | }, 128 | "engines": { 129 | "node": ">=22.0.0", 130 | "npm": ">=10.5.1", 131 | "snowdev": ">=2.3.x" 132 | } 133 | }, 134 | "node_modules/pex-geom": { 135 | "version": "3.0.2", 136 | "resolved": "https://registry.npmjs.org/pex-geom/-/pex-geom-3.0.2.tgz", 137 | "integrity": "sha512-jLx3ISMMrVuDouPvkmN1hLTqeJE/45otdNVIuoMHKxcz7LZb8k9+XuV7VmsAPA+/TySJyvO+gVvFopUVVG40jg==", 138 | "dev": true, 139 | "license": "MIT", 140 | "dependencies": { 141 | "pex-math": "^4.1.1" 142 | }, 143 | "engines": { 144 | "node": ">=22.0.0", 145 | "npm": ">=10.5.1", 146 | "snowdev": ">=2.2.x" 147 | } 148 | }, 149 | "node_modules/pex-gl": { 150 | "version": "3.0.2", 151 | "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.2.tgz", 152 | "integrity": "sha512-q0MEUXyRlXEa7A/QGQn7GvrkLf+Yc+tRpTRFW0gmbG1RnJpKC0U504WYaB9O47OoMmy9LlKGCaolP2cJJUw5gQ==", 153 | "dev": true, 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">=22.0.0", 157 | "npm": ">=10.5.1", 158 | "snowdev": ">=2.2.x" 159 | } 160 | }, 161 | "node_modules/pex-math": { 162 | "version": "4.1.1", 163 | "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-4.1.1.tgz", 164 | "integrity": "sha512-jhFPZfgdhUczx8EJsV7PwFTDjvhDMhNiabtK+rOLsfkWl33LNV97hUYB0URlMiTf9UR4nNpPcTLuha96aniyQg==", 165 | "dev": true, 166 | "license": "MIT", 167 | "engines": { 168 | "node": ">=22.0.0", 169 | "npm": ">=10.5.1", 170 | "snowdev": ">=2.2.x" 171 | } 172 | }, 173 | "node_modules/tslib": { 174 | "version": "2.6.3", 175 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", 176 | "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", 177 | "dev": true, 178 | "license": "0BSD" 179 | }, 180 | "node_modules/tweakpane": { 181 | "version": "4.0.5", 182 | "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.5.tgz", 183 | "integrity": "sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==", 184 | "dev": true, 185 | "license": "MIT", 186 | "funding": { 187 | "url": "https://github.com/sponsors/cocopon" 188 | } 189 | }, 190 | "node_modules/typed-array-interleave": { 191 | "version": "2.0.1", 192 | "resolved": "https://registry.npmjs.org/typed-array-interleave/-/typed-array-interleave-2.0.1.tgz", 193 | "integrity": "sha512-RTidx5Oq8GSiRPhNs57IU6fhlD9XU6uQGqPLSZCS5tHnAuIBxYsQI/NjKe2D07n0QIq6A+hURNgOVG4ECSFokQ==", 194 | "dev": true, 195 | "funding": [ 196 | { 197 | "type": "individual", 198 | "url": "https://paypal.me/dmnsgn" 199 | }, 200 | { 201 | "type": "individual", 202 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 203 | } 204 | ], 205 | "license": "MIT", 206 | "engines": { 207 | "node": ">=22.0.0", 208 | "npm": ">=10.5.1", 209 | "snowdev": ">=2.2.x" 210 | } 211 | } 212 | }, 213 | "dependencies": { 214 | "async-preloader": { 215 | "version": "8.0.4", 216 | "resolved": "https://registry.npmjs.org/async-preloader/-/async-preloader-8.0.4.tgz", 217 | "integrity": "sha512-7RXL6f/aFQ4/sVZP3WXW9VroKb2QmDEHfJPRyZXllDXp349IoPrFqHvc+L1u4+sk9Asr6U1WA2xVRYTbqwtJxw==", 218 | "dev": true, 219 | "requires": { 220 | "fontfaceobserver-es": "^3.3.3", 221 | "tslib": "^2.6.3" 222 | } 223 | }, 224 | "cameras": { 225 | "version": "3.1.1", 226 | "resolved": "https://registry.npmjs.org/cameras/-/cameras-3.1.1.tgz", 227 | "integrity": "sha512-hm8z4IIMgz8ygz6PGVb9l+29qBuXHahzlw2CNsVKq8i09RnylhsrlLwoCZRuik3vbl0i1w2hJFfS0OBVh1WDNQ==", 228 | "dev": true, 229 | "requires": { 230 | "clamp": "^1.0.1", 231 | "gl-matrix": "^3.3.0", 232 | "normalize-wheel": "^1.0.1" 233 | } 234 | }, 235 | "clamp": { 236 | "version": "1.0.1", 237 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", 238 | "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=", 239 | "dev": true 240 | }, 241 | "es-module-shims": { 242 | "version": "2.5.1", 243 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-2.5.1.tgz", 244 | "integrity": "sha512-rx57z9aYi2XWJggkIoR0knHT2yr9kQ1vyXkva+oMNlplJr46Aj/1CI6c+GHGxEH6ljzoEhtAYxk080U5NpUpmg==", 245 | "dev": true 246 | }, 247 | "fontfaceobserver-es": { 248 | "version": "3.3.3", 249 | "resolved": "https://registry.npmjs.org/fontfaceobserver-es/-/fontfaceobserver-es-3.3.3.tgz", 250 | "integrity": "sha512-1EyweL1ryy0koZWk80Kr0UgWq9KhbOl2cD6x1O8BtFu0gNqCWIXK1lVFSUb37DI5FMn1m86I6X7N1JRBy8wVfw==", 251 | "dev": true 252 | }, 253 | "gl-matrix": { 254 | "version": "3.4.3", 255 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 256 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", 257 | "dev": true 258 | }, 259 | "normalize-wheel": { 260 | "version": "1.0.1", 261 | "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", 262 | "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=", 263 | "dev": true 264 | }, 265 | "pex-context": { 266 | "version": "3.2.0", 267 | "resolved": "https://registry.npmjs.org/pex-context/-/pex-context-3.2.0.tgz", 268 | "integrity": "sha512-yM7XS0SCbWWM/QbonhNnwVxD58ZDjcOJ1VwONeJGj0kFoMHkJskoAFSJH3i4fGAEte6YiHt361LX9D/waP1IAw==", 269 | "dev": true, 270 | "requires": { 271 | "pex-gl": "^3.0.2" 272 | } 273 | }, 274 | "pex-geom": { 275 | "version": "3.0.2", 276 | "resolved": "https://registry.npmjs.org/pex-geom/-/pex-geom-3.0.2.tgz", 277 | "integrity": "sha512-jLx3ISMMrVuDouPvkmN1hLTqeJE/45otdNVIuoMHKxcz7LZb8k9+XuV7VmsAPA+/TySJyvO+gVvFopUVVG40jg==", 278 | "dev": true, 279 | "requires": { 280 | "pex-math": "^4.1.1" 281 | } 282 | }, 283 | "pex-gl": { 284 | "version": "3.0.2", 285 | "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.2.tgz", 286 | "integrity": "sha512-q0MEUXyRlXEa7A/QGQn7GvrkLf+Yc+tRpTRFW0gmbG1RnJpKC0U504WYaB9O47OoMmy9LlKGCaolP2cJJUw5gQ==", 287 | "dev": true 288 | }, 289 | "pex-math": { 290 | "version": "4.1.1", 291 | "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-4.1.1.tgz", 292 | "integrity": "sha512-jhFPZfgdhUczx8EJsV7PwFTDjvhDMhNiabtK+rOLsfkWl33LNV97hUYB0URlMiTf9UR4nNpPcTLuha96aniyQg==", 293 | "dev": true 294 | }, 295 | "tslib": { 296 | "version": "2.6.3", 297 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", 298 | "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", 299 | "dev": true 300 | }, 301 | "tweakpane": { 302 | "version": "4.0.5", 303 | "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.5.tgz", 304 | "integrity": "sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==", 305 | "dev": true 306 | }, 307 | "typed-array-interleave": { 308 | "version": "2.0.1", 309 | "resolved": "https://registry.npmjs.org/typed-array-interleave/-/typed-array-interleave-2.0.1.tgz", 310 | "integrity": "sha512-RTidx5Oq8GSiRPhNs57IU6fhlD9XU6uQGqPLSZCS5tHnAuIBxYsQI/NjKe2D07n0QIq6A+hURNgOVG4ECSFokQ==", 311 | "dev": true 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primitive-geometry", 3 | "version": "2.11.0", 4 | "description": "Geometries for 3D rendering, including normals, UVs and cell indices (faces). Perfect if you want to supercharge your dependency folder... with 30KB of geometries.", 5 | "keywords": [ 6 | "simplicial", 7 | "complex", 8 | "mesh", 9 | "geom", 10 | "geometry", 11 | "primitive", 12 | "stackgl", 13 | "glsl", 14 | "webgl", 15 | "gl", 16 | "quad", 17 | "plane", 18 | "cube", 19 | "rounded-cube", 20 | "cylinder", 21 | "cone", 22 | "capsule", 23 | "sphere", 24 | "icosphere", 25 | "ellipsoid", 26 | "torus", 27 | "platonic-solid", 28 | "tetrahedron", 29 | "icosahedron", 30 | "ellipse", 31 | "disc", 32 | "squircle", 33 | "superellipse", 34 | "reuleux", 35 | "box", 36 | "circle" 37 | ], 38 | "homepage": "https://github.com/dmnsgn/primitive-geometry", 39 | "bugs": "https://github.com/dmnsgn/primitive-geometry/issues", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/dmnsgn/primitive-geometry.git" 43 | }, 44 | "funding": [ 45 | { 46 | "type": "individual", 47 | "url": "https://paypal.me/dmnsgn" 48 | }, 49 | { 50 | "type": "individual", 51 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" 52 | } 53 | ], 54 | "license": "MIT", 55 | "author": "Damien Seguin (https://github.com/dmnsgn)", 56 | "type": "module", 57 | "exports": { 58 | ".": { 59 | "types": "./types/index.d.ts", 60 | "default": "./index.js" 61 | } 62 | }, 63 | "main": "index.js", 64 | "types": "types/index.d.ts", 65 | "scripts": { 66 | "build": "npx snowdev build", 67 | "deps": "npx snowdev install", 68 | "dev": "npx snowdev dev" 69 | }, 70 | "devDependencies": { 71 | "async-preloader": "^8.0.4", 72 | "cameras": "^3.1.1", 73 | "es-module-shims": "^2.5.1", 74 | "gl-matrix": "^3.4.3", 75 | "pex-context": "^3.2.0", 76 | "pex-geom": "^3.0.2", 77 | "tweakpane": "^4.0.5", 78 | "typed-array-interleave": "^2.0.1" 79 | }, 80 | "engines": { 81 | "node": ">=22.0.0", 82 | "npm": ">=10.5.1", 83 | "snowdev": ">=2.3.x" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmnsgn/primitive-geometry/36966fdd65921cb6dae505c5be99ff69bce238cb/screenshot.gif -------------------------------------------------------------------------------- /src/annulus.js: -------------------------------------------------------------------------------- 1 | /** @module annulus */ 2 | import ellipse from "./ellipse.js"; 3 | import { concentric } from "./mappings.js"; 4 | import { checkArguments, TAU } from "./utils.js"; 5 | 6 | /** 7 | * @typedef {object} AnnulusOptions 8 | * @property {number} [sx=1] 9 | * @property {number} [sy=1] 10 | * @property {number} [radius=0.5] 11 | * @property {number} [segments=32] 12 | * @property {number} [innerSegments=16] 13 | * @property {number} [theta=TAU] 14 | * @property {number} [thetaOffset=0] 15 | * @property {number} [innerRadius=radius * 0.5] 16 | * @property {Function} [mapping=mappings.concentric] 17 | */ 18 | 19 | /** 20 | * @alias module:annulus 21 | * @param {AnnulusOptions} [options={}] 22 | * @returns {import("../types.js").SimplicialComplex} 23 | */ 24 | function annulus({ 25 | sx = 1, 26 | sy = 1, 27 | radius = 0.5, 28 | segments = 32, 29 | innerSegments = 16, 30 | theta = TAU, 31 | thetaOffset = 0, 32 | innerRadius = radius * 0.5, 33 | mapping = concentric, 34 | } = {}) { 35 | checkArguments(arguments); 36 | 37 | return ellipse({ 38 | sx, 39 | sy, 40 | radius, 41 | segments, 42 | innerSegments, 43 | theta, 44 | thetaOffset, 45 | innerRadius, 46 | mergeCentroid: false, 47 | mapping, 48 | }); 49 | } 50 | 51 | export default annulus; 52 | -------------------------------------------------------------------------------- /src/box.js: -------------------------------------------------------------------------------- 1 | /** @module box */ 2 | import { checkArguments } from "./utils.js"; 3 | 4 | /** 5 | * @typedef {object} BoxOptions 6 | * @property {number} [sx=1] 7 | * @property {number} [sy=sx] 8 | * @property {number} [sz=sx] 9 | */ 10 | 11 | /** 12 | * @alias module:box 13 | * @param {BoxOptions} [options={}] 14 | * @returns {import("../types.js").BasicSimplicialComplex} 15 | */ 16 | function box({ sx = 1, sy = sx, sz = sx } = {}) { 17 | checkArguments(arguments); 18 | 19 | const x = sx / 2; 20 | const y = sy / 2; 21 | const z = sz / 2; 22 | 23 | return { 24 | // prettier-ignore 25 | positions: Float32Array.of( 26 | -x, y, z, 27 | -x, -y, z, 28 | x, -y, z, 29 | x, y, z, 30 | 31 | // -z 32 | x, y, -z, 33 | x, -y, -z, 34 | -x, -y, -z, 35 | -x, y, -z, 36 | ), 37 | // prettier-ignore 38 | cells: Uint8Array.of( 39 | 0, 1, 2, 3, // +z 40 | 3, 2, 5, 4, // +x 41 | 4, 5, 6, 7, // -z 42 | 7, 6, 1, 0, // -x 43 | 7, 0, 3, 4, // +y 44 | 1, 6, 5, 2, // -y 45 | ), 46 | }; 47 | } 48 | 49 | export default box; 50 | -------------------------------------------------------------------------------- /src/capsule.js: -------------------------------------------------------------------------------- 1 | /** @module capsule */ 2 | import { checkArguments, getCellsTypedArray, TAU } from "./utils.js"; 3 | 4 | /** 5 | * @typedef {object} CapsuleOptions 6 | * @property {number} [height=0.5] 7 | * @property {number} [radius=0.25] 8 | * @property {number} [nx=16] 9 | * @property {number} [ny=1] 10 | * @property {number} [roundSegments=32] 11 | * @property {number} [phi=TAU] 12 | */ 13 | 14 | /** 15 | * @alias module:capsule 16 | * @param {CapsuleOptions} [options={}] 17 | * @returns {import("../types.js").SimplicialComplex} 18 | */ 19 | 20 | function capsule({ 21 | height = 0.5, 22 | radius = 0.25, 23 | nx = 16, 24 | ny = 1, 25 | roundSegments = 16, 26 | phi = TAU, 27 | } = {}) { 28 | checkArguments(arguments); 29 | 30 | const ringsBody = ny + 1; 31 | const ringsCap = roundSegments * 2; 32 | const ringsTotal = ringsCap + ringsBody; 33 | 34 | const size = ringsTotal * nx; 35 | 36 | const positions = new Float32Array(size * 3); 37 | const normals = new Float32Array(size * 3); 38 | const uvs = new Float32Array(size * 2); 39 | const cells = new (getCellsTypedArray(size))((ringsTotal - 1) * (nx - 1) * 6); 40 | 41 | let vertexIndex = 0; 42 | let cellIndex = 0; 43 | 44 | const segmentIncrement = 1 / (nx - 1); 45 | const ringIncrement = 1 / (ringsCap - 1); 46 | const bodyIncrement = 1 / (ringsBody - 1); 47 | 48 | function computeRing(r, y, dy) { 49 | for (let s = 0; s < nx; s++, vertexIndex++) { 50 | const x = -Math.cos(s * segmentIncrement * phi) * r; 51 | const z = Math.sin(s * segmentIncrement * phi) * r; 52 | 53 | const py = radius * y + height * dy; 54 | 55 | positions[vertexIndex * 3] = radius * x; 56 | positions[vertexIndex * 3 + 1] = py; 57 | positions[vertexIndex * 3 + 2] = radius * z; 58 | 59 | normals[vertexIndex * 3] = x; 60 | normals[vertexIndex * 3 + 1] = y; 61 | normals[vertexIndex * 3 + 2] = z; 62 | 63 | uvs[vertexIndex * 2] = s * segmentIncrement; 64 | uvs[vertexIndex * 2 + 1] = 1 - (0.5 - py / (2 * radius + height)); 65 | } 66 | } 67 | 68 | for (let r = 0; r < roundSegments; r++) { 69 | computeRing( 70 | Math.sin(Math.PI * r * ringIncrement), 71 | Math.sin(Math.PI * (r * ringIncrement - 0.5)), 72 | -0.5, 73 | ); 74 | } 75 | 76 | for (let r = 0; r < ringsBody; r++) { 77 | computeRing(1, 0, r * bodyIncrement - 0.5); 78 | } 79 | 80 | for (let r = roundSegments; r < ringsCap; r++) { 81 | computeRing( 82 | Math.sin(Math.PI * r * ringIncrement), 83 | Math.sin(Math.PI * (r * ringIncrement - 0.5)), 84 | 0.5, 85 | ); 86 | } 87 | 88 | for (let r = 0; r < ringsTotal - 1; r++) { 89 | for (let s = 0; s < nx - 1; s++) { 90 | const a = r * nx; 91 | const b = (r + 1) * nx; 92 | const s1 = s + 1; 93 | cells[cellIndex] = a + s; 94 | cells[cellIndex + 1] = a + s1; 95 | cells[cellIndex + 2] = b + s1; 96 | 97 | cells[cellIndex + 3] = a + s; 98 | cells[cellIndex + 4] = b + s1; 99 | cells[cellIndex + 5] = b + s; 100 | 101 | cellIndex += 6; 102 | } 103 | } 104 | 105 | return { 106 | positions, 107 | normals, 108 | uvs, 109 | cells, 110 | }; 111 | } 112 | 113 | export default capsule; 114 | -------------------------------------------------------------------------------- /src/circle.js: -------------------------------------------------------------------------------- 1 | /** @module circle */ 2 | import { checkArguments, getCellsTypedArray, TAU } from "./utils.js"; 3 | 4 | /** 5 | * @typedef {object} CircleOptions 6 | * @property {number} [radius=0.5] 7 | * @property {number} [segments=32] 8 | * @property {number} [theta=TAU] 9 | * @property {number} [thetaOffset=0] 10 | * @property {boolean} [closed=false] 11 | */ 12 | 13 | /** 14 | * @alias module:circle 15 | * @param {CircleOptions} [options={}] 16 | * @returns {import("../types.js").BasicSimplicialComplex} 17 | */ 18 | function circle({ 19 | radius = 0.5, 20 | segments = 32, 21 | theta = TAU, 22 | thetaOffset = 0, 23 | closed = false, 24 | } = {}) { 25 | checkArguments(arguments); 26 | 27 | const positions = new Float32Array(segments * 3); 28 | const cells = new (getCellsTypedArray(segments))( 29 | (segments - (closed ? 0 : 1)) * 2, 30 | ); 31 | 32 | for (let i = 0; i < segments; i++) { 33 | const t = (i / segments) * theta + thetaOffset; 34 | positions[i * 3] = radius * Math.cos(t); 35 | positions[i * 3 + 1] = radius * Math.sin(t); 36 | 37 | if (i > 0) { 38 | cells[(i - 1) * 2] = i - 1; 39 | cells[(i - 1) * 2 + 1] = i; 40 | } 41 | } 42 | 43 | if (closed) { 44 | cells[(segments - 1) * 2] = segments - 1; 45 | cells[(segments - 1) * 2 + 1] = 0; 46 | } 47 | 48 | return { positions, cells }; 49 | } 50 | 51 | export default circle; 52 | -------------------------------------------------------------------------------- /src/cone.js: -------------------------------------------------------------------------------- 1 | /** @module cone */ 2 | import cylinder from "./cylinder.js"; 3 | import { checkArguments } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} ConeOptions 7 | * @property {number} [height=1] 8 | * @property {number} [radius=0.25] 9 | * @property {number} [nx=16] 10 | * @property {number} [ny=1] 11 | * @property {number} [capSegments=1] 12 | * @property {boolean} [capBase=true] 13 | * @property {number} [phi=TAU] 14 | */ 15 | 16 | /** 17 | * @alias module:cone 18 | * @param {ConeOptions} [options={}] 19 | * @returns {import("../types.js").SimplicialComplex} 20 | */ 21 | function cone({ height, radius, nx, ny, capSegments, capBase, phi } = {}) { 22 | checkArguments(arguments); 23 | 24 | return cylinder({ 25 | height, 26 | radius, 27 | nx, 28 | ny, 29 | capSegments, 30 | capBase, 31 | phi, 32 | 33 | radiusApex: 0, 34 | capApex: false, 35 | }); 36 | } 37 | 38 | export default cone; 39 | -------------------------------------------------------------------------------- /src/cube.js: -------------------------------------------------------------------------------- 1 | /** @module cube */ 2 | 3 | import { checkArguments, computePlane, getCellsTypedArray } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} CubeOptions 7 | * @property {number} [sx=1] 8 | * @property {number} [sy=sx] 9 | * @property {number} [sz=sx] 10 | * @property {number} [nx=1] 11 | * @property {number} [ny=nx] 12 | * @property {number} [nz=nx] 13 | */ 14 | 15 | /** 16 | * @alias module:cube 17 | * @param {CubeOptions} [options={}] 18 | * @returns {import("../types.js").SimplicialComplex} 19 | */ 20 | function cube({ sx = 1, sy = sx, sz = sx, nx = 1, ny = nx, nz = nx } = {}) { 21 | checkArguments(arguments); 22 | 23 | const size = 24 | (nx + 1) * (ny + 1) * 2 + (nx + 1) * (nz + 1) * 2 + (nz + 1) * (ny + 1) * 2; 25 | 26 | const geometry = { 27 | positions: new Float32Array(size * 3), 28 | normals: new Float32Array(size * 3), 29 | uvs: new Float32Array(size * 2), 30 | cells: new (getCellsTypedArray(size))( 31 | (nx * ny * 2 + nx * nz * 2 + nz * ny * 2) * 6, 32 | ), 33 | }; 34 | 35 | const halfSX = sx * 0.5; 36 | const halfSY = sy * 0.5; 37 | const halfSZ = sz * 0.5; 38 | 39 | const indices = { vertex: 0, cell: 0 }; 40 | 41 | computePlane(geometry, indices, sx, sy, nx, ny, "z", halfSZ); 42 | computePlane(geometry, indices, sx, sy, nx, ny, "-z", -halfSZ); 43 | computePlane(geometry, indices, sz, sy, nz, ny, "-x", -halfSX); 44 | computePlane(geometry, indices, sz, sy, nz, ny, "x", halfSX); 45 | computePlane(geometry, indices, sx, sz, nx, nz, "y", halfSY); 46 | computePlane(geometry, indices, sx, sz, nx, nz, "-y", -halfSY); 47 | 48 | return geometry; 49 | } 50 | 51 | export default cube; 52 | -------------------------------------------------------------------------------- /src/cylinder.js: -------------------------------------------------------------------------------- 1 | /** @module cylinder */ 2 | import { 3 | checkArguments, 4 | getCellsTypedArray, 5 | normalize, 6 | TAU, 7 | TMP, 8 | } from "./utils.js"; 9 | 10 | /** 11 | * @typedef {object} CylinderOptions 12 | * @property {number} [height=1] 13 | * @property {number} [radius=0.25] 14 | * @property {number} [nx=16] 15 | * @property {number} [ny=1] 16 | * @property {number} [radiusApex=radius] 17 | * @property {number} [capSegments=1] 18 | * @property {boolean} [capApex=true] 19 | * @property {boolean} [capBase=true] 20 | * @property {number} [phi=TAU] 21 | */ 22 | 23 | /** 24 | * @alias module:cylinder 25 | * @param {CylinderOptions} [options={}] 26 | * @returns {import("../types.js").SimplicialComplex} 27 | */ 28 | function cylinder({ 29 | height = 1, 30 | radius = 0.25, 31 | nx = 16, 32 | ny = 1, 33 | 34 | radiusApex = radius, 35 | capSegments = 1, 36 | capApex = true, 37 | capBase = true, 38 | capBaseSegments = capSegments, 39 | phi = TAU, 40 | } = {}) { 41 | checkArguments(arguments); 42 | 43 | let capCount = 0; 44 | if (capApex) capCount += capSegments; 45 | if (capBase) capCount += capBaseSegments; 46 | 47 | const segments = nx + 1; 48 | const slices = ny + 1; 49 | 50 | const size = segments * slices + segments * 2 * capCount; 51 | 52 | const positions = new Float32Array(size * 3); 53 | const normals = new Float32Array(size * 3); 54 | const uvs = new Float32Array(size * 2); 55 | const cells = new (getCellsTypedArray(size))((nx * ny + nx * capCount) * 6); 56 | 57 | let vertexIndex = 0; 58 | let cellIndex = 0; 59 | 60 | const halfHeight = height / 2; 61 | const segmentIncrement = 1 / (segments - 1); 62 | const ringIncrement = 1 / (slices - 1); 63 | 64 | for (let i = 0; i < segments; i++) { 65 | const u = i * segmentIncrement; 66 | 67 | for (let j = 0; j < slices; j++) { 68 | const v = j * ringIncrement; 69 | const p = u * phi; 70 | const cosPhi = -Math.cos(p); 71 | const sinPhi = Math.sin(p); 72 | 73 | const r = radius * (1 - v) + radiusApex * v; 74 | positions[vertexIndex * 3] = r * cosPhi; 75 | positions[vertexIndex * 3 + 1] = height * v - halfHeight; 76 | positions[vertexIndex * 3 + 2] = r * sinPhi; 77 | 78 | TMP[0] = height * cosPhi; 79 | TMP[1] = radius - radiusApex; 80 | TMP[2] = height * sinPhi; 81 | normalize(TMP); 82 | 83 | normals[vertexIndex * 3] = TMP[0]; 84 | normals[vertexIndex * 3 + 1] = TMP[1]; 85 | normals[vertexIndex * 3 + 2] = TMP[2]; 86 | 87 | uvs[vertexIndex * 2] = u; 88 | uvs[vertexIndex * 2 + 1] = v; 89 | 90 | vertexIndex++; 91 | } 92 | } 93 | 94 | for (let j = 0; j < slices - 1; j++) { 95 | for (let i = 0; i < segments - 1; i++) { 96 | cells[cellIndex + 0] = (i + 0) * slices + (j + 0); 97 | cells[cellIndex + 1] = (i + 1) * slices + (j + 0); 98 | cells[cellIndex + 2] = (i + 1) * slices + (j + 1); 99 | 100 | cells[cellIndex + 3] = (i + 0) * slices + (j + 0); 101 | cells[cellIndex + 4] = (i + 1) * slices + (j + 1); 102 | cells[cellIndex + 5] = (i + 0) * slices + (j + 1); 103 | 104 | cellIndex += 6; 105 | } 106 | } 107 | 108 | function computeCap(flip, height, radius, capSegments) { 109 | const index = vertexIndex; 110 | 111 | const segmentIncrement = 1 / (segments - 1); 112 | for (let r = 0; r < capSegments; r++) { 113 | for (let i = 0; i < segments; i++) { 114 | const p = i * segmentIncrement * phi; 115 | const cosPhi = -Math.cos(p); 116 | const sinPhi = Math.sin(p); 117 | 118 | // inner point 119 | positions[vertexIndex * 3] = (radius * cosPhi * r) / capSegments; 120 | positions[vertexIndex * 3 + 1] = height; 121 | positions[vertexIndex * 3 + 2] = (radius * sinPhi * r) / capSegments; 122 | 123 | normals[vertexIndex * 3 + 1] = -flip; 124 | 125 | uvs[vertexIndex * 2] = (0.5 * cosPhi * r) / capSegments + 0.5; 126 | uvs[vertexIndex * 2 + 1] = (0.5 * sinPhi * r) / capSegments + 0.5; 127 | 128 | vertexIndex++; 129 | 130 | // outer point 131 | positions[vertexIndex * 3] = (radius * cosPhi * (r + 1)) / capSegments; 132 | positions[vertexIndex * 3 + 1] = height; 133 | positions[vertexIndex * 3 + 2] = 134 | (radius * sinPhi * (r + 1)) / capSegments; 135 | 136 | normals[vertexIndex * 3 + 1] = -flip; 137 | 138 | uvs[vertexIndex * 2] = (0.5 * (cosPhi * (r + 1))) / capSegments + 0.5; 139 | uvs[vertexIndex * 2 + 1] = 140 | (0.5 * (sinPhi * (r + 1))) / capSegments + 0.5; 141 | 142 | vertexIndex++; 143 | } 144 | } 145 | 146 | for (let r = 0; r < capSegments; r++) { 147 | for (let i = 0; i < segments - 1; i++) { 148 | const n = index + r * segments * 2 + i * 2; 149 | const a = n + 0; 150 | const b = n + 1; 151 | const c = n + 2; 152 | const d = n + 3; 153 | 154 | if (flip === 1) { 155 | cells[cellIndex] = a; 156 | cells[cellIndex + 1] = c; 157 | cells[cellIndex + 2] = d; 158 | 159 | cells[cellIndex + 3] = a; 160 | cells[cellIndex + 4] = d; 161 | cells[cellIndex + 5] = b; 162 | } else { 163 | cells[cellIndex + 0] = a; 164 | cells[cellIndex + 1] = d; 165 | cells[cellIndex + 2] = c; 166 | 167 | cells[cellIndex + 3] = a; 168 | cells[cellIndex + 4] = b; 169 | cells[cellIndex + 5] = d; 170 | } 171 | cellIndex += 6; 172 | } 173 | } 174 | } 175 | 176 | if (capBase) computeCap(1, -halfHeight, radius, capBaseSegments); 177 | if (capApex) computeCap(-1, halfHeight, radiusApex, capSegments); 178 | 179 | return { 180 | positions, 181 | normals, 182 | uvs, 183 | cells, 184 | }; 185 | } 186 | 187 | export default cylinder; 188 | -------------------------------------------------------------------------------- /src/disc.js: -------------------------------------------------------------------------------- 1 | /** @module disc */ 2 | import ellipse from "./ellipse.js"; 3 | import { concentric } from "./mappings.js"; 4 | import { checkArguments, TAU } from "./utils.js"; 5 | 6 | /** 7 | * @typedef {object} DiscOptions 8 | * @property {number} [radius=0.5] 9 | * @property {number} [segments=32] 10 | * @property {number} [innerSegments=16] 11 | * @property {number} [theta=TAU] 12 | * @property {number} [thetaOffset=0] 13 | * @property {boolean} [mergeCentroid=true] 14 | * @property {Function} [mapping=mappings.concentric] 15 | */ 16 | 17 | /** 18 | * @alias module:disc 19 | * @param {DiscOptions} [options={}] 20 | * @returns {import("../types.js").SimplicialComplex} 21 | */ 22 | function disc({ 23 | radius = 0.5, 24 | segments = 32, 25 | innerSegments = 16, 26 | theta = TAU, 27 | thetaOffset = 0, 28 | mergeCentroid = true, 29 | mapping = concentric, 30 | } = {}) { 31 | checkArguments(arguments); 32 | 33 | return ellipse({ 34 | sx: 1, 35 | sy: 1, 36 | radius, 37 | segments, 38 | innerSegments, 39 | theta, 40 | thetaOffset, 41 | mergeCentroid, 42 | mapping, 43 | }); 44 | } 45 | 46 | export default disc; 47 | -------------------------------------------------------------------------------- /src/ellipse.js: -------------------------------------------------------------------------------- 1 | /** @module ellipse */ 2 | import { elliptical } from "./mappings.js"; 3 | import { checkArguments, getCellsTypedArray, TAU } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} EllipseOptions 7 | * @property {number} [sx=1] 8 | * @property {number} [sy=0.5] 9 | * @property {number} [radius=0.5] 10 | * @property {number} [segments=32] 11 | * @property {number} [innerSegments=16] 12 | * @property {number} [theta=TAU] 13 | * @property {number} [thetaOffset=0] 14 | * @property {boolean} [mergeCentroid=true] 15 | * @property {Function} [mapping=mappings.elliptical] 16 | */ 17 | 18 | /** 19 | * @alias module:ellipse 20 | * @param {EllipseOptions} [options={}] 21 | * @returns {import("../types.js").SimplicialComplex} 22 | */ 23 | function ellipse({ 24 | sx = 1, 25 | sy = 0.5, 26 | radius = 0.5, 27 | segments = 32, 28 | innerSegments = 16, 29 | theta = TAU, 30 | thetaOffset = 0, 31 | innerRadius = 0, 32 | mergeCentroid = true, 33 | mapping = elliptical, 34 | equation = ({ rx, ry, cosTheta, sinTheta }) => [rx * cosTheta, ry * sinTheta], 35 | } = {}) { 36 | checkArguments(arguments); 37 | 38 | const size = mergeCentroid 39 | ? 1 + (segments + 1) + (innerSegments - 1) * (segments + 1) 40 | : (segments + 1) * (innerSegments + 1); 41 | 42 | const positions = new Float32Array(size * 3); 43 | const normals = new Float32Array(size * 3); 44 | const uvs = new Float32Array(size * 2); 45 | const cells = new (getCellsTypedArray(size))( 46 | mergeCentroid 47 | ? segments * 3 + (innerSegments - 1) * segments * 6 48 | : size * 6, 49 | ); 50 | 51 | if (mergeCentroid) { 52 | normals[2] = 1; 53 | uvs[0] = 0.5; 54 | uvs[1] = 0.5; 55 | } 56 | 57 | let vertexIndex = mergeCentroid ? 1 : 0; 58 | let cellIndex = 0; 59 | 60 | for (let j = vertexIndex; j <= innerSegments; j++) { 61 | const radiusRatio = j / innerSegments; 62 | 63 | const r = innerRadius + (radius - innerRadius) * radiusRatio; 64 | 65 | for (let i = 0; i <= segments; i++, vertexIndex++) { 66 | const thetaRatio = i / segments; 67 | const t = thetaOffset + thetaRatio * theta; 68 | 69 | const cosTheta = Math.cos(t); 70 | const sinTheta = Math.sin(t); 71 | 72 | const [x, y] = equation({ 73 | rx: sx * r, 74 | ry: sy * r, 75 | cosTheta, 76 | sinTheta, 77 | s: radiusRatio, 78 | t, 79 | }); 80 | 81 | positions[vertexIndex * 3] = x; 82 | positions[vertexIndex * 3 + 1] = y; 83 | 84 | normals[vertexIndex * 3 + 2] = 1; 85 | 86 | mapping({ 87 | uvs, 88 | index: vertexIndex * 2, 89 | u: radiusRatio * cosTheta, 90 | v: radiusRatio * sinTheta, 91 | radius, 92 | radiusRatio, 93 | thetaRatio, 94 | t, 95 | // For rectangular 96 | x, 97 | y, 98 | sx, 99 | sy, 100 | }); 101 | 102 | if (i < segments) { 103 | if (mergeCentroid && j === 1) { 104 | cells[cellIndex] = i + 1; 105 | cells[cellIndex + 1] = i + 2; 106 | 107 | cellIndex += 3; 108 | } else { 109 | let a; 110 | 111 | if (mergeCentroid) { 112 | a = 1 + (j - 2) * (segments + 1) + i; 113 | } else if (j < innerSegments) { 114 | a = j * (segments + 1) + i; 115 | } 116 | 117 | if (a !== undefined) { 118 | const b = a + segments + 1; 119 | const c = a + segments + 2; 120 | const d = a + 1; 121 | 122 | cells[cellIndex] = a; 123 | cells[cellIndex + 1] = b; 124 | cells[cellIndex + 2] = d; 125 | 126 | cells[cellIndex + 3] = b; 127 | cells[cellIndex + 4] = c; 128 | cells[cellIndex + 5] = d; 129 | 130 | cellIndex += 6; 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | return { positions, normals, uvs, cells }; 138 | } 139 | 140 | export default ellipse; 141 | -------------------------------------------------------------------------------- /src/ellipsoid.js: -------------------------------------------------------------------------------- 1 | /** @module ellipsoid */ 2 | import { 3 | checkArguments, 4 | getCellsTypedArray, 5 | normalize, 6 | TAU, 7 | TMP, 8 | } from "./utils.js"; 9 | 10 | /** 11 | * @typedef {object} EllipsoidOptions 12 | * @property {number} [radius=0.5] 13 | * @property {number} [nx=32] 14 | * @property {number} [ny=16] 15 | * @property {number} [rx=1] 16 | * @property {number} [ry=0.5] 17 | * @property {number} [rz=ry] 18 | * @property {number} [theta=Math.PI] 19 | * @property {number} [thetaOffset=0] 20 | * @property {number} [phi=TAU] 21 | * @property {number} [phiOffset=0] 22 | */ 23 | 24 | /** 25 | * Default to an oblate spheroid. 26 | * @alias module:ellipsoid 27 | * @param {EllipsoidOptions} [options={}] 28 | * @returns {import("../types.js").SimplicialComplex} 29 | */ 30 | function ellipsoid({ 31 | radius = 1, 32 | nx = 32, 33 | ny = 16, 34 | rx = 0.5, 35 | ry = 0.25, 36 | rz = ry, 37 | theta = Math.PI, 38 | thetaOffset = 0, 39 | phi = TAU, 40 | phiOffset = 0, 41 | } = {}) { 42 | checkArguments(arguments); 43 | 44 | const size = (ny + 1) * (nx + 1); 45 | 46 | const positions = new Float32Array(size * 3); 47 | const normals = new Float32Array(size * 3); 48 | const uvs = new Float32Array(size * 2); 49 | const cells = new (getCellsTypedArray(size))(ny * nx * 6); 50 | 51 | let vertexIndex = 0; 52 | let cellIndex = 0; 53 | 54 | for (let y = 0; y <= ny; y++) { 55 | const v = y / ny; 56 | const t = v * theta + thetaOffset; 57 | const cosTheta = Math.cos(t); 58 | const sinTheta = Math.sin(t); 59 | 60 | for (let x = 0; x <= nx; x++) { 61 | const u = x / nx; 62 | const p = u * phi + phiOffset; 63 | const cosPhi = Math.cos(p); 64 | const sinPhi = Math.sin(p); 65 | 66 | TMP[0] = -rx * cosPhi * sinTheta; 67 | TMP[1] = -ry * cosTheta; 68 | TMP[2] = rz * sinPhi * sinTheta; 69 | 70 | positions[vertexIndex * 3] = radius * TMP[0]; 71 | positions[vertexIndex * 3 + 1] = radius * TMP[1]; 72 | positions[vertexIndex * 3 + 2] = radius * TMP[2]; 73 | 74 | normalize(TMP); 75 | 76 | normals[vertexIndex * 3] = TMP[0]; 77 | normals[vertexIndex * 3 + 1] = TMP[1]; 78 | normals[vertexIndex * 3 + 2] = TMP[2]; 79 | 80 | uvs[vertexIndex * 2] = u; 81 | uvs[vertexIndex * 2 + 1] = v; 82 | 83 | vertexIndex++; 84 | } 85 | 86 | if (y > 0) { 87 | for (let i = vertexIndex - 2 * (nx + 1); i + nx + 2 < vertexIndex; i++) { 88 | const a = i; 89 | const b = i + 1; 90 | const c = i + nx + 1; 91 | const d = i + nx + 2; 92 | cells[cellIndex] = a; 93 | cells[cellIndex + 1] = b; 94 | cells[cellIndex + 2] = c; 95 | 96 | cells[cellIndex + 3] = c; 97 | cells[cellIndex + 4] = b; 98 | cells[cellIndex + 5] = d; 99 | 100 | cellIndex += 6; 101 | } 102 | } 103 | } 104 | 105 | return { 106 | positions, 107 | normals, 108 | uvs, 109 | cells, 110 | }; 111 | } 112 | 113 | export default ellipsoid; 114 | -------------------------------------------------------------------------------- /src/icosahedron.js: -------------------------------------------------------------------------------- 1 | /** @module icosahedron */ 2 | import icosphere from "./icosphere.js"; 3 | import { checkArguments } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} IcosahedronOptions 7 | * @property {number} [radius=0.5] 8 | */ 9 | 10 | /** 11 | * @alias module:icosahedron 12 | * @param {IcosahedronOptions} [options={}] 13 | * @returns {import("../types.js").SimplicialComplex} 14 | */ 15 | function icosahedron({ radius } = {}) { 16 | checkArguments(arguments); 17 | 18 | return icosphere({ subdivisions: 0, radius }); 19 | } 20 | 21 | export default icosahedron; 22 | -------------------------------------------------------------------------------- /src/icosphere.js: -------------------------------------------------------------------------------- 1 | /** @module icosphere */ 2 | import { checkArguments, getCellsTypedArray, TAU } from "./utils.js"; 3 | 4 | const f = 0.5 + Math.sqrt(5) / 2; 5 | 6 | /** 7 | * @typedef {object} IcosphereOptions 8 | * @property {number} [radius=0.5] 9 | * @property {number} [subdivisions=2] 10 | */ 11 | 12 | /** 13 | * @alias module:icosphere 14 | * @param {IcosphereOptions} [options={}] 15 | * @returns {import("../types.js").SimplicialComplex} 16 | */ 17 | function icosphere({ radius = 0.5, subdivisions = 2 } = {}) { 18 | checkArguments(arguments); 19 | 20 | if (subdivisions > 10) throw new Error("Max subdivisions is 10."); 21 | 22 | const T = Math.pow(4, subdivisions); 23 | 24 | const numVertices = 10 * T + 2; 25 | const numDuplicates = 26 | subdivisions === 0 ? 3 : Math.pow(2, subdivisions) * 3 + 9; 27 | 28 | const size = numVertices + numDuplicates; 29 | 30 | const positions = new Float32Array(size * 3); 31 | const uvs = new Float32Array(size * 2); 32 | 33 | // prettier-ignore 34 | positions.set(Float32Array.of( 35 | -1, f, 0, 36 | 1, f, 0, 37 | -1, -f, 0, 38 | 1, -f, 0, 39 | 40 | 0, -1, f, 41 | 0, 1, f, 42 | 0, -1, -f, 43 | 0, 1, -f, 44 | 45 | f, 0, -1, 46 | f, 0, 1, 47 | -f, 0, -1, 48 | -f, 0, 1, 49 | )); 50 | // prettier-ignore 51 | let cells = Uint16Array.of( 52 | 0, 11, 5, 53 | 0, 5, 1, 54 | 0, 1, 7, 55 | 0, 7, 10, 56 | 0, 10, 11, 57 | 58 | 11, 10, 2, 59 | 5, 11, 4, 60 | 1, 5, 9, 61 | 7, 1, 8, 62 | 10, 7, 6, 63 | 64 | 3, 9, 4, 65 | 3, 4, 2, 66 | 3, 2, 6, 67 | 3, 6, 8, 68 | 3, 8, 9, 69 | 70 | 9, 8, 1, 71 | 4, 9, 5, 72 | 2, 4, 11, 73 | 6, 2, 10, 74 | 8, 6, 7, 75 | ); 76 | 77 | let vertexIndex = 12; 78 | 79 | const midCache = subdivisions ? {} : null; 80 | 81 | function addMidPoint(a, b) { 82 | // Cantor's pairing function 83 | const key = Math.floor(((a + b) * (a + b + 1)) / 2 + Math.min(a, b)); 84 | const i = midCache[key]; 85 | if (i !== undefined) { 86 | delete midCache[key]; 87 | return i; 88 | } 89 | midCache[key] = vertexIndex; 90 | positions[3 * vertexIndex + 0] = 91 | (positions[3 * a + 0] + positions[3 * b + 0]) * 0.5; 92 | positions[3 * vertexIndex + 1] = 93 | (positions[3 * a + 1] + positions[3 * b + 1]) * 0.5; 94 | positions[3 * vertexIndex + 2] = 95 | (positions[3 * a + 2] + positions[3 * b + 2]) * 0.5; 96 | return vertexIndex++; 97 | } 98 | 99 | let cellsPrev = cells; 100 | const IndexArray = subdivisions > 5 ? Uint32Array : getCellsTypedArray(size); 101 | 102 | // Subdivide 103 | for (let i = 0; i < subdivisions; i++) { 104 | const prevLen = cellsPrev.length; 105 | cells = new IndexArray(prevLen * 4); 106 | 107 | for (let k = 0; k < prevLen; k += 3) { 108 | const v1 = cellsPrev[k + 0]; 109 | const v2 = cellsPrev[k + 1]; 110 | const v3 = cellsPrev[k + 2]; 111 | 112 | const a = addMidPoint(v1, v2); 113 | const b = addMidPoint(v2, v3); 114 | const c = addMidPoint(v3, v1); 115 | 116 | cells[k * 4 + 0] = v1; 117 | cells[k * 4 + 1] = a; 118 | cells[k * 4 + 2] = c; 119 | 120 | cells[k * 4 + 3] = v2; 121 | cells[k * 4 + 4] = b; 122 | cells[k * 4 + 5] = a; 123 | 124 | cells[k * 4 + 6] = v3; 125 | cells[k * 4 + 7] = c; 126 | cells[k * 4 + 8] = b; 127 | 128 | cells[k * 4 + 9] = a; 129 | cells[k * 4 + 10] = b; 130 | cells[k * 4 + 11] = c; 131 | } 132 | cellsPrev = cells; 133 | } 134 | 135 | // Normalize 136 | for (let i = 0; i < numVertices * 3; i += 3) { 137 | const v1 = positions[i + 0]; 138 | const v2 = positions[i + 1]; 139 | const v3 = positions[i + 2]; 140 | const m = 1 / Math.sqrt(v1 * v1 + v2 * v2 + v3 * v3); 141 | positions[i + 0] *= m; 142 | positions[i + 1] *= m; 143 | positions[i + 2] *= m; 144 | } 145 | 146 | for (let i = 0; i < numVertices; i++) { 147 | uvs[2 * i + 0] = 148 | -Math.atan2(positions[3 * i + 2], positions[3 * i]) / TAU + 0.5; 149 | uvs[2 * i + 1] = Math.asin(positions[3 * i + 1]) / Math.PI + 0.5; 150 | } 151 | 152 | const duplicates = {}; 153 | function addDuplicate(i, uvx, uvy, cached) { 154 | if (cached) { 155 | const dupe = duplicates[i]; 156 | if (dupe !== undefined) return dupe; 157 | } 158 | positions[3 * vertexIndex + 0] = positions[3 * i + 0]; 159 | positions[3 * vertexIndex + 1] = positions[3 * i + 1]; 160 | positions[3 * vertexIndex + 2] = positions[3 * i + 2]; 161 | 162 | uvs[2 * vertexIndex + 0] = uvx; 163 | uvs[2 * vertexIndex + 1] = uvy; 164 | 165 | if (cached) duplicates[i] = vertexIndex; 166 | return vertexIndex++; 167 | } 168 | 169 | for (let i = 0; i < cells.length; i += 3) { 170 | const a = cells[i + 0]; 171 | const b = cells[i + 1]; 172 | const c = cells[i + 2]; 173 | 174 | let ax = uvs[2 * a]; 175 | let bx = uvs[2 * b]; 176 | let cx = uvs[2 * c]; 177 | 178 | const ay = uvs[2 * a + 1]; 179 | const by = uvs[2 * b + 1]; 180 | const cy = uvs[2 * c + 1]; 181 | 182 | if (ax - bx >= 0.5 && ay !== 1) bx += 1; 183 | if (bx - cx > 0.5) cx += 1; 184 | if ((ax < 0.5 && cx - ax > 0.5) || (ax === 1 && cy === 0)) ax += 1; 185 | if (bx < 0.5 && ax - bx > 0.5) bx += 1; 186 | 187 | // Poles 188 | const isPoleA = ay === 0 || ay === 1; 189 | const isPoleB = by === 0 || by === 1; 190 | const isPoleC = cy === 0 || cy === 1; 191 | 192 | if (isPoleA) { 193 | ax = (bx + cx) * 0.5; 194 | if (ay === 1 - bx) { 195 | uvs[2 * a] = ax; 196 | } else { 197 | cells[i + 0] = addDuplicate(a, ax, ay, false); 198 | } 199 | } else if (isPoleB) { 200 | bx = (ax + cx) * 0.5; 201 | if (by === ax) { 202 | uvs[2 * b] = bx; 203 | } else { 204 | cells[i + 1] = addDuplicate(b, bx, by, false); 205 | } 206 | } else if (isPoleC) { 207 | cx = (ax + bx) * 0.5; 208 | if (cy === ax) { 209 | uvs[2 * c] = cx; 210 | } else { 211 | cells[i + 2] = addDuplicate(c, cx, cy, false); 212 | } 213 | } 214 | 215 | // Seam zipper 216 | if (ax !== uvs[2 * a] && !isPoleA) { 217 | cells[i + 0] = addDuplicate(a, ax, ay, true); 218 | } 219 | if (bx !== uvs[2 * b] && !isPoleB) { 220 | cells[i + 1] = addDuplicate(b, bx, by, true); 221 | } 222 | if (cx !== uvs[2 * c] && !isPoleC) { 223 | cells[i + 2] = addDuplicate(c, cx, cy, true); 224 | } 225 | } 226 | 227 | return { 228 | positions: positions.map((v) => v * radius), 229 | normals: positions, 230 | uvs, 231 | cells, 232 | }; 233 | } 234 | 235 | export default icosphere; 236 | -------------------------------------------------------------------------------- /src/mappings.js: -------------------------------------------------------------------------------- 1 | /** @module mappings */ 2 | import { HALF_PI, SQRT2, TAU } from "./utils.js"; 3 | 4 | const safeSqrt = (x) => Math.sqrt(Math.max(x, 0)); 5 | const safeDivide = (x, y) => x / (y + Number.EPSILON); 6 | const isNegligeable = (x) => Math.abs(x) < Number.EPSILON * 2; 7 | 8 | const remapRectangular = (x, radius) => (x / radius + 1) / 2; 9 | const remap = (x) => (x + 1) / 2; // From [-1, 1] to [0, 1] 10 | 11 | export function rectangular({ uvs, index, x, y, radius, sx = 1, sy = 1 }) { 12 | uvs[index] = remapRectangular(x, radius * sx); 13 | uvs[index + 1] = remapRectangular(y, radius * sy); 14 | } 15 | 16 | export function polar({ uvs, index, radiusRatio, thetaRatio }) { 17 | uvs[index] = radiusRatio; 18 | uvs[index + 1] = thetaRatio; 19 | } 20 | 21 | // Basic 22 | export function radial({ uvs, index, u, v, radius }) { 23 | const x = safeDivide( 24 | Math.sqrt(u ** 2 + v ** 2), 25 | Math.max(Math.abs(u), Math.abs(v)), 26 | ); 27 | 28 | uvs[index] = remap(x * u, radius); 29 | uvs[index + 1] = remap(x * v, radius); 30 | } 31 | 32 | const FOUR_OVER_PI = 4 / Math.PI; 33 | 34 | export function concentric({ uvs, index, u, v, radius }) { 35 | const u2 = u ** 2; 36 | const v2 = v ** 2; 37 | const x = Math.sqrt(u2 + v2); 38 | 39 | if (u2 > v2) { 40 | uvs[index] = remap(x * Math.sign(u), radius); 41 | uvs[index + 1] = remap( 42 | x * (FOUR_OVER_PI * Math.atan(safeDivide(v, Math.abs(u)))), 43 | radius, 44 | ); 45 | } else { 46 | uvs[index] = remap( 47 | x * (FOUR_OVER_PI * Math.atan(safeDivide(u, Math.abs(v)))), 48 | radius, 49 | ); 50 | uvs[index + 1] = remap(x * Math.sign(v), radius); 51 | } 52 | } 53 | export function lamé({ uvs, index, u, v, radius }) { 54 | const u2 = u ** 2; 55 | const v2 = v ** 2; 56 | uvs[index] = remap(Math.sign(u) * Math.abs(u) ** (1 - u2 - v2), radius); 57 | uvs[index + 1] = remap(Math.sign(v) * Math.abs(v) ** (1 - u2 - v2), radius); 58 | } 59 | export function elliptical({ uvs, index, u, v, radius }) { 60 | const t = u ** 2 - v ** 2; 61 | const pu1 = 0.5 * safeSqrt(2 + t + 2 * SQRT2 * u); 62 | const pu2 = 0.5 * safeSqrt(2 + t - 2 * SQRT2 * u); 63 | const pv1 = 0.5 * safeSqrt(2 - t + 2 * SQRT2 * v); 64 | const pv2 = 0.5 * safeSqrt(2 - t - 2 * SQRT2 * v); 65 | 66 | uvs[index] = remap(pu1 - pu2, radius); 67 | uvs[index + 1] = remap(pv1 - pv2, radius); 68 | } 69 | 70 | // Radial, all variations of FG squircular: 71 | function fixFGSingularities(uvs, index, u, v, radius) { 72 | if (isNegligeable(u) || isNegligeable(v)) { 73 | uvs[index] = remap(u, radius); 74 | uvs[index + 1] = remap(v, radius); 75 | } else { 76 | return true; 77 | } 78 | } 79 | 80 | export function fgSquircular({ uvs, index, u, v, radius }) { 81 | const ok = fixFGSingularities(uvs, index, u, v, radius); 82 | if (ok) { 83 | const u2 = u ** 2; 84 | const v2 = v ** 2; 85 | const sign = Math.sign(u * v); 86 | const uv2Sum = u2 + v2; 87 | const sqrtUV = Math.sqrt( 88 | uv2Sum - safeSqrt(uv2Sum * (uv2Sum - 4 * u2 * v2)), 89 | ); 90 | uvs[index] = remap((sign / (v * SQRT2)) * sqrtUV, radius); 91 | uvs[index + 1] = remap((sign / (u * SQRT2)) * sqrtUV, radius); 92 | } 93 | } 94 | export function twoSquircular({ uvs, index, u, v, radius }) { 95 | const ok = fixFGSingularities(uvs, index, u, v, radius); 96 | if (ok) { 97 | const sign = Math.sign(u * v); 98 | const sqrtUV = Math.sqrt(1 - safeSqrt(1 - 4 * u ** 2 * v ** 2)); 99 | uvs[index] = remap((sign / (v * SQRT2)) * sqrtUV, radius); 100 | uvs[index + 1] = remap((sign / (u * SQRT2)) * sqrtUV, radius); 101 | } 102 | } 103 | export function threeSquircular({ uvs, index, u, v, radius }) { 104 | const ok = fixFGSingularities(uvs, index, u, v, radius); 105 | if (ok) { 106 | const u2 = u ** 2; 107 | const v2 = v ** 2; 108 | const sign = Math.sign(u * v); 109 | const sqrtUV = Math.sqrt( 110 | (1 - safeSqrt(1 - 4 * u ** 4 * v2 - 4 * u2 * v ** 4)) / (2 * (u2 + v2)), 111 | ); 112 | uvs[index] = remap((sign / v) * sqrtUV, radius); 113 | uvs[index + 1] = remap((sign / u) * sqrtUV, radius); 114 | } 115 | } 116 | export function cornerificTapered2({ uvs, index, u, v, radius }) { 117 | const ok = fixFGSingularities(uvs, index, u, v, radius); 118 | if (ok) { 119 | const u2 = u ** 2; 120 | const v2 = v ** 2; 121 | const sign = Math.sign(u * v); 122 | const uv2Sum = u2 + v2; 123 | const sqrtUV = Math.sqrt( 124 | (uv2Sum - Math.sqrt(uv2Sum * (uv2Sum - 4 * u2 * v2 * (2 - u2 - v2)))) / 125 | (2 * (2 - u2 - v2)), 126 | ); 127 | uvs[index] = remap((sign / v) * sqrtUV, radius); 128 | uvs[index + 1] = remap((sign / u) * sqrtUV, radius); 129 | } 130 | } 131 | export function tapered4({ uvs, index, u, v, radius }) { 132 | const ok = fixFGSingularities(uvs, index, u, v, radius); 133 | if (ok) { 134 | const u2 = u ** 2; 135 | const v2 = v ** 2; 136 | const sign = Math.sign(u * v); 137 | const uv2Sum = u2 + v2; 138 | const divider = 3 - u ** 4 - 2 * u2 * v2 - v ** 4; 139 | const sqrtUV = Math.sqrt( 140 | (uv2Sum - safeSqrt(uv2Sum * (uv2Sum - 2 * u2 * v2 * divider))) / divider, 141 | ); 142 | uvs[index] = remap((sign / v) * sqrtUV, radius); 143 | uvs[index + 1] = remap((sign / u) * sqrtUV, radius); 144 | } 145 | } 146 | 147 | // Non-axial 148 | const FOURTH_SQRT2 = 2 ** (1 / 4); 149 | 150 | export function nonAxial2Pinch({ uvs, index, u, v, radius }) { 151 | const u2 = u ** 2; 152 | const v2 = v ** 2; 153 | const sign = Math.sign(u * v); 154 | const uv2Sum = u2 + v2; 155 | 156 | const sqrtUV = 157 | (uv2Sum - 2 * u2 * v2 - safeSqrt((uv2Sum - 4 * u2 * v2) * uv2Sum)) ** 158 | (1 / 4); 159 | 160 | if (isNegligeable(v)) { 161 | uvs[index] = remap(Math.sign(u) * Math.sqrt(Math.abs(u)), radius); 162 | uvs[index + 1] = remap(safeDivide(sign, u * FOURTH_SQRT2) * sqrtUV, radius); 163 | } else { 164 | uvs[index] = remap(safeDivide(sign, v * FOURTH_SQRT2) * sqrtUV, radius); 165 | uvs[index + 1] = remap( 166 | isNegligeable(u) 167 | ? Math.sign(v) * Math.sqrt(Math.abs(v)) 168 | : safeDivide(sign, u * FOURTH_SQRT2) * sqrtUV, 169 | radius, 170 | ); 171 | } 172 | } 173 | export function nonAxialHalfPinch({ uvs, index, u, v, radius }) { 174 | const u2 = u ** 2; 175 | const v2 = v ** 2; 176 | const sign = Math.sign(u * v); 177 | const uv2Sum = u2 + v2; 178 | 179 | const sqrtUV = Math.sqrt( 180 | safeDivide(1 - safeSqrt(1 - 4 * u2 * v2 * uv2Sum ** 2), 2 * uv2Sum), 181 | ); 182 | 183 | if (isNegligeable(v)) { 184 | uvs[index] = remap(Math.sign(u) * u2, radius); 185 | uvs[index + 1] = remap(safeDivide(sign, u) * sqrtUV, radius); 186 | } else { 187 | uvs[index] = remap(safeDivide(sign, v) * sqrtUV, radius); 188 | uvs[index + 1] = remap( 189 | isNegligeable(u) ? Math.sign(v) * v2 : safeDivide(sign, u) * sqrtUV, 190 | radius, 191 | ); 192 | } 193 | } 194 | 195 | // Variations of elliptical 196 | export function squelched({ uvs, index, u, v, radius, t }) { 197 | uvs[index] = [HALF_PI, TAU - HALF_PI].includes(t) 198 | ? 0.5 199 | : remap(u / Math.sqrt(1 - v ** 2), radius); 200 | uvs[index + 1] = [0, TAU, Math.PI].includes(t) 201 | ? 0.5 202 | : remap(v / Math.sqrt(1 - u ** 2), radius); 203 | } 204 | export function squelchedVertical({ uvs, index, u, v, radius, t }) { 205 | uvs[index] = remap(u, radius); 206 | uvs[index + 1] = [0, TAU, Math.PI].includes(t) 207 | ? 0.5 208 | : remap(v / Math.sqrt(1 - u ** 2), radius); 209 | } 210 | export function squelchedHorizontal({ uvs, index, u, v, radius, t }) { 211 | uvs[index] = [HALF_PI, TAU - HALF_PI].includes(t) 212 | ? 0.5 213 | : remap(u / Math.sqrt(1 - v ** 2), radius); 214 | uvs[index + 1] = remap(v, radius); 215 | } 216 | -------------------------------------------------------------------------------- /src/plane.js: -------------------------------------------------------------------------------- 1 | /** @module plane */ 2 | 3 | import { checkArguments, computePlane, getCellsTypedArray } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} PlaneOptions 7 | * @property {number} [sx=1] 8 | * @property {number} [sy=sx] 9 | * @property {number} [nx=1] 10 | * @property {number} [ny=nx] 11 | * @property {PlaneDirection} [direction="z"] 12 | * @property {boolean} [quads=false] 13 | */ 14 | 15 | /** 16 | * @typedef {"x" | "-x" | "y" | "-y" | "z" | "-z"} PlaneDirection 17 | */ 18 | 19 | /** 20 | * @alias module:plane 21 | * @param {PlaneOptions} [options={}] 22 | * @returns {import("../types.js").SimplicialComplex} 23 | */ 24 | function plane({ 25 | sx = 1, 26 | sy = sx, 27 | nx = 1, 28 | ny = nx, 29 | direction = "z", 30 | quads = false, 31 | } = {}) { 32 | checkArguments(arguments); 33 | 34 | const size = (nx + 1) * (ny + 1); 35 | 36 | return computePlane( 37 | { 38 | positions: new Float32Array(size * 3), 39 | normals: new Float32Array(size * 3), 40 | uvs: new Float32Array(size * 2), 41 | cells: new (getCellsTypedArray(size))(nx * ny * (quads ? 4 : 6)), 42 | }, 43 | { vertex: 0, cell: 0 }, 44 | sx, 45 | sy, 46 | nx, 47 | ny, 48 | direction, 49 | 0, 50 | quads, 51 | ); 52 | } 53 | 54 | export default plane; 55 | -------------------------------------------------------------------------------- /src/quad.js: -------------------------------------------------------------------------------- 1 | /** @module quad */ 2 | 3 | import { checkArguments, getCellsTypedArray } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} QuadOptions 7 | * @property {number} [scale=0.5] 8 | */ 9 | 10 | /** 11 | * @alias module:quad 12 | * @param {QuadOptions} [options={}] 13 | * @returns {import("../types.js").SimplicialComplex} 14 | */ 15 | function quad({ scale = 0.5 } = {}) { 16 | checkArguments(arguments); 17 | 18 | return { 19 | // prettier-ignore 20 | positions: Float32Array.of( 21 | -scale, -scale, 0, 22 | scale, -scale, 0, 23 | scale, scale, 0, 24 | -scale, scale, 0, 25 | ), 26 | // prettier-ignore 27 | normals: Int8Array.of( 28 | 0, 0, 1, 29 | 0, 0, 1, 30 | 0, 0, 1, 31 | 0, 0, 1, 32 | ), 33 | // prettier-ignore 34 | uvs: Uint8Array.of( 35 | 0, 0, 36 | 1, 0, 37 | 1, 1, 38 | 0, 1 39 | ), 40 | // prettier-ignore 41 | cells: (getCellsTypedArray(12)).of( 42 | 0, 1, 2, 43 | 2, 3, 0 44 | ), 45 | }; 46 | } 47 | 48 | export default quad; 49 | -------------------------------------------------------------------------------- /src/reuleux.js: -------------------------------------------------------------------------------- 1 | /** @module reuleux */ 2 | import ellipse from "./ellipse.js"; 3 | import { concentric } from "./mappings.js"; 4 | import { checkArguments, TAU } from "./utils.js"; 5 | 6 | /** 7 | * @typedef {object} ReuleuxOptions 8 | * @property {number} [radius=0.5] 9 | * @property {number} [segments=32] 10 | * @property {number} [innerSegments=16] 11 | * @property {number} [theta=TAU] 12 | * @property {number} [thetaOffset=0] 13 | * @property {boolean} [mergeCentroid=true] 14 | * @property {Function} [mapping=mappings.concentric] 15 | * @property {number} [n=3] 16 | */ 17 | 18 | /** 19 | * @see [Parametric equations for regular and Reuleaux polygons]{@link https://tpfto.wordpress.com/2011/09/15/parametric-equations-for-regular-and-reuleaux-polygons/} 20 | * 21 | * @alias module:reuleux 22 | * @param {ReuleuxOptions} [options={}] 23 | * @returns {import("../types.js").SimplicialComplex} 24 | */ 25 | function reuleux({ 26 | radius = 0.5, 27 | segments = 32, 28 | innerSegments = 16, 29 | theta = TAU, 30 | thetaOffset = 0, 31 | mergeCentroid = true, 32 | mapping = concentric, 33 | n = 3, 34 | } = {}) { 35 | checkArguments(arguments); 36 | 37 | const cosN = 2 * Math.cos(Math.PI / (2 * n)); 38 | const PIoverN = Math.PI / n; 39 | 40 | return ellipse({ 41 | sx: 1, 42 | sy: 1, 43 | radius, 44 | segments, 45 | innerSegments, 46 | theta, 47 | thetaOffset, 48 | mergeCentroid, 49 | mapping, 50 | equation: ({ rx, ry, t }) => [ 51 | rx * 52 | (cosN * 53 | Math.cos(0.5 * (t + PIoverN * (2 * Math.floor((n * t) / TAU) + 1))) - 54 | Math.cos(PIoverN * (2 * Math.floor((n * t) / TAU) + 1))), 55 | ry * 56 | (cosN * 57 | Math.sin(0.5 * (t + PIoverN * (2 * Math.floor((n * t) / TAU) + 1))) - 58 | Math.sin(PIoverN * (2 * Math.floor((n * t) / TAU) + 1))), 59 | ], 60 | }); 61 | } 62 | 63 | export default reuleux; 64 | -------------------------------------------------------------------------------- /src/rounded-cube.js: -------------------------------------------------------------------------------- 1 | /** @module roundedCube */ 2 | import { 3 | checkArguments, 4 | computePlane, 5 | getCellsTypedArray, 6 | normalize, 7 | TMP, 8 | } from "./utils.js"; 9 | 10 | /** 11 | * @typedef {object} RoundedCubeOptions 12 | * @property {number} [sx=1] 13 | * @property {number} [sy=sx] 14 | * @property {number} [sz=sx] 15 | * @property {number} [nx=1] 16 | * @property {number} [ny=nx] 17 | * @property {number} [nz=nx] 18 | * @property {number} [radius=sx * 0.25] 19 | * @property {number} [roundSegments=8] 20 | * @property {number} [edgeSegments=1] 21 | */ 22 | 23 | /** 24 | * @alias module:roundedCube 25 | * @param {RoundedCubeOptions} [options={}] 26 | * @returns {import("../types.js").SimplicialComplex} 27 | */ 28 | function roundedCube({ 29 | sx = 1, 30 | sy = sx, 31 | sz = sx, 32 | nx = 1, 33 | ny = nx, 34 | nz = nx, 35 | radius = sx * 0.25, 36 | roundSegments = 8, 37 | edgeSegments = 1, 38 | } = {}) { 39 | checkArguments(arguments); 40 | 41 | const size = 42 | (nx + 1) * (ny + 1) * 2 + 43 | (nx + 1) * (nz + 1) * 2 + 44 | (nz + 1) * (ny + 1) * 2 + 45 | (roundSegments + 1) * (roundSegments + 1) * 24 + 46 | (roundSegments + 1) * (edgeSegments + 1) * 24; 47 | 48 | const geometry = { 49 | positions: new Float32Array(size * 3), 50 | normals: new Float32Array(size * 3), 51 | uvs: new Float32Array(size * 2), 52 | cells: new (getCellsTypedArray(size))( 53 | (nx * ny * 2 + 54 | nx * nz * 2 + 55 | nz * ny * 2 + 56 | roundSegments * roundSegments * 24 + 57 | roundSegments * edgeSegments * 24) * 58 | 6, 59 | ), 60 | }; 61 | 62 | const halfSX = sx * 0.5; 63 | const halfSY = sy * 0.5; 64 | const halfSZ = sz * 0.5; 65 | 66 | const r2 = radius * 2; 67 | const widthX = sx - r2; 68 | const widthY = sy - r2; 69 | const widthZ = sz - r2; 70 | 71 | const faceSX = widthX / sx; 72 | const faceSY = widthY / sy; 73 | const faceSZ = widthZ / sz; 74 | 75 | const radiusSX = radius / sx; 76 | const radiusSY = radius / sy; 77 | const radiusSZ = radius / sz; 78 | 79 | const indices = { vertex: 0, cell: 0 }; 80 | 81 | const PLANES = [ 82 | [ 83 | widthX, 84 | widthY, 85 | nx, 86 | ny, 87 | "z", 88 | halfSZ, 89 | [faceSX, faceSY], 90 | [radiusSX, radiusSY], 91 | (x, y) => [x, y, 0], 92 | ], 93 | [ 94 | widthX, 95 | widthY, 96 | nx, 97 | ny, 98 | "-z", 99 | -halfSZ, 100 | [faceSX, faceSY], 101 | [radiusSX, radiusSY], 102 | (x, y) => [-x, y, 0], 103 | ], 104 | [ 105 | widthZ, 106 | widthY, 107 | nz, 108 | ny, 109 | "-x", 110 | -halfSX, 111 | [faceSZ, faceSY], 112 | [radiusSZ, radiusSY], 113 | (x, y) => [0, y, x], 114 | ], 115 | [ 116 | widthZ, 117 | widthY, 118 | nz, 119 | ny, 120 | "x", 121 | halfSX, 122 | [faceSZ, faceSY], 123 | [radiusSZ, radiusSY], 124 | (x, y) => [0, y, -x], 125 | ], 126 | [ 127 | widthX, 128 | widthZ, 129 | nx, 130 | nz, 131 | "y", 132 | halfSY, 133 | [faceSX, faceSZ], 134 | [radiusSX, radiusSZ], 135 | (x, y) => [x, 0, -y], 136 | ], 137 | [ 138 | widthX, 139 | widthZ, 140 | nx, 141 | nz, 142 | "-y", 143 | -halfSY, 144 | [faceSX, faceSZ], 145 | [radiusSX, radiusSZ], 146 | (x, y) => [x, 0, y], 147 | ], 148 | ]; 149 | 150 | const uvOffsetCorner = (su, sv) => [ 151 | [0, 0], 152 | [1 - radius / (su + r2), 0], 153 | [1 - radius / (su + r2), 1 - radius / (sv + r2)], 154 | [0, 1 - radius / (sv + r2)], 155 | ]; 156 | const uvOffsetStart = (_, sv) => [0, radius / (sv + r2)]; 157 | const uvOffsetEnd = (su, sv) => [1 - radius / (su + r2), radius / (sv + r2)]; 158 | 159 | for (let j = 0; j < PLANES.length; j++) { 160 | const [su, sv, nu, nv, direction, pw, uvScale, uvOffset, center] = 161 | PLANES[j]; 162 | 163 | // Cube faces 164 | computePlane( 165 | geometry, 166 | indices, 167 | su, 168 | sv, 169 | nu, 170 | nv, 171 | direction, 172 | pw, 173 | false, 174 | uvScale, 175 | uvOffset, 176 | ); 177 | 178 | // Corner order: ccw uv-like order and L/B (0) R/T (2) 179 | // 0,1 -- 1,1 180 | // | -- | 181 | // 0,0 -- 1,0 182 | for (let i = 0; i < 4; i++) { 183 | const ceil = Math.ceil(i / 2) % 2; 184 | const floor = Math.floor(i / 2) % 2; 185 | 186 | const x = (ceil === 0 ? -1 : 1) * (su + radius) * 0.5; 187 | const y = (floor === 0 ? -1 : 1) * (sv + radius) * 0.5; 188 | 189 | // Corners 190 | computePlane( 191 | geometry, 192 | indices, 193 | radius, 194 | radius, 195 | roundSegments, 196 | roundSegments, 197 | direction, 198 | pw, 199 | false, 200 | [radius / (su + r2), radius / (sv + r2)], 201 | uvOffsetCorner(su, sv)[i], 202 | center(x, y), 203 | ); 204 | 205 | // Edges 206 | if (i === 0 || i === 2) { 207 | // Left / Right 208 | computePlane( 209 | geometry, 210 | indices, 211 | radius, 212 | sv, 213 | roundSegments, 214 | edgeSegments, 215 | direction, 216 | pw, 217 | false, 218 | [uvOffset[0], uvScale[1]], 219 | ceil === 0 ? uvOffsetStart(su, sv) : uvOffsetEnd(su, sv), 220 | center(x, 0), 221 | ); 222 | // Bottom/Top 223 | computePlane( 224 | geometry, 225 | indices, 226 | su, 227 | radius, 228 | edgeSegments, 229 | roundSegments, 230 | direction, 231 | pw, 232 | false, 233 | [uvScale[0], uvOffset[1]], 234 | floor === 0 235 | ? [...uvOffsetStart(sv, su)].reverse() 236 | : [...uvOffsetEnd(sv, su)].reverse(), 237 | center(0, y), 238 | ); 239 | } 240 | } 241 | } 242 | 243 | const rx = widthX * 0.5; 244 | const ry = widthY * 0.5; 245 | const rz = widthZ * 0.5; 246 | 247 | for (let i = 0; i < geometry.positions.length; i += 3) { 248 | const position = [ 249 | geometry.positions[i], 250 | geometry.positions[i + 1], 251 | geometry.positions[i + 2], 252 | ]; 253 | TMP[0] = position[0]; 254 | TMP[1] = position[1]; 255 | TMP[2] = position[2]; 256 | 257 | if (position[0] < -rx) { 258 | position[0] = -rx; 259 | } else if (position[0] > rx) { 260 | position[0] = rx; 261 | } 262 | 263 | if (position[1] < -ry) { 264 | position[1] = -ry; 265 | } else if (position[1] > ry) { 266 | position[1] = ry; 267 | } 268 | 269 | if (position[2] < -rz) { 270 | position[2] = -rz; 271 | } else if (position[2] > rz) { 272 | position[2] = rz; 273 | } 274 | 275 | TMP[0] -= position[0]; 276 | TMP[1] -= position[1]; 277 | TMP[2] -= position[2]; 278 | 279 | normalize(TMP); 280 | 281 | geometry.normals[i] = TMP[0]; 282 | geometry.normals[i + 1] = TMP[1]; 283 | geometry.normals[i + 2] = TMP[2]; 284 | 285 | geometry.positions[i] = position[0] + radius * TMP[0]; 286 | geometry.positions[i + 1] = position[1] + radius * TMP[1]; 287 | geometry.positions[i + 2] = position[2] + radius * TMP[2]; 288 | } 289 | 290 | return geometry; 291 | } 292 | 293 | export default roundedCube; 294 | -------------------------------------------------------------------------------- /src/rounded-rectangle.js: -------------------------------------------------------------------------------- 1 | /** @module roundedRectangle */ 2 | import { 3 | checkArguments, 4 | computePlane, 5 | getCellsTypedArray, 6 | TMP, 7 | } from "./utils.js"; 8 | 9 | /** 10 | * @typedef {object} RoundedCubeOptions 11 | * @property {number} [sx=1] 12 | * @property {number} [sy=sx] 13 | * @property {number} [nx=1] 14 | * @property {number} [ny=nx] 15 | * @property {number} [radius=sx * 0.25] 16 | * @property {number} [roundSegments=8] 17 | * @property {number} [edgeSegments=1] 18 | */ 19 | 20 | /** 21 | * @alias module:roundedRectangle 22 | * @param {RoundedCubeOptions} [options={}] 23 | * @returns {import("../types.js").SimplicialComplex} 24 | */ 25 | function roundedRectangle({ 26 | sx = 1, 27 | sy = sx, 28 | nx = 1, 29 | ny = nx, 30 | radius = sx * 0.25, 31 | roundSegments = 8, 32 | edgeSegments = 1, 33 | } = {}) { 34 | checkArguments(arguments); 35 | 36 | const size = 37 | (nx + 1) * (ny + 1) + 38 | (roundSegments + 1) * (roundSegments + 1) * 4 + 39 | (roundSegments + 1) * (edgeSegments + 1) * 4; 40 | 41 | const geometry = { 42 | positions: new Float32Array(size * 3), 43 | normals: new Float32Array(size * 3), 44 | uvs: new Float32Array(size * 2), 45 | cells: new (getCellsTypedArray(size))( 46 | (nx * ny + 47 | roundSegments * roundSegments * 4 + 48 | roundSegments * edgeSegments * 4) * 49 | 6, 50 | ), 51 | }; 52 | 53 | const r2 = radius * 2; 54 | const widthX = sx - r2; 55 | const widthY = sy - r2; 56 | 57 | const faceSX = widthX / sx; 58 | const faceSY = widthY / sy; 59 | 60 | const radiusSX = radius / sx; 61 | const radiusSY = radius / sy; 62 | 63 | const indices = { vertex: 0, cell: 0 }; 64 | 65 | const uvOffsetCorner = (su, sv) => [ 66 | [radius / (su + r2), 0], 67 | [1 - radius / (su + r2), 0], 68 | [1, 1 - radius / (sv + r2)], 69 | [0, 1 - radius / (sv + r2)], 70 | ]; 71 | const uvOffsetStart = (_, sv) => [0, radius / (sv + r2)]; 72 | const uvOffsetEnd = (su, sv) => [1 - radius / (su + r2), radius / (sv + r2)]; 73 | 74 | const [su, sv, nu, nv, direction, pw, uvScale, uvOffset, center] = [ 75 | widthX, 76 | widthY, 77 | nx, 78 | ny, 79 | "z", 80 | 0, 81 | [faceSX, faceSY], 82 | [radiusSX, radiusSY], 83 | (x, y) => [x, y, 0], 84 | ]; 85 | 86 | // Plane face 87 | computePlane( 88 | geometry, 89 | indices, 90 | su, 91 | sv, 92 | nu, 93 | nv, 94 | direction, 95 | pw, 96 | false, 97 | uvScale, 98 | uvOffset, 99 | ); 100 | 101 | // Corner order: ccw uv-like order and L/B (0) R/T (2) 102 | // 0,1 -- 1,1 103 | // | -- | 104 | // 0,0 -- 1,0 105 | for (let i = 0; i < 4; i++) { 106 | const ceil = Math.ceil(i / 2) % 2; 107 | const floor = Math.floor(i / 2) % 2; 108 | 109 | const x = (ceil === 0 ? -1 : 1) * (su + radius) * 0.5; 110 | const y = (floor === 0 ? -1 : 1) * (sv + radius) * 0.5; 111 | 112 | // Flip for quad seams to be radial 113 | const flip = i % 2 === 0; 114 | 115 | // Corners 116 | computePlane( 117 | geometry, 118 | indices, 119 | radius, 120 | radius, 121 | roundSegments, 122 | roundSegments, 123 | flip ? "-z" : "z", 124 | pw, 125 | false, 126 | [(flip ? -1 : 1) * (radius / (su + r2)), radius / (sv + r2)], 127 | uvOffsetCorner(su, sv)[i], 128 | center(x, y), 129 | !flip, 130 | ); 131 | 132 | // Edges 133 | if (i === 0 || i === 2) { 134 | // Left / Right 135 | computePlane( 136 | geometry, 137 | indices, 138 | radius, 139 | sv, 140 | roundSegments, 141 | edgeSegments, 142 | direction, 143 | pw, 144 | false, 145 | [uvOffset[0], uvScale[1]], 146 | ceil === 0 ? uvOffsetStart(su, sv) : uvOffsetEnd(su, sv), 147 | center(x, 0), 148 | ); 149 | // Bottom/Top 150 | computePlane( 151 | geometry, 152 | indices, 153 | su, 154 | radius, 155 | edgeSegments, 156 | roundSegments, 157 | direction, 158 | pw, 159 | false, 160 | [uvScale[0], uvOffset[1]], 161 | floor === 0 162 | ? [...uvOffsetStart(sv, su)].reverse() 163 | : [...uvOffsetEnd(sv, su)].reverse(), 164 | center(0, y), 165 | ); 166 | } 167 | } 168 | 169 | const rx = widthX * 0.5; 170 | const ry = widthY * 0.5; 171 | 172 | for (let i = 0; i < geometry.positions.length; i += 3) { 173 | const position = [ 174 | geometry.positions[i], 175 | geometry.positions[i + 1], 176 | geometry.positions[i + 2], 177 | ]; 178 | TMP[0] = position[0]; 179 | TMP[1] = position[1]; 180 | TMP[2] = position[2]; 181 | 182 | let needsRounding = false; 183 | 184 | if (position[0] < -rx) { 185 | if (position[1] < -ry) { 186 | position[0] = -rx; 187 | position[1] = -ry; 188 | needsRounding = true; 189 | } else if (position[1] > ry) { 190 | position[0] = -rx; 191 | position[1] = ry; 192 | needsRounding = true; 193 | } 194 | } else if (position[0] > rx) { 195 | if (position[1] < -ry) { 196 | position[0] = rx; 197 | position[1] = -ry; 198 | needsRounding = true; 199 | } else if (position[1] > ry) { 200 | position[0] = rx; 201 | position[1] = ry; 202 | needsRounding = true; 203 | } 204 | } 205 | 206 | TMP[0] -= position[0]; 207 | TMP[1] -= position[1]; 208 | 209 | geometry.normals[i + 2] = 1; 210 | 211 | if (needsRounding) { 212 | const x = 213 | Math.sqrt(TMP[0] ** 2 + TMP[1] ** 2) / 214 | Math.max(Math.abs(TMP[0]), Math.abs(TMP[1])); 215 | 216 | geometry.positions[i] = position[0] + TMP[0] / x; 217 | geometry.positions[i + 1] = position[1] + TMP[1] / x; 218 | } 219 | } 220 | 221 | return geometry; 222 | } 223 | 224 | export default roundedRectangle; 225 | -------------------------------------------------------------------------------- /src/sphere.js: -------------------------------------------------------------------------------- 1 | /** @module sphere */ 2 | import ellipsoid from "./ellipsoid.js"; 3 | import { checkArguments } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} SphereOptions 7 | * @property {number} [radius=0.5] 8 | * @property {number} [nx=32] 9 | * @property {number} [ny=16] 10 | * @property {number} [theta=Math.PI] 11 | * @property {number} [thetaOffset=0] 12 | * @property {number} [phi=TAU] 13 | * @property {number} [phiOffset=0] 14 | */ 15 | 16 | /** 17 | * @alias module:sphere 18 | * @param {SphereOptions} [options={}] 19 | * @returns {import("../types.js").SimplicialComplex} 20 | */ 21 | function sphere({ 22 | radius = 0.5, 23 | nx = 32, 24 | ny = 16, 25 | theta, 26 | thetaOffset, 27 | phi, 28 | phiOffset, 29 | } = {}) { 30 | checkArguments(arguments); 31 | 32 | return ellipsoid({ 33 | radius, 34 | nx, 35 | ny, 36 | theta, 37 | thetaOffset, 38 | phi, 39 | phiOffset, 40 | rx: 1, 41 | ry: 1, 42 | }); 43 | } 44 | 45 | export default sphere; 46 | -------------------------------------------------------------------------------- /src/squircle.js: -------------------------------------------------------------------------------- 1 | /** @module squircle */ 2 | import ellipse from "./ellipse.js"; 3 | import { fgSquircular } from "./mappings.js"; 4 | import { checkArguments, HALF_PI, SQRT2, TAU } from "./utils.js"; 5 | 6 | /** 7 | * @typedef {object} SquircleOptions 8 | * @property {number} [sx=1] 9 | * @property {number} [sy=1] 10 | * @property {number} [radius=0.5] 11 | * @property {number} [segments=128] 12 | * @property {number} [innerSegments=16] 13 | * @property {number} [theta=TAU] 14 | * @property {number} [thetaOffset=0] 15 | * @property {boolean} [mergeCentroid=true] 16 | * @property {Function} [mapping=mappings.fgSquircular] 17 | * @property {number} [squareness=0.95] Squareness (0 < s <= 1) 18 | */ 19 | 20 | /** 21 | * Fernández-Guasti squircle 22 | * @see [Squircular Calculations – Chamberlain Fong]{@link https://arxiv.org/vc/arxiv/papers/1604/1604.02174v1.pdf} 23 | * 24 | * @alias module:squircle 25 | * @param {SquircleOptions} [options={}] 26 | * @returns {import("../types.js").SimplicialComplex} 27 | */ 28 | function squircle({ 29 | sx = 1, 30 | sy = 1, 31 | radius = 0.5, 32 | segments = 128, 33 | innerSegments = 16, 34 | theta = TAU, 35 | thetaOffset = 0, 36 | mergeCentroid = true, 37 | mapping = fgSquircular, 38 | squareness = 0.95, 39 | } = {}) { 40 | checkArguments(arguments); 41 | 42 | return ellipse({ 43 | sx, 44 | sy, 45 | radius, 46 | segments, 47 | innerSegments, 48 | theta, 49 | thetaOffset, 50 | mergeCentroid, 51 | mapping, 52 | equation: ({ rx, ry, cosTheta, sinTheta, t }) => { 53 | // Fix singularities 54 | // https://codereview.stackexchange.com/questions/233496/handling-singularities-in-squircle-parametric-equations 55 | if (t === 0 || t === TAU) { 56 | return [rx, 0]; 57 | } else if (t === HALF_PI) { 58 | return [0, ry]; 59 | } else if (t === Math.PI) { 60 | return [-rx, 0]; 61 | } else if (t === TAU - HALF_PI) { 62 | return [0, -ry]; 63 | } else { 64 | const sqrt = Math.sqrt( 65 | 1 - Math.sqrt(1 - squareness ** 2 * Math.sin(2 * t) ** 2), 66 | ); 67 | 68 | return [ 69 | ((rx * Math.sign(cosTheta)) / 70 | (squareness * SQRT2 * Math.abs(sinTheta))) * 71 | sqrt, 72 | ((ry * Math.sign(sinTheta)) / 73 | (squareness * SQRT2 * Math.abs(cosTheta))) * 74 | sqrt, 75 | ]; 76 | } 77 | }, 78 | }); 79 | } 80 | 81 | export default squircle; 82 | -------------------------------------------------------------------------------- /src/stadium.js: -------------------------------------------------------------------------------- 1 | /** @module stadium */ 2 | import roundedRectangle from "./rounded-rectangle.js"; 3 | import { checkArguments } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} StadiumOptions 7 | * @property {number} [sx=1] 8 | * @property {number} [sy=sx] 9 | * @property {number} [nx=1] 10 | * @property {number} [ny=nx] 11 | * @property {number} [roundSegments=8] 12 | * @property {number} [edgeSegments=1] 13 | */ 14 | 15 | /** 16 | * @alias module:stadium 17 | * @param {StadiumOptions} [options={}] 18 | * @returns {import("../types.js").SimplicialComplex} 19 | */ 20 | function stadium({ 21 | sx = 1, 22 | sy = 0.5, 23 | nx, 24 | ny, 25 | roundSegments, 26 | edgeSegments, 27 | } = {}) { 28 | checkArguments(arguments); 29 | 30 | return roundedRectangle({ 31 | sx, 32 | sy, 33 | nx, 34 | ny, 35 | radius: Math.min(sx, sy) * 0.5, 36 | roundSegments, 37 | edgeSegments, 38 | }); 39 | } 40 | 41 | export default stadium; 42 | -------------------------------------------------------------------------------- /src/superellipse.js: -------------------------------------------------------------------------------- 1 | /** @module superellipse */ 2 | import ellipse from "./ellipse.js"; 3 | import { lamé } from "./mappings.js"; 4 | import { checkArguments, TAU } from "./utils.js"; 5 | 6 | /** 7 | * @typedef {object} SuperellipseOptions 8 | * @property {number} [sx=1] 9 | * @property {number} [sy=0.5] 10 | * @property {number} [radius=0.5] 11 | * @property {number} [segments=32] 12 | * @property {number} [innerSegments=16] 13 | * @property {number} [theta=TAU] 14 | * @property {number} [thetaOffset=0] 15 | * @property {boolean} [mergeCentroid=true] 16 | * @property {Function} [mapping=mappings.lamé] 17 | * @property {number} [m=2] 18 | * @property {number} [n=m] 19 | */ 20 | 21 | /** 22 | * Lamé curve 23 | * See elliptical-mapping example for a few special cases 24 | * @see [Wolfram MathWorld – Superellipse]{@link https://mathworld.wolfram.com/Superellipse.html} 25 | * @see [Wikipedia – Superellipse]{@link https://en.wikipedia.org/wiki/Superellipse} 26 | * @alias module:superellipse 27 | * @param {SuperellipseOptions} [options={}] 28 | * @returns {import("../types.js").SimplicialComplex} 29 | */ 30 | function superellipse({ 31 | sx = 1, 32 | sy = 0.5, 33 | radius = 0.5, 34 | segments = 32, 35 | innerSegments = 16, 36 | theta = TAU, 37 | thetaOffset = 0, 38 | mergeCentroid = true, 39 | mapping = lamé, 40 | m = 2, 41 | n = m, 42 | } = {}) { 43 | checkArguments(arguments); 44 | 45 | return ellipse({ 46 | sx, 47 | sy, 48 | radius, 49 | segments, 50 | innerSegments, 51 | theta, 52 | thetaOffset, 53 | mergeCentroid, 54 | mapping, 55 | equation: ({ rx, ry, cosTheta, sinTheta }) => [ 56 | rx * Math.abs(cosTheta) ** (2 / m) * Math.sign(cosTheta), 57 | ry * Math.abs(sinTheta) ** (2 / n) * Math.sign(sinTheta), 58 | ], 59 | }); 60 | } 61 | 62 | export default superellipse; 63 | -------------------------------------------------------------------------------- /src/tetrahedron.js: -------------------------------------------------------------------------------- 1 | /** @module tetrahedron */ 2 | import cylinder from "./cylinder.js"; 3 | import { checkArguments } from "./utils.js"; 4 | 5 | /** 6 | * @typedef {object} TetrahedronOptions 7 | * @property {number} [radius=0.5] 8 | */ 9 | 10 | /** 11 | * @alias module:tetrahedron 12 | * @param {TetrahedronOptions} [options={}] 13 | * @returns {import("../types.js").SimplicialComplex} 14 | */ 15 | function tetrahedron({ radius = 0.5 } = {}) { 16 | checkArguments(arguments); 17 | 18 | return cylinder({ 19 | height: radius * 1.5, 20 | radius, 21 | nx: 3, 22 | ny: 1, 23 | 24 | radiusApex: 0, 25 | capSegments: 0, 26 | capApex: false, 27 | capBaseSegments: 1, 28 | }); 29 | } 30 | 31 | export default tetrahedron; 32 | -------------------------------------------------------------------------------- /src/torus.js: -------------------------------------------------------------------------------- 1 | /** @module torus */ 2 | import { 3 | checkArguments, 4 | getCellsTypedArray, 5 | normalize, 6 | TAU, 7 | TMP, 8 | } from "./utils.js"; 9 | 10 | /** 11 | * @typedef {object} TorusOptions 12 | * @property {number} [radius=0.4] 13 | * @property {number} [segments=64] 14 | * @property {number} [minorRadius=0.1] 15 | * @property {number} [minorSegments=32] 16 | * @property {number} [theta=TAU] 17 | * @property {number} [thetaOffset=0] 18 | * @property {number} [phi=TAU] 19 | * @property {number} [phiOffset=0] 20 | */ 21 | 22 | /** 23 | * @alias module:torus 24 | * @param {TorusOptions} [options={}] 25 | * @returns {import("../types.js").SimplicialComplex} 26 | */ 27 | function torus({ 28 | radius = 0.4, 29 | segments = 64, 30 | 31 | minorRadius = 0.1, 32 | minorSegments = 32, 33 | theta = TAU, 34 | thetaOffset = 0, 35 | phi = TAU, 36 | phiOffset = 0, 37 | } = {}) { 38 | checkArguments(arguments); 39 | 40 | const size = (minorSegments + 1) * (segments + 1); 41 | 42 | const positions = new Float32Array(size * 3); 43 | const normals = new Float32Array(size * 3); 44 | const uvs = new Float32Array(size * 2); 45 | const cells = new (getCellsTypedArray(size))(minorSegments * segments * 6); 46 | 47 | let vertexIndex = 0; 48 | let cellIndex = 0; 49 | 50 | for (let j = 0; j <= minorSegments; j++) { 51 | const v = j / minorSegments; 52 | 53 | for (let i = 0; i <= segments; i++, vertexIndex++) { 54 | const u = i / segments; 55 | 56 | const p = u * phi + phiOffset; 57 | const cosPhi = -Math.cos(p); 58 | const sinPhi = Math.sin(p); 59 | 60 | const t = v * theta + thetaOffset; 61 | const cosTheta = -Math.cos(t); 62 | const sinTheta = Math.sin(t); 63 | 64 | TMP[0] = (radius + minorRadius * cosTheta) * cosPhi; 65 | TMP[1] = (radius + minorRadius * cosTheta) * sinPhi; 66 | TMP[2] = minorRadius * sinTheta; 67 | 68 | positions[vertexIndex * 3] = TMP[0]; 69 | positions[vertexIndex * 3 + 1] = TMP[1]; 70 | positions[vertexIndex * 3 + 2] = TMP[2]; 71 | 72 | TMP[0] -= radius * cosPhi; 73 | TMP[1] -= radius * sinPhi; 74 | 75 | normalize(TMP); 76 | 77 | normals[vertexIndex * 3] = TMP[0]; 78 | normals[vertexIndex * 3 + 1] = TMP[1]; 79 | normals[vertexIndex * 3 + 2] = TMP[2]; 80 | 81 | uvs[vertexIndex * 2] = u; 82 | uvs[vertexIndex * 2 + 1] = v; 83 | 84 | if (j > 0 && i > 0) { 85 | const a = (segments + 1) * j + i - 1; 86 | const b = (segments + 1) * (j - 1) + i - 1; 87 | const c = (segments + 1) * (j - 1) + i; 88 | const d = (segments + 1) * j + i; 89 | 90 | cells[cellIndex] = a; 91 | cells[cellIndex + 1] = b; 92 | cells[cellIndex + 2] = d; 93 | 94 | cells[cellIndex + 3] = b; 95 | cells[cellIndex + 4] = c; 96 | cells[cellIndex + 5] = d; 97 | 98 | cellIndex += 6; 99 | } 100 | } 101 | } 102 | 103 | return { 104 | positions, 105 | normals, 106 | uvs, 107 | cells, 108 | }; 109 | } 110 | 111 | export default torus; 112 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** @module utils */ 2 | 3 | /** 4 | * Two times PI. 5 | * @constant {number} 6 | */ 7 | export const TAU = Math.PI * 2; 8 | 9 | /** 10 | * Two times PI. 11 | * @constant {number} 12 | */ 13 | export const HALF_PI = Math.PI / 2; 14 | 15 | /** 16 | * Square root of 2. 17 | * @constant {number} 18 | */ 19 | export const SQRT2 = Math.sqrt(2); 20 | 21 | /** 22 | * Normalize a vector 3. 23 | * @param {number[]} v Vector 3 array 24 | * @returns {number[]} Normalized vector 25 | */ 26 | export function normalize(v) { 27 | const l = 1 / (Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) || 1); 28 | v[0] *= l; 29 | v[1] *= l; 30 | v[2] *= l; 31 | return v; 32 | } 33 | 34 | /** 35 | * Ensure first argument passed to the primitive functions is an object 36 | * @param {...*} args 37 | */ 38 | export function checkArguments(args) { 39 | const argumentType = typeof args[0]; 40 | if (argumentType !== "object" && argumentType !== "undefined") { 41 | console.error("First argument must be an object."); 42 | } 43 | } 44 | 45 | /** 46 | * @private 47 | */ 48 | let TYPED_ARRAY_TYPE; 49 | 50 | /** 51 | * Enforce a typed array constructor for cells 52 | * @param {(Class|Class|Class)} type 53 | */ 54 | export function setTypedArrayType(type) { 55 | TYPED_ARRAY_TYPE = type; 56 | } 57 | 58 | /** 59 | * Select cells typed array from a size determined by amount of vertices. 60 | * 61 | * @param {number} size The max value expected 62 | * @returns {(Uint8Array|Uint16Array|Uint32Array)} 63 | * @see [MDN TypedArray objects]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects} 64 | */ 65 | export const getCellsTypedArray = (size) => 66 | TYPED_ARRAY_TYPE || 67 | (size <= 255 ? Uint8Array : size <= 65535 ? Uint16Array : Uint32Array); 68 | 69 | /** 70 | * @private 71 | */ 72 | export const TMP = [0, 0, 0]; 73 | 74 | /** 75 | * @private 76 | */ 77 | export const PLANE_DIRECTIONS = { 78 | z: [0, 1, 2, 1, -1, 1], 79 | "-z": [0, 1, 2, -1, -1, -1], 80 | "-x": [2, 1, 0, 1, -1, -1], 81 | x: [2, 1, 0, -1, -1, 1], 82 | y: [0, 2, 1, 1, 1, 1], 83 | "-y": [0, 2, 1, 1, -1, -1], 84 | }; 85 | 86 | /** 87 | * @private 88 | */ 89 | export function computePlane( 90 | geometry, 91 | indices, 92 | su, 93 | sv, 94 | nu, 95 | nv, 96 | direction = "z", 97 | pw = 0, 98 | quads = false, 99 | uvScale = [1, 1], 100 | uvOffset = [0, 0], 101 | center = [0, 0, 0], 102 | ccw = true, 103 | ) { 104 | const { positions, normals, uvs, cells } = geometry; 105 | const [u, v, w, flipU, flipV, normal] = PLANE_DIRECTIONS[direction]; 106 | 107 | const vertexOffset = indices.vertex; 108 | 109 | for (let j = 0; j <= nv; j++) { 110 | for (let i = 0; i <= nu; i++) { 111 | positions[indices.vertex * 3 + u] = 112 | (-su / 2 + (i * su) / nu) * flipU + center[u]; 113 | positions[indices.vertex * 3 + v] = 114 | (-sv / 2 + (j * sv) / nv) * flipV + center[v]; 115 | positions[indices.vertex * 3 + w] = pw + center[w]; 116 | 117 | normals[indices.vertex * 3 + w] = normal; 118 | 119 | uvs[indices.vertex * 2] = (i / nu) * uvScale[0] + uvOffset[0]; 120 | uvs[indices.vertex * 2 + 1] = (1 - j / nv) * uvScale[1] + uvOffset[1]; 121 | 122 | indices.vertex++; 123 | 124 | if (j < nv && i < nu) { 125 | const n = vertexOffset + j * (nu + 1) + i; 126 | if (quads) { 127 | const o = vertexOffset + (j + 1) * (nu + 1) + i; 128 | cells[indices.cell] = n; 129 | cells[indices.cell + 1] = o; 130 | cells[indices.cell + 2] = o + 1; 131 | cells[indices.cell + 3] = n + 1; 132 | } else { 133 | cells[indices.cell] = n; 134 | cells[indices.cell + (ccw ? 1 : 2)] = n + nu + 1; 135 | cells[indices.cell + (ccw ? 2 : 1)] = n + nu + 2; 136 | 137 | cells[indices.cell + 3] = n; 138 | cells[indices.cell + (ccw ? 4 : 5)] = n + nu + 2; 139 | cells[indices.cell + (ccw ? 5 : 4)] = n + 1; 140 | } 141 | indices.cell += quads ? 4 : 6; 142 | } 143 | } 144 | } 145 | 146 | return geometry; 147 | } 148 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {object} BasicSimplicialComplex Geometry definition without normals and UVs. 3 | * @property {Float32Array} positions 4 | * @property {(Uint8Array|Uint16Array|Uint32Array)} cells 5 | */ 6 | 7 | /** 8 | * @typedef {object} SimplicialComplex Geometry definition. 9 | * @property {Float32Array} positions 10 | * @property {Float32Array} normals 11 | * @property {Float32Array} uvs 12 | * @property {(Uint8Array|Uint16Array|Uint32Array)} cells 13 | */ 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /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/async-preloader.js: -------------------------------------------------------------------------------- 1 | /* Font Face Observer v3.3.1 - © Bram Stein - Damien Seguin. License: BSD-3-Clause */ function _classCallCheck(instance, Constructor) { 2 | if (!(instance instanceof Constructor)) { 3 | throw new TypeError("Cannot call a class as a function"); 4 | } 5 | } 6 | function _defineProperties(target, props) { 7 | for(var i = 0; i < props.length; i++){ 8 | var descriptor = props[i]; 9 | descriptor.enumerable = descriptor.enumerable || false; 10 | descriptor.configurable = true; 11 | if ("value" in descriptor) descriptor.writable = true; 12 | Object.defineProperty(target, descriptor.key, descriptor); 13 | } 14 | } 15 | function _createClass(Constructor, protoProps, staticProps) { 16 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 17 | if (staticProps) _defineProperties(Constructor, staticProps); 18 | return Constructor; 19 | } 20 | function _defineProperty(obj, key, value) { 21 | if (key in obj) { 22 | Object.defineProperty(obj, key, { 23 | value: value, 24 | enumerable: true, 25 | configurable: true, 26 | writable: true 27 | }); 28 | } else { 29 | obj[key] = value; 30 | } 31 | return obj; 32 | } 33 | function _objectSpread(target) { 34 | for(var i = 1; i < arguments.length; i++){ 35 | var source = arguments[i] != null ? arguments[i] : {}; 36 | var ownKeys = Object.keys(source); 37 | if (typeof Object.getOwnPropertySymbols === 'function') { 38 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) { 39 | return Object.getOwnPropertyDescriptor(source, sym).enumerable; 40 | })); 41 | } 42 | ownKeys.forEach(function(key) { 43 | _defineProperty(target, key, source[key]); 44 | }); 45 | } 46 | return target; 47 | } 48 | var styles = { 49 | maxWidth: "none", 50 | display: "inline-block", 51 | position: "absolute", 52 | height: "100%", 53 | width: "100%", 54 | overflow: "scroll", 55 | fontSize: "16px" 56 | }; 57 | var collapsibleInnerStyles = { 58 | display: "inline-block", 59 | height: "200%", 60 | width: "200%", 61 | fontSize: "16px", 62 | maxWidth: "none" 63 | }; 64 | var fontStyle = { 65 | maxWidth: "none", 66 | minWidth: "20px", 67 | minHeight: "20px", 68 | display: "inline-block", 69 | overflow: "hidden", 70 | position: "absolute", 71 | width: "auto", 72 | margin: "0", 73 | padding: "0", 74 | top: "-999px", 75 | whiteSpace: "nowrap", 76 | fontSynthesis: "none" 77 | }; 78 | var Ruler = /*#__PURE__*/ function() { 79 | /** 80 | * 81 | * @param {string} text 82 | */ function Ruler(text) { 83 | _classCallCheck(this, Ruler); 84 | this.element = document.createElement("div"); 85 | this.element.setAttribute("aria-hidden", "true"); 86 | this.element.appendChild(document.createTextNode(text)); 87 | this.collapsible = document.createElement("span"); 88 | this.expandable = document.createElement("span"); 89 | this.collapsibleInner = document.createElement("span"); 90 | this.expandableInner = document.createElement("span"); 91 | this.lastOffsetWidth = -1; 92 | Object.assign(this.collapsible.style, styles); 93 | Object.assign(this.expandable.style, styles); 94 | Object.assign(this.expandableInner.style, styles); 95 | Object.assign(this.collapsibleInner.style, collapsibleInnerStyles); 96 | this.collapsible.appendChild(this.collapsibleInner); 97 | this.expandable.appendChild(this.expandableInner); 98 | this.element.appendChild(this.collapsible); 99 | this.element.appendChild(this.expandable); 100 | } 101 | /** 102 | * @return {Element} 103 | */ _createClass(Ruler, [ 104 | { 105 | key: "getElement", 106 | value: function getElement() { 107 | return this.element; 108 | } 109 | }, 110 | { 111 | key: "setFont", 112 | value: function setFont(font) { 113 | Object.assign(this.element.style, _objectSpread({}, fontStyle, { 114 | font: font 115 | })); 116 | } 117 | }, 118 | { 119 | key: "getWidth", 120 | value: function getWidth() { 121 | return this.element.offsetWidth; 122 | } 123 | }, 124 | { 125 | key: "setWidth", 126 | value: function setWidth(width) { 127 | this.element.style.width = width + "px"; 128 | } 129 | }, 130 | { 131 | key: "reset", 132 | value: function reset() { 133 | var offsetWidth = this.getWidth(); 134 | var width = offsetWidth + 100; 135 | this.expandableInner.style.width = width + "px"; 136 | this.expandable.scrollLeft = width; 137 | this.collapsible.scrollLeft = this.collapsible.scrollWidth + 100; 138 | if (this.lastOffsetWidth !== offsetWidth) { 139 | this.lastOffsetWidth = offsetWidth; 140 | return true; 141 | } else { 142 | return false; 143 | } 144 | } 145 | }, 146 | { 147 | key: "onScroll", 148 | value: function onScroll(callback) { 149 | if (this.reset() && this.element.parentNode !== null) { 150 | callback(this.lastOffsetWidth); 151 | } 152 | } 153 | }, 154 | { 155 | key: "onResize", 156 | value: function onResize(callback) { 157 | var that = this; 158 | function onScroll() { 159 | that.onScroll(callback); 160 | } 161 | this.collapsible.addEventListener("scroll", onScroll); 162 | this.expandable.addEventListener("scroll", onScroll); 163 | this.reset(); 164 | } 165 | } 166 | ]); 167 | return Ruler; 168 | }(); 169 | function onReady(callback) { 170 | document.body ? callback() : document.addEventListener ? document.addEventListener("DOMContentLoaded", function c() { 171 | document.removeEventListener("DOMContentLoaded", c); 172 | callback(); 173 | }) : document.attachEvent("onreadystatechange", function k() { 174 | if ("interactive" == document.readyState || "complete" == document.readyState) document.detachEvent("onreadystatechange", k), callback(); 175 | }); 176 | } 177 | /** Class for FontFaceObserver. */ var FontFaceObserver = /*#__PURE__*/ function() { 178 | _createClass(FontFaceObserver, null, [ 179 | { 180 | key: "getUserAgent", 181 | /** 182 | * @type {null|boolean} 183 | */ /** 184 | * @type {null|boolean} 185 | */ /** 186 | * @type {null|boolean} 187 | */ /** 188 | * @type {null|boolean} 189 | */ /** 190 | * @type {number} 191 | */ /** 192 | * @return {string} 193 | */ value: function getUserAgent() { 194 | return window.navigator.userAgent; 195 | } 196 | }, 197 | { 198 | key: "getNavigatorVendor", 199 | value: function getNavigatorVendor() { 200 | return window.navigator.vendor; 201 | } 202 | }, 203 | { 204 | key: "hasWebKitFallbackBug", 205 | value: function hasWebKitFallbackBug() { 206 | if (FontFaceObserver.HAS_WEBKIT_FALLBACK_BUG === null) { 207 | var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(FontFaceObserver.getUserAgent()); 208 | FontFaceObserver.HAS_WEBKIT_FALLBACK_BUG = !!match && (parseInt(match[1], 10) < 536 || parseInt(match[1], 10) === 536 && parseInt(match[2], 10) <= 11); 209 | } 210 | return FontFaceObserver.HAS_WEBKIT_FALLBACK_BUG; 211 | } 212 | }, 213 | { 214 | key: "hasSafari10Bug", 215 | value: function hasSafari10Bug() { 216 | if (FontFaceObserver.HAS_SAFARI_10_BUG === null) { 217 | if (FontFaceObserver.supportsNativeFontLoading() && /Apple/.test(FontFaceObserver.getNavigatorVendor())) { 218 | var match = /AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(FontFaceObserver.getUserAgent()); 219 | FontFaceObserver.HAS_SAFARI_10_BUG = !!match && parseInt(match[1], 10) < 603; 220 | } else { 221 | FontFaceObserver.HAS_SAFARI_10_BUG = false; 222 | } 223 | } 224 | return FontFaceObserver.HAS_SAFARI_10_BUG; 225 | } 226 | }, 227 | { 228 | key: "supportsNativeFontLoading", 229 | value: function supportsNativeFontLoading() { 230 | if (FontFaceObserver.SUPPORTS_NATIVE_FONT_LOADING === null) { 231 | FontFaceObserver.SUPPORTS_NATIVE_FONT_LOADING = !!document["fonts"]; 232 | } 233 | return FontFaceObserver.SUPPORTS_NATIVE_FONT_LOADING; 234 | } 235 | }, 236 | { 237 | key: "supportStretch", 238 | value: function supportStretch() { 239 | if (FontFaceObserver.SUPPORTS_STRETCH === null) { 240 | var div = document.createElement("div"); 241 | try { 242 | div.style.font = "condensed 100px sans-serif"; 243 | } catch (e) {} 244 | FontFaceObserver.SUPPORTS_STRETCH = div.style.font !== ""; 245 | } 246 | return FontFaceObserver.SUPPORTS_STRETCH; 247 | } 248 | } 249 | ]); 250 | function FontFaceObserver(family) { 251 | var descriptors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 252 | _classCallCheck(this, FontFaceObserver); 253 | this.family = family; 254 | this.style = descriptors.style || "normal"; 255 | this.weight = descriptors.weight || "normal"; 256 | this.stretch = descriptors.stretch || "normal"; 257 | return this; 258 | } 259 | /** 260 | * @param {string=} text Optional test string to use for detecting if a font 261 | * is available. 262 | * @param {number=} timeout Optional timeout for giving up on font load 263 | * detection and rejecting the promise (defaults to 3 seconds). 264 | * @return {Promise.} 265 | */ _createClass(FontFaceObserver, [ 266 | { 267 | key: "load", 268 | value: function load(text, timeout) { 269 | var that = this; 270 | var testString = text || "BESbswy"; 271 | var timeoutId = 0; 272 | var timeoutValue = timeout || FontFaceObserver.DEFAULT_TIMEOUT; 273 | var start = that.getTime(); 274 | return new Promise(function(resolve, reject) { 275 | if (FontFaceObserver.supportsNativeFontLoading() && !FontFaceObserver.hasSafari10Bug()) { 276 | var loader = new Promise(function(resolve, reject) { 277 | var check = function check() { 278 | var now = that.getTime(); 279 | if (now - start >= timeoutValue) { 280 | reject(new Error("" + timeoutValue + "ms timeout exceeded")); 281 | } else { 282 | document.fonts.load(that.getStyle('"' + that["family"] + '"'), testString).then(function(fonts) { 283 | if (fonts.length >= 1) { 284 | resolve(); 285 | } else { 286 | setTimeout(check, 25); 287 | } 288 | }, reject); 289 | } 290 | }; 291 | check(); 292 | }); 293 | var timer = new Promise(function(resolve, reject) { 294 | timeoutId = setTimeout(function() { 295 | reject(new Error("" + timeoutValue + "ms timeout exceeded")); 296 | }, timeoutValue); 297 | }); 298 | Promise.race([ 299 | timer, 300 | loader 301 | ]).then(function() { 302 | clearTimeout(timeoutId); 303 | resolve(that); 304 | }, reject); 305 | } else { 306 | onReady(function() { 307 | var rulerA = new Ruler(testString); 308 | var rulerB = new Ruler(testString); 309 | var rulerC = new Ruler(testString); 310 | var widthA = -1; 311 | var widthB = -1; 312 | var widthC = -1; 313 | var fallbackWidthA = -1; 314 | var fallbackWidthB = -1; 315 | var fallbackWidthC = -1; 316 | var container = document.createElement("div"); 317 | /** 318 | * @private 319 | */ function removeContainer() { 320 | if (container.parentNode !== null) { 321 | container.parentNode.removeChild(container); 322 | } 323 | } 324 | /** 325 | * @private 326 | * 327 | * If metric compatible fonts are detected, one of the widths will be 328 | * -1. This is because a metric compatible font won't trigger a scroll 329 | * event. We work around this by considering a font loaded if at least 330 | * two of the widths are the same. Because we have three widths, this 331 | * still prevents false positives. 332 | * 333 | * Cases: 334 | * 1) Font loads: both a, b and c are called and have the same value. 335 | * 2) Font fails to load: resize callback is never called and timeout 336 | * happens. 337 | * 3) WebKit bug: both a, b and c are called and have the same value, 338 | * but the values are equal to one of the last resort fonts, we 339 | * ignore this and continue waiting until we get new values (or a 340 | * timeout). 341 | */ function check() { 342 | if (widthA != -1 && widthB != -1 || widthA != -1 && widthC != -1 || widthB != -1 && widthC != -1) { 343 | if (widthA == widthB || widthA == widthC || widthB == widthC) { 344 | // All values are the same, so the browser has most likely 345 | // loaded the web font 346 | if (FontFaceObserver.hasWebKitFallbackBug()) { 347 | // Except if the browser has the WebKit fallback bug, in which 348 | // case we check to see if all values are set to one of the 349 | // last resort fonts. 350 | if (widthA == fallbackWidthA && widthB == fallbackWidthA && widthC == fallbackWidthA || widthA == fallbackWidthB && widthB == fallbackWidthB && widthC == fallbackWidthB || widthA == fallbackWidthC && widthB == fallbackWidthC && widthC == fallbackWidthC) { 351 | // The width we got matches some of the known last resort 352 | // fonts, so let's assume we're dealing with the last resort 353 | // font. 354 | return; 355 | } 356 | } 357 | removeContainer(); 358 | clearTimeout(timeoutId); 359 | resolve(that); 360 | } 361 | } 362 | } // This ensures the scroll direction is correct. 363 | container.dir = "ltr"; 364 | rulerA.setFont(that.getStyle("sans-serif")); 365 | rulerB.setFont(that.getStyle("serif")); 366 | rulerC.setFont(that.getStyle("monospace")); 367 | container.appendChild(rulerA.getElement()); 368 | container.appendChild(rulerB.getElement()); 369 | container.appendChild(rulerC.getElement()); 370 | document.body.appendChild(container); 371 | fallbackWidthA = rulerA.getWidth(); 372 | fallbackWidthB = rulerB.getWidth(); 373 | fallbackWidthC = rulerC.getWidth(); 374 | function checkForTimeout() { 375 | var now = that.getTime(); 376 | if (now - start >= timeoutValue) { 377 | removeContainer(); 378 | reject(new Error("" + timeoutValue + "ms timeout exceeded")); 379 | } else { 380 | var hidden = document["hidden"]; 381 | if (hidden === true || hidden === undefined) { 382 | widthA = rulerA.getWidth(); 383 | widthB = rulerB.getWidth(); 384 | widthC = rulerC.getWidth(); 385 | check(); 386 | } 387 | timeoutId = setTimeout(checkForTimeout, 50); 388 | } 389 | } 390 | checkForTimeout(); 391 | rulerA.onResize(function(width) { 392 | widthA = width; 393 | check(); 394 | }); 395 | rulerA.setFont(that.getStyle('"' + that["family"] + '",sans-serif')); 396 | rulerB.onResize(function(width) { 397 | widthB = width; 398 | check(); 399 | }); 400 | rulerB.setFont(that.getStyle('"' + that["family"] + '",serif')); 401 | rulerC.onResize(function(width) { 402 | widthC = width; 403 | check(); 404 | }); 405 | rulerC.setFont(that.getStyle('"' + that["family"] + '",monospace')); 406 | }); 407 | } 408 | }); 409 | } 410 | }, 411 | { 412 | key: "getStyle", 413 | value: function getStyle(family) { 414 | return [ 415 | this.style, 416 | this.weight, 417 | FontFaceObserver.supportStretch() ? this.stretch : "", 418 | "100px", 419 | family 420 | ].join(" "); 421 | } 422 | }, 423 | { 424 | key: "getTime", 425 | value: function getTime() { 426 | return new Date().getTime(); 427 | } 428 | } 429 | ]); 430 | return FontFaceObserver; 431 | }(); 432 | _defineProperty(FontFaceObserver, "Ruler", Ruler); 433 | _defineProperty(FontFaceObserver, "HAS_WEBKIT_FALLBACK_BUG", null); 434 | _defineProperty(FontFaceObserver, "HAS_SAFARI_10_BUG", null); 435 | _defineProperty(FontFaceObserver, "SUPPORTS_STRETCH", null); 436 | _defineProperty(FontFaceObserver, "SUPPORTS_NATIVE_FONT_LOADING", null); 437 | _defineProperty(FontFaceObserver, "DEFAULT_TIMEOUT", 3000); 438 | 439 | /** 440 | * Keys used for the {@link AsyncPreloader.loaders} 441 | */ var LoaderKey; 442 | (function(LoaderKey) { 443 | LoaderKey["Json"] = "Json"; 444 | LoaderKey["ArrayBuffer"] = "ArrayBuffer"; 445 | LoaderKey["Blob"] = "Blob"; 446 | LoaderKey["FormData"] = "FormData"; 447 | LoaderKey["Text"] = "Text"; 448 | LoaderKey["Image"] = "Image"; 449 | LoaderKey["Video"] = "Video"; 450 | LoaderKey["Audio"] = "Audio"; 451 | LoaderKey["Xml"] = "Xml"; 452 | LoaderKey["Font"] = "Font"; 453 | })(LoaderKey || (LoaderKey = {})); 454 | 455 | const isSafari = /^((?!chrome|android).)*safari/i.test(globalThis.navigator?.userAgent) === true; 456 | /** 457 | * AsyncPreloader: assets preloader using ES2017 async/await and fetch. 458 | * 459 | * It exports an instance of itself as default so you can: 460 | * 461 | * ```js 462 | * import Preloader from "async-preloader"; 463 | * 464 | * await Preloader.loadItems([]); 465 | * ``` 466 | * 467 | * to use directly as a singleton or 468 | * 469 | * ```js 470 | * import { AsyncPreloader as Preloader } from "async-preloader"; 471 | * 472 | * const preloader = new Preloader(); 473 | * await preloader.loadItems([]); 474 | * ``` 475 | * if you need more than one instance. 476 | */ class AsyncPreloader { 477 | constructor(){ 478 | // Properties 479 | /** 480 | * Object that contains the loaded items 481 | */ this.items = new Map(); 482 | /** 483 | * Default body method to be called on the Response from fetch if no body option is specified on the LoadItem 484 | */ this.defaultBodyMethod = "blob"; 485 | /** 486 | * Default loader to use if no loader key is specified in the {@link LoadItem} or if the extension doesn't match any of the {@link AsyncPreloader.loaders} extensions 487 | */ this.defaultLoader = LoaderKey.Text; 488 | // API 489 | /** 490 | * Load the specified manifest (array of items) 491 | * 492 | * @param items Items to load 493 | * @returns Resolve when all items are loaded, reject for any error 494 | */ this.loadItems = async (items)=>{ 495 | return await Promise.all(items.map(this.loadItem)); 496 | }; 497 | /** 498 | * Load a single item 499 | * 500 | * @param item Item to load 501 | * @returns Resolve when item is loaded, reject for any error 502 | */ this.loadItem = async (item)=>{ 503 | if (typeof item === "string") item = { 504 | src: item 505 | }; 506 | const extension = AsyncPreloader.getFileExtension(item.src || ""); 507 | const loaderKey = item.loader || AsyncPreloader.getLoaderKey(extension); 508 | const loadedItem = await this[`load` + loaderKey](item); 509 | this.items.set(item.id || item.src, loadedItem); 510 | return loadedItem; 511 | }; 512 | // Special loaders 513 | /** 514 | * Load a manifest of items 515 | * 516 | * @param src Manifest src url 517 | * @param key Manifest key in the JSON object containing the array of LoadItem. 518 | * @returns 519 | */ this.loadManifest = async (src, key)=>{ 520 | if (key === void 0) key = "items"; 521 | const loadedManifest = await this.loadJson({ 522 | src 523 | }); 524 | const items = AsyncPreloader.getProp(loadedManifest, key); 525 | return await this.loadItems(items); 526 | }; 527 | // Text loaders 528 | /** 529 | * Load an item and parse the Response as text 530 | * 531 | * @param item Item to load 532 | * @returns Fulfilled value of parsed Response 533 | */ this.loadText = async (item)=>{ 534 | const response = await AsyncPreloader.fetchItem(item); 535 | return await response.text(); 536 | }; 537 | /** 538 | * Load an item and parse the Response as json 539 | * 540 | * @param item Item to load 541 | * @returns Fulfilled value of parsed Response 542 | */ this.loadJson = async (item)=>{ 543 | const response = await AsyncPreloader.fetchItem(item); 544 | return await response.json(); 545 | }; 546 | /** 547 | * Load an item and parse the Response as arrayBuffer 548 | * 549 | * @param item Item to load 550 | * @returns Fulfilled value of parsed Response 551 | */ this.loadArrayBuffer = async (item)=>{ 552 | const response = await AsyncPreloader.fetchItem(item); 553 | return await response.arrayBuffer(); 554 | }; 555 | /** 556 | * Load an item and parse the Response as blob 557 | * 558 | * @param item Item to load 559 | * @returns Fulfilled value of parsed Response 560 | */ this.loadBlob = async (item)=>{ 561 | const response = await AsyncPreloader.fetchItem(item); 562 | return await response.blob(); 563 | }; 564 | /** 565 | * Load an item and parse the Response as formData 566 | * 567 | * @param item Item to load 568 | * @returns Fulfilled value of parsed Response 569 | */ this.loadFormData = async (item)=>{ 570 | const response = await AsyncPreloader.fetchItem(item); 571 | return await response.formData(); 572 | }; 573 | // Custom loaders 574 | /** 575 | * Load an item in one of the following cases: 576 | * - item's "loader" option set as "Image" 577 | * - item's "src" option extensions matching the loaders Map 578 | * - direct call of the method 579 | * 580 | * @param item Item to load 581 | * @returns Fulfilled value with a decoded HTMLImageElement instance of or a parsed Response according to the "body" option. Defaults to a decoded HTMLImageElement. 582 | */ this.loadImage = async (item)=>{ 583 | const image = new Image(); 584 | if (item.body) { 585 | const response = await AsyncPreloader.fetchItem(item); 586 | const data = await response[item.body](); 587 | if (item.body !== "blob") return data; 588 | return await new Promise((resolve, reject)=>{ 589 | image.addEventListener("load", function load() { 590 | image.removeEventListener("load", load); 591 | resolve(image); 592 | }); 593 | image.addEventListener("error", function error(event) { 594 | image.removeEventListener("error", error); 595 | reject(event); 596 | }); 597 | image.src = URL.createObjectURL(data); 598 | }); 599 | } 600 | image.src = item.src; 601 | if (!item.noDecode) await image.decode(); 602 | return image; 603 | }; 604 | /** 605 | * Load an item in one of the following cases: 606 | * - item's "loader" option set as "Video" 607 | * - item's "src" option extensions matching the loaders Map 608 | * - direct call of the method 609 | * 610 | * @param item Item to load 611 | * @returns Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as srcObject or src. 612 | */ this.loadVideo = async (item)=>{ 613 | const response = await AsyncPreloader.fetchItem(item); 614 | const data = await response[item.body || this.defaultBodyMethod](); 615 | if (item.body) return data; 616 | const video = document.createElement("video"); 617 | return await new Promise((resolve, reject)=>{ 618 | video.addEventListener("canplaythrough", function canplaythrough() { 619 | video.removeEventListener("canplaythrough", canplaythrough); 620 | resolve(video); 621 | }); 622 | video.addEventListener("error", function error(event) { 623 | video.removeEventListener("error", error); 624 | reject(event); 625 | }); 626 | try { 627 | if (isSafari) throw ""; 628 | video.srcObject = data; 629 | } catch (error) { 630 | video.src = URL.createObjectURL(data); 631 | } 632 | video.load(); 633 | }); 634 | }; 635 | /** 636 | * Load an item in one of the following cases: 637 | * - item's "loader" option set as "Audio" 638 | * - item's "src" option extensions matching the loaders Map 639 | * - direct call of the method 640 | * 641 | * @param item Item to load 642 | * @returns Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLAudioElement with a blob as srcObject or src. 643 | */ this.loadAudio = async (item)=>{ 644 | const response = await AsyncPreloader.fetchItem(item); 645 | const data = await response[item.body || this.defaultBodyMethod](); 646 | if (item.body) return data; 647 | const audio = document.createElement("audio"); 648 | audio.autoplay = false; 649 | audio.preload = "auto"; 650 | return await new Promise((resolve, reject)=>{ 651 | audio.addEventListener("canplaythrough", function canplaythrough() { 652 | audio.removeEventListener("canplaythrough", canplaythrough); 653 | resolve(audio); 654 | }); 655 | audio.addEventListener("error", function error(event) { 656 | audio.removeEventListener("error", error); 657 | reject(event); 658 | }); 659 | try { 660 | if (isSafari) throw ""; 661 | audio.srcObject = data; 662 | } catch (error) { 663 | audio.src = URL.createObjectURL(data); 664 | } 665 | audio.load(); 666 | }); 667 | }; 668 | /** 669 | * Load an item in one of the following cases: 670 | * - item's "loader" option set as "Xml" 671 | * - item's "src" option extensions matching the loaders Map 672 | * - direct call of the method 673 | * 674 | * @param item Item to load (need a mimeType specified or default to "application/xml") 675 | * @returns Result of Response parsed as a document. 676 | */ this.loadXml = async (item)=>{ 677 | if (!item.mimeType) { 678 | const extension = AsyncPreloader.getFileExtension(item.src); 679 | item = { 680 | ...item, 681 | mimeType: AsyncPreloader.getMimeType(LoaderKey.Xml, extension) 682 | }; 683 | } 684 | if (!AsyncPreloader.domParser) { 685 | throw new Error("DomParser is not supported."); 686 | } 687 | const response = await AsyncPreloader.fetchItem(item); 688 | const data = await response.text(); 689 | return AsyncPreloader.domParser.parseFromString(data, item.mimeType); 690 | }; 691 | /** 692 | * Load a font via FontFace or check a font is loaded via FontFaceObserver instance 693 | * 694 | * @param item Item to load (id correspond to the font family name). 695 | * @returns Fulfilled value with FontFace instance or initial id if no src provided. 696 | */ this.loadFont = async (item)=>{ 697 | const fontName = item.id || AsyncPreloader.getFileName(item.src); 698 | const options = item.fontOptions || {}; 699 | if (!item.src) { 700 | const font = new FontFaceObserver(fontName, options.variant || {}); 701 | await font.load(options.testString, options.timeout); 702 | return fontName; 703 | } 704 | const source = item.body === "arrayBuffer" ? await this.loadArrayBuffer({ 705 | src: item.src 706 | }) : `url(${item.src})`; 707 | const font = new FontFace(fontName, source, options.descriptors); 708 | return await font.load().then((font)=>{ 709 | document.fonts.add(font); 710 | return font; 711 | }); 712 | }; 713 | } 714 | // Utils 715 | /** 716 | * Fetch wrapper for LoadItem 717 | * 718 | * @param item Item to fetch 719 | * @returns Fetch response 720 | */ static fetchItem(item) { 721 | return fetch(item.src, item.options || {}); 722 | } 723 | /** 724 | * Get an object property by its path in the form 'a[0].b.c' or ['a', '0', 'b', 'c']. 725 | * Similar to [lodash.get](https://lodash.com/docs/4.17.5#get). 726 | * 727 | * @param object Object with nested properties 728 | * @param path Path to the desired property 729 | * @returns The returned object property 730 | */ static getProp(object, path) { 731 | const p = Array.isArray(path) ? path : path.split(".").filter((index)=>index.length); 732 | if (!p.length) return object; 733 | return AsyncPreloader.getProp(object[p.shift()], p); 734 | } 735 | /** 736 | * Get file extension 737 | * 738 | * @param path 739 | * @returns 740 | */ static getFileExtension(path) { 741 | return (path?.match(/[^\\/]\.([^.\\/]+)$/) || [ 742 | null 743 | ]).pop(); 744 | } 745 | /** 746 | * Get file base name 747 | * 748 | * @param path 749 | * @returns 750 | */ static getFileBaseName(path) { 751 | return path.split(/[\\/]/).pop(); 752 | } 753 | /** 754 | * Get file name 755 | * 756 | * @param path 757 | * @returns 758 | */ static getFileName(path) { 759 | return AsyncPreloader.getFileBaseName(path).split(".").slice(0, -1).join(".") || path; 760 | } 761 | /** 762 | * Retrieve loader key from extension (when the loader option isn't specified in the LoadItem) 763 | * 764 | * @param extension 765 | * @returns 766 | */ static getLoaderKey(extension) { 767 | const loader = Array.from(AsyncPreloader.loaders).find((loader)=>loader[1].extensions.includes(extension)); 768 | return loader ? loader[0] : LoaderKey.Text; 769 | } 770 | /** 771 | * Retrieve mime type from extension 772 | * 773 | * @param loaderKey 774 | * @param extension 775 | * @returns 776 | */ static getMimeType(loaderKey, extension) { 777 | const loader = AsyncPreloader.loaders.get(loaderKey); 778 | return loader.mimeType[extension] || loader.defaultMimeType; 779 | } 780 | } 781 | /** 782 | * Loader types and the extensions they handle 783 | * 784 | * Allows the omission of the loader key in a {@link LoadItem.loader} for some generic extensions 785 | */ AsyncPreloader.loaders = new Map().set(LoaderKey.Text, { 786 | extensions: [ 787 | "txt" 788 | ] 789 | }).set(LoaderKey.Json, { 790 | extensions: [ 791 | "json" 792 | ] 793 | }).set(LoaderKey.Image, { 794 | extensions: [ 795 | "jpeg", 796 | "jpg", 797 | "gif", 798 | "png", 799 | "webp" 800 | ] 801 | }).set(LoaderKey.Video, { 802 | extensions: [ 803 | "webm", 804 | "ogg", 805 | "mp4" 806 | ] 807 | }).set(LoaderKey.Audio, { 808 | extensions: [ 809 | "webm", 810 | "ogg", 811 | "mp3", 812 | "wav", 813 | "flac" 814 | ] 815 | }).set(LoaderKey.Xml, { 816 | extensions: [ 817 | "xml", 818 | "svg", 819 | "html" 820 | ], 821 | mimeType: { 822 | xml: "text/xml", 823 | svg: "image/svg+xml", 824 | html: "text/html" 825 | }, 826 | defaultMimeType: "text/xml" 827 | }).set(LoaderKey.Font, { 828 | extensions: [ 829 | "woff2", 830 | "woff", 831 | "ttf", 832 | "otf", 833 | "eot" 834 | ] 835 | }); 836 | /** 837 | * DOMParser instance for the XML loader 838 | */ AsyncPreloader.domParser = typeof DOMParser !== "undefined" && new DOMParser(); 839 | const AsyncPreloaderInstance = new AsyncPreloader(); 840 | 841 | export { AsyncPreloader, AsyncPreloaderInstance as default }; 842 | -------------------------------------------------------------------------------- /web_modules/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "async-preloader": "./async-preloader.js", 4 | "cameras": "./cameras.js", 5 | "es-module-shims": "./es-module-shims.js", 6 | "es-module-shims/debug": "./es-module-shims/debug.js", 7 | "es-module-shims/wasm": "./es-module-shims/wasm.js", 8 | "es-module-shims/typescript-transform": "./es-module-shims/typescript-transform.js", 9 | "gl-matrix": "./gl-matrix.js", 10 | "pex-context": "./pex-context.js", 11 | "pex-geom": "./pex-geom.js", 12 | "tweakpane": "./tweakpane.js", 13 | "typed-array-interleave": "./typed-array-interleave.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web_modules/pex-geom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints a vector to a string. 3 | * @param {import("./types.js").vec2} a 4 | * @param {number} [precision=4] 5 | * @returns {string} 6 | */ function toString$5(a, precision) { 7 | if (precision === void 0) precision = 4; 8 | const scale = 10 ** precision; 9 | // prettier-ignore 10 | return `[${Math.floor(a[0] * scale) / scale}, ${Math.floor(a[1] * scale) / scale}]`; 11 | } 12 | 13 | /** @module vec3 */ /** 14 | * Returns a new vec3 at 0, 0, 0. 15 | * @returns {import("./types.js").vec3} 16 | */ function create$4() { 17 | return [ 18 | 0, 19 | 0, 20 | 0 21 | ]; 22 | } 23 | /** 24 | * Sets a vector to another vector. 25 | * @param {import("./types.js").vec3} a 26 | * @param {import("./types.js").vec3} b 27 | * @returns {import("./types.js").vec3} 28 | */ function set$2(a, b) { 29 | a[0] = b[0]; 30 | a[1] = b[1]; 31 | a[2] = b[2]; 32 | return a; 33 | } 34 | /** 35 | * Adds a vector to another. 36 | * @param {import("./types.js").vec3} a 37 | * @param {import("./types.js").vec3} b 38 | * @returns {import("./types.js").vec3} 39 | */ function add(a, b) { 40 | a[0] += b[0]; 41 | a[1] += b[1]; 42 | a[2] += b[2]; 43 | return a; 44 | } 45 | /** 46 | * Subtracts a vector from another. 47 | * @param {import("./types.js").vec3} a 48 | * @param {import("./types.js").vec3} b 49 | * @returns {import("./types.js").vec3} 50 | */ function sub(a, b) { 51 | a[0] -= b[0]; 52 | a[1] -= b[1]; 53 | a[2] -= b[2]; 54 | return a; 55 | } 56 | /** 57 | * Scales a vector by a number. 58 | * @param {import("./types.js").vec3} a 59 | * @param {number} s 60 | * @returns {import("./types.js").vec3} 61 | */ function scale$1(a, s) { 62 | a[0] *= s; 63 | a[1] *= s; 64 | a[2] *= s; 65 | return a; 66 | } 67 | /** 68 | * Calculates the dot product of two vectors. 69 | * @param {import("./types.js").vec3} a 70 | * @param {import("./types.js").vec3} b 71 | * @returns {number} 72 | */ function dot(a, b) { 73 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 74 | } 75 | /** 76 | * Calculates the cross product of two vectors. 77 | * @param {import("./types.js").vec3} a 78 | * @param {import("./types.js").vec3} b 79 | * @returns {import("./types.js").vec3} 80 | */ function cross(a, b) { 81 | const x = a[0]; 82 | const y = a[1]; 83 | const z = a[2]; 84 | const vx = b[0]; 85 | const vy = b[1]; 86 | const vz = b[2]; 87 | a[0] = y * vz - vy * z; 88 | a[1] = z * vx - vz * x; 89 | a[2] = x * vy - vx * y; 90 | return a; 91 | } 92 | /** 93 | * Calculates the length of a vector. 94 | * @param {import("./types.js").vec3} a 95 | * @returns {number} 96 | */ function length(a) { 97 | const x = a[0]; 98 | const y = a[1]; 99 | const z = a[2]; 100 | return Math.sqrt(x * x + y * y + z * z); 101 | } 102 | /** 103 | * Normalises a vector. 104 | * @param {import("./types.js").vec3} a 105 | * @returns {import("./types.js").vec3} 106 | */ function normalize(a) { 107 | const x = a[0]; 108 | const y = a[1]; 109 | const z = a[2]; 110 | let l = Math.sqrt(x * x + y * y + z * z); 111 | l = 1 / (l || 1); 112 | a[0] *= l; 113 | a[1] *= l; 114 | a[2] *= l; 115 | return a; 116 | } 117 | /** 118 | * Prints a vector to a string. 119 | * @param {import("./types.js").vec3} a 120 | * @param {number} [precision=4] 121 | * @returns {string} 122 | */ function toString$4(a, precision) { 123 | if (precision === void 0) precision = 4; 124 | const scale = 10 ** precision; 125 | // prettier-ignore 126 | return `[${Math.floor(a[0] * scale) / scale}, ${Math.floor(a[1] * scale) / scale}, ${Math.floor(a[2] * scale) / scale}]`; 127 | } 128 | 129 | /** 130 | * Sets a vector components. 131 | * @param {import("./types.js").avec3} a 132 | * @param {number} i 133 | * @param {number} x 134 | * @param {number} y 135 | * @param {number} z 136 | */ function set3(a, i, x, y, z) { 137 | a[i * 3] = x; 138 | a[i * 3 + 1] = y; 139 | a[i * 3 + 2] = z; 140 | } 141 | 142 | /** 143 | * Creates a new bounding box. 144 | * @returns {import("./types.js").aabb} 145 | */ function create$3() { 146 | // [min, max] 147 | return [ 148 | [ 149 | Infinity, 150 | Infinity, 151 | Infinity 152 | ], 153 | [ 154 | -Infinity, 155 | -Infinity, 156 | -Infinity 157 | ] 158 | ]; 159 | } 160 | /** 161 | * Reset a bounding box. 162 | * @param {import("./types.js").aabb} a 163 | * @returns {import("./types.js").rect} 164 | */ function empty$1(a) { 165 | a[0][0] = Infinity; 166 | a[0][1] = Infinity; 167 | a[0][2] = Infinity; 168 | a[1][0] = -Infinity; 169 | a[1][1] = -Infinity; 170 | a[1][2] = -Infinity; 171 | return a; 172 | } 173 | /** 174 | * Copies a bounding box. 175 | * @param {import("./types.js").aabb} a 176 | * @returns {import("./types.js").aabb} 177 | */ function copy$1(a) { 178 | return [ 179 | a[0].slice(), 180 | a[1].slice() 181 | ]; 182 | } 183 | /** 184 | * Sets a bounding box to another. 185 | * @param {import("./types.js").aabb} a 186 | * @param {import("./types.js").aabb} b 187 | * @returns {import("./types.js").aabb} 188 | */ function set$1(a, b) { 189 | a[0][0] = b[0][0]; 190 | a[0][1] = b[0][1]; 191 | a[0][2] = b[0][2]; 192 | a[1][0] = b[1][0]; 193 | a[1][1] = b[1][1]; 194 | a[1][2] = b[1][2]; 195 | return a; 196 | } 197 | /** 198 | * Checks if a bounding box is empty. 199 | * @param {import("./types.js").aabb} a 200 | * @returns {boolean} 201 | */ function isEmpty$1(a) { 202 | return a[0][0] > a[1][0] || a[0][1] > a[1][1] || a[0][2] > a[1][2]; 203 | } 204 | /** 205 | * Updates a bounding box from a list of points. 206 | * @param {import("./types.js").aabb} a 207 | * @param {import("./types.js").vec3[] | import("./types.js").TypedArray} points 208 | * @returns {import("./types.js").aabb} 209 | */ function fromPoints$1(a, points) { 210 | const isFlatArray = !points[0]?.length; 211 | const l = points.length / (isFlatArray ? 3 : 1); 212 | for(let i = 0; i < l; i++){ 213 | if (isFlatArray) { 214 | includePoint$1(a, points, i * 3); 215 | } else { 216 | includePoint$1(a, points[i]); 217 | } 218 | } 219 | return a; 220 | } 221 | /** 222 | * Returns a list of 8 points from a bounding box. 223 | * @param {import("./types.js").aabb} a 224 | * @param {import("./types.js").vec3[]} [points] 225 | * @returns {import("./types.js").vec3[]} 226 | */ function getCorners$1(a, points) { 227 | if (points === void 0) points = Array.from({ 228 | length: 8 229 | }, ()=>[]); 230 | set3(points[0], 0, a[0][0], a[0][1], a[0][2]); 231 | set3(points[1], 0, a[1][0], a[0][1], a[0][2]); 232 | set3(points[2], 0, a[1][0], a[0][1], a[1][2]); 233 | set3(points[3], 0, a[0][0], a[0][1], a[1][2]); 234 | set3(points[4], 0, a[0][0], a[1][1], a[0][2]); 235 | set3(points[5], 0, a[1][0], a[1][1], a[0][2]); 236 | set3(points[6], 0, a[1][0], a[1][1], a[1][2]); 237 | set3(points[7], 0, a[0][0], a[1][1], a[1][2]); 238 | return points; 239 | } 240 | /** 241 | * Returns the center of a bounding box. 242 | * @param {import("./types.js").aabb} a 243 | * @param {import("./types.js").vec3} out 244 | * @returns {import("./types.js").vec3} 245 | */ function center$1(a, out) { 246 | if (out === void 0) out = [ 247 | 0, 248 | 0, 249 | 0 250 | ]; 251 | out[0] = (a[0][0] + a[1][0]) / 2; 252 | out[1] = (a[0][1] + a[1][1]) / 2; 253 | out[2] = (a[0][2] + a[1][2]) / 2; 254 | return out; 255 | } 256 | /** 257 | * Returns the size of a bounding box. 258 | * @param {import("./types.js").aabb} a 259 | * @param {import("./types.js").vec3} out 260 | * @returns {import("./types.js").vec3} 261 | */ function size$1(a, out) { 262 | if (out === void 0) out = [ 263 | 0, 264 | 0, 265 | 0 266 | ]; 267 | out[0] = Math.abs(a[1][0] - a[0][0]); 268 | out[1] = Math.abs(a[1][1] - a[0][1]); 269 | out[2] = Math.abs(a[1][2] - a[0][2]); 270 | return out; 271 | } 272 | /** 273 | * Checks if a point is inside a bounding box. 274 | * @param {import("./types.js").aabb} a 275 | * @param {import("./types.js").vec3} p 276 | * @returns {boolean} 277 | */ function containsPoint$1(a, param) { 278 | let [x, y, z] = param; 279 | 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]; 280 | } 281 | /** 282 | * Includes a bounding box in another. 283 | * @param {import("./types.js").aabb} a 284 | * @param {import("./types.js").aabb} b 285 | * @returns {import("./types.js").aabb} 286 | */ function includeAABB(a, b) { 287 | if (isEmpty$1(a)) { 288 | set$1(a, b); 289 | } else if (isEmpty$1(b)) ; else { 290 | a[0][0] = Math.min(a[0][0], b[0][0]); 291 | a[0][1] = Math.min(a[0][1], b[0][1]); 292 | a[0][2] = Math.min(a[0][2], b[0][2]); 293 | a[1][0] = Math.max(a[1][0], b[1][0]); 294 | a[1][1] = Math.max(a[1][1], b[1][1]); 295 | a[1][2] = Math.max(a[1][2], b[1][2]); 296 | } 297 | return a; 298 | } 299 | /** 300 | * Includes a point in a bounding box. 301 | * @param {import("./types.js").aabb} a 302 | * @param {import("./types.js").vec3} p 303 | * @param {number} [i=0] offset in the point array 304 | * @returns {import("./types.js").vec3} 305 | */ function includePoint$1(a, p, i) { 306 | if (i === void 0) i = 0; 307 | a[0][0] = Math.min(a[0][0], p[i + 0]); 308 | a[0][1] = Math.min(a[0][1], p[i + 1]); 309 | a[0][2] = Math.min(a[0][2], p[i + 2]); 310 | a[1][0] = Math.max(a[1][0], p[i + 0]); 311 | a[1][1] = Math.max(a[1][1], p[i + 1]); 312 | a[1][2] = Math.max(a[1][2], p[i + 2]); 313 | return a; 314 | } 315 | /** 316 | * Prints a bounding box to a string. 317 | * @param {import("./types.js").aabb} a 318 | * @param {number} [precision=4] 319 | * @returns {string} 320 | */ function toString$3(a, precision) { 321 | if (precision === void 0) precision = 4; 322 | // prettier-ignore 323 | return `[${toString$4(a[0], precision)}, ${toString$4(a[1], precision)}]`; 324 | } 325 | 326 | var aabb = /*#__PURE__*/Object.freeze({ 327 | __proto__: null, 328 | center: center$1, 329 | containsPoint: containsPoint$1, 330 | copy: copy$1, 331 | create: create$3, 332 | empty: empty$1, 333 | fromPoints: fromPoints$1, 334 | getCorners: getCorners$1, 335 | includeAABB: includeAABB, 336 | includePoint: includePoint$1, 337 | isEmpty: isEmpty$1, 338 | set: set$1, 339 | size: size$1, 340 | toString: toString$3 341 | }); 342 | 343 | /** 344 | * Enum for different side values 345 | * @readonly 346 | * @enum {number} 347 | */ const Side = Object.freeze({ 348 | OnPlane: 0, 349 | Same: -1, 350 | Opposite: 1 351 | }); 352 | const TEMP_0$1 = create$4(); 353 | /** 354 | * Creates a new plane 355 | * @returns {import("./types.js").plane} 356 | */ function create$2() { 357 | return [ 358 | [ 359 | 0, 360 | 0, 361 | 0 362 | ], 363 | [ 364 | 0, 365 | 1, 366 | 0 367 | ] 368 | ]; 369 | } 370 | /** 371 | * Returns on which side a point is. 372 | * @param {import("./types.js").plane} plane 373 | * @param {import("./types.js").vec3} point 374 | * @returns {number} 375 | */ function side(param, point) { 376 | let [planePoint, planeNormal] = param; 377 | set$2(TEMP_0$1, planePoint); 378 | sub(TEMP_0$1, point); 379 | normalize(TEMP_0$1); 380 | const dot$1 = dot(TEMP_0$1, planeNormal); 381 | if (dot$1 > 0) return Side.Opposite; 382 | if (dot$1 < 0) return Side.Same; 383 | return Side.OnPlane; 384 | } 385 | /** 386 | * Prints a plane to a string. 387 | * @param {import("./types.js").plane} a 388 | * @param {number} [precision=4] 389 | * @returns {string} 390 | */ function toString$2(a, precision) { 391 | if (precision === void 0) precision = 4; 392 | // prettier-ignore 393 | return `[${toString$4(a[0], precision)}, ${toString$4(a[1], precision)}]`; 394 | } 395 | 396 | var plane = /*#__PURE__*/Object.freeze({ 397 | __proto__: null, 398 | Side: Side, 399 | create: create$2, 400 | side: side, 401 | toString: toString$2 402 | }); 403 | 404 | /** 405 | * Enum for different intersections values 406 | * @readonly 407 | * @enum {number} 408 | */ const Intersections = Object.freeze({ 409 | Intersect: 1, 410 | NoIntersect: 0, 411 | SamePlane: -1, 412 | Parallel: -2, 413 | TriangleDegenerate: -2 414 | }); 415 | const TEMP_0 = create$4(); 416 | const TEMP_1 = create$4(); 417 | const TEMP_2 = create$4(); 418 | const TEMP_3 = create$4(); 419 | const TEMP_4 = create$4(); 420 | const TEMP_5 = create$4(); 421 | const TEMP_6 = create$4(); 422 | const TEMP_7 = create$4(); 423 | const EPSILON = 1e-6; 424 | /** 425 | * Creates a new ray 426 | * @returns {import("./types.js").ray} 427 | */ function create$1() { 428 | return [ 429 | [ 430 | 0, 431 | 0, 432 | 0 433 | ], 434 | [ 435 | 0, 436 | 0, 437 | 1 438 | ] 439 | ]; 440 | } 441 | /** 442 | * Determines if a ray intersect a plane and set intersection point 443 | * @see {@link https://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm} 444 | * @param {import("./types.js").ray} ray 445 | * @param {import("./types.js").plane} plane 446 | * @param {import("./types.js").vec3} out 447 | * @returns {number} 448 | */ function hitTestPlane(param, param1, out) { 449 | let [origin, direction] = param; 450 | let [point, normal] = param1; 451 | if (out === void 0) out = create$4(); 452 | set$2(TEMP_0, origin); 453 | set$2(TEMP_1, direction); 454 | const dotDirectionNormal = dot(TEMP_1, normal); 455 | if (dotDirectionNormal === 0) return Intersections.SamePlane; 456 | set$2(TEMP_2, point); 457 | const t = dot(sub(TEMP_2, TEMP_0), normal) / dotDirectionNormal; 458 | if (t < 0) return Intersections.Parallel; 459 | set$2(out, add(TEMP_0, scale$1(TEMP_1, t))); 460 | return Intersections.Intersect; 461 | } 462 | /** 463 | * Determines if a ray intersect a triangle and set intersection point 464 | * @see {@link http://geomalgorithms.com/a06-_intersect-2.html#intersect3D_RayTriangle()} 465 | * @param {import("./types.js").ray} ray 466 | * @param {import("./types.js").triangle} triangle 467 | * @param {import("./types.js").vec3} out 468 | * @returns {number} 469 | */ function hitTestTriangle(param, param1, out) { 470 | let [origin, direction] = param; 471 | let [p0, p1, p2] = param1; 472 | if (out === void 0) out = create$4(); 473 | // get triangle edge vectors and plane normal 474 | const u = sub(set$2(TEMP_0, p1), p0); 475 | const v = sub(set$2(TEMP_1, p2), p0); 476 | const n = cross(set$2(TEMP_2, u), v); 477 | if (length(n) < EPSILON) return Intersections.TriangleDegenerate; 478 | // ray vectors 479 | const w0 = sub(set$2(TEMP_3, origin), p0); 480 | // params to calc ray-plane intersect 481 | const a = -dot(n, w0); 482 | const b = dot(n, direction); 483 | if (Math.abs(b) < EPSILON) { 484 | if (a === 0) return Intersections.SamePlane; 485 | return Intersections.NoIntersect; 486 | } 487 | // get intersect point of ray with triangle plane 488 | const r = a / b; 489 | // ray goes away from triangle 490 | if (r < -1e-6) return Intersections.NoIntersect; 491 | // for a segment, also test if (r > 1.0) => no intersect 492 | // intersect point of ray and plane 493 | const I = add(set$2(TEMP_4, origin), scale$1(set$2(TEMP_5, direction), r)); 494 | const uu = dot(u, u); 495 | const uv = dot(u, v); 496 | const vv = dot(v, v); 497 | const w = sub(set$2(TEMP_6, I), p0); 498 | const wu = dot(w, u); 499 | const wv = dot(w, v); 500 | const D = uv * uv - uu * vv; 501 | // get and test parametric coords 502 | const s = (uv * wv - vv * wu) / D; 503 | if (s < -1e-6 || s > 1.0 + EPSILON) return Intersections.NoIntersect; 504 | const t = (uv * wu - uu * wv) / D; 505 | if (t < -1e-6 || s + t > 1.0 + EPSILON) return Intersections.NoIntersect; 506 | set$2(out, u); 507 | scale$1(out, s); 508 | add(out, scale$1(set$2(TEMP_7, v), t)); 509 | add(out, p0); 510 | return Intersections.Intersect; 511 | } 512 | /** 513 | * Determines if a ray intersect an AABB bounding box 514 | * @see {@link http://gamedev.stackexchange.com/questions/18436/most-efficient-aabb-vs-ray-collision-algorithms} 515 | * @param {import("./types.js").ray} ray 516 | * @param {import("./types.js").aabb} aabb 517 | * @returns {boolean} 518 | */ function hitTestAABB(param, aabb) { 519 | let [origin, direction] = param; 520 | const dirFracx = 1.0 / direction[0]; 521 | const dirFracy = 1.0 / direction[1]; 522 | const dirFracz = 1.0 / direction[2]; 523 | const min = aabb[0]; 524 | const max = aabb[1]; 525 | const minx = min[0]; 526 | const miny = min[1]; 527 | const minz = min[2]; 528 | const maxx = max[0]; 529 | const maxy = max[1]; 530 | const maxz = max[2]; 531 | const t1 = (minx - origin[0]) * dirFracx; 532 | const t2 = (maxx - origin[0]) * dirFracx; 533 | const t3 = (miny - origin[1]) * dirFracy; 534 | const t4 = (maxy - origin[1]) * dirFracy; 535 | const t5 = (minz - origin[2]) * dirFracz; 536 | const t6 = (maxz - origin[2]) * dirFracz; 537 | const tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); 538 | const tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); 539 | return !(tmax < 0 || tmin > tmax); 540 | } 541 | /** 542 | * Alias for {@link hitTestAABB} 543 | * @function 544 | */ const intersectsAABB = hitTestAABB; 545 | /** 546 | * Prints a plane to a string. 547 | * @param {import("./types.js").ray} a 548 | * @param {number} [precision=4] 549 | * @returns {string} 550 | */ function toString$1(a, precision) { 551 | if (precision === void 0) precision = 4; 552 | // prettier-ignore 553 | return `[${toString$4(a[0], precision)}, ${toString$4(a[1], precision)}]`; 554 | } 555 | 556 | var ray = /*#__PURE__*/Object.freeze({ 557 | __proto__: null, 558 | Intersections: Intersections, 559 | create: create$1, 560 | hitTestAABB: hitTestAABB, 561 | hitTestPlane: hitTestPlane, 562 | hitTestTriangle: hitTestTriangle, 563 | intersectsAABB: intersectsAABB, 564 | toString: toString$1 565 | }); 566 | 567 | /** 568 | * Creates a new rectangle. 569 | * @returns {import("./types.js").rect} 570 | */ function create() { 571 | return [ 572 | [ 573 | Infinity, 574 | Infinity 575 | ], 576 | [ 577 | -Infinity, 578 | -Infinity 579 | ] 580 | ]; 581 | } 582 | /** 583 | * Reset a rectangle. 584 | * @param {import("./types.js").rect} a 585 | * @returns {import("./types.js").rect} 586 | */ function empty(a) { 587 | a[0][0] = a[0][1] = Infinity; 588 | a[1][0] = a[1][1] = -Infinity; 589 | return a; 590 | } 591 | /** 592 | * Copies a rectangle. 593 | * @param {import("./types.js").rect} a 594 | * @returns {import("./types.js").rect} 595 | */ function copy(a) { 596 | return [ 597 | a[0].slice(), 598 | a[1].slice() 599 | ]; 600 | } 601 | /** 602 | * Sets a rectangle to another. 603 | * @param {import("./types.js").rect} a 604 | * @param {import("./types.js").rect} b 605 | * @returns {import("./types.js").rect} 606 | */ function set(a, b) { 607 | a[0][0] = b[0][0]; 608 | a[0][1] = b[0][1]; 609 | a[1][0] = b[1][0]; 610 | a[1][1] = b[1][1]; 611 | return a; 612 | } 613 | /** 614 | * Checks if a rectangle is empty. 615 | * @param {import("./types.js").rect} a 616 | * @returns {boolean} 617 | */ function isEmpty(a) { 618 | return a[0][0] > a[1][0] || a[0][1] > a[1][1]; 619 | } 620 | /** 621 | * Updates a rectangle from a list of points. 622 | * @param {import("./types.js").rect} a 623 | * @param {import("./types.js").vec2[] | import("./types.js").TypedArray} points 624 | * @returns {import("./types.js").rect} 625 | */ function fromPoints(a, points) { 626 | const isTypedArray = !Array.isArray(points); 627 | for(let i = 0; i < points.length / (isTypedArray ? 2 : 1); i++){ 628 | includePoint(a, isTypedArray ? points.slice(i * 2) : points[i]); 629 | } 630 | return a; 631 | } 632 | /** 633 | * Returns a list of 4 points from a rectangle. 634 | * @param {import("./types.js").rect} a 635 | * @param {import("./types.js").vec2[]} points 636 | * @returns {import("./types.js").vec2[]} 637 | */ function getCorners(a, points) { 638 | if (points === void 0) points = []; 639 | points[0] = a[0].slice(); 640 | points[1] = [ 641 | a[0][1], 642 | a[1][0] 643 | ]; 644 | points[2] = a[1].slice(); 645 | points[3] = [ 646 | a[1][0], 647 | a[0][1] 648 | ]; 649 | return points; 650 | } 651 | /** 652 | * Scales a rectangle. 653 | * @param {import("./types.js").rect} a 654 | * @param {number} n 655 | * @returns {import("./types.js").rect} 656 | */ function scale(a, n) { 657 | a[0][0] *= n; 658 | a[0][1] *= n; 659 | a[1][0] *= n; 660 | a[1][1] *= n; 661 | return a; 662 | } 663 | /** 664 | * Sets the size of a rectangle using width and height. 665 | * @param {import("./types.js").rect} a 666 | * @param {import("./types.js").vec2} size 667 | * @returns {import("./types.js").rect} 668 | */ function setSize(a, size) { 669 | a[1][0] = a[0][0] + size[0]; 670 | a[1][1] = a[0][1] + size[1]; 671 | return a; 672 | } 673 | /** 674 | * Returns the size of a rectangle. 675 | * @param {import("./types.js").rect} a 676 | * @param {import("./types.js").vec2} out 677 | * @returns {import("./types.js").vec2} 678 | */ function size(a, out) { 679 | if (out === void 0) out = []; 680 | out[0] = width(a); 681 | out[1] = height(a); 682 | return out; 683 | } 684 | /** 685 | * Returns the width of a rectangle. 686 | * @param {import("./types.js").rect} a 687 | * @returns {number} 688 | */ function width(a) { 689 | return a[1][0] - a[0][0]; 690 | } 691 | /** 692 | * Returns the height of a rectangle. 693 | * @param {import("./types.js").rect} a 694 | * @returns {number} 695 | */ function height(a) { 696 | return a[1][1] - a[0][1]; 697 | } 698 | /** 699 | * Returns the aspect ratio of a rectangle. 700 | * @param {import("./types.js").rect} a 701 | * @returns {number} 702 | */ function aspectRatio(a) { 703 | return width(a) / height(a); 704 | } 705 | /** 706 | * Sets the position of a rectangle. 707 | * @param {import("./types.js").rect} a 708 | * @param {import("./types.js").vec2} p 709 | * @returns {import("./types.js").rect} 710 | */ function setPosition(a, param) { 711 | let [x, y] = param; 712 | const w = width(a); 713 | const h = height(a); 714 | a[0][0] = x; 715 | a[0][1] = y; 716 | a[1][0] = x + w; 717 | a[1][1] = y + h; 718 | return a; 719 | } 720 | /** 721 | * Returns the center of a rectangle. 722 | * @param {import("./types.js").rect} a 723 | * @param {import("./types.js").vec2} out 724 | * @returns {import("./types.js").rect} 725 | */ function center(a, out) { 726 | if (out === void 0) out = []; 727 | out[0] = a[0][0] + width(a) * 0.5; 728 | out[1] = a[0][1] + height(a) * 0.5; 729 | return out; 730 | } 731 | /** 732 | * Checks if a point is inside a rectangle. 733 | * @param {import("./types.js").rect} a 734 | * @param {import("./types.js").vec2} p 735 | * @returns {boolean} 736 | */ function containsPoint(a, param) { 737 | let [x, y] = param; 738 | return x >= a[0][0] && x <= a[1][0] && y >= a[0][1] && y <= a[1][1]; 739 | } 740 | /** 741 | * Checks if a rectangle is inside another rectangle. 742 | * @param {import("./types.js").rect} a 743 | * @param {import("./types.js").rect} b 744 | * @returns {boolean} 745 | */ function containsRect(a, b) { 746 | return containsPoint(a, b[0]) && containsPoint(a, b[1]); 747 | } 748 | /** 749 | * Includes a point in a rectangle. 750 | * @param {import("./types.js").rect} a 751 | * @param {import("./types.js").vec2} p 752 | * @returns {import("./types.js").rect} 753 | */ function includePoint(a, param) { 754 | let [x, y] = param; 755 | const minx = a[0][0]; 756 | const miny = a[0][1]; 757 | const maxx = a[1][0]; 758 | const maxy = a[1][1]; 759 | a[0][0] = minx > x ? x : minx; 760 | a[0][1] = miny > y ? y : miny; 761 | a[1][0] = maxx < x ? x : maxx; 762 | a[1][1] = maxy < y ? y : maxy; 763 | return a; 764 | } 765 | /** 766 | * Includes a rectangle in another rectangle. 767 | * @param {import("./types.js").rect} a 768 | * @param {import("./types.js").rect} b 769 | * @returns {import("./types.js").rect} 770 | */ function includeRect(a, b) { 771 | includePoint(a, b[0]); 772 | includePoint(a, b[1]); 773 | return a; 774 | } 775 | /** 776 | * Maps a point into the dimensions of a rectangle. 777 | * @param {import("./types.js").rect} a 778 | * @param {import("./types.js").vec2} p 779 | * @returns {import("./types.js").vec2} 780 | */ function mapPoint(a, p) { 781 | const minx = a[0][0]; 782 | const miny = a[0][1]; 783 | const maxx = a[1][0]; 784 | const maxy = a[1][1]; 785 | p[0] = Math.max(minx, Math.min(p[0], maxx)) - minx; 786 | p[1] = Math.max(miny, Math.min(p[1], maxy)) - miny; 787 | return p; 788 | } 789 | /** 790 | * Clamps a point into the dimensions of a rectangle. 791 | * @param {import("./types.js").rect} a 792 | * @param {import("./types.js").vec2} p 793 | * @returns {import("./types.js").vec2} 794 | */ function clampPoint(a, p) { 795 | const minx = a[0][0]; 796 | const miny = a[0][1]; 797 | const maxx = a[1][0]; 798 | const maxy = a[1][1]; 799 | p[0] = Math.max(minx, Math.min(p[0], maxx)); 800 | p[1] = Math.max(miny, Math.min(p[1], maxy)); 801 | return p; 802 | } 803 | /** 804 | * Prints a rect to a string. 805 | * @param {import("./types.js").rect} a 806 | * @param {number} [precision=4] 807 | * @returns {string} 808 | */ function toString(a, precision) { 809 | if (precision === void 0) precision = 4; 810 | // prettier-ignore 811 | return `[${toString$5(a[0], precision)}, ${toString$5(a[1], precision)}]`; 812 | } 813 | 814 | var rect = /*#__PURE__*/Object.freeze({ 815 | __proto__: null, 816 | aspectRatio: aspectRatio, 817 | center: center, 818 | clampPoint: clampPoint, 819 | containsPoint: containsPoint, 820 | containsRect: containsRect, 821 | copy: copy, 822 | create: create, 823 | empty: empty, 824 | fromPoints: fromPoints, 825 | getCorners: getCorners, 826 | height: height, 827 | includePoint: includePoint, 828 | includeRect: includeRect, 829 | isEmpty: isEmpty, 830 | mapPoint: mapPoint, 831 | scale: scale, 832 | set: set, 833 | setPosition: setPosition, 834 | setSize: setSize, 835 | size: size, 836 | toString: toString, 837 | width: width 838 | }); 839 | 840 | export { aabb, plane, ray, rect }; 841 | -------------------------------------------------------------------------------- /web_modules/typed-array-interleave.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module typedArrayInterleave 3 | */ /** 4 | * Interleave n typed arrays 5 | * 6 | * @alias module:typedArrayInterleave 7 | * @param {TypedArray} ResultConstructor Returned typed array constructor 8 | * @param {Array} elements Number of elements to group for each typed array 9 | * @param {...TypedArray} arrays Arrays to interleave 10 | * @returns {TypedArray} 11 | */ function typedArrayInterleave(ResultConstructor, elements) { 12 | for(var _len = arguments.length, arrays = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++){ 13 | arrays[_key - 2] = arguments[_key]; 14 | } 15 | const totalLength = arrays.reduce((total, arr)=>total + arr.length, 0); 16 | const result = new ResultConstructor(totalLength); 17 | const stride = elements.reduce((a, b)=>a + b); 18 | for(let i = 0; i < totalLength; i++){ 19 | let offset = 0; 20 | for(let j = 0; j < elements.length; j++){ 21 | for(let k = 0; k < elements[j]; k++){ 22 | result[i * stride + offset] = arrays[j][elements[j] * i + k]; 23 | offset++; 24 | } 25 | } 26 | } 27 | return result; 28 | } 29 | 30 | export { typedArrayInterleave as default }; 31 | --------------------------------------------------------------------------------