├── .nojekyll ├── docs ├── .nojekyll ├── assets │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ └── search.json ├── globals.html ├── index.html └── modules │ └── _glea_.html ├── .prettierignore ├── .prettierrc.yaml ├── .gitignore ├── .travis.yml ├── jest.config.js ├── server.js ├── examples ├── hello-world.html ├── multi-shaders.html ├── hello-world.mjs ├── multi-shaders.mjs └── vendor │ ├── ella.d.ts │ └── ella.esm.js ├── rollup.config.js ├── CONTRIBUTING.md ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── index.html ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── src ├── glea.test.ts └── glea.ts ├── tsconfig.json ├── dist ├── glea.min.js ├── glea.umd.min.js ├── glea.d.ts ├── glea.js └── glea.umd.js └── README.md /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | docs 3 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: true 2 | singleQuote: true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | coverage 4 | 5 | node_modules 6 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learosema/glea/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learosema/glea/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learosema/glea/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | os: linux 5 | dist: xenial 6 | install: 7 | - npm i 8 | -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learosema/glea/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | setupFiles: ['jest-webgl-canvas-mock'], 5 | }; 6 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const PORT = 1337 || process.env.PORT; 2 | const HOST = 'localhost' || process.env.HOST; 3 | const express = require('express'); 4 | const app = express(); 5 | app.use(express.static('.')); 6 | 7 | app.listen(PORT, HOST, () => 8 | console.log(`Server listening on http://${HOST}:${PORT}/`) 9 | ); 10 | -------------------------------------------------------------------------------- /examples/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello World 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/multi-shaders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Multiple Shaders 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/glea.ts', 5 | output: [ 6 | { 7 | file: 'dist/glea.js', 8 | format: 'es', 9 | }, 10 | { 11 | file: 'dist/glea.umd.js', 12 | format: 'umd', 13 | name: 'GLea', 14 | }, 15 | ], 16 | plugins: [typescript()], 17 | }; 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing. 4 | 5 | Currently, this is a one-woman project and there are no strict coding conventions yet. 6 | 7 | The only prerequisite is to don't be an asshole and follow the Code of Conduct. 8 | 9 | Feel free to fork and help improving the project by creating a pull request. 10 | 11 | Don't be shy. Aside to code changes, help improving the documentation, 12 | this contribution guideline or the description of open issues is highly appreciated. 13 | 14 | Cheers, 15 | 16 | Lea 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lea Rosema 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/hello-world.mjs: -------------------------------------------------------------------------------- 1 | import GLea from '../dist/glea.js'; 2 | 3 | const vert = ` 4 | precision highp float; 5 | attribute vec2 position; 6 | 7 | void main() { 8 | gl_Position = vec4(position, 0, 1.0); 9 | } 10 | `; 11 | 12 | const frag = ` 13 | precision highp float; 14 | uniform float time; 15 | uniform vec2 resolution; 16 | 17 | void main() { 18 | float vmin = min(resolution.y, resolution.x); 19 | vec2 p = (gl_FragCoord.xy - .5 * resolution) / vmin; 20 | float r = .5 + .5 * sin(5. * log(length(p)) - time * 1.2); 21 | float g = .5 + .5 * sin(5. * log(length(p)) + sin(time + 2. * p.x)); 22 | float b = .5 + .5 * sin(.2 + 5. * log(length(p)) + sin(time * .4 + 4. * p.y)); 23 | gl_FragColor = vec4(r, g, b, 1.); 24 | } 25 | `; 26 | 27 | const glea = new GLea({ 28 | shaders: [GLea.fragmentShader(frag), GLea.vertexShader(vert)], 29 | buffers: { 30 | // create a position attribute bound 31 | // to a buffer with 4 2D coordinates 32 | position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]), 33 | }, 34 | }).create(); 35 | 36 | function loop(time) { 37 | const { gl, width, height } = glea; 38 | glea.clear(); 39 | glea.uniV('resolution', [width, height]); 40 | glea.uni('time', time * 1e-3); 41 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 42 | requestAnimationFrame(loop); 43 | } 44 | 45 | function setup() { 46 | const { gl } = glea; 47 | window.addEventListener('resize', () => { 48 | glea.resize(); 49 | }); 50 | loop(0); 51 | } 52 | 53 | setup(); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glea", 3 | "version": "1.3.1", 4 | "description": "GL experience artistry", 5 | "repository": "github:terabaud/glea/", 6 | "homepage": "https://terabaud.github.io/glea/docs/", 7 | "main": "dist/glea.umd.min.js", 8 | "module": "dist/glea.min.js", 9 | "types": "dist/glea.d.ts", 10 | "keywords": [ 11 | "webgl" 12 | ], 13 | "scripts": { 14 | "docs": "typedoc && touch docs/.nojekyll", 15 | "build:types": "tsc -t esnext --moduleResolution node -d --emitDeclarationOnly --outFile dist/glea.d.ts src/glea.ts", 16 | "build:js": "rollup -c rollup.config.js", 17 | "build:min": "terser dist/glea.js --compress --mangle > dist/glea.min.js", 18 | "build:min-umd": "terser dist/glea.umd.js --compress --mangle > dist/glea.umd.min.js", 19 | "build": "npm run build:js -s && npm run build:min -s && npm run build:min-umd -s && npm run build:types -s", 20 | "test": "jest --coverage", 21 | "server": "node server" 22 | }, 23 | "files": [ 24 | "src", 25 | "dist" 26 | ], 27 | "exports": { 28 | ".": { 29 | "require": "./dist/glea.umd.min.js", 30 | "import": "./dist/glea.min.js" 31 | } 32 | }, 33 | "author": "Lea Rosema", 34 | "license": "MIT", 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "@rollup/plugin-typescript": "^6.1.0", 38 | "@types/jest": "^26.0.15", 39 | "express": "^4.17.1", 40 | "husky": "^4.3.0", 41 | "jest": "^26.6.3", 42 | "jest-webgl-canvas-mock": "^0.2.3", 43 | "prettier": "^2.2.1", 44 | "pretty-quick": "^3.1.0", 45 | "rollup": "^2.33.3", 46 | "terser": "^5.5.1", 47 | "ts-jest": "^26.4.4", 48 | "tslib": "^2.0.3", 49 | "typedoc": "^0.19.2", 50 | "typescript": "^4.1.2" 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "pretty-quick --staged" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * Deliberate use of rejected pronouns and/or dead or rejected names 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. Representation of a project may be 54 | further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at terabaud@gmail.com. All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | glea 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

glea

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |

Index

68 |
69 |
70 |
71 |

Modules

72 | 75 |
76 |
77 |
78 |
79 |
80 | 96 |
97 |
98 | 112 |
113 |

Generated using TypeDoc

114 |
115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /src/glea.test.ts: -------------------------------------------------------------------------------- 1 | import GLea from './glea'; 2 | 3 | const frag = 'void main() {}'; 4 | const vert = 'void main() {}'; 5 | 6 | const glea = new GLea({ 7 | shaders: [GLea.fragmentShader(frag), GLea.vertexShader(vert)], 8 | }).create(); 9 | 10 | describe('GLea initialization', () => { 11 | it('should create an instance of GLea', () => { 12 | expect(glea).toBeInstanceOf(GLea); 13 | }); 14 | 15 | it('should create a default position buffer for you', () => { 16 | expect(glea.bufferFactory).toBeTruthy(); 17 | expect(glea.bufferFactory.position).toBeTruthy(); 18 | }); 19 | 20 | it('should automagically create a canvas element', () => { 21 | const canvas = document.querySelector('canvas'); 22 | expect(canvas).not.toBeNull(); 23 | }); 24 | 25 | it('should hold an underlying WebGLRenderingContext', () => { 26 | expect(glea.gl).toBeInstanceOf(WebGLRenderingContext); 27 | }); 28 | 29 | it('should automagically provide a default css style for you', () => { 30 | const style = document.querySelector('style'); 31 | expect(style).not.toBeNull(); 32 | }); 33 | 34 | it('should carry the canvas with and height', () => { 35 | const width = glea.width; 36 | const height = glea.height; 37 | expect(width).not.toBeNaN(); 38 | expect(height).not.toBeNaN(); 39 | }); 40 | }); 41 | 42 | describe('createTexture helper', () => { 43 | it('should return a WebGLTexture object', () => { 44 | const texture = glea.createTexture(0); 45 | expect(texture).toBeTruthy(); 46 | }); 47 | }); 48 | 49 | describe('uniform helper', () => { 50 | it('should accept a number', () => { 51 | expect(() => { 52 | // prettier-ignore 53 | glea.uni('val', 1); 54 | }).not.toThrowError(); 55 | }); 56 | }); 57 | 58 | describe('uniform int helper', () => { 59 | it('should accept a number', () => { 60 | expect(() => { 61 | // prettier-ignore 62 | glea.uniI('intval', 2); 63 | }).not.toThrowError(); 64 | }); 65 | }); 66 | 67 | describe('uniform matrix helper', () => { 68 | it('should accept 2D matrices', () => { 69 | expect(() => { 70 | // prettier-ignore 71 | glea.uniM('matrix2D', [ 72 | 1, 0, 73 | 0, 1 74 | ]); 75 | }).not.toThrowError(); 76 | }); 77 | 78 | it('should accept 3D matrices', () => { 79 | expect(() => { 80 | // prettier-ignore 81 | glea.uniM('matrix3D', [ 82 | 1, 0, 0, 83 | 0, 1, 0, 84 | 0, 0, 1 85 | ]); 86 | }).not.toThrowError(); 87 | }); 88 | 89 | it('should accept 4D matrices', () => { 90 | expect(() => { 91 | // prettier-ignore 92 | glea.uniM('matrix4D', [ 93 | 1, 0, 0, 0, 94 | 0, 1, 0, 0, 95 | 0, 0, 1, 0, 96 | 0, 0, 0, 1 97 | ]); 98 | }).not.toThrowError(); 99 | }); 100 | 101 | it('should throw an error for anything else than 2D, 3D, 4D matrices', () => { 102 | expect(() => { 103 | // prettier-ignore 104 | glea.uniM('unsupported_matrix', [ 105 | 1, 0, 0 106 | ]); 107 | }).toThrowError(); 108 | }); 109 | }); 110 | 111 | describe('uniform vector helper', () => { 112 | it('should accept 2D vectors', () => { 113 | expect(() => { 114 | glea.uniV('v2', [1, 2]); 115 | }).not.toThrowError(); 116 | }); 117 | 118 | it('should accept 3D vectors', () => { 119 | expect(() => { 120 | glea.uniV('v3', [1, 2, 3]); 121 | }).not.toThrowError(); 122 | }); 123 | 124 | it('should accept 4D vectors', () => { 125 | expect(() => { 126 | glea.uniV('v4', [1, 2, 3, 4]); 127 | }).not.toThrowError(); 128 | }); 129 | 130 | it('should throw an error with an array length other than 2,3 or 4.', () => { 131 | expect(() => { 132 | glea.uniV('v5', [1]); 133 | }).toThrowError(); 134 | }); 135 | }); 136 | 137 | describe('uniform int vector helper', () => { 138 | it('should accept 2D vectors', () => { 139 | expect(() => { 140 | glea.uniIV('v2', [1, 2]); 141 | }).not.toThrowError(); 142 | }); 143 | 144 | it('should accept 3D vectors', () => { 145 | expect(() => { 146 | glea.uniIV('v3', [1, 2, 3]); 147 | }).not.toThrowError(); 148 | }); 149 | 150 | it('should accept 4D vectors', () => { 151 | expect(() => { 152 | glea.uniIV('v4', [1, 2, 3, 4]); 153 | }).not.toThrowError(); 154 | }); 155 | 156 | it('should throw an error with an array length other than 2,3 or 4.', () => { 157 | expect(() => { 158 | glea.uniIV('v5', [1]); 159 | }).toThrowError(); 160 | }); 161 | }); 162 | 163 | describe('clear', () => { 164 | it('should not throw an error', () => { 165 | expect(() => { 166 | glea.clear(); 167 | }).not.toThrowError(); 168 | }); 169 | }); 170 | 171 | describe('updateBuffer', () => { 172 | it('should not throw an error', () => { 173 | expect(() => { 174 | glea.updateBuffer('position'); 175 | }).not.toThrowError(); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "lib": [ 8 | "DOM", 9 | "DOM.Iterable", 10 | "ESNext" 11 | ] /* Specify library files to be included in the compilation. */, 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | // "declaration": true /* Generates corresponding '.d.ts' file. */, 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | // "sourceMap": true /* Generates corresponding '.map' file. */, 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | // "outDir": "./dist" /* Redirect output structure to the directory. */, 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 32 | "strictNullChecks": true /* Enable strict null checks. */, 33 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 34 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 35 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 36 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 37 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 38 | 39 | /* Additional Checks */ 40 | "noUnusedLocals": true /* Report errors on unused locals. */, 41 | "noUnusedParameters": true /* Report errors on unused parameters. */, 42 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 43 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": ["src/*.ts"], 71 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] 72 | } 73 | -------------------------------------------------------------------------------- /dist/glea.min.js: -------------------------------------------------------------------------------- 1 | const SHADER_HEAD="precision highp float;",VERT_DEFAULT=SHADER_HEAD+"attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}",FRAG_DEFAULT=SHADER_HEAD+"precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}";function convertArray(t,e=WebGLRenderingContext.FLOAT){if(e===WebGLRenderingContext.FLOAT)return new Float32Array(t);if(e===WebGLRenderingContext.BYTE)return new Uint8Array(t);throw Error("type not supported")}function shader(t,e){return{shaderType:e,init:r=>{const i=/frag/.test(e)?WebGLRenderingContext.FRAGMENT_SHADER:WebGLRenderingContext.VERTEX_SHADER,n=r.createShader(i);if(!n)throw Error("shader type not supported");if(r.shaderSource(n,t),r.compileShader(n),!r.getShaderParameter(n,r.COMPILE_STATUS))throw"Could not compile Shader.\n\n"+r.getShaderInfoLog(n);return n}}}class GLea{constructor({canvas:t,gl:e,contextType:r="webgl",shaders:i,buffers:n,devicePixelRatio:a=1,glOptions:s}){if(this.canvas=document.createElement("canvas"),this.canvas=t||document.querySelector("canvas"),this.canvas||(this.canvas=document.createElement("canvas"),document.body.appendChild(this.canvas)),!document.querySelector("link[rel=stylesheet], style")){const t=document.createElement("style");t.innerHTML="body{margin:0}canvas{display:block;width:100vw;height:100vh}",document.head.appendChild(t)}this.contextType=r,this.glOptions=s,this.gl=e||this.getContext(r,s);const o=this.gl.createProgram();this.program=o,this.buffers={},this.shaderFactory=i,this.bufferFactory=n||this.getDefaultBuffers(),this.textures=[],this.devicePixelRatio=a}getDefaultBuffers(){return{position:GLea.buffer(2,[1,1,-1,1,1,-1,-1,-1])}}getContext(t,e){if("webgl"===t)return this.canvas.getContext("webgl",e)||this.canvas.getContext("experimental-webgl",e);if("webgl2"===t)return this.canvas.getContext("webgl2",e);throw Error(`no ${t} context available.`)}static vertexShader(t=VERT_DEFAULT){return shader(t,"vert")}static fragmentShader(t=FRAG_DEFAULT){return shader(t,"frag")}prog(t,e,r){const i=t.createProgram(),n=e.init(t),a=r.init(t);if(t.attachShader(i,n),t.attachShader(i,a),t.linkProgram(i),t.validateProgram(i),!t.getProgramParameter(i,t.LINK_STATUS)){throw"Could not compile WebGL program. \n\n"+t.getProgramInfoLog(i)}return i}static buffer(t,e,r=WebGLRenderingContext.STATIC_DRAW,i=WebGLRenderingContext.FLOAT,n=!1,a=0,s=0){return(o,h,c)=>{const f=h.getAttribLocation(c,o);h.enableVertexAttribArray(f);const u=h.createBuffer(),g=e instanceof Array?convertArray(e,i):e;return h.bindBuffer(h.ARRAY_BUFFER,u),h.bufferData(h.ARRAY_BUFFER,g,r),h.vertexAttribPointer(f,t,i,n,a,s),{id:u,name:o,data:g,loc:f,type:i,size:t,normalized:n,stride:a,offset:s}}}drawArrays(t,e=0,r){if(void 0===r){const t=Object.keys(this.buffers);if(0===t.length)return;const e=t[0],i=this.buffers[e];r=i.data.length/i.size}this.gl.drawArrays(t,e,r)}disableAttribs(){const{gl:t,program:e,buffers:r}=this;for(let i of Object.keys(r)){const r=t.getAttribLocation(e,i);t.disableVertexAttribArray(r)}}enableAttribs(){const{gl:t,program:e,buffers:r}=this;this.use();for(let i of Object.keys(r)){const n=r[i],a=t.getAttribLocation(e,i);t.enableVertexAttribArray(a),t.bindBuffer(t.ARRAY_BUFFER,r[i].id),t.vertexAttribPointer(a,n.size,n.type,n.normalized,n.stride,n.offset)}}create(){const{gl:t}=this;return this.program=this.prog(t,this.shaderFactory[0],this.shaderFactory[1]),this.use(),Object.keys(this.bufferFactory).forEach((e=>{const r=this.bufferFactory[e];this.buffers[e]=r(e,t,this.program)})),this.parent||this.resize(),this}replaceCanvas(){const{canvas:t}=this,e=t.cloneNode();t.parentNode&&(t.parentNode.insertBefore(e,t),t.parentNode.removeChild(t)),this.canvas=e}restart(){return this.replaceCanvas(),this.gl=this.getContext(this.contextType,this.glOptions),this.create(),this}add({shaders:t,buffers:e}){const r=new GLea({canvas:this.canvas,gl:this.gl,shaders:t,buffers:e||this.getDefaultBuffers()});return r.parent=this.parent||this,r.create(),r}setActiveTexture(t,e){const{gl:r}=this;r.activeTexture(r.TEXTURE0+t),r.bindTexture(r.TEXTURE_2D,e)}createTexture(t=0,e={textureWrapS:"clampToEdge",textureWrapT:"clampToEdge",textureMinFilter:"nearest",textureMagFilter:"nearest"}){const r=(t="")=>/^[A-Z0-9_]+$/.test(t)?t:t.replace(/([A-Z])/g,"_$1").toUpperCase(),{gl:i}=this,n=i.createTexture();i.activeTexture(i.TEXTURE0+t),i.bindTexture(i.TEXTURE_2D,n);for(let t in e)if(e.hasOwnProperty(t)){const n=r(t),a=r(e[t]);n in i&&a in i&&i.texParameteri(i.TEXTURE_2D,i[n],i[a])}return this.textures.push(n),n}updateBuffer(t,e=0){const{gl:r}=this,i=this.buffers[t];r.bindBuffer(r.ARRAY_BUFFER,i.id),r.bufferSubData(r.ARRAY_BUFFER,e,i.data)}resize(){const{canvas:t,gl:e,devicePixelRatio:r}=this;t&&(t.width=t.clientWidth*r,t.height=t.clientHeight*r,e.viewport(0,0,e.drawingBufferWidth,e.drawingBufferHeight))}get width(){return this.canvas?this.canvas.width:NaN}get height(){return this.canvas?this.canvas.height:NaN}use(){return this.gl.useProgram(this.program),this}uniM(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(4===e.length)return r.uniformMatrix2fv(n,!1,e),n;if(9===e.length)return r.uniformMatrix3fv(n,!1,e),n;if(16===e.length)return r.uniformMatrix4fv(n,!1,e),n;throw Error("unsupported uniform matrix type")}uniV(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(2===e.length)return r.uniform2fv(n,e),n;if(3===e.length)return r.uniform3fv(n,e),n;if(4===e.length)return r.uniform4fv(n,e),n;throw Error("unsupported uniform vector type")}uniIV(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(2===e.length)return r.uniform2iv(n,e),n;if(3===e.length)return r.uniform3iv(n,e),n;if(4===e.length)return r.uniform4iv(n,e),n;throw Error("unsupported uniform vector type")}uni(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);return"number"==typeof e&&r.uniform1f(n,e),n}uniI(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);"number"==typeof e&&r.uniform1i(n,e)}clear(t=null){const{gl:e}=this;t&&e.clearColor(t[0],t[1],t[2],1),e.clear(e.COLOR_BUFFER_BIT|e.DEPTH_BUFFER_BIT)}destroy(){const{gl:t,program:e}=this;try{t.deleteProgram(e),Object.values(this.buffers).forEach((e=>{t.deleteBuffer(e.id)})),this.buffers={},this.textures.forEach((e=>{t.deleteTexture(e)})),this.textures=[],t.getExtension("WEBGL_lose_context").loseContext(),this.replaceCanvas()}catch(t){console.error(t)}}}export default GLea; 2 | -------------------------------------------------------------------------------- /dist/glea.umd.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).GLea=e()}(this,(function(){"use strict";function t(t,e){return{shaderType:e,init:r=>{const i=/frag/.test(e)?WebGLRenderingContext.FRAGMENT_SHADER:WebGLRenderingContext.VERTEX_SHADER,n=r.createShader(i);if(!n)throw Error("shader type not supported");if(r.shaderSource(n,t),r.compileShader(n),!r.getShaderParameter(n,r.COMPILE_STATUS))throw"Could not compile Shader.\n\n"+r.getShaderInfoLog(n);return n}}}class e{constructor({canvas:t,gl:e,contextType:r="webgl",shaders:i,buffers:n,devicePixelRatio:o=1,glOptions:s}){if(this.canvas=document.createElement("canvas"),this.canvas=t||document.querySelector("canvas"),this.canvas||(this.canvas=document.createElement("canvas"),document.body.appendChild(this.canvas)),!document.querySelector("link[rel=stylesheet], style")){const t=document.createElement("style");t.innerHTML="body{margin:0}canvas{display:block;width:100vw;height:100vh}",document.head.appendChild(t)}this.contextType=r,this.glOptions=s,this.gl=e||this.getContext(r,s);const a=this.gl.createProgram();this.program=a,this.buffers={},this.shaderFactory=i,this.bufferFactory=n||this.getDefaultBuffers(),this.textures=[],this.devicePixelRatio=o}getDefaultBuffers(){return{position:e.buffer(2,[1,1,-1,1,1,-1,-1,-1])}}getContext(t,e){if("webgl"===t)return this.canvas.getContext("webgl",e)||this.canvas.getContext("experimental-webgl",e);if("webgl2"===t)return this.canvas.getContext("webgl2",e);throw Error(`no ${t} context available.`)}static vertexShader(e="precision highp float;attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}"){return t(e,"vert")}static fragmentShader(e="precision highp float;precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}"){return t(e,"frag")}prog(t,e,r){const i=t.createProgram(),n=e.init(t),o=r.init(t);if(t.attachShader(i,n),t.attachShader(i,o),t.linkProgram(i),t.validateProgram(i),!t.getProgramParameter(i,t.LINK_STATUS)){throw"Could not compile WebGL program. \n\n"+t.getProgramInfoLog(i)}return i}static buffer(t,e,r=WebGLRenderingContext.STATIC_DRAW,i=WebGLRenderingContext.FLOAT,n=!1,o=0,s=0){return(a,h,c)=>{const f=h.getAttribLocation(c,a);h.enableVertexAttribArray(f);const u=h.createBuffer(),l=e instanceof Array?function(t,e=WebGLRenderingContext.FLOAT){if(e===WebGLRenderingContext.FLOAT)return new Float32Array(t);if(e===WebGLRenderingContext.BYTE)return new Uint8Array(t);throw Error("type not supported")}(e,i):e;return h.bindBuffer(h.ARRAY_BUFFER,u),h.bufferData(h.ARRAY_BUFFER,l,r),h.vertexAttribPointer(f,t,i,n,o,s),{id:u,name:a,data:l,loc:f,type:i,size:t,normalized:n,stride:o,offset:s}}}drawArrays(t,e=0,r){if(void 0===r){const t=Object.keys(this.buffers);if(0===t.length)return;const e=t[0],i=this.buffers[e];r=i.data.length/i.size}this.gl.drawArrays(t,e,r)}disableAttribs(){const{gl:t,program:e,buffers:r}=this;for(let i of Object.keys(r)){const r=t.getAttribLocation(e,i);t.disableVertexAttribArray(r)}}enableAttribs(){const{gl:t,program:e,buffers:r}=this;this.use();for(let i of Object.keys(r)){const n=r[i],o=t.getAttribLocation(e,i);t.enableVertexAttribArray(o),t.bindBuffer(t.ARRAY_BUFFER,r[i].id),t.vertexAttribPointer(o,n.size,n.type,n.normalized,n.stride,n.offset)}}create(){const{gl:t}=this;return this.program=this.prog(t,this.shaderFactory[0],this.shaderFactory[1]),this.use(),Object.keys(this.bufferFactory).forEach((e=>{const r=this.bufferFactory[e];this.buffers[e]=r(e,t,this.program)})),this.parent||this.resize(),this}replaceCanvas(){const{canvas:t}=this,e=t.cloneNode();t.parentNode&&(t.parentNode.insertBefore(e,t),t.parentNode.removeChild(t)),this.canvas=e}restart(){return this.replaceCanvas(),this.gl=this.getContext(this.contextType,this.glOptions),this.create(),this}add({shaders:t,buffers:r}){const i=new e({canvas:this.canvas,gl:this.gl,shaders:t,buffers:r||this.getDefaultBuffers()});return i.parent=this.parent||this,i.create(),i}setActiveTexture(t,e){const{gl:r}=this;r.activeTexture(r.TEXTURE0+t),r.bindTexture(r.TEXTURE_2D,e)}createTexture(t=0,e={textureWrapS:"clampToEdge",textureWrapT:"clampToEdge",textureMinFilter:"nearest",textureMagFilter:"nearest"}){const r=(t="")=>/^[A-Z0-9_]+$/.test(t)?t:t.replace(/([A-Z])/g,"_$1").toUpperCase(),{gl:i}=this,n=i.createTexture();i.activeTexture(i.TEXTURE0+t),i.bindTexture(i.TEXTURE_2D,n);for(let t in e)if(e.hasOwnProperty(t)){const n=r(t),o=r(e[t]);n in i&&o in i&&i.texParameteri(i.TEXTURE_2D,i[n],i[o])}return this.textures.push(n),n}updateBuffer(t,e=0){const{gl:r}=this,i=this.buffers[t];r.bindBuffer(r.ARRAY_BUFFER,i.id),r.bufferSubData(r.ARRAY_BUFFER,e,i.data)}resize(){const{canvas:t,gl:e,devicePixelRatio:r}=this;t&&(t.width=t.clientWidth*r,t.height=t.clientHeight*r,e.viewport(0,0,e.drawingBufferWidth,e.drawingBufferHeight))}get width(){return this.canvas?this.canvas.width:NaN}get height(){return this.canvas?this.canvas.height:NaN}use(){return this.gl.useProgram(this.program),this}uniM(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(4===e.length)return r.uniformMatrix2fv(n,!1,e),n;if(9===e.length)return r.uniformMatrix3fv(n,!1,e),n;if(16===e.length)return r.uniformMatrix4fv(n,!1,e),n;throw Error("unsupported uniform matrix type")}uniV(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(2===e.length)return r.uniform2fv(n,e),n;if(3===e.length)return r.uniform3fv(n,e),n;if(4===e.length)return r.uniform4fv(n,e),n;throw Error("unsupported uniform vector type")}uniIV(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);if(2===e.length)return r.uniform2iv(n,e),n;if(3===e.length)return r.uniform3iv(n,e),n;if(4===e.length)return r.uniform4iv(n,e),n;throw Error("unsupported uniform vector type")}uni(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);return"number"==typeof e&&r.uniform1f(n,e),n}uniI(t,e){const{gl:r,program:i}=this,n=r.getUniformLocation(i,t);"number"==typeof e&&r.uniform1i(n,e)}clear(t=null){const{gl:e}=this;t&&e.clearColor(t[0],t[1],t[2],1),e.clear(e.COLOR_BUFFER_BIT|e.DEPTH_BUFFER_BIT)}destroy(){const{gl:t,program:e}=this;try{t.deleteProgram(e),Object.values(this.buffers).forEach((e=>{t.deleteBuffer(e.id)})),this.buffers={},this.textures.forEach((e=>{t.deleteTexture(e)})),this.textures=[],t.getExtension("WEBGL_lose_context").loseContext(),this.replaceCanvas()}catch(t){console.error(t)}}}return e})); 2 | -------------------------------------------------------------------------------- /examples/multi-shaders.mjs: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import GLea from '../dist/glea.js'; 4 | import * as Ella from './vendor/ella.esm.js'; 5 | 6 | const { Vec } = Ella; 7 | 8 | // VSCode: glsl literal extension for code highlighting 9 | const glsl = (x) => x[0].trim(); 10 | 11 | const vert = glsl` 12 | precision highp float; 13 | attribute vec2 position; 14 | 15 | void main() { 16 | gl_Position = vec4(position, 0, 1.); 17 | } 18 | `; 19 | 20 | const frag = glsl` 21 | precision highp float; 22 | uniform float time; 23 | uniform vec2 resolution; 24 | 25 | #define PI 3.141592654 26 | 27 | vec3 palette(in float t) { 28 | vec3 a = vec3(.5); 29 | vec3 b = vec3(.5); 30 | vec3 c = vec3(1.0, 1.0, 1.0); 31 | vec3 d = vec3(0.3, 0.2, 0.2); 32 | return a + b*cos(PI * 2. * (c*t+d) ); 33 | } 34 | 35 | 36 | void main() { 37 | float vmin = min(resolution.y, resolution.x); 38 | vec2 p = (gl_FragCoord.xy - .5 * resolution) / vmin; 39 | float t = mod(time * .1 + smoothstep(-.9, .9, sin(5. * log(length(p)) - time * 1.2) * sin(time * .4 + 12. * p.x) * cos(time * .4 + 12. * p.y)), 1.); 40 | gl_FragColor = vec4(palette(t), 1.0); 41 | } 42 | `; 43 | 44 | const vert2 = glsl` 45 | precision highp float; 46 | attribute vec3 position; 47 | varying vec3 vPosition; 48 | varying mat4 vM; 49 | uniform vec2 resolution; 50 | uniform float time; 51 | uniform mat4 uVM; 52 | uniform mat4 uPM; 53 | 54 | #define PI 3.141592654 55 | 56 | 57 | mat4 translate(in vec3 p) { 58 | return mat4( 59 | vec4(1.0, 0.0, 0.0, 0.0), 60 | vec4(0.0, 1.0, 0.0, 0.0), 61 | vec4(0.0, 0.0, 1.0, 0.0), 62 | vec4(p.x, p.y, p.z, 1.0) 63 | ); 64 | } 65 | 66 | 67 | mat4 rotX(in float angle) { 68 | float S = sin(angle); 69 | float C = cos(angle); 70 | return mat4( 71 | vec4(1.0, 0, 0, 0), 72 | vec4(0 , C, S, 0), 73 | vec4(0 ,-S, C, 0), 74 | vec4(0 , 0, 0, 1.0) 75 | ); 76 | } 77 | 78 | 79 | mat4 rotY(in float angle) { 80 | float S = sin(angle); 81 | float C = cos(angle); 82 | return mat4( 83 | vec4(C, 0 ,-S, 0), 84 | vec4(0, 1.0, 0, 0), 85 | vec4(S, 0 , C, 0), 86 | vec4(0, 0 , 0, 1.0) 87 | ); 88 | } 89 | 90 | 91 | mat4 rotZ(in float angle) { 92 | float S = sin(angle); 93 | float C = cos(angle); 94 | return mat4( 95 | vec4( C, S, 0 , 0), 96 | vec4(-S, C, 0 , 0), 97 | vec4( 0, 0, 1.0, 0), 98 | vec4( 0, 0, 0 , 1.0) 99 | ); 100 | } 101 | 102 | // glFrustum(left, right, bottom, top, zNear, zFar) 103 | mat4 frustum(in float left, in float right, in float bottom, in float top, in float zNear, in float zFar) { 104 | float t1 = 2.0 * zNear; 105 | float t2 = right - left; 106 | float t3 = top - bottom; 107 | float t4 = zFar - zNear; 108 | return mat4( 109 | vec4(t1 / t2, 0, 0, 0), 110 | vec4(0, t1 / t3, 0, 0), 111 | vec4((right + left) / t2, (top + bottom) / t3, (-zFar - zNear) / t4, -1.0), 112 | vec4(0, 0, (-t1*zFar) / t4, 0)); 113 | } 114 | 115 | // gluPerspective(fieldOfView, aspectRatio, zNear, zFar) 116 | mat4 perspective(in float fieldOfView, in float aspectRatio, in float zNear, in float zFar) { 117 | float y = zNear * tan(fieldOfView * PI / 360.0); 118 | float x = y * aspectRatio; 119 | return frustum(-x, x, -y, y, zNear, zFar); 120 | } 121 | 122 | 123 | void main() { 124 | vPosition = position; 125 | 126 | mat4 pM = uPM; 127 | // alternatively: 128 | // mat4 pM = perspective(45.0, resolution.x / resolution.y, 0.1, 1000.0); 129 | 130 | mat4 tM = translate(vec3(sin(2.0 * time * .1) * 0.2, sin(3.0 * time * .1) * .1, -1.6 + sin(time * .1))); 131 | mat4 rM = rotX(time * 0.1) * rotY(time * 0.1) * rotZ(time * 0.25); 132 | mat4 M = pM * tM * rM * uVM; 133 | vM = M; 134 | 135 | gl_Position = M * vec4(position, 1.); 136 | } 137 | `; 138 | 139 | const frag2 = glsl` 140 | precision highp float; 141 | uniform float time; 142 | uniform vec2 resolution; 143 | 144 | #define PI 3.141592654 145 | 146 | varying vec3 vPosition; 147 | varying mat4 vM; 148 | 149 | // by IQ 150 | // https://iquilezles.org/www/articles/palettes/palettes.htm 151 | vec3 palette(in float t) 152 | { 153 | vec3 a = vec3(.5, .5, .5); 154 | vec3 c = vec3(2., 1., 0.); 155 | vec3 d = vec3(.5, .2, .25); 156 | return a + a*cos(PI * 2. * (c*t+d) ); 157 | } 158 | 159 | void main() { 160 | vec4 v = vM * vec4(vPosition, 1.0); 161 | float aR = resolution.x / resolution.y; 162 | float t = mod(time * .1 + .8 * smoothstep(-.5,.5, cos(v.x * 33. * aR + time * 10.) * sin(v.y * 33. + time * 10.)), 1.0); 163 | gl_FragColor = vec4(palette(t), 1.); 164 | } 165 | `; 166 | 167 | class App { 168 | constructor() { 169 | this.projectionMat = Ella.Mat4.identity(); 170 | this.viewMat = Ella.Mat4.identity(); 171 | this.loop = this.loop.bind(this); 172 | this.onResize = this.onResize.bind(this); 173 | } 174 | 175 | setProjectionMatrix() { 176 | const w = document.body.clientWidth; 177 | const h = document.body.clientHeight; 178 | this.projectionMat = Ella.perspective(60, w / h, 0.1, 1000); 179 | this.viewMat = Ella.lookAt( 180 | new Vec(5, 0, 2), 181 | new Vec(0, 0, 0), 182 | new Vec(0, 1, 0) 183 | ); 184 | } 185 | 186 | setup() { 187 | const sphere = Ella.Geometry.sphere(0.25, 32, 16); 188 | const sphereTriangles = sphere.toTriangles(); 189 | 190 | this.prg1 = new GLea({ 191 | shaders: [GLea.vertexShader(vert), GLea.fragmentShader(frag)], 192 | }).create(); 193 | 194 | this.prg2 = this.prg1.add({ 195 | shaders: [GLea.vertexShader(vert2), GLea.fragmentShader(frag2)], 196 | buffers: { 197 | position: GLea.buffer(3, sphereTriangles), 198 | }, 199 | }); 200 | 201 | window.addEventListener('resize', this.onResize, false); 202 | this.onResize(); 203 | } 204 | 205 | destroy() { 206 | window.removeEventListener('resize', this.onResize); 207 | } 208 | 209 | loop(time = 0) { 210 | const { gl, width, height } = this.prg1; 211 | const { prg1, prg2 } = this; 212 | 213 | prg1.clear(); 214 | 215 | // Shader 1 does the background animation 216 | gl.disable(gl.DEPTH_TEST); 217 | prg1.enableAttribs(); 218 | prg1.uniV('resolution', [width, height]); 219 | prg1.uni('time', time * 1e-3); 220 | prg1.drawArrays(gl.TRIANGLE_STRIP); 221 | prg1.disableAttribs(); 222 | 223 | // Shader 2 renders a sphere 224 | gl.enable(gl.DEPTH_TEST); 225 | prg2.enableAttribs(); 226 | prg2.uniV('resolution', [width, height]); 227 | prg2.uni('time', time * 1e-3); 228 | prg2.uniM('uPM', this.projectionMat.toArray()); 229 | prg2.uniM('uVM', this.viewMat.toArray()); 230 | prg2.drawArrays(gl.TRIANGLES); 231 | prg2.disableAttribs(); 232 | 233 | requestAnimationFrame(this.loop); 234 | } 235 | 236 | onResize() { 237 | this.prg1.resize(); 238 | this.setProjectionMatrix(); 239 | } 240 | } 241 | 242 | const app = new App(); 243 | 244 | app.setup(); 245 | app.loop(); 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GLea - GL experience artistry 2 | 3 | GLea is a low-level WebGL library with a minimal footprint. 4 | It provides helper functions for creating a WebGL program, compiling shaders and passing data from JavaScript to the shader language. 5 | 6 | ## Introduction 7 | 8 | There are several options to embed GLea into your project. You can load GLea directly via script tag: 9 | 10 | ```html 11 | 12 | ``` 13 | 14 | Inside a JavaScript ES module: 15 | 16 | ```js 17 | import GLea from 'https://cdn.skypack.dev/glea'; 18 | ``` 19 | 20 | Or via NPM, you can install GLea via `npm i glea` and import it like this: 21 | 22 | ```js 23 | import GLea from 'glea'; 24 | ``` 25 | 26 | ### Initialization 27 | 28 | By default, GLea looks for a canvas element in your HTML and uses that. If there is no canvas element existing, GLea creates one for you. 29 | 30 | If your HTML document doesn't include any CSS (neither a `style` nor a `link` tag, a minimal stylesheet is provided that sizes the canvas to the browser's viewport size. 31 | 32 | The GLea instance expects a shaders property, containing your fragment and vertex shader. 33 | Also, a buffers property, which contains the data that is passed as attributes to the vertex shader. 34 | 35 | If no buffers are provided, GLea provides a default position attribute with a buffer containing 4 vec2 values for a triangle strip, defining a plane filling the screen. 36 | 37 | ### Setting uniforms 38 | 39 | GLea provides several helper functions to set uniforms to pass data from JavaScript to GLSL. These are: 40 | 41 | ```js 42 | // set uniform float 43 | glea.uni('pi', Math.PI); 44 | 45 | // set uniform int 46 | glea.uniI('width', innerWidth); 47 | 48 | // set uniform float vector (supported types are vec2, vec3, vec4) 49 | glea.uniV('vector', [Math.sqrt(2), Math.sqrt(3)]); 50 | 51 | // set uniform int vector 52 | glea.uniIV('resolution', [innerWidth, innerHeight]); 53 | 54 | // set uniform matrix 55 | // HEADS UP: it is the other way round as you would write it down on paper 56 | // prettier-ignore 57 | glea.uniM('translateMatrix', [ 58 | 1, 0, 0, 0, // column 1 59 | 0, 1, 0, 0, // column 2 60 | 0, 0, 1, 0, // column 3 61 | x, y, z, 1, // column 4 62 | ]); 63 | ``` 64 | 65 | ### Draw 66 | 67 | GLea provides a wrapper to [drawArrays](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawArrays) from the underlying WebGLRenderingContext. It works exactly like the original drawArrays function, but if you don't provide any vertex count, it is determined 68 | automatically from the buffers. 69 | 70 | ```js 71 | const { gl } = glea; 72 | 73 | glea.drawArrays(gl.TRIANGLE_STRIP); 74 | 75 | // The same as: 76 | const numVertices = 4; 77 | glea.gl.drawArrays(gl.TRIANGLE_STRIP, 0, numVertices); 78 | ``` 79 | 80 | ### Multiple programs and switching 81 | 82 | GLea supports multiple programs. 83 | 84 | ```js 85 | const prg1 = new GLea({ 86 | shaders: [GLea.vertexShader(vert), GLea.fragmentShader(frag)], 87 | }).create(); 88 | 89 | const prg2 = prg1.add({ 90 | shaders: [GLea.vertexShader(vert2), GLea.fragmentShader(frag2)], 91 | buffers: { 92 | position: GLea.buffer(3, Ella.Geometry.sphere(0.25, 32, 16).toTriangles()), 93 | }, 94 | }); 95 | ``` 96 | 97 | The the main instance `prg1` and its child `prg2` use the same underlying WebGLRenderingContext. 98 | In the example `prg1` renders a plane geometry (GLea provides a `position` attribute with a plane geometry by default), 99 | and `prg2` provides a sphere geometry. The sphere geometry is provided by [ella-math](https://github.com/terabaud/ella-math). 100 | 101 | In the draw loop, the switching between programs is done via `enableAttribs` and `disableAttribs`: 102 | 103 | ```js 104 | // Shader 1 does the background animation 105 | prg1.gl.disable(gl.DEPTH_TEST); 106 | prg1.enableAttribs(); 107 | prg1.uniV('resolution', [width, height]); 108 | prg1.uni('time', time * 1e-3); 109 | prg1.drawArrays(gl.TRIANGLE_STRIP); 110 | prg1.disableAttribs(); 111 | 112 | // Shader 2 renders a sphere 113 | gl.enable(gl.DEPTH_TEST); 114 | prg2.enableAttribs(); 115 | prg2.uniV('resolution', [width, height]); 116 | prg2.uni('time', time * 1e-3); 117 | prg2.uniM('uPM', this.projectionMat.toArray()); 118 | prg2.uniM('uVM', this.viewMat.toArray()); 119 | prg2.drawArrays(gl.TRIANGLES); 120 | prg2.disableAttribs(); 121 | ``` 122 | 123 | [Full example](https://codepen.io/terabaud/pen/wvMQQyr) 124 | 125 | ### Loading textures 126 | 127 | I'm using a loadImage helper function that wraps `img.onload` into a Promise: 128 | 129 | ```js 130 | function loadImage(url) { 131 | return new Promise((resolve, reject) => { 132 | const img = new Image(); 133 | img.crossOrigin = 'Anonymous'; 134 | img.src = url; 135 | img.onload = () => { 136 | resolve(img); 137 | }; 138 | img.onerror = () => { 139 | reject(img); 140 | }; 141 | }); 142 | } 143 | 144 | async function setup() { 145 | const img = await loadImage('https://placekitten.com/256/256/'); 146 | const textureIndex = 0; 147 | glea.createTexture(textureIndex); 148 | glea.gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 149 | glea.uniI('texture0', textureIndex); 150 | } 151 | 152 | setup(); 153 | ``` 154 | 155 | In GLSL, you can access the texture like this: 156 | 157 | ```glsl 158 | uniform sampler2D texture0; 159 | 160 | void main() { 161 | vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height); 162 | gl_FragColor = texture2D(texture1, coord); 163 | } 164 | ``` 165 | 166 | ## Example 167 | 168 | ```js 169 | import GLea from 'https://cdn.skypack.dev/glea'; 170 | 171 | const vert = ` 172 | precision highp float; 173 | attribute vec2 position; 174 | 175 | void main() { 176 | gl_Position = vec4(position, 0, 1.0); 177 | } 178 | `; 179 | 180 | const frag = ` 181 | precision highp float; 182 | uniform float time; 183 | uniform vec2 resolution; 184 | 185 | void main() { 186 | float vmin = min(resolution.y, resolution.x); 187 | vec2 p = (gl_FragCoord.xy - .5 * resolution) / vmin; 188 | float r = .5 + .5 * sin(5. * log(length(p)) - time * 1.2); 189 | float g = .5 + .5 * sin(5. * log(length(p)) + sin(time + 2. * p.x)); 190 | float b = .5 + .5 * sin(.2 + 5. * log(length(p)) + sin(time * .4 + 4. * p.y)); 191 | gl_FragColor = vec4(r, g, b, 1.); 192 | } 193 | `; 194 | 195 | const glea = new GLea({ 196 | shaders: [GLea.fragmentShader(frag), GLea.vertexShader(vert)], 197 | buffers: { 198 | // create a position attribute bound 199 | // to a buffer with 4 2D coordinates 200 | // this is what GLea provides by default if you omit buffers in the constructor 201 | position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]), 202 | }, 203 | }).create(); 204 | 205 | function loop(time) { 206 | const { gl, width, height } = glea; 207 | glea.clear(); 208 | glea.uniV('resolution', [width, height]); 209 | glea.uni('time', time * 1e-3); 210 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 211 | requestAnimationFrame(loop); 212 | } 213 | 214 | function setup() { 215 | const { gl } = glea; 216 | window.addEventListener('resize', () => { 217 | glea.resize(); 218 | }); 219 | loop(0); 220 | } 221 | 222 | setup(); 223 | ``` 224 | 225 | - [Try this on Codepen](https://codepen.io/terabaud/pen/PoPJqvM) 226 | 227 | ## Exampes 228 | 229 | - [Example 01: Triangle](https://codepen.io/terabaud/pen/OKVpYV) 230 | - [Example 02: Full screen plane](https://codepen.io/terabaud/pen/eqNjjY) 231 | - [Example 03: Cube](https://codepen.io/terabaud/pen/EqgpbQ) 232 | - [Example 04: Circle Heart Morph](https://codepen.io/terabaud/pen/BaNRbXL) 233 | - [Example 05: Rotating heart yin yang morph pattern](https://codepen.io/terabaud/pen/VwLbVjE) 234 | - [Example 06: Fun with circles](https://codepen.io/terabaud/pen/xxGdeEe) 235 | - [Example 07: Retro Style Dither Cam](https://codepen.io/terabaud/pen/WNvoOgK) 236 | - [Example 08: Signed Distance Field Symmetric Diff](https://codepen.io/terabaud/pen/dyoXjVv) 237 | - [Example 09: Hypnotizing Cyclone 2.0](https://codepen.io/terabaud/pen/PowKxNp) 238 | - [Example 10: Hypnotizing Cyclone 3.0](https://codepen.io/terabaud/pen/bGNMGvb) 239 | - [Example 11: numeric spiral](https://codepen.io/terabaud/pen/poogqxq) 240 | - [Example 12: Evil virus](https://codepen.io/terabaud/pen/ZgreLo) 241 | - [Example 13: Halftone Circles](https://codepen.io/terabaud/pen/BajJbgd) 242 | 243 | ### More examples 244 | 245 | - There is more: https://terabaud.github.io/hello-webgl/ 246 | 247 | ## Additional WebGL resources 248 | 249 | - [WebGL Fundamentals](https://webglfundamentals.org/) 250 | - [WebGL 2 Fundamentals](https://webgl2fundamentals.org/) 251 | -------------------------------------------------------------------------------- /dist/glea.d.ts: -------------------------------------------------------------------------------- 1 | declare module "glea" { 2 | /** 3 | * GLea - GL experience artistry Library 4 | * @module glea 5 | */ 6 | export type GLeaContext = WebGLRenderingContext | WebGL2RenderingContext; 7 | /** 8 | * store for an attribute and a buffer 9 | */ 10 | export type GLeaBuffer = { 11 | id: WebGLBuffer; 12 | name: string; 13 | data: ArrayBuffer; 14 | loc: number; 15 | type: number; 16 | size: number; 17 | normalized: boolean; 18 | stride: number; 19 | offset: number; 20 | }; 21 | /** 22 | * function that compiles a shader 23 | */ 24 | export type GLeaShaderFactory = { 25 | shaderType: string; 26 | init: (gl: GLeaContext) => WebGLShader; 27 | }; 28 | /** 29 | * function that registers an attribute and binds a buffer to it 30 | */ 31 | export type GLeaBufferFactory = (name: string, gl: GLeaContext, program: WebGLProgram) => GLeaBuffer; 32 | export type GLeaConstructorParams = { 33 | canvas?: HTMLCanvasElement; 34 | gl?: WebGLRenderingContext | WebGL2RenderingContext; 35 | contextType?: string; 36 | shaders: GLeaShaderFactory[]; 37 | buffers?: Record; 38 | devicePixelRatio?: number; 39 | glOptions?: WebGLContextAttributes; 40 | }; 41 | /** Class GLea */ 42 | class GLea { 43 | canvas: HTMLCanvasElement; 44 | contextType: string; 45 | glOptions?: WebGLContextAttributes; 46 | gl: WebGLRenderingContext | WebGL2RenderingContext; 47 | shaderFactory: GLeaShaderFactory[]; 48 | bufferFactory: Record; 49 | program: WebGLProgram; 50 | buffers: Record; 51 | textures: WebGLTexture[]; 52 | devicePixelRatio: number; 53 | parent?: GLea; 54 | constructor({ canvas, gl, contextType, shaders, buffers, devicePixelRatio, glOptions, }: GLeaConstructorParams); 55 | /** 56 | * By default, GLea provides a position buffer containing 4 2D coordinates 57 | * A triangle strip plane that consists of 2 triangles 58 | */ 59 | private getDefaultBuffers; 60 | /** 61 | * Used to create a WebGLRenderingContext 62 | * @param contextType webgl or webgl2. Also detects if webgl is only available via the context `experimental-webgl` 63 | * @param glOptions see WebGLContextAttributes 64 | */ 65 | private getContext; 66 | /** 67 | * Create a vertex shader 68 | * 69 | * @param code shader code 70 | */ 71 | static vertexShader(code?: string): GLeaShaderFactory; 72 | /** 73 | * Create a fragment shader 74 | * 75 | * @param {string} code fragment shader code 76 | */ 77 | static fragmentShader(code?: string): GLeaShaderFactory; 78 | /** 79 | * Create a webgl program from a vertex and fragment shader (no matter which order) 80 | * @param shader1 a factory created by GLea.vertexShader or GLea.fragmentShader 81 | * @param shader2 a factory created by GLea.vertexShader or GLea.fragmentShader 82 | */ 83 | private prog; 84 | /** 85 | * Create Buffer 86 | * 87 | * @param {number} size record size (2 for vec2, 3 for vec3, 4 for vec4) 88 | * @param {number[]} data buffer data 89 | * @param {number} usage usage, by default gl.STATIC_DRAW 90 | * @param {number} type data type, by default gl.FLOAT 91 | * @param {boolean} normalized normalize data, false 92 | * @param {number} stride stride, by default 0 93 | * @param {number} offset offset, by default 0 94 | */ 95 | static buffer(size: number, data: number[] | Uint8Array | Float32Array, usage?: number, type?: number, normalized?: boolean, stride?: number, offset?: number): GLeaBufferFactory; 96 | /** 97 | * Wrapper for gl.drawArrays 98 | * 99 | * @param {number} drawMode gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, ... 100 | * @param {number} first offset of first vertex 101 | * @param {number} count count of vertices. If not provided, it is determined from the provided buffers 102 | */ 103 | drawArrays(drawMode: number, first?: number, count?: number): void; 104 | /** 105 | * Disable attribs (useful for switching between GLea instances) 106 | */ 107 | disableAttribs(): void; 108 | /** 109 | * Enable attribs 110 | */ 111 | enableAttribs(): void; 112 | /** 113 | * init WebGLRenderingContext 114 | * @returns {GLea} glea instance 115 | */ 116 | create(): this; 117 | private replaceCanvas; 118 | /** 119 | * Deletes the canvas element and replaces it with a cloned node and calls create() again 120 | */ 121 | restart(): this; 122 | /** 123 | * Create a new instance with another program and reuse the rendering context 124 | * @param param0 buffers and shaders 125 | */ 126 | add({ shaders, buffers }: GLeaConstructorParams): GLea; 127 | /** 128 | * Set active texture 129 | * @param {number} textureIndex texture index in the range [0 .. gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] 130 | * @param {WebGLTexture} texture webgl texture object 131 | */ 132 | setActiveTexture(textureIndex: number, texture: WebGLTexture): void; 133 | /** 134 | * @typedef GLeaTextureOptions 135 | * @property {string} textureWrapS default: clampToEdge 136 | * @property {string} textureWrapT default: clampToEdge 137 | * @property {string} textureMinFilter default: nearest 138 | * @property {string} textureMagFilter default: nearest 139 | */ 140 | /** 141 | * Create a texture object 142 | * 143 | * @param {number} textureIndex 144 | * @param {GLeaTextureOptions} params configuration options 145 | * @returns texture WebGLTexture object 146 | */ 147 | createTexture(textureIndex?: number, params?: Record): WebGLTexture; 148 | /** 149 | * Update buffer data 150 | * @param {string} name name 151 | * @param {number} offset default: 0 152 | */ 153 | updateBuffer(name: string, offset?: number): void; 154 | /** 155 | * Resize canvas and webgl viewport 156 | */ 157 | resize(): void; 158 | /** 159 | * Get canvas width 160 | * @returns {number} canvas width 161 | */ 162 | get width(): number; 163 | /** 164 | * Get canvas height 165 | * @returns {number} canvas height 166 | */ 167 | get height(): number; 168 | /** 169 | * Use program 170 | */ 171 | use(): GLea; 172 | /** 173 | * set uniform matrix (mat2, mat3, mat4) 174 | * @param name uniform name 175 | * @param data array of numbers (4 for mat2, 9 for mat3, 16 for mat4) 176 | * @returns location id of the uniform 177 | */ 178 | uniM(name: string, data: Float32Array | number[]): WebGLUniformLocation; 179 | /** 180 | * Set uniform float vector 181 | * 182 | * @param {string} name uniform variable name 183 | * @param {number[]} data uniform float vector 184 | */ 185 | uniV(name: string, data: Float32Array | number[]): WebGLUniformLocation; 186 | /** 187 | * Set uniform int vector 188 | * 189 | * @param {string} name uniform variable name 190 | * @param {number[]} data uniform int vector 191 | * @returns uniform location 192 | */ 193 | uniIV(name: string, data: Int32Array | number[]): WebGLUniformLocation; 194 | /** 195 | * Set uniform float 196 | * 197 | * @param {string} name uniform variable name 198 | * @param {number} data data 199 | */ 200 | uni(name: string, data: number): WebGLUniformLocation; 201 | /** 202 | * Set uniform int 203 | * @param {string} name uniform variable name 204 | * @param {number} data data 205 | */ 206 | uniI(name: string, data: number): void; 207 | /** 208 | * Clear screen 209 | * 210 | * @param {number[]} clearColor 211 | */ 212 | clear(clearColor?: number[] | null): void; 213 | /** 214 | * destroys the WebGLRendering context by deleting all textures, buffers and shaders. 215 | * Additionally, it calls loseContext. 216 | * Also the canvas element is removed from the DOM and replaced by a new cloned canvas element 217 | */ 218 | destroy(): void; 219 | } 220 | export default GLea; 221 | } 222 | -------------------------------------------------------------------------------- /examples/vendor/ella.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vector' { 2 | export class Vec { 3 | readonly values: number[]; 4 | constructor(...values: number[]); 5 | get dim(): number; 6 | get x(): number; 7 | get y(): number; 8 | get z(): number; 9 | get w(): number; 10 | get xy(): Vec; 11 | get xz(): Vec; 12 | get yz(): Vec; 13 | get xyz(): Vec; 14 | /** 15 | * Create vector from Array 16 | * @param arr array of numbers 17 | */ 18 | static fromArray(arr: number[]): Vec; 19 | /** 20 | * Create vector with x = y = n 21 | * @param n the number 22 | * @param dim the dimension 23 | */ 24 | static fromNumber(n: number, dim: number): Vec; 25 | /** 26 | * clone vector 27 | */ 28 | clone(): Vec; 29 | /** 30 | * add vector 31 | * @param otherVec addend 32 | * @returns addition result 33 | */ 34 | add(otherVec: Vec): Vec; 35 | /** 36 | * subtract vector 37 | * @param otherVec addend 38 | * @returns subtraction result 39 | */ 40 | sub(otherVec: Vec): Vec; 41 | /** 42 | * multiply vector with scalar 43 | * @param value scalar 44 | * @returns multiplication result 45 | */ 46 | mul(value: number): Vec; 47 | /** 48 | * divide vector with scalar 49 | * @param value scalar 50 | * @returns multiplication result 51 | */ 52 | div(value: number): Vec; 53 | /** 54 | * dot product 55 | * @param otherVec 56 | */ 57 | dot(otherVec: Vec): number; 58 | /** 59 | * check for equality 60 | * @param otherVec 61 | */ 62 | equals(otherVec: Vec): boolean; 63 | /** 64 | * Calculate length 65 | */ 66 | get length(): number; 67 | /** 68 | * Convert to array 69 | */ 70 | toArray(): number[]; 71 | /** 72 | * Convert to string, in the form of `(x, y)` 73 | */ 74 | toString(): string; 75 | /** 76 | * cross product 77 | * @param otherVec 78 | * @returns new Vec3 instance containing cross product 79 | */ 80 | cross(otherVec: Vec): Vec; 81 | /** 82 | * normalized vector, 83 | * @returns vector normalized to length = 1 84 | */ 85 | get normalized(): Vec; 86 | } 87 | } 88 | declare module 'matrix' { 89 | import { Vec } from 'vector'; 90 | /** @class Mat */ 91 | export class Mat { 92 | values: number[]; 93 | numRows: number; 94 | numCols: number; 95 | constructor( 96 | values: number[], 97 | options?: { 98 | numRows: number; 99 | numCols: number; 100 | } 101 | ); 102 | /** 103 | * create identity matrix 104 | * @param dimension dimension of the matrix 105 | */ 106 | static identity(dimension: number): Mat; 107 | /** 108 | * Converts a vector with dimension n into a matrix with 1 col and n rows 109 | * useful for matrix multiplication 110 | * @param value the input vector 111 | */ 112 | static fromVector(value: Vec): Mat; 113 | /** 114 | * Converts a bunch of vectors into a matrix 115 | */ 116 | static fromVectors(vectors: Vec[]): Mat; 117 | /** 118 | * convert to array 119 | */ 120 | toArray(): number[]; 121 | /** 122 | * get value at a given position 123 | * @param row row index starting from 0 124 | * @param column column index starting from 0 125 | */ 126 | valueAt(row: number, column: number): number; 127 | /** 128 | * get column at a given index 129 | * @param column index of column starting from 0 130 | * @returns column as an array of numbers 131 | */ 132 | colAt(column: number): number[]; 133 | /** 134 | * get row at a given index 135 | * @param row index of row starting from 0 136 | * @returns row as an array of numbers 137 | */ 138 | rowAt(row: number): number[]; 139 | /** 140 | * returns transposed matrix 141 | */ 142 | transpose(): Mat; 143 | /** 144 | * check for equality 145 | * @param otherMatrix matrix to compare 146 | * @returns true or false 147 | */ 148 | equals(otherMatrix: Mat): boolean; 149 | /** 150 | * add two matrices 151 | * @param otherMatrix matrix to add 152 | * @returns result matrix 153 | */ 154 | add(otherMatrix: Mat): Mat; 155 | /** 156 | * subtract another matrix 157 | * @param otherMatrix matrix to subtract 158 | * @returns result matrix 159 | */ 160 | sub(otherMatrix: Mat): Mat; 161 | /** 162 | * matrix multiplication 163 | * @param param can be a matrix, a number or a vector 164 | * @returns a vector in case of matrix vector multiplication, else a matrix 165 | * @throws dimension Mismatch if dimensions doesn't match 166 | */ 167 | mul(param: Mat | number | Vec): Mat | Vec; 168 | /** 169 | * calculate determinant 170 | */ 171 | determinant(): number; 172 | /** 173 | * convert matrix to string 174 | * @returns a string containing matROWSxCOLS(comma-separated-values) 175 | */ 176 | toString(): string; 177 | } 178 | export const Mat2: { 179 | /** 180 | * create rotation matrix 181 | * @param angle angle in radians 182 | */ 183 | rotation(angle: number): Mat; 184 | /** 185 | * create scaling matrix 186 | * @param sx X-scale factor 187 | * @param sy Y-scale factor 188 | */ 189 | scaling(sx: number, sy: number): Mat; 190 | }; 191 | export const Mat3: { 192 | /** 193 | * create translation matrix 194 | * @param x translation in x-direction 195 | * @param y translation in y-direction 196 | * @returns 3x3 translation matrix 197 | */ 198 | translation(x: number, y: number): Mat; 199 | /** 200 | * create scaling matrix 201 | * @param sx scale X factor 202 | * @param sy scale Y factor 203 | * @param sz scale Z factor 204 | * @returns 3x3 scale matrix 205 | */ 206 | scaling(sx: number, sy: number, sz: number): Mat; 207 | /** 208 | * create X-rotation matrix 209 | * @param angle rotation in radians 210 | */ 211 | rotX(angle: number): Mat; 212 | /** 213 | * create Y-rotation matrix 214 | * @param angle angle in radians 215 | */ 216 | rotY(angle: number): Mat; 217 | /** 218 | * create Z-rotation matrix 219 | * @param angle angle in radians 220 | */ 221 | rotZ(angle: number): Mat; 222 | }; 223 | export const Mat4: { 224 | /** 225 | * create 4x4 identity matrix 226 | */ 227 | identity(): Mat; 228 | /** 229 | * create translation matrix 230 | * @param x translation in X direction 231 | * @param y translation in Y direction 232 | * @param z translation in Z direction 233 | */ 234 | translation(x: number, y: number, z: number): Mat; 235 | /** 236 | * create scaling matrix 237 | * @param sx X-scale factor 238 | * @param sy Y-scale factor 239 | * @param sz Z-scale factor 240 | */ 241 | scaling(sx: number, sy: number, sz: number): Mat; 242 | /** 243 | * create x-rotation matrix 244 | * @param angle angle in radians 245 | */ 246 | rotX(angle: number): Mat; 247 | /** 248 | * create y-rotation matrix 249 | * @param angle angle in radians 250 | */ 251 | rotY(angle: number): Mat; 252 | /** 253 | * create z-rotation matrix 254 | * @param angle angle in radians 255 | */ 256 | rotZ(angle: number): Mat; 257 | }; 258 | } 259 | declare module 'lookat' { 260 | import { Vec } from 'vector'; 261 | import { Mat } from 'matrix'; 262 | /** 263 | * Create a view matrix 264 | * @param eye position of the eye is (where you are) 265 | * @param center position where you want to look at 266 | * @param up it's a normalized vector, quite often (0,1,0) 267 | * @returns view matrix 268 | * @see https://www.khronos.org/opengl/wiki/GluLookAt_code 269 | */ 270 | export function lookAt(eye: Vec, center: Vec, up: Vec): Mat; 271 | } 272 | declare module 'perspective' { 273 | import { Mat } from 'matrix'; 274 | /** 275 | * creates a transformation that produces a parallel projection 276 | * @param left coordinate for the left vertical clipping planes. 277 | * @param right coordinate for the right vertical clipping planes. 278 | * @param bottom coordinate for the bottom horizontal clippling pane. 279 | * @param top coordinate for the top horizontal clipping pane 280 | * @param zNear Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer. 281 | * @param zFar Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer. 282 | * @returns 4x4 orthographic transformation matrix 283 | * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glOrtho.xml 284 | */ 285 | export function ortho( 286 | left: number, 287 | right: number, 288 | bottom: number, 289 | top: number, 290 | zNear: number, 291 | zFar: number 292 | ): Mat; 293 | /** 294 | * creates a perspective matrix that produces a perspective projection 295 | * @param left coordinates for the vertical left clipping pane 296 | * @param right coordinates for the vertical right clipping pane 297 | * @param bottom coordinates for the horizontal bottom clipping pane 298 | * @param top coodinates for the top horizontal clipping pane 299 | * @param zNear Specify the distances to the near depth clipping plane. Must be positive. 300 | * @param zFar Specify the distances to the far depth clipping planes. Must be positive. 301 | * @returns 4x4 perspective projection matrix 302 | * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml 303 | */ 304 | export function frustum( 305 | left: number, 306 | right: number, 307 | bottom: number, 308 | top: number, 309 | zNear: number, 310 | zFar: number 311 | ): Mat; 312 | /** 313 | * creates a perspective projection matrix 314 | * @param fieldOfView Specifies the field of view angle, in degrees, in the y direction. 315 | * @param aspectRatio Specifies the aspect ratio that determines the field of view in the x direction. The aspect ratio is the ratio of x (width) to y (height). 316 | * @param zNear Specifies the distance from the viewer to the near clipping plane (always positive). 317 | * @param zFar Specifies the distance from the viewer to the far clipping plane (always positive). 318 | * @returns 4x4 perspective projection matrix 319 | */ 320 | export function perspective( 321 | fieldOfView: number, 322 | aspectRatio: number, 323 | zNear: number, 324 | zFar: number 325 | ): Mat; 326 | } 327 | declare module 'geometry' { 328 | import { Vec } from 'vector'; 329 | /** @class Geometry */ 330 | export class Geometry { 331 | vertices: Vec[]; 332 | faces: number[][]; 333 | normals: Vec[]; 334 | texCoords: Vec[]; 335 | constructor( 336 | vertices: Vec[], 337 | faces: number[][], 338 | normals: Vec[], 339 | texCoords: Vec[] 340 | ); 341 | /** 342 | * converts to triangle array 343 | */ 344 | toTriangles(): number[]; 345 | /** 346 | * Calculate the surface normal of a triangle 347 | * @param p1 3d vector of point 1 348 | * @param p2 3d vector of point 2 349 | * @param p3 3d vector of point 3 350 | */ 351 | static calculateSurfaceNormal(p1: Vec, p2: Vec, p3: Vec): Vec; 352 | /** 353 | * Create a box geometry with the sizes a * b * c, 354 | * centered at (0, 0, 0), 2 triangles per side. 355 | * 356 | * @name box 357 | * @param {number} sizeA 358 | * @param {number} sizeB 359 | * @param {number} sizeC 360 | */ 361 | static box(sizeA?: number, sizeB?: number, sizeC?: number): Geometry; 362 | /** 363 | * create a cube 364 | * @param size 365 | */ 366 | static cube(size?: number): Geometry; 367 | /** 368 | * create a plane grid mesh 369 | * @param x x-coord of the top left corner 370 | * @param y y-coord of the top left corner 371 | * @param width width of the plane 372 | * @param height height of the plane 373 | * @param rows number of rows 374 | * @param cols number of columns 375 | */ 376 | static grid( 377 | x: number, 378 | y: number, 379 | width: number, 380 | height: number, 381 | rows: number, 382 | cols: number 383 | ): Geometry; 384 | /** 385 | * Create sphere geometry 386 | * @param r radius 387 | * @param sides number of sides (around the sphere) 388 | * @param segments number of segments (from top to bottom) 389 | * @see adapted from https://vorg.github.io/pex/docs/pex-gen/Sphere.html 390 | */ 391 | static sphere(r?: number, sides?: number, segments?: number): Geometry; 392 | } 393 | } 394 | declare module 'ella' { 395 | import { Vec } from 'vector'; 396 | import { Mat, Mat2, Mat3, Mat4 } from 'matrix'; 397 | import { lookAt } from 'lookat'; 398 | import { perspective, frustum, ortho } from 'perspective'; 399 | import { Geometry } from 'geometry'; 400 | export { 401 | Vec, 402 | Mat, 403 | Mat2, 404 | Mat3, 405 | Mat4, 406 | Geometry, 407 | perspective, 408 | frustum, 409 | ortho, 410 | lookAt, 411 | }; 412 | } 413 | -------------------------------------------------------------------------------- /dist/glea.js: -------------------------------------------------------------------------------- 1 | const SHADER_HEAD = 'precision highp float;'; 2 | const VERT_DEFAULT = SHADER_HEAD + 3 | 'attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}'; 4 | const FRAG_DEFAULT = SHADER_HEAD + 5 | `precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}`; 6 | /** 7 | * @hidden hide internal function from documentation 8 | */ 9 | function convertArray(data, type = WebGLRenderingContext.FLOAT) { 10 | if (type === WebGLRenderingContext.FLOAT) { 11 | return new Float32Array(data); 12 | } 13 | if (type === WebGLRenderingContext.BYTE) { 14 | return new Uint8Array(data); 15 | } 16 | throw Error('type not supported'); 17 | } 18 | /** 19 | * @hidden hide internal function from documentation 20 | */ 21 | function shader(code, shaderType) { 22 | const init = (gl) => { 23 | const glShaderType = /frag/.test(shaderType) 24 | ? WebGLRenderingContext.FRAGMENT_SHADER 25 | : WebGLRenderingContext.VERTEX_SHADER; 26 | const sh = gl.createShader(glShaderType); 27 | if (!sh) { 28 | throw Error('shader type not supported'); 29 | } 30 | gl.shaderSource(sh, code); 31 | gl.compileShader(sh); 32 | if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { 33 | throw 'Could not compile Shader.\n\n' + gl.getShaderInfoLog(sh); 34 | } 35 | return sh; 36 | }; 37 | return { 38 | shaderType, 39 | init, 40 | }; 41 | } 42 | /** Class GLea */ 43 | class GLea { 44 | constructor({ canvas, gl, contextType = 'webgl', shaders, buffers, devicePixelRatio = 1, glOptions, }) { 45 | this.canvas = document.createElement('canvas'); 46 | this.canvas = canvas || document.querySelector('canvas'); 47 | if (!this.canvas) { 48 | this.canvas = document.createElement('canvas'); 49 | document.body.appendChild(this.canvas); 50 | } 51 | if (!document.querySelector('link[rel=stylesheet], style')) { 52 | // if there's no css, provide some minimal defaults 53 | const style = document.createElement('style'); 54 | style.innerHTML = 55 | 'body{margin:0}canvas{display:block;width:100vw;height:100vh}'; 56 | document.head.appendChild(style); 57 | } 58 | this.contextType = contextType; 59 | this.glOptions = glOptions; 60 | this.gl = gl || this.getContext(contextType, glOptions); 61 | const program = this.gl.createProgram(); 62 | this.program = program; 63 | this.buffers = {}; 64 | this.shaderFactory = shaders; 65 | this.bufferFactory = buffers || this.getDefaultBuffers(); 66 | this.textures = []; 67 | this.devicePixelRatio = devicePixelRatio; 68 | } 69 | /** 70 | * By default, GLea provides a position buffer containing 4 2D coordinates 71 | * A triangle strip plane that consists of 2 triangles 72 | */ 73 | getDefaultBuffers() { 74 | return { 75 | // create a position attribute bound 76 | // to a buffer with 4 2D coordinates 77 | position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]), 78 | }; 79 | } 80 | /** 81 | * Used to create a WebGLRenderingContext 82 | * @param contextType webgl or webgl2. Also detects if webgl is only available via the context `experimental-webgl` 83 | * @param glOptions see WebGLContextAttributes 84 | */ 85 | getContext(contextType, glOptions) { 86 | if (contextType === 'webgl') { 87 | return (this.canvas.getContext('webgl', glOptions) || 88 | this.canvas.getContext('experimental-webgl', glOptions)); 89 | } 90 | if (contextType === 'webgl2') { 91 | return this.canvas.getContext('webgl2', glOptions); 92 | } 93 | throw Error(`no ${contextType} context available.`); 94 | } 95 | /** 96 | * Create a vertex shader 97 | * 98 | * @param code shader code 99 | */ 100 | static vertexShader(code = VERT_DEFAULT) { 101 | return shader(code, 'vert'); 102 | } 103 | /** 104 | * Create a fragment shader 105 | * 106 | * @param {string} code fragment shader code 107 | */ 108 | static fragmentShader(code = FRAG_DEFAULT) { 109 | return shader(code, 'frag'); 110 | } 111 | /** 112 | * Create a webgl program from a vertex and fragment shader (no matter which order) 113 | * @param shader1 a factory created by GLea.vertexShader or GLea.fragmentShader 114 | * @param shader2 a factory created by GLea.vertexShader or GLea.fragmentShader 115 | */ 116 | prog(gl, shader1, shader2) { 117 | const p = gl.createProgram(); 118 | const s1 = shader1.init(gl); 119 | const s2 = shader2.init(gl); 120 | gl.attachShader(p, s1); 121 | gl.attachShader(p, s2); 122 | gl.linkProgram(p); 123 | gl.validateProgram(p); 124 | if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { 125 | const info = gl.getProgramInfoLog(p); 126 | throw 'Could not compile WebGL program. \n\n' + info; 127 | } 128 | return p; 129 | } 130 | /** 131 | * Create Buffer 132 | * 133 | * @param {number} size record size (2 for vec2, 3 for vec3, 4 for vec4) 134 | * @param {number[]} data buffer data 135 | * @param {number} usage usage, by default gl.STATIC_DRAW 136 | * @param {number} type data type, by default gl.FLOAT 137 | * @param {boolean} normalized normalize data, false 138 | * @param {number} stride stride, by default 0 139 | * @param {number} offset offset, by default 0 140 | */ 141 | static buffer(size, data, usage = WebGLRenderingContext.STATIC_DRAW, type = WebGLRenderingContext.FLOAT, normalized = false, stride = 0, offset = 0) { 142 | return (name, gl, program) => { 143 | const loc = gl.getAttribLocation(program, name); 144 | gl.enableVertexAttribArray(loc); 145 | // create buffer: 146 | const id = gl.createBuffer(); 147 | const bufferData = data instanceof Array ? convertArray(data, type) : data; 148 | gl.bindBuffer(gl.ARRAY_BUFFER, id); 149 | gl.bufferData(gl.ARRAY_BUFFER, bufferData, usage); 150 | gl.vertexAttribPointer(loc, size, type, normalized, stride, offset); 151 | return { 152 | id, 153 | name, 154 | data: bufferData, 155 | loc, 156 | type, 157 | size, 158 | normalized, 159 | stride, 160 | offset, 161 | }; 162 | }; 163 | } 164 | /** 165 | * Wrapper for gl.drawArrays 166 | * 167 | * @param {number} drawMode gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, ... 168 | * @param {number} first offset of first vertex 169 | * @param {number} count count of vertices. If not provided, it is determined from the provided buffers 170 | */ 171 | drawArrays(drawMode, first = 0, count) { 172 | if (typeof count === 'undefined') { 173 | const attributes = Object.keys(this.buffers); 174 | if (attributes.length === 0) { 175 | return; 176 | } 177 | const firstAttributeName = attributes[0]; 178 | const firstBuffer = this.buffers[firstAttributeName]; 179 | const len = firstBuffer.data.length; 180 | count = len / firstBuffer.size; 181 | } 182 | this.gl.drawArrays(drawMode, first, count); 183 | } 184 | /** 185 | * Disable attribs (useful for switching between GLea instances) 186 | */ 187 | disableAttribs() { 188 | const { gl, program, buffers } = this; 189 | for (let key of Object.keys(buffers)) { 190 | const loc = gl.getAttribLocation(program, key); 191 | gl.disableVertexAttribArray(loc); 192 | } 193 | } 194 | /** 195 | * Enable attribs 196 | */ 197 | enableAttribs() { 198 | const { gl, program, buffers } = this; 199 | this.use(); 200 | for (let key of Object.keys(buffers)) { 201 | const b = buffers[key]; 202 | const loc = gl.getAttribLocation(program, key); 203 | gl.enableVertexAttribArray(loc); 204 | gl.bindBuffer(gl.ARRAY_BUFFER, buffers[key].id); 205 | gl.vertexAttribPointer(loc, b.size, b.type, b.normalized, b.stride, b.offset); 206 | } 207 | } 208 | /** 209 | * init WebGLRenderingContext 210 | * @returns {GLea} glea instance 211 | */ 212 | create() { 213 | const { gl } = this; 214 | this.program = this.prog(gl, this.shaderFactory[0], this.shaderFactory[1]); 215 | this.use(); 216 | Object.keys(this.bufferFactory).forEach((name) => { 217 | const bufferFunc = this.bufferFactory[name]; 218 | this.buffers[name] = bufferFunc(name, gl, this.program); 219 | }); 220 | if (!this.parent) { 221 | this.resize(); 222 | } 223 | return this; 224 | } 225 | replaceCanvas() { 226 | const { canvas } = this; 227 | const newCanvas = canvas.cloneNode(); 228 | if (canvas.parentNode) { 229 | canvas.parentNode.insertBefore(newCanvas, canvas); 230 | canvas.parentNode.removeChild(canvas); 231 | } 232 | this.canvas = newCanvas; 233 | } 234 | /** 235 | * Deletes the canvas element and replaces it with a cloned node and calls create() again 236 | */ 237 | restart() { 238 | this.replaceCanvas(); 239 | this.gl = this.getContext(this.contextType, this.glOptions); 240 | this.create(); 241 | return this; 242 | } 243 | /** 244 | * Create a new instance with another program and reuse the rendering context 245 | * @param param0 buffers and shaders 246 | */ 247 | add({ shaders, buffers }) { 248 | const instance = new GLea({ 249 | canvas: this.canvas, 250 | gl: this.gl, 251 | shaders, 252 | buffers: buffers || this.getDefaultBuffers(), 253 | }); 254 | instance.parent = this.parent || this; 255 | instance.create(); 256 | return instance; 257 | } 258 | /** 259 | * Set active texture 260 | * @param {number} textureIndex texture index in the range [0 .. gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] 261 | * @param {WebGLTexture} texture webgl texture object 262 | */ 263 | setActiveTexture(textureIndex, texture) { 264 | const { gl } = this; 265 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 266 | gl.bindTexture(gl.TEXTURE_2D, texture); 267 | } 268 | /** 269 | * @typedef GLeaTextureOptions 270 | * @property {string} textureWrapS default: clampToEdge 271 | * @property {string} textureWrapT default: clampToEdge 272 | * @property {string} textureMinFilter default: nearest 273 | * @property {string} textureMagFilter default: nearest 274 | */ 275 | /** 276 | * Create a texture object 277 | * 278 | * @param {number} textureIndex 279 | * @param {GLeaTextureOptions} params configuration options 280 | * @returns texture WebGLTexture object 281 | */ 282 | createTexture(textureIndex = 0, params = { 283 | textureWrapS: 'clampToEdge', 284 | textureWrapT: 'clampToEdge', 285 | textureMinFilter: 'nearest', 286 | textureMagFilter: 'nearest', 287 | }) { 288 | const scream = (str = '') => /^[A-Z0-9_]+$/.test(str) 289 | ? str 290 | : str.replace(/([A-Z])/g, '_$1').toUpperCase(); 291 | const { gl } = this; 292 | const texture = gl.createTexture(); 293 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 294 | gl.bindTexture(gl.TEXTURE_2D, texture); 295 | for (let key in params) { 296 | if (params.hasOwnProperty(key)) { 297 | const KEY = scream(key); 298 | const VAL = scream(params[key]); 299 | if (KEY in gl && VAL in gl) { 300 | // @ts-ignore indexing gl by string 301 | gl.texParameteri(gl.TEXTURE_2D, gl[KEY], gl[VAL]); 302 | } 303 | } 304 | } 305 | this.textures.push(texture); 306 | return texture; 307 | } 308 | /** 309 | * Update buffer data 310 | * @param {string} name name 311 | * @param {number} offset default: 0 312 | */ 313 | updateBuffer(name, offset = 0) { 314 | const { gl } = this; 315 | const buffer = this.buffers[name]; 316 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.id); 317 | gl.bufferSubData(gl.ARRAY_BUFFER, offset, buffer.data); 318 | } 319 | /** 320 | * Resize canvas and webgl viewport 321 | */ 322 | resize() { 323 | const { canvas, gl, devicePixelRatio } = this; 324 | if (canvas) { 325 | canvas.width = canvas.clientWidth * devicePixelRatio; 326 | canvas.height = canvas.clientHeight * devicePixelRatio; 327 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 328 | } 329 | } 330 | /** 331 | * Get canvas width 332 | * @returns {number} canvas width 333 | */ 334 | get width() { 335 | return this.canvas ? this.canvas.width : NaN; 336 | } 337 | /** 338 | * Get canvas height 339 | * @returns {number} canvas height 340 | */ 341 | get height() { 342 | return this.canvas ? this.canvas.height : NaN; 343 | } 344 | /** 345 | * Use program 346 | */ 347 | use() { 348 | this.gl.useProgram(this.program); 349 | return this; 350 | } 351 | /** 352 | * set uniform matrix (mat2, mat3, mat4) 353 | * @param name uniform name 354 | * @param data array of numbers (4 for mat2, 9 for mat3, 16 for mat4) 355 | * @returns location id of the uniform 356 | */ 357 | uniM(name, data) { 358 | const { gl, program } = this; 359 | const loc = gl.getUniformLocation(program, name); 360 | if (data.length === 4) { 361 | gl.uniformMatrix2fv(loc, false, data); 362 | return loc; 363 | } 364 | if (data.length === 9) { 365 | gl.uniformMatrix3fv(loc, false, data); 366 | return loc; 367 | } 368 | if (data.length === 16) { 369 | gl.uniformMatrix4fv(loc, false, data); 370 | return loc; 371 | } 372 | throw Error('unsupported uniform matrix type'); 373 | } 374 | /** 375 | * Set uniform float vector 376 | * 377 | * @param {string} name uniform variable name 378 | * @param {number[]} data uniform float vector 379 | */ 380 | uniV(name, data) { 381 | const { gl, program } = this; 382 | const loc = gl.getUniformLocation(program, name); 383 | if (data.length === 2) { 384 | gl.uniform2fv(loc, data); 385 | return loc; 386 | } 387 | if (data.length === 3) { 388 | gl.uniform3fv(loc, data); 389 | return loc; 390 | } 391 | if (data.length === 4) { 392 | gl.uniform4fv(loc, data); 393 | return loc; 394 | } 395 | throw Error('unsupported uniform vector type'); 396 | } 397 | /** 398 | * Set uniform int vector 399 | * 400 | * @param {string} name uniform variable name 401 | * @param {number[]} data uniform int vector 402 | * @returns uniform location 403 | */ 404 | uniIV(name, data) { 405 | const { gl, program } = this; 406 | const loc = gl.getUniformLocation(program, name); 407 | if (data.length === 2) { 408 | gl.uniform2iv(loc, data); 409 | return loc; 410 | } 411 | if (data.length === 3) { 412 | gl.uniform3iv(loc, data); 413 | return loc; 414 | } 415 | if (data.length === 4) { 416 | gl.uniform4iv(loc, data); 417 | return loc; 418 | } 419 | throw Error('unsupported uniform vector type'); 420 | } 421 | /** 422 | * Set uniform float 423 | * 424 | * @param {string} name uniform variable name 425 | * @param {number} data data 426 | */ 427 | uni(name, data) { 428 | const { gl, program } = this; 429 | const loc = gl.getUniformLocation(program, name); 430 | if (typeof data === 'number') { 431 | gl.uniform1f(loc, data); 432 | } 433 | return loc; 434 | } 435 | /** 436 | * Set uniform int 437 | * @param {string} name uniform variable name 438 | * @param {number} data data 439 | */ 440 | uniI(name, data) { 441 | const { gl, program } = this; 442 | const loc = gl.getUniformLocation(program, name); 443 | if (typeof data === 'number') { 444 | gl.uniform1i(loc, data); 445 | } 446 | } 447 | /** 448 | * Clear screen 449 | * 450 | * @param {number[]} clearColor 451 | */ 452 | clear(clearColor = null) { 453 | const { gl } = this; 454 | if (clearColor) { 455 | gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1); 456 | } 457 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 458 | } 459 | /** 460 | * destroys the WebGLRendering context by deleting all textures, buffers and shaders. 461 | * Additionally, it calls loseContext. 462 | * Also the canvas element is removed from the DOM and replaced by a new cloned canvas element 463 | */ 464 | destroy() { 465 | const { gl, program } = this; 466 | try { 467 | gl.deleteProgram(program); 468 | Object.values(this.buffers).forEach((buffer) => { 469 | gl.deleteBuffer(buffer.id); 470 | }); 471 | this.buffers = {}; 472 | this.textures.forEach((texture) => { 473 | gl.deleteTexture(texture); 474 | }); 475 | this.textures = []; 476 | // @ts-ignore TS doesn't know about getExtension 477 | gl.getExtension('WEBGL_lose_context').loseContext(); 478 | this.replaceCanvas(); 479 | } 480 | catch (err) { 481 | console.error(err); 482 | } 483 | } 484 | } 485 | 486 | export default GLea; 487 | -------------------------------------------------------------------------------- /docs/assets/js/search.json: -------------------------------------------------------------------------------- 1 | {"kinds":{"1":"Module","32":"Variable","128":"Class","512":"Constructor","1024":"Property","2048":"Method","65536":"Type literal","262144":"Accessor","4194304":"Type alias"},"rows":[{"id":0,"kind":1,"name":"\"glea\"","url":"modules/_glea_.html","classes":"tsd-kind-module"},{"id":1,"kind":128,"name":"GLea","url":"classes/_glea_.glea.html","classes":"tsd-kind-class tsd-parent-kind-module","parent":"\"glea\""},{"id":2,"kind":1024,"name":"canvas","url":"classes/_glea_.glea.html#canvas","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":3,"kind":1024,"name":"contextType","url":"classes/_glea_.glea.html#contexttype","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":4,"kind":1024,"name":"glOptions","url":"classes/_glea_.glea.html#gloptions","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":5,"kind":1024,"name":"gl","url":"classes/_glea_.glea.html#gl","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":6,"kind":1024,"name":"shaderFactory","url":"classes/_glea_.glea.html#shaderfactory","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":7,"kind":1024,"name":"bufferFactory","url":"classes/_glea_.glea.html#bufferfactory","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":8,"kind":1024,"name":"program","url":"classes/_glea_.glea.html#program","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":9,"kind":1024,"name":"buffers","url":"classes/_glea_.glea.html#buffers","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":10,"kind":1024,"name":"textures","url":"classes/_glea_.glea.html#textures","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":11,"kind":1024,"name":"devicePixelRatio","url":"classes/_glea_.glea.html#devicepixelratio","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":12,"kind":1024,"name":"parent","url":"classes/_glea_.glea.html#parent","classes":"tsd-kind-property tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":13,"kind":512,"name":"constructor","url":"classes/_glea_.glea.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":14,"kind":2048,"name":"getDefaultBuffers","url":"classes/_glea_.glea.html#getdefaultbuffers","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-private","parent":"\"glea\".GLea"},{"id":15,"kind":2048,"name":"getContext","url":"classes/_glea_.glea.html#getcontext","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-private","parent":"\"glea\".GLea"},{"id":16,"kind":2048,"name":"vertexShader","url":"classes/_glea_.glea.html#vertexshader","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"\"glea\".GLea"},{"id":17,"kind":2048,"name":"fragmentShader","url":"classes/_glea_.glea.html#fragmentshader","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"\"glea\".GLea"},{"id":18,"kind":2048,"name":"prog","url":"classes/_glea_.glea.html#prog","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-private","parent":"\"glea\".GLea"},{"id":19,"kind":2048,"name":"buffer","url":"classes/_glea_.glea.html#buffer","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"\"glea\".GLea"},{"id":20,"kind":2048,"name":"drawArrays","url":"classes/_glea_.glea.html#drawarrays","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":21,"kind":2048,"name":"disableAttribs","url":"classes/_glea_.glea.html#disableattribs","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":22,"kind":2048,"name":"enableAttribs","url":"classes/_glea_.glea.html#enableattribs","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":23,"kind":2048,"name":"create","url":"classes/_glea_.glea.html#create","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":24,"kind":2048,"name":"replaceCanvas","url":"classes/_glea_.glea.html#replacecanvas","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-private","parent":"\"glea\".GLea"},{"id":25,"kind":2048,"name":"restart","url":"classes/_glea_.glea.html#restart","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":26,"kind":2048,"name":"add","url":"classes/_glea_.glea.html#add","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":27,"kind":2048,"name":"setActiveTexture","url":"classes/_glea_.glea.html#setactivetexture","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":28,"kind":2048,"name":"createTexture","url":"classes/_glea_.glea.html#createtexture","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":29,"kind":2048,"name":"updateBuffer","url":"classes/_glea_.glea.html#updatebuffer","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":30,"kind":2048,"name":"resize","url":"classes/_glea_.glea.html#resize","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":31,"kind":262144,"name":"width","url":"classes/_glea_.glea.html#width","classes":"tsd-kind-get-signature tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":32,"kind":262144,"name":"height","url":"classes/_glea_.glea.html#height","classes":"tsd-kind-get-signature tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":33,"kind":2048,"name":"use","url":"classes/_glea_.glea.html#use","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":34,"kind":2048,"name":"uniM","url":"classes/_glea_.glea.html#unim","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":35,"kind":2048,"name":"uniV","url":"classes/_glea_.glea.html#univ","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":36,"kind":2048,"name":"uniIV","url":"classes/_glea_.glea.html#uniiv","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":37,"kind":2048,"name":"uni","url":"classes/_glea_.glea.html#uni","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":38,"kind":2048,"name":"uniI","url":"classes/_glea_.glea.html#unii","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":39,"kind":2048,"name":"clear","url":"classes/_glea_.glea.html#clear","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":40,"kind":2048,"name":"destroy","url":"classes/_glea_.glea.html#destroy","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"glea\".GLea"},{"id":41,"kind":4194304,"name":"GLeaContext","url":"modules/_glea_.html#gleacontext","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"\"glea\""},{"id":42,"kind":32,"name":"SHADER_HEAD","url":"modules/_glea_.html#shader_head","classes":"tsd-kind-variable tsd-parent-kind-module tsd-is-not-exported","parent":"\"glea\""},{"id":43,"kind":32,"name":"VERT_DEFAULT","url":"modules/_glea_.html#vert_default","classes":"tsd-kind-variable tsd-parent-kind-module tsd-is-not-exported","parent":"\"glea\""},{"id":44,"kind":32,"name":"FRAG_DEFAULT","url":"modules/_glea_.html#frag_default","classes":"tsd-kind-variable tsd-parent-kind-module tsd-is-not-exported","parent":"\"glea\""},{"id":45,"kind":4194304,"name":"GLeaBuffer","url":"modules/_glea_.html#gleabuffer","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"\"glea\""},{"id":46,"kind":65536,"name":"__type","url":"modules/_glea_.html#gleabuffer.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"\"glea\".GLeaBuffer"},{"id":47,"kind":32,"name":"id","url":"modules/_glea_.html#gleabuffer.__type.id","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":48,"kind":32,"name":"name","url":"modules/_glea_.html#gleabuffer.__type.name","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":49,"kind":32,"name":"data","url":"modules/_glea_.html#gleabuffer.__type.data","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":50,"kind":32,"name":"loc","url":"modules/_glea_.html#gleabuffer.__type.loc","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":51,"kind":32,"name":"type","url":"modules/_glea_.html#gleabuffer.__type.type","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":52,"kind":32,"name":"size","url":"modules/_glea_.html#gleabuffer.__type.size","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":53,"kind":32,"name":"normalized","url":"modules/_glea_.html#gleabuffer.__type.normalized","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":54,"kind":32,"name":"stride","url":"modules/_glea_.html#gleabuffer.__type.stride","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":55,"kind":32,"name":"offset","url":"modules/_glea_.html#gleabuffer.__type.offset","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaBuffer.__type"},{"id":56,"kind":4194304,"name":"GLeaShaderFactory","url":"modules/_glea_.html#gleashaderfactory","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"\"glea\""},{"id":57,"kind":65536,"name":"__type","url":"modules/_glea_.html#gleashaderfactory.__type-3","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"\"glea\".GLeaShaderFactory"},{"id":58,"kind":32,"name":"shaderType","url":"modules/_glea_.html#gleashaderfactory.__type-3.shadertype","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaShaderFactory.__type"},{"id":59,"kind":32,"name":"init","url":"modules/_glea_.html#gleashaderfactory.__type-3.init","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaShaderFactory.__type"},{"id":60,"kind":65536,"name":"__type","url":"modules/_glea_.html#gleashaderfactory.__type-3.init.__type-4","classes":"tsd-kind-type-literal tsd-parent-kind-variable","parent":"\"glea\".GLeaShaderFactory.__type.init"},{"id":61,"kind":4194304,"name":"GLeaBufferFactory","url":"modules/_glea_.html#gleabufferfactory","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"\"glea\""},{"id":62,"kind":65536,"name":"__type","url":"modules/_glea_.html#gleabufferfactory.__type-1","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"\"glea\".GLeaBufferFactory"},{"id":63,"kind":4194304,"name":"GLeaConstructorParams","url":"modules/_glea_.html#gleaconstructorparams","classes":"tsd-kind-type-alias tsd-parent-kind-module","parent":"\"glea\""},{"id":64,"kind":65536,"name":"__type","url":"modules/_glea_.html#gleaconstructorparams.__type-2","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias","parent":"\"glea\".GLeaConstructorParams"},{"id":65,"kind":32,"name":"canvas","url":"modules/_glea_.html#gleaconstructorparams.__type-2.canvas","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":66,"kind":32,"name":"gl","url":"modules/_glea_.html#gleaconstructorparams.__type-2.gl","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":67,"kind":32,"name":"contextType","url":"modules/_glea_.html#gleaconstructorparams.__type-2.contexttype","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":68,"kind":32,"name":"shaders","url":"modules/_glea_.html#gleaconstructorparams.__type-2.shaders","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":69,"kind":32,"name":"buffers","url":"modules/_glea_.html#gleaconstructorparams.__type-2.buffers","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":70,"kind":32,"name":"devicePixelRatio","url":"modules/_glea_.html#gleaconstructorparams.__type-2.devicepixelratio","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"},{"id":71,"kind":32,"name":"glOptions","url":"modules/_glea_.html#gleaconstructorparams.__type-2.gloptions","classes":"tsd-kind-variable tsd-parent-kind-type-literal","parent":"\"glea\".GLeaConstructorParams.__type"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,18.481]],["parent/0",[]],["name/1",[0,18.481]],["parent/1",[0,1.838]],["name/2",[1,33.742]],["parent/2",[2,0.611]],["name/3",[3,33.742]],["parent/3",[2,0.611]],["name/4",[4,33.742]],["parent/4",[2,0.611]],["name/5",[5,33.742]],["parent/5",[2,0.611]],["name/6",[6,38.85]],["parent/6",[2,0.611]],["name/7",[7,38.85]],["parent/7",[2,0.611]],["name/8",[8,38.85]],["parent/8",[2,0.611]],["name/9",[9,33.742]],["parent/9",[2,0.611]],["name/10",[10,38.85]],["parent/10",[2,0.611]],["name/11",[11,33.742]],["parent/11",[2,0.611]],["name/12",[12,38.85]],["parent/12",[2,0.611]],["name/13",[13,38.85]],["parent/13",[2,0.611]],["name/14",[14,38.85]],["parent/14",[2,0.611]],["name/15",[15,38.85]],["parent/15",[2,0.611]],["name/16",[16,38.85]],["parent/16",[2,0.611]],["name/17",[17,38.85]],["parent/17",[2,0.611]],["name/18",[18,38.85]],["parent/18",[2,0.611]],["name/19",[19,38.85]],["parent/19",[2,0.611]],["name/20",[20,38.85]],["parent/20",[2,0.611]],["name/21",[21,38.85]],["parent/21",[2,0.611]],["name/22",[22,38.85]],["parent/22",[2,0.611]],["name/23",[23,38.85]],["parent/23",[2,0.611]],["name/24",[24,38.85]],["parent/24",[2,0.611]],["name/25",[25,38.85]],["parent/25",[2,0.611]],["name/26",[26,38.85]],["parent/26",[2,0.611]],["name/27",[27,38.85]],["parent/27",[2,0.611]],["name/28",[28,38.85]],["parent/28",[2,0.611]],["name/29",[29,38.85]],["parent/29",[2,0.611]],["name/30",[30,38.85]],["parent/30",[2,0.611]],["name/31",[31,38.85]],["parent/31",[2,0.611]],["name/32",[32,38.85]],["parent/32",[2,0.611]],["name/33",[33,38.85]],["parent/33",[2,0.611]],["name/34",[34,38.85]],["parent/34",[2,0.611]],["name/35",[35,38.85]],["parent/35",[2,0.611]],["name/36",[36,38.85]],["parent/36",[2,0.611]],["name/37",[37,38.85]],["parent/37",[2,0.611]],["name/38",[38,38.85]],["parent/38",[2,0.611]],["name/39",[39,38.85]],["parent/39",[2,0.611]],["name/40",[40,38.85]],["parent/40",[2,0.611]],["name/41",[41,38.85]],["parent/41",[0,1.838]],["name/42",[42,38.85]],["parent/42",[0,1.838]],["name/43",[43,38.85]],["parent/43",[0,1.838]],["name/44",[44,38.85]],["parent/44",[0,1.838]],["name/45",[45,38.85]],["parent/45",[0,1.838]],["name/46",[46,25.857]],["parent/46",[47,3.863]],["name/47",[48,38.85]],["parent/47",[49,2.027]],["name/48",[50,38.85]],["parent/48",[49,2.027]],["name/49",[51,38.85]],["parent/49",[49,2.027]],["name/50",[52,38.85]],["parent/50",[49,2.027]],["name/51",[53,38.85]],["parent/51",[49,2.027]],["name/52",[54,38.85]],["parent/52",[49,2.027]],["name/53",[55,38.85]],["parent/53",[49,2.027]],["name/54",[56,38.85]],["parent/54",[49,2.027]],["name/55",[57,38.85]],["parent/55",[49,2.027]],["name/56",[58,38.85]],["parent/56",[0,1.838]],["name/57",[46,25.857]],["parent/57",[59,3.863]],["name/58",[60,38.85]],["parent/58",[61,3.355]],["name/59",[62,38.85]],["parent/59",[61,3.355]],["name/60",[46,25.857]],["parent/60",[63,3.863]],["name/61",[64,38.85]],["parent/61",[0,1.838]],["name/62",[46,25.857]],["parent/62",[65,3.863]],["name/63",[66,38.85]],["parent/63",[0,1.838]],["name/64",[46,25.857]],["parent/64",[67,3.863]],["name/65",[1,33.742]],["parent/65",[68,2.263]],["name/66",[5,33.742]],["parent/66",[68,2.263]],["name/67",[3,33.742]],["parent/67",[68,2.263]],["name/68",[69,38.85]],["parent/68",[68,2.263]],["name/69",[9,33.742]],["parent/69",[68,2.263]],["name/70",[11,33.742]],["parent/70",[68,2.263]],["name/71",[4,33.742]],["parent/71",[68,2.263]]],"invertedIndex":[["__type",{"_index":46,"name":{"46":{},"57":{},"60":{},"62":{},"64":{}},"parent":{}}],["add",{"_index":26,"name":{"26":{}},"parent":{}}],["buffer",{"_index":19,"name":{"19":{}},"parent":{}}],["bufferfactory",{"_index":7,"name":{"7":{}},"parent":{}}],["buffers",{"_index":9,"name":{"9":{},"69":{}},"parent":{}}],["canvas",{"_index":1,"name":{"2":{},"65":{}},"parent":{}}],["clear",{"_index":39,"name":{"39":{}},"parent":{}}],["constructor",{"_index":13,"name":{"13":{}},"parent":{}}],["contexttype",{"_index":3,"name":{"3":{},"67":{}},"parent":{}}],["create",{"_index":23,"name":{"23":{}},"parent":{}}],["createtexture",{"_index":28,"name":{"28":{}},"parent":{}}],["data",{"_index":51,"name":{"49":{}},"parent":{}}],["destroy",{"_index":40,"name":{"40":{}},"parent":{}}],["devicepixelratio",{"_index":11,"name":{"11":{},"70":{}},"parent":{}}],["disableattribs",{"_index":21,"name":{"21":{}},"parent":{}}],["drawarrays",{"_index":20,"name":{"20":{}},"parent":{}}],["enableattribs",{"_index":22,"name":{"22":{}},"parent":{}}],["frag_default",{"_index":44,"name":{"44":{}},"parent":{}}],["fragmentshader",{"_index":17,"name":{"17":{}},"parent":{}}],["getcontext",{"_index":15,"name":{"15":{}},"parent":{}}],["getdefaultbuffers",{"_index":14,"name":{"14":{}},"parent":{}}],["gl",{"_index":5,"name":{"5":{},"66":{}},"parent":{}}],["glea",{"_index":0,"name":{"0":{},"1":{}},"parent":{"1":{},"41":{},"42":{},"43":{},"44":{},"45":{},"56":{},"61":{},"63":{}}}],["glea\".glea",{"_index":2,"name":{},"parent":{"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"40":{}}}],["glea\".gleabuffer",{"_index":47,"name":{},"parent":{"46":{}}}],["glea\".gleabuffer.__type",{"_index":49,"name":{},"parent":{"47":{},"48":{},"49":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{}}}],["glea\".gleabufferfactory",{"_index":65,"name":{},"parent":{"62":{}}}],["glea\".gleaconstructorparams",{"_index":67,"name":{},"parent":{"64":{}}}],["glea\".gleaconstructorparams.__type",{"_index":68,"name":{},"parent":{"65":{},"66":{},"67":{},"68":{},"69":{},"70":{},"71":{}}}],["glea\".gleashaderfactory",{"_index":59,"name":{},"parent":{"57":{}}}],["glea\".gleashaderfactory.__type",{"_index":61,"name":{},"parent":{"58":{},"59":{}}}],["glea\".gleashaderfactory.__type.init",{"_index":63,"name":{},"parent":{"60":{}}}],["gleabuffer",{"_index":45,"name":{"45":{}},"parent":{}}],["gleabufferfactory",{"_index":64,"name":{"61":{}},"parent":{}}],["gleaconstructorparams",{"_index":66,"name":{"63":{}},"parent":{}}],["gleacontext",{"_index":41,"name":{"41":{}},"parent":{}}],["gleashaderfactory",{"_index":58,"name":{"56":{}},"parent":{}}],["gloptions",{"_index":4,"name":{"4":{},"71":{}},"parent":{}}],["height",{"_index":32,"name":{"32":{}},"parent":{}}],["id",{"_index":48,"name":{"47":{}},"parent":{}}],["init",{"_index":62,"name":{"59":{}},"parent":{}}],["loc",{"_index":52,"name":{"50":{}},"parent":{}}],["name",{"_index":50,"name":{"48":{}},"parent":{}}],["normalized",{"_index":55,"name":{"53":{}},"parent":{}}],["offset",{"_index":57,"name":{"55":{}},"parent":{}}],["parent",{"_index":12,"name":{"12":{}},"parent":{}}],["prog",{"_index":18,"name":{"18":{}},"parent":{}}],["program",{"_index":8,"name":{"8":{}},"parent":{}}],["replacecanvas",{"_index":24,"name":{"24":{}},"parent":{}}],["resize",{"_index":30,"name":{"30":{}},"parent":{}}],["restart",{"_index":25,"name":{"25":{}},"parent":{}}],["setactivetexture",{"_index":27,"name":{"27":{}},"parent":{}}],["shader_head",{"_index":42,"name":{"42":{}},"parent":{}}],["shaderfactory",{"_index":6,"name":{"6":{}},"parent":{}}],["shaders",{"_index":69,"name":{"68":{}},"parent":{}}],["shadertype",{"_index":60,"name":{"58":{}},"parent":{}}],["size",{"_index":54,"name":{"52":{}},"parent":{}}],["stride",{"_index":56,"name":{"54":{}},"parent":{}}],["textures",{"_index":10,"name":{"10":{}},"parent":{}}],["type",{"_index":53,"name":{"51":{}},"parent":{}}],["uni",{"_index":37,"name":{"37":{}},"parent":{}}],["unii",{"_index":38,"name":{"38":{}},"parent":{}}],["uniiv",{"_index":36,"name":{"36":{}},"parent":{}}],["unim",{"_index":34,"name":{"34":{}},"parent":{}}],["univ",{"_index":35,"name":{"35":{}},"parent":{}}],["updatebuffer",{"_index":29,"name":{"29":{}},"parent":{}}],["use",{"_index":33,"name":{"33":{}},"parent":{}}],["vert_default",{"_index":43,"name":{"43":{}},"parent":{}}],["vertexshader",{"_index":16,"name":{"16":{}},"parent":{}}],["width",{"_index":31,"name":{"31":{}},"parent":{}}]],"pipeline":[]}} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | glea 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

glea

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 |

GLea - GL experience artistry

69 |
70 |

GLea is a low-level WebGL library with a minimal footprint. 71 | It provides helper functions for creating a WebGL program, compiling shaders and passing data from JavaScript to the shader language.

72 | 73 |

Introduction

74 |
75 |

There are several options to embed GLea into your project. You can load GLea directly via script tag:

76 |
<script src="https://unpkg.com/glea@latest/dist/glea.umd.min.js"></script>
77 |

Inside a JavaScript ES module:

78 |
import GLea from 'https://cdn.skypack.dev/glea';
79 |

Or via NPM, you can install GLea via npm i glea and import it like this:

80 |
import GLea from 'glea';
81 | 82 |

Initialization

83 |
84 |

By default, GLea looks for a canvas element in your HTML and uses that. If there is no canvas element existing, GLea creates one for you.

85 |

If your HTML document doesn't include any CSS (neither a style nor a link tag, a minimal stylesheet is provided that sizes the canvas to the browser's viewport size.

86 |

The GLea instance expects a shaders property, containing your fragment and vertex shader. 87 | Also, a buffers property, which contains the data that is passed as attributes to the vertex shader.

88 |

If no buffers are provided, GLea provides a default position attribute with a buffer containing 4 vec2 values for a triangle strip, defining a plane filling the screen.

89 | 90 |

Setting uniforms

91 |
92 |

GLea provides several helper functions to set uniforms to pass data from JavaScript to GLSL. These are:

93 |
// set uniform float
 94 | glea.uni('pi', Math.PI);
 95 | 
 96 | // set uniform int
 97 | glea.uniI('width', innerWidth);
 98 | 
 99 | // set uniform float vector (supported types are vec2, vec3, vec4)
100 | glea.uniV('vector', [Math.sqrt(2), Math.sqrt(3)]);
101 | 
102 | // set uniform int vector
103 | glea.uniIV('resolution', [innerWidth, innerHeight]);
104 | 
105 | // set uniform matrix
106 | // HEADS UP: it is the other way round as you would write it down on paper
107 | // prettier-ignore
108 | glea.uniM('translateMatrix', [
109 |   1, 0, 0, 0, // column 1
110 |   0, 1, 0, 0, // column 2
111 |   0, 0, 1, 0, // column 3
112 |   x, y, z, 1, // column 4
113 | ]);
114 | 115 |

Draw

116 |
117 |

GLea provides a wrapper to drawArrays from the underlying WebGLRenderingContext. It works exactly like the original drawArrays function, but if you don't provide any vertex count, it is determined 118 | automatically from the buffers.

119 |
const { gl } = glea;
120 | 
121 | glea.drawArrays(gl.TRIANGLE_STRIP);
122 | 
123 | // The same as:
124 | const numVertices = 4;
125 | glea.gl.drawArrays(gl.TRIANGLE_STRIP, 0, numVertices);
126 | 127 |

Multiple programs and switching

128 |
129 |

GLea supports multiple programs.

130 |
const prg1 = new GLea({
131 |   shaders: [GLea.vertexShader(vert), GLea.fragmentShader(frag)],
132 | }).create();
133 | 
134 | const prg2 = prg1.add({
135 |   shaders: [GLea.vertexShader(vert2), GLea.fragmentShader(frag2)],
136 |   buffers: {
137 |     position: GLea.buffer(3, Ella.Geometry.sphere(0.25, 32, 16).toTriangles()),
138 |   },
139 | });
140 |

The the main instance prg1 and its child prg2 use the same underlying WebGLRenderingContext. 141 | In the example prg1 renders a plane geometry (GLea provides a position attribute with a plane geometry by default), 142 | and prg2 provides a sphere geometry. The sphere geometry is provided by ella-math.

143 |

In the draw loop, the switching between programs is done via enableAttribs and disableAttribs:

144 |
// Shader 1 does the background animation
145 | prg1.gl.disable(gl.DEPTH_TEST);
146 | prg1.enableAttribs();
147 | prg1.uniV('resolution', [width, height]);
148 | prg1.uni('time', time * 1e-3);
149 | prg1.drawArrays(gl.TRIANGLE_STRIP);
150 | prg1.disableAttribs();
151 | 
152 | // Shader 2 renders a sphere
153 | gl.enable(gl.DEPTH_TEST);
154 | prg2.enableAttribs();
155 | prg2.uniV('resolution', [width, height]);
156 | prg2.uni('time', time * 1e-3);
157 | prg2.uniM('uPM', this.projectionMat.toArray());
158 | prg2.uniM('uVM', this.viewMat.toArray());
159 | prg2.drawArrays(gl.TRIANGLES);
160 | prg2.disableAttribs();
161 |

Full example

162 | 163 |

Loading textures

164 |
165 |

I'm using a loadImage helper function that wraps img.onload into a Promise:

166 |
function loadImage(url) {
167 |   return new Promise((resolve, reject) => {
168 |     const img = new Image();
169 |     img.crossOrigin = 'Anonymous';
170 |     img.src = url;
171 |     img.onload = () => {
172 |       resolve(img);
173 |     };
174 |     img.onerror = () => {
175 |       reject(img);
176 |     };
177 |   });
178 | }
179 | 
180 | async function setup() {
181 |   const img = await loadImage('https://placekitten.com/256/256/');
182 |   const textureIndex = 0;
183 |   glea.createTexture(textureIndex);
184 |   glea.gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
185 |   glea.uniI('texture0', textureIndex);
186 | }
187 | 
188 | setup();
189 |

In GLSL, you can access the texture like this:

190 |
uniform sampler2D texture0;
191 | 
192 | void main() {
193 |   vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
194 |   gl_FragColor = texture2D(texture1, coord);
195 | }
196 | 197 |

Example

198 |
199 |
import GLea from 'https://cdn.skypack.dev/glea';
200 | 
201 | const vert = `
202 | precision highp float;
203 | attribute vec2 position;
204 | 
205 | void main() {
206 |   gl_Position = vec4(position, 0, 1.0);
207 | }
208 | `;
209 | 
210 | const frag = `
211 | precision highp float;
212 | uniform float time;
213 | uniform vec2 resolution;
214 | 
215 | void main() {
216 |   float vmin = min(resolution.y, resolution.x);
217 |   vec2 p = (gl_FragCoord.xy - .5 * resolution) / vmin;
218 |   float r = .5 + .5 * sin(5. * log(length(p)) - time * 1.2);
219 |   float g = .5 + .5 * sin(5. * log(length(p)) + sin(time + 2. * p.x));  
220 |   float b = .5 + .5 * sin(.2 + 5. * log(length(p)) + sin(time * .4 + 4. * p.y));
221 |   gl_FragColor = vec4(r, g, b, 1.);
222 | }
223 | `;
224 | 
225 | const glea = new GLea({
226 |   shaders: [GLea.fragmentShader(frag), GLea.vertexShader(vert)],
227 |   buffers: {
228 |     // create a position attribute bound
229 |     // to a buffer with 4 2D coordinates
230 |     // this is what GLea provides by default if you omit buffers in the constructor
231 |     position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]),
232 |   },
233 | }).create();
234 | 
235 | function loop(time) {
236 |   const { gl, width, height } = glea;
237 |   glea.clear();
238 |   glea.uniV('resolution', [width, height]);
239 |   glea.uni('time', time * 1e-3);
240 |   gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
241 |   requestAnimationFrame(loop);
242 | }
243 | 
244 | function setup() {
245 |   const { gl } = glea;
246 |   window.addEventListener('resize', () => {
247 |     glea.resize();
248 |   });
249 |   loop(0);
250 | }
251 | 
252 | setup();
253 | 256 | 257 |

Exampes

258 |
259 | 274 | 275 |

More examples

276 |
277 | 280 | 281 |

Additional WebGL resources

282 |
283 | 287 |
288 |
289 | 305 |
306 |
307 |
308 |
309 |

Legend

310 |
311 |
    312 |
  • Variable
  • 313 |
  • Type alias
  • 314 |
315 |
    316 |
  • Class
  • 317 |
318 |
319 |
320 |
321 |
322 |

Generated using TypeDoc

323 |
324 |
325 | 326 | 327 | -------------------------------------------------------------------------------- /src/glea.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GLea - GL experience artistry Library 3 | * @module glea 4 | */ 5 | export type GLeaContext = WebGLRenderingContext | WebGL2RenderingContext; 6 | 7 | const SHADER_HEAD = 'precision highp float;'; 8 | const VERT_DEFAULT = 9 | SHADER_HEAD + 10 | 'attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}'; 11 | const FRAG_DEFAULT = 12 | SHADER_HEAD + 13 | `precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}`; 14 | 15 | /** 16 | * store for an attribute and a buffer 17 | */ 18 | export type GLeaBuffer = { 19 | id: WebGLBuffer; 20 | name: string; 21 | data: ArrayBuffer; 22 | loc: number; 23 | type: number; 24 | size: number; 25 | normalized: boolean; 26 | stride: number; 27 | offset: number; 28 | }; 29 | 30 | /** 31 | * function that compiles a shader 32 | */ 33 | export type GLeaShaderFactory = { 34 | shaderType: string; 35 | init: (gl: GLeaContext) => WebGLShader; 36 | }; 37 | 38 | /** 39 | * function that registers an attribute and binds a buffer to it 40 | */ 41 | export type GLeaBufferFactory = ( 42 | name: string, 43 | gl: GLeaContext, 44 | program: WebGLProgram 45 | ) => GLeaBuffer; 46 | 47 | export type GLeaConstructorParams = { 48 | canvas?: HTMLCanvasElement; 49 | gl?: WebGLRenderingContext | WebGL2RenderingContext; 50 | contextType?: string; 51 | shaders: GLeaShaderFactory[]; 52 | buffers?: Record; 53 | devicePixelRatio?: number; 54 | glOptions?: WebGLContextAttributes; 55 | }; 56 | 57 | /** 58 | * @hidden hide internal function from documentation 59 | */ 60 | function convertArray( 61 | data: number[], 62 | type = WebGLRenderingContext.FLOAT 63 | ): ArrayBuffer { 64 | if (type === WebGLRenderingContext.FLOAT) { 65 | return new Float32Array(data); 66 | } 67 | if (type === WebGLRenderingContext.BYTE) { 68 | return new Uint8Array(data); 69 | } 70 | throw Error('type not supported'); 71 | } 72 | 73 | /** 74 | * @hidden hide internal function from documentation 75 | */ 76 | function shader(code: string, shaderType: 'frag' | 'vert'): GLeaShaderFactory { 77 | const init = (gl: WebGLRenderingContext | WebGL2RenderingContext) => { 78 | const glShaderType = /frag/.test(shaderType) 79 | ? WebGLRenderingContext.FRAGMENT_SHADER 80 | : WebGLRenderingContext.VERTEX_SHADER; 81 | const sh = gl.createShader(glShaderType); 82 | if (!sh) { 83 | throw Error('shader type not supported'); 84 | } 85 | gl.shaderSource(sh, code); 86 | gl.compileShader(sh); 87 | if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { 88 | throw 'Could not compile Shader.\n\n' + gl.getShaderInfoLog(sh); 89 | } 90 | return sh; 91 | }; 92 | return { 93 | shaderType, 94 | init, 95 | }; 96 | } 97 | 98 | /** Class GLea */ 99 | class GLea { 100 | canvas: HTMLCanvasElement = document.createElement('canvas'); 101 | contextType: string; 102 | glOptions?: WebGLContextAttributes; 103 | gl: WebGLRenderingContext | WebGL2RenderingContext; 104 | shaderFactory: GLeaShaderFactory[]; 105 | bufferFactory: Record; 106 | 107 | program: WebGLProgram; 108 | buffers: Record; 109 | textures: WebGLTexture[]; 110 | devicePixelRatio: number; 111 | parent?: GLea; 112 | 113 | constructor({ 114 | canvas, 115 | gl, 116 | contextType = 'webgl', 117 | shaders, 118 | buffers, 119 | devicePixelRatio = 1, 120 | glOptions, 121 | }: GLeaConstructorParams) { 122 | this.canvas = canvas || document.querySelector('canvas'); 123 | if (!this.canvas) { 124 | this.canvas = document.createElement('canvas') as HTMLCanvasElement; 125 | document.body.appendChild(this.canvas); 126 | } 127 | if (!document.querySelector('link[rel=stylesheet], style')) { 128 | // if there's no css, provide some minimal defaults 129 | const style = document.createElement('style') as HTMLStyleElement; 130 | style.innerHTML = 131 | 'body{margin:0}canvas{display:block;width:100vw;height:100vh}'; 132 | document.head.appendChild(style); 133 | } 134 | this.contextType = contextType; 135 | this.glOptions = glOptions; 136 | this.gl = gl || this.getContext(contextType, glOptions); 137 | const program = this.gl.createProgram() as WebGLProgram; 138 | this.program = program; 139 | this.buffers = {}; 140 | this.shaderFactory = shaders; 141 | this.bufferFactory = buffers || this.getDefaultBuffers(); 142 | this.textures = []; 143 | this.devicePixelRatio = devicePixelRatio; 144 | } 145 | 146 | /** 147 | * By default, GLea provides a position buffer containing 4 2D coordinates 148 | * A triangle strip plane that consists of 2 triangles 149 | */ 150 | private getDefaultBuffers() { 151 | return { 152 | // create a position attribute bound 153 | // to a buffer with 4 2D coordinates 154 | position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]), 155 | }; 156 | } 157 | 158 | /** 159 | * Used to create a WebGLRenderingContext 160 | * @param contextType webgl or webgl2. Also detects if webgl is only available via the context `experimental-webgl` 161 | * @param glOptions see WebGLContextAttributes 162 | */ 163 | private getContext( 164 | contextType: string, 165 | glOptions?: WebGLContextAttributes 166 | ): GLeaContext { 167 | if (contextType === 'webgl') { 168 | return (this.canvas.getContext('webgl', glOptions) || 169 | this.canvas.getContext( 170 | 'experimental-webgl', 171 | glOptions 172 | )) as WebGLRenderingContext; 173 | } 174 | if (contextType === 'webgl2') { 175 | return this.canvas.getContext( 176 | 'webgl2', 177 | glOptions 178 | ) as WebGL2RenderingContext; 179 | } 180 | throw Error(`no ${contextType} context available.`); 181 | } 182 | 183 | /** 184 | * Create a vertex shader 185 | * 186 | * @param code shader code 187 | */ 188 | static vertexShader(code: string = VERT_DEFAULT) { 189 | return shader(code, 'vert'); 190 | } 191 | 192 | /** 193 | * Create a fragment shader 194 | * 195 | * @param {string} code fragment shader code 196 | */ 197 | static fragmentShader(code: string = FRAG_DEFAULT) { 198 | return shader(code, 'frag'); 199 | } 200 | 201 | /** 202 | * Create a webgl program from a vertex and fragment shader (no matter which order) 203 | * @param shader1 a factory created by GLea.vertexShader or GLea.fragmentShader 204 | * @param shader2 a factory created by GLea.vertexShader or GLea.fragmentShader 205 | */ 206 | private prog( 207 | gl: GLeaContext, 208 | shader1: GLeaShaderFactory, 209 | shader2: GLeaShaderFactory 210 | ) { 211 | const p = gl.createProgram() as WebGLProgram; 212 | const s1 = shader1.init(gl); 213 | const s2 = shader2.init(gl); 214 | gl.attachShader(p, s1); 215 | gl.attachShader(p, s2); 216 | gl.linkProgram(p); 217 | gl.validateProgram(p); 218 | if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { 219 | const info = gl.getProgramInfoLog(p); 220 | throw 'Could not compile WebGL program. \n\n' + info; 221 | } 222 | return p; 223 | } 224 | 225 | /** 226 | * Create Buffer 227 | * 228 | * @param {number} size record size (2 for vec2, 3 for vec3, 4 for vec4) 229 | * @param {number[]} data buffer data 230 | * @param {number} usage usage, by default gl.STATIC_DRAW 231 | * @param {number} type data type, by default gl.FLOAT 232 | * @param {boolean} normalized normalize data, false 233 | * @param {number} stride stride, by default 0 234 | * @param {number} offset offset, by default 0 235 | */ 236 | static buffer( 237 | size: number, 238 | data: number[] | Uint8Array | Float32Array, 239 | usage = WebGLRenderingContext.STATIC_DRAW, 240 | type = WebGLRenderingContext.FLOAT, 241 | normalized = false, 242 | stride = 0, 243 | offset = 0 244 | ): GLeaBufferFactory { 245 | return ( 246 | name: string, 247 | gl: GLeaContext, 248 | program: WebGLProgram 249 | ): GLeaBuffer => { 250 | const loc = gl.getAttribLocation(program, name); 251 | gl.enableVertexAttribArray(loc); 252 | // create buffer: 253 | const id = gl.createBuffer() as WebGLBuffer; 254 | const bufferData = 255 | data instanceof Array ? convertArray(data, type) : data; 256 | gl.bindBuffer(gl.ARRAY_BUFFER, id); 257 | gl.bufferData(gl.ARRAY_BUFFER, bufferData as ArrayBuffer, usage); 258 | gl.vertexAttribPointer(loc, size, type, normalized, stride, offset); 259 | return { 260 | id, 261 | name, 262 | data: bufferData, 263 | loc, 264 | type, 265 | size, 266 | normalized, 267 | stride, 268 | offset, 269 | }; 270 | }; 271 | } 272 | 273 | /** 274 | * Wrapper for gl.drawArrays 275 | * 276 | * @param {number} drawMode gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, ... 277 | * @param {number} first offset of first vertex 278 | * @param {number} count count of vertices. If not provided, it is determined from the provided buffers 279 | */ 280 | drawArrays(drawMode: number, first = 0, count?: number) { 281 | if (typeof count === 'undefined') { 282 | const attributes = Object.keys(this.buffers); 283 | if (attributes.length === 0) { 284 | return; 285 | } 286 | const firstAttributeName = attributes[0]; 287 | const firstBuffer = this.buffers[firstAttributeName]; 288 | const len = (firstBuffer.data as Float32Array).length; 289 | count = len / firstBuffer.size; 290 | } 291 | this.gl.drawArrays(drawMode, first, count); 292 | } 293 | 294 | /** 295 | * Disable attribs (useful for switching between GLea instances) 296 | */ 297 | disableAttribs() { 298 | const { gl, program, buffers } = this; 299 | for (let key of Object.keys(buffers)) { 300 | const loc = gl.getAttribLocation(program, key); 301 | gl.disableVertexAttribArray(loc); 302 | } 303 | } 304 | 305 | /** 306 | * Enable attribs 307 | */ 308 | enableAttribs() { 309 | const { gl, program, buffers } = this; 310 | this.use(); 311 | for (let key of Object.keys(buffers)) { 312 | const b = buffers[key]; 313 | const loc = gl.getAttribLocation(program, key); 314 | gl.enableVertexAttribArray(loc); 315 | gl.bindBuffer(gl.ARRAY_BUFFER, buffers[key].id); 316 | gl.vertexAttribPointer( 317 | loc, 318 | b.size, 319 | b.type, 320 | b.normalized, 321 | b.stride, 322 | b.offset 323 | ); 324 | } 325 | } 326 | 327 | /** 328 | * init WebGLRenderingContext 329 | * @returns {GLea} glea instance 330 | */ 331 | create() { 332 | const { gl } = this; 333 | this.program = this.prog(gl, this.shaderFactory[0], this.shaderFactory[1]); 334 | this.use(); 335 | Object.keys(this.bufferFactory).forEach((name) => { 336 | const bufferFunc = this.bufferFactory[name]; 337 | this.buffers[name] = bufferFunc(name, gl, this.program); 338 | }); 339 | if (!this.parent) { 340 | this.resize(); 341 | } 342 | return this; 343 | } 344 | 345 | private replaceCanvas() { 346 | const { canvas } = this; 347 | const newCanvas = canvas.cloneNode() as HTMLCanvasElement; 348 | if (canvas.parentNode) { 349 | canvas.parentNode.insertBefore(newCanvas, canvas); 350 | canvas.parentNode.removeChild(canvas); 351 | } 352 | this.canvas = newCanvas; 353 | } 354 | 355 | /** 356 | * Deletes the canvas element and replaces it with a cloned node and calls create() again 357 | */ 358 | restart() { 359 | this.replaceCanvas(); 360 | this.gl = this.getContext(this.contextType, this.glOptions); 361 | this.create(); 362 | return this; 363 | } 364 | 365 | /** 366 | * Create a new instance with another program and reuse the rendering context 367 | * @param param0 buffers and shaders 368 | */ 369 | add({ shaders, buffers }: GLeaConstructorParams) { 370 | const instance = new GLea({ 371 | canvas: this.canvas, 372 | gl: this.gl, 373 | shaders, 374 | buffers: buffers || this.getDefaultBuffers(), 375 | }); 376 | instance.parent = this.parent || this; 377 | instance.create(); 378 | return instance; 379 | } 380 | 381 | /** 382 | * Set active texture 383 | * @param {number} textureIndex texture index in the range [0 .. gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] 384 | * @param {WebGLTexture} texture webgl texture object 385 | */ 386 | setActiveTexture(textureIndex: number, texture: WebGLTexture) { 387 | const { gl } = this; 388 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 389 | gl.bindTexture(gl.TEXTURE_2D, texture); 390 | } 391 | 392 | /** 393 | * @typedef GLeaTextureOptions 394 | * @property {string} textureWrapS default: clampToEdge 395 | * @property {string} textureWrapT default: clampToEdge 396 | * @property {string} textureMinFilter default: nearest 397 | * @property {string} textureMagFilter default: nearest 398 | */ 399 | 400 | /** 401 | * Create a texture object 402 | * 403 | * @param {number} textureIndex 404 | * @param {GLeaTextureOptions} params configuration options 405 | * @returns texture WebGLTexture object 406 | */ 407 | createTexture( 408 | textureIndex = 0, 409 | params: Record = { 410 | textureWrapS: 'clampToEdge', 411 | textureWrapT: 'clampToEdge', 412 | textureMinFilter: 'nearest', 413 | textureMagFilter: 'nearest', 414 | } 415 | ): WebGLTexture { 416 | const scream = (str = '') => 417 | /^[A-Z0-9_]+$/.test(str) 418 | ? str 419 | : str.replace(/([A-Z])/g, '_$1').toUpperCase(); 420 | const { gl } = this; 421 | const texture = gl.createTexture() as WebGLTexture; 422 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 423 | gl.bindTexture(gl.TEXTURE_2D, texture); 424 | for (let key in params) { 425 | if (params.hasOwnProperty(key)) { 426 | const KEY = scream(key); 427 | const VAL = scream(params[key]); 428 | if (KEY in gl && VAL in gl) { 429 | // @ts-ignore indexing gl by string 430 | gl.texParameteri(gl.TEXTURE_2D, gl[KEY], gl[VAL]); 431 | } 432 | } 433 | } 434 | this.textures.push(texture); 435 | return texture; 436 | } 437 | 438 | /** 439 | * Update buffer data 440 | * @param {string} name name 441 | * @param {number} offset default: 0 442 | */ 443 | updateBuffer(name: string, offset = 0): void { 444 | const { gl } = this; 445 | const buffer = this.buffers[name]; 446 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.id); 447 | gl.bufferSubData(gl.ARRAY_BUFFER, offset, buffer.data); 448 | } 449 | 450 | /** 451 | * Resize canvas and webgl viewport 452 | */ 453 | resize(): void { 454 | const { canvas, gl, devicePixelRatio } = this; 455 | if (canvas) { 456 | canvas.width = canvas.clientWidth * devicePixelRatio; 457 | canvas.height = canvas.clientHeight * devicePixelRatio; 458 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 459 | } 460 | } 461 | 462 | /** 463 | * Get canvas width 464 | * @returns {number} canvas width 465 | */ 466 | get width(): number { 467 | return this.canvas ? this.canvas.width : NaN; 468 | } 469 | 470 | /** 471 | * Get canvas height 472 | * @returns {number} canvas height 473 | */ 474 | get height(): number { 475 | return this.canvas ? this.canvas.height : NaN; 476 | } 477 | 478 | /** 479 | * Use program 480 | */ 481 | use(): GLea { 482 | this.gl.useProgram(this.program); 483 | return this; 484 | } 485 | 486 | /** 487 | * set uniform matrix (mat2, mat3, mat4) 488 | * @param name uniform name 489 | * @param data array of numbers (4 for mat2, 9 for mat3, 16 for mat4) 490 | * @returns location id of the uniform 491 | */ 492 | uniM(name: string, data: Float32Array | number[]): WebGLUniformLocation { 493 | const { gl, program } = this; 494 | const loc = gl.getUniformLocation(program, name) as WebGLUniformLocation; 495 | if (data.length === 4) { 496 | gl.uniformMatrix2fv(loc, false, data); 497 | return loc; 498 | } 499 | if (data.length === 9) { 500 | gl.uniformMatrix3fv(loc, false, data); 501 | return loc; 502 | } 503 | if (data.length === 16) { 504 | gl.uniformMatrix4fv(loc, false, data); 505 | return loc; 506 | } 507 | throw Error('unsupported uniform matrix type'); 508 | } 509 | 510 | /** 511 | * Set uniform float vector 512 | * 513 | * @param {string} name uniform variable name 514 | * @param {number[]} data uniform float vector 515 | */ 516 | uniV(name: string, data: Float32Array | number[]): WebGLUniformLocation { 517 | const { gl, program } = this; 518 | const loc = gl.getUniformLocation(program, name) as WebGLUniformLocation; 519 | if (data.length === 2) { 520 | gl.uniform2fv(loc, data); 521 | return loc; 522 | } 523 | if (data.length === 3) { 524 | gl.uniform3fv(loc, data); 525 | return loc; 526 | } 527 | if (data.length === 4) { 528 | gl.uniform4fv(loc, data); 529 | return loc; 530 | } 531 | throw Error('unsupported uniform vector type'); 532 | } 533 | 534 | /** 535 | * Set uniform int vector 536 | * 537 | * @param {string} name uniform variable name 538 | * @param {number[]} data uniform int vector 539 | * @returns uniform location 540 | */ 541 | uniIV(name: string, data: Int32Array | number[]): WebGLUniformLocation { 542 | const { gl, program } = this; 543 | const loc = gl.getUniformLocation(program, name); 544 | if (data.length === 2) { 545 | gl.uniform2iv(loc, data); 546 | return loc as WebGLUniformLocation; 547 | } 548 | if (data.length === 3) { 549 | gl.uniform3iv(loc, data); 550 | return loc as WebGLUniformLocation; 551 | } 552 | if (data.length === 4) { 553 | gl.uniform4iv(loc, data); 554 | return loc as WebGLUniformLocation; 555 | } 556 | throw Error('unsupported uniform vector type'); 557 | } 558 | 559 | /** 560 | * Set uniform float 561 | * 562 | * @param {string} name uniform variable name 563 | * @param {number} data data 564 | */ 565 | uni(name: string, data: number) { 566 | const { gl, program } = this; 567 | const loc = gl.getUniformLocation(program, name); 568 | if (typeof data === 'number') { 569 | gl.uniform1f(loc, data); 570 | } 571 | return loc; 572 | } 573 | 574 | /** 575 | * Set uniform int 576 | * @param {string} name uniform variable name 577 | * @param {number} data data 578 | */ 579 | uniI(name: string, data: number) { 580 | const { gl, program } = this; 581 | const loc = gl.getUniformLocation(program, name); 582 | if (typeof data === 'number') { 583 | gl.uniform1i(loc, data); 584 | } 585 | } 586 | 587 | /** 588 | * Clear screen 589 | * 590 | * @param {number[]} clearColor 591 | */ 592 | clear(clearColor: number[] | null = null) { 593 | const { gl } = this; 594 | if (clearColor) { 595 | gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1); 596 | } 597 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 598 | } 599 | 600 | /** 601 | * destroys the WebGLRendering context by deleting all textures, buffers and shaders. 602 | * Additionally, it calls loseContext. 603 | * Also the canvas element is removed from the DOM and replaced by a new cloned canvas element 604 | */ 605 | destroy() { 606 | const { gl, program } = this; 607 | try { 608 | gl.deleteProgram(program); 609 | Object.values(this.buffers).forEach((buffer) => { 610 | gl.deleteBuffer(buffer.id); 611 | }); 612 | this.buffers = {}; 613 | this.textures.forEach((texture) => { 614 | gl.deleteTexture(texture); 615 | }); 616 | this.textures = []; 617 | const extLC = gl.getExtension('WEBGL_lose_context'); 618 | if (extLC && typeof extLC.loseContext === "function") { 619 | extLC.loseContext(); 620 | } 621 | this.replaceCanvas(); 622 | } catch (err) { 623 | console.error(err); 624 | } 625 | } 626 | } 627 | 628 | export default GLea; 629 | -------------------------------------------------------------------------------- /dist/glea.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.GLea = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | const SHADER_HEAD = 'precision highp float;'; 8 | const VERT_DEFAULT = SHADER_HEAD + 9 | 'attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}'; 10 | const FRAG_DEFAULT = SHADER_HEAD + 11 | `precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}`; 12 | /** 13 | * @hidden hide internal function from documentation 14 | */ 15 | function convertArray(data, type = WebGLRenderingContext.FLOAT) { 16 | if (type === WebGLRenderingContext.FLOAT) { 17 | return new Float32Array(data); 18 | } 19 | if (type === WebGLRenderingContext.BYTE) { 20 | return new Uint8Array(data); 21 | } 22 | throw Error('type not supported'); 23 | } 24 | /** 25 | * @hidden hide internal function from documentation 26 | */ 27 | function shader(code, shaderType) { 28 | const init = (gl) => { 29 | const glShaderType = /frag/.test(shaderType) 30 | ? WebGLRenderingContext.FRAGMENT_SHADER 31 | : WebGLRenderingContext.VERTEX_SHADER; 32 | const sh = gl.createShader(glShaderType); 33 | if (!sh) { 34 | throw Error('shader type not supported'); 35 | } 36 | gl.shaderSource(sh, code); 37 | gl.compileShader(sh); 38 | if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { 39 | throw 'Could not compile Shader.\n\n' + gl.getShaderInfoLog(sh); 40 | } 41 | return sh; 42 | }; 43 | return { 44 | shaderType, 45 | init, 46 | }; 47 | } 48 | /** Class GLea */ 49 | class GLea { 50 | constructor({ canvas, gl, contextType = 'webgl', shaders, buffers, devicePixelRatio = 1, glOptions, }) { 51 | this.canvas = document.createElement('canvas'); 52 | this.canvas = canvas || document.querySelector('canvas'); 53 | if (!this.canvas) { 54 | this.canvas = document.createElement('canvas'); 55 | document.body.appendChild(this.canvas); 56 | } 57 | if (!document.querySelector('link[rel=stylesheet], style')) { 58 | // if there's no css, provide some minimal defaults 59 | const style = document.createElement('style'); 60 | style.innerHTML = 61 | 'body{margin:0}canvas{display:block;width:100vw;height:100vh}'; 62 | document.head.appendChild(style); 63 | } 64 | this.contextType = contextType; 65 | this.glOptions = glOptions; 66 | this.gl = gl || this.getContext(contextType, glOptions); 67 | const program = this.gl.createProgram(); 68 | this.program = program; 69 | this.buffers = {}; 70 | this.shaderFactory = shaders; 71 | this.bufferFactory = buffers || this.getDefaultBuffers(); 72 | this.textures = []; 73 | this.devicePixelRatio = devicePixelRatio; 74 | } 75 | /** 76 | * By default, GLea provides a position buffer containing 4 2D coordinates 77 | * A triangle strip plane that consists of 2 triangles 78 | */ 79 | getDefaultBuffers() { 80 | return { 81 | // create a position attribute bound 82 | // to a buffer with 4 2D coordinates 83 | position: GLea.buffer(2, [1, 1, -1, 1, 1, -1, -1, -1]), 84 | }; 85 | } 86 | /** 87 | * Used to create a WebGLRenderingContext 88 | * @param contextType webgl or webgl2. Also detects if webgl is only available via the context `experimental-webgl` 89 | * @param glOptions see WebGLContextAttributes 90 | */ 91 | getContext(contextType, glOptions) { 92 | if (contextType === 'webgl') { 93 | return (this.canvas.getContext('webgl', glOptions) || 94 | this.canvas.getContext('experimental-webgl', glOptions)); 95 | } 96 | if (contextType === 'webgl2') { 97 | return this.canvas.getContext('webgl2', glOptions); 98 | } 99 | throw Error(`no ${contextType} context available.`); 100 | } 101 | /** 102 | * Create a vertex shader 103 | * 104 | * @param code shader code 105 | */ 106 | static vertexShader(code = VERT_DEFAULT) { 107 | return shader(code, 'vert'); 108 | } 109 | /** 110 | * Create a fragment shader 111 | * 112 | * @param {string} code fragment shader code 113 | */ 114 | static fragmentShader(code = FRAG_DEFAULT) { 115 | return shader(code, 'frag'); 116 | } 117 | /** 118 | * Create a webgl program from a vertex and fragment shader (no matter which order) 119 | * @param shader1 a factory created by GLea.vertexShader or GLea.fragmentShader 120 | * @param shader2 a factory created by GLea.vertexShader or GLea.fragmentShader 121 | */ 122 | prog(gl, shader1, shader2) { 123 | const p = gl.createProgram(); 124 | const s1 = shader1.init(gl); 125 | const s2 = shader2.init(gl); 126 | gl.attachShader(p, s1); 127 | gl.attachShader(p, s2); 128 | gl.linkProgram(p); 129 | gl.validateProgram(p); 130 | if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { 131 | const info = gl.getProgramInfoLog(p); 132 | throw 'Could not compile WebGL program. \n\n' + info; 133 | } 134 | return p; 135 | } 136 | /** 137 | * Create Buffer 138 | * 139 | * @param {number} size record size (2 for vec2, 3 for vec3, 4 for vec4) 140 | * @param {number[]} data buffer data 141 | * @param {number} usage usage, by default gl.STATIC_DRAW 142 | * @param {number} type data type, by default gl.FLOAT 143 | * @param {boolean} normalized normalize data, false 144 | * @param {number} stride stride, by default 0 145 | * @param {number} offset offset, by default 0 146 | */ 147 | static buffer(size, data, usage = WebGLRenderingContext.STATIC_DRAW, type = WebGLRenderingContext.FLOAT, normalized = false, stride = 0, offset = 0) { 148 | return (name, gl, program) => { 149 | const loc = gl.getAttribLocation(program, name); 150 | gl.enableVertexAttribArray(loc); 151 | // create buffer: 152 | const id = gl.createBuffer(); 153 | const bufferData = data instanceof Array ? convertArray(data, type) : data; 154 | gl.bindBuffer(gl.ARRAY_BUFFER, id); 155 | gl.bufferData(gl.ARRAY_BUFFER, bufferData, usage); 156 | gl.vertexAttribPointer(loc, size, type, normalized, stride, offset); 157 | return { 158 | id, 159 | name, 160 | data: bufferData, 161 | loc, 162 | type, 163 | size, 164 | normalized, 165 | stride, 166 | offset, 167 | }; 168 | }; 169 | } 170 | /** 171 | * Wrapper for gl.drawArrays 172 | * 173 | * @param {number} drawMode gl.POINTS, gl.TRIANGLES, gl.TRIANGLE_STRIP, ... 174 | * @param {number} first offset of first vertex 175 | * @param {number} count count of vertices. If not provided, it is determined from the provided buffers 176 | */ 177 | drawArrays(drawMode, first = 0, count) { 178 | if (typeof count === 'undefined') { 179 | const attributes = Object.keys(this.buffers); 180 | if (attributes.length === 0) { 181 | return; 182 | } 183 | const firstAttributeName = attributes[0]; 184 | const firstBuffer = this.buffers[firstAttributeName]; 185 | const len = firstBuffer.data.length; 186 | count = len / firstBuffer.size; 187 | } 188 | this.gl.drawArrays(drawMode, first, count); 189 | } 190 | /** 191 | * Disable attribs (useful for switching between GLea instances) 192 | */ 193 | disableAttribs() { 194 | const { gl, program, buffers } = this; 195 | for (let key of Object.keys(buffers)) { 196 | const loc = gl.getAttribLocation(program, key); 197 | gl.disableVertexAttribArray(loc); 198 | } 199 | } 200 | /** 201 | * Enable attribs 202 | */ 203 | enableAttribs() { 204 | const { gl, program, buffers } = this; 205 | this.use(); 206 | for (let key of Object.keys(buffers)) { 207 | const b = buffers[key]; 208 | const loc = gl.getAttribLocation(program, key); 209 | gl.enableVertexAttribArray(loc); 210 | gl.bindBuffer(gl.ARRAY_BUFFER, buffers[key].id); 211 | gl.vertexAttribPointer(loc, b.size, b.type, b.normalized, b.stride, b.offset); 212 | } 213 | } 214 | /** 215 | * init WebGLRenderingContext 216 | * @returns {GLea} glea instance 217 | */ 218 | create() { 219 | const { gl } = this; 220 | this.program = this.prog(gl, this.shaderFactory[0], this.shaderFactory[1]); 221 | this.use(); 222 | Object.keys(this.bufferFactory).forEach((name) => { 223 | const bufferFunc = this.bufferFactory[name]; 224 | this.buffers[name] = bufferFunc(name, gl, this.program); 225 | }); 226 | if (!this.parent) { 227 | this.resize(); 228 | } 229 | return this; 230 | } 231 | replaceCanvas() { 232 | const { canvas } = this; 233 | const newCanvas = canvas.cloneNode(); 234 | if (canvas.parentNode) { 235 | canvas.parentNode.insertBefore(newCanvas, canvas); 236 | canvas.parentNode.removeChild(canvas); 237 | } 238 | this.canvas = newCanvas; 239 | } 240 | /** 241 | * Deletes the canvas element and replaces it with a cloned node and calls create() again 242 | */ 243 | restart() { 244 | this.replaceCanvas(); 245 | this.gl = this.getContext(this.contextType, this.glOptions); 246 | this.create(); 247 | return this; 248 | } 249 | /** 250 | * Create a new instance with another program and reuse the rendering context 251 | * @param param0 buffers and shaders 252 | */ 253 | add({ shaders, buffers }) { 254 | const instance = new GLea({ 255 | canvas: this.canvas, 256 | gl: this.gl, 257 | shaders, 258 | buffers: buffers || this.getDefaultBuffers(), 259 | }); 260 | instance.parent = this.parent || this; 261 | instance.create(); 262 | return instance; 263 | } 264 | /** 265 | * Set active texture 266 | * @param {number} textureIndex texture index in the range [0 .. gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] 267 | * @param {WebGLTexture} texture webgl texture object 268 | */ 269 | setActiveTexture(textureIndex, texture) { 270 | const { gl } = this; 271 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 272 | gl.bindTexture(gl.TEXTURE_2D, texture); 273 | } 274 | /** 275 | * @typedef GLeaTextureOptions 276 | * @property {string} textureWrapS default: clampToEdge 277 | * @property {string} textureWrapT default: clampToEdge 278 | * @property {string} textureMinFilter default: nearest 279 | * @property {string} textureMagFilter default: nearest 280 | */ 281 | /** 282 | * Create a texture object 283 | * 284 | * @param {number} textureIndex 285 | * @param {GLeaTextureOptions} params configuration options 286 | * @returns texture WebGLTexture object 287 | */ 288 | createTexture(textureIndex = 0, params = { 289 | textureWrapS: 'clampToEdge', 290 | textureWrapT: 'clampToEdge', 291 | textureMinFilter: 'nearest', 292 | textureMagFilter: 'nearest', 293 | }) { 294 | const scream = (str = '') => /^[A-Z0-9_]+$/.test(str) 295 | ? str 296 | : str.replace(/([A-Z])/g, '_$1').toUpperCase(); 297 | const { gl } = this; 298 | const texture = gl.createTexture(); 299 | gl.activeTexture(gl.TEXTURE0 + textureIndex); 300 | gl.bindTexture(gl.TEXTURE_2D, texture); 301 | for (let key in params) { 302 | if (params.hasOwnProperty(key)) { 303 | const KEY = scream(key); 304 | const VAL = scream(params[key]); 305 | if (KEY in gl && VAL in gl) { 306 | // @ts-ignore indexing gl by string 307 | gl.texParameteri(gl.TEXTURE_2D, gl[KEY], gl[VAL]); 308 | } 309 | } 310 | } 311 | this.textures.push(texture); 312 | return texture; 313 | } 314 | /** 315 | * Update buffer data 316 | * @param {string} name name 317 | * @param {number} offset default: 0 318 | */ 319 | updateBuffer(name, offset = 0) { 320 | const { gl } = this; 321 | const buffer = this.buffers[name]; 322 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.id); 323 | gl.bufferSubData(gl.ARRAY_BUFFER, offset, buffer.data); 324 | } 325 | /** 326 | * Resize canvas and webgl viewport 327 | */ 328 | resize() { 329 | const { canvas, gl, devicePixelRatio } = this; 330 | if (canvas) { 331 | canvas.width = canvas.clientWidth * devicePixelRatio; 332 | canvas.height = canvas.clientHeight * devicePixelRatio; 333 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 334 | } 335 | } 336 | /** 337 | * Get canvas width 338 | * @returns {number} canvas width 339 | */ 340 | get width() { 341 | return this.canvas ? this.canvas.width : NaN; 342 | } 343 | /** 344 | * Get canvas height 345 | * @returns {number} canvas height 346 | */ 347 | get height() { 348 | return this.canvas ? this.canvas.height : NaN; 349 | } 350 | /** 351 | * Use program 352 | */ 353 | use() { 354 | this.gl.useProgram(this.program); 355 | return this; 356 | } 357 | /** 358 | * set uniform matrix (mat2, mat3, mat4) 359 | * @param name uniform name 360 | * @param data array of numbers (4 for mat2, 9 for mat3, 16 for mat4) 361 | * @returns location id of the uniform 362 | */ 363 | uniM(name, data) { 364 | const { gl, program } = this; 365 | const loc = gl.getUniformLocation(program, name); 366 | if (data.length === 4) { 367 | gl.uniformMatrix2fv(loc, false, data); 368 | return loc; 369 | } 370 | if (data.length === 9) { 371 | gl.uniformMatrix3fv(loc, false, data); 372 | return loc; 373 | } 374 | if (data.length === 16) { 375 | gl.uniformMatrix4fv(loc, false, data); 376 | return loc; 377 | } 378 | throw Error('unsupported uniform matrix type'); 379 | } 380 | /** 381 | * Set uniform float vector 382 | * 383 | * @param {string} name uniform variable name 384 | * @param {number[]} data uniform float vector 385 | */ 386 | uniV(name, data) { 387 | const { gl, program } = this; 388 | const loc = gl.getUniformLocation(program, name); 389 | if (data.length === 2) { 390 | gl.uniform2fv(loc, data); 391 | return loc; 392 | } 393 | if (data.length === 3) { 394 | gl.uniform3fv(loc, data); 395 | return loc; 396 | } 397 | if (data.length === 4) { 398 | gl.uniform4fv(loc, data); 399 | return loc; 400 | } 401 | throw Error('unsupported uniform vector type'); 402 | } 403 | /** 404 | * Set uniform int vector 405 | * 406 | * @param {string} name uniform variable name 407 | * @param {number[]} data uniform int vector 408 | * @returns uniform location 409 | */ 410 | uniIV(name, data) { 411 | const { gl, program } = this; 412 | const loc = gl.getUniformLocation(program, name); 413 | if (data.length === 2) { 414 | gl.uniform2iv(loc, data); 415 | return loc; 416 | } 417 | if (data.length === 3) { 418 | gl.uniform3iv(loc, data); 419 | return loc; 420 | } 421 | if (data.length === 4) { 422 | gl.uniform4iv(loc, data); 423 | return loc; 424 | } 425 | throw Error('unsupported uniform vector type'); 426 | } 427 | /** 428 | * Set uniform float 429 | * 430 | * @param {string} name uniform variable name 431 | * @param {number} data data 432 | */ 433 | uni(name, data) { 434 | const { gl, program } = this; 435 | const loc = gl.getUniformLocation(program, name); 436 | if (typeof data === 'number') { 437 | gl.uniform1f(loc, data); 438 | } 439 | return loc; 440 | } 441 | /** 442 | * Set uniform int 443 | * @param {string} name uniform variable name 444 | * @param {number} data data 445 | */ 446 | uniI(name, data) { 447 | const { gl, program } = this; 448 | const loc = gl.getUniformLocation(program, name); 449 | if (typeof data === 'number') { 450 | gl.uniform1i(loc, data); 451 | } 452 | } 453 | /** 454 | * Clear screen 455 | * 456 | * @param {number[]} clearColor 457 | */ 458 | clear(clearColor = null) { 459 | const { gl } = this; 460 | if (clearColor) { 461 | gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1); 462 | } 463 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 464 | } 465 | /** 466 | * destroys the WebGLRendering context by deleting all textures, buffers and shaders. 467 | * Additionally, it calls loseContext. 468 | * Also the canvas element is removed from the DOM and replaced by a new cloned canvas element 469 | */ 470 | destroy() { 471 | const { gl, program } = this; 472 | try { 473 | gl.deleteProgram(program); 474 | Object.values(this.buffers).forEach((buffer) => { 475 | gl.deleteBuffer(buffer.id); 476 | }); 477 | this.buffers = {}; 478 | this.textures.forEach((texture) => { 479 | gl.deleteTexture(texture); 480 | }); 481 | this.textures = []; 482 | // @ts-ignore TS doesn't know about getExtension 483 | gl.getExtension('WEBGL_lose_context').loseContext(); 484 | this.replaceCanvas(); 485 | } 486 | catch (err) { 487 | console.error(err); 488 | } 489 | } 490 | } 491 | 492 | return GLea; 493 | 494 | }))); 495 | -------------------------------------------------------------------------------- /docs/modules/_glea_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "glea" | glea 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "glea"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Type aliases

81 | 88 |
89 |
90 |

Variables

91 | 96 |
97 |
98 |
99 |
100 |
101 |

Type aliases

102 |
103 | 104 |

GLeaBuffer

105 |
GLeaBuffer: { data: ArrayBuffer; id: WebGLBuffer; loc: number; name: string; normalized: boolean; offset: number; size: number; stride: number; type: number }
106 | 111 |
112 |
113 |

store for an attribute and a buffer

114 |
115 |
116 |
117 |

Type declaration

118 |
    119 |
  • 120 |
    data: ArrayBuffer
    121 |
  • 122 |
  • 123 |
    id: WebGLBuffer
    124 |
  • 125 |
  • 126 |
    loc: number
    127 |
  • 128 |
  • 129 |
    name: string
    130 |
  • 131 |
  • 132 |
    normalized: boolean
    133 |
  • 134 |
  • 135 |
    offset: number
    136 |
  • 137 |
  • 138 |
    size: number
    139 |
  • 140 |
  • 141 |
    stride: number
    142 |
  • 143 |
  • 144 |
    type: number
    145 |
  • 146 |
147 |
148 |
149 |
150 | 151 |

GLeaBufferFactory

152 |
GLeaBufferFactory: (name: string, gl: GLeaContext, program: WebGLProgram) => GLeaBuffer
153 | 158 |
159 |
160 |

function that registers an attribute and binds a buffer to it

161 |
162 |
163 |
164 |

Type declaration

165 |
    166 |
  • 167 | 170 |
      171 |
    • 172 |

      Parameters

      173 |
        174 |
      • 175 |
        name: string
        176 |
      • 177 |
      • 178 |
        gl: GLeaContext
        179 |
      • 180 |
      • 181 |
        program: WebGLProgram
        182 |
      • 183 |
      184 |

      Returns GLeaBuffer

      185 |
    • 186 |
    187 |
  • 188 |
189 |
190 |
191 |
192 | 193 |

GLeaConstructorParams

194 |
GLeaConstructorParams: { buffers?: Record<string, GLeaBufferFactory>; canvas?: HTMLCanvasElement; contextType?: undefined | string; devicePixelRatio?: undefined | number; gl?: WebGLRenderingContext | WebGL2RenderingContext; glOptions?: WebGLContextAttributes; shaders: GLeaShaderFactory[] }
195 | 200 |
201 |

Type declaration

202 |
    203 |
  • 204 |
    Optional buffers?: Record<string, GLeaBufferFactory>
    205 |
  • 206 |
  • 207 |
    Optional canvas?: HTMLCanvasElement
    208 |
  • 209 |
  • 210 |
    Optional contextType?: undefined | string
    211 |
  • 212 |
  • 213 |
    Optional devicePixelRatio?: undefined | number
    214 |
  • 215 |
  • 216 |
    Optional gl?: WebGLRenderingContext | WebGL2RenderingContext
    217 |
  • 218 |
  • 219 |
    Optional glOptions?: WebGLContextAttributes
    220 |
  • 221 |
  • 222 |
    shaders: GLeaShaderFactory[]
    223 |
  • 224 |
225 |
226 |
227 |
228 | 229 |

GLeaContext

230 |
GLeaContext: WebGLRenderingContext | WebGL2RenderingContext
231 | 236 |
237 |
238 |

GLea - GL experience artistry Library

239 |
240 |
241 |
module
242 |

glea

243 |
244 |
245 |
246 |
247 |
248 | 249 |

GLeaShaderFactory

250 |
GLeaShaderFactory: { init: (gl: GLeaContext) => WebGLShader; shaderType: string }
251 | 256 |
257 |
258 |

function that compiles a shader

259 |
260 |
261 |
262 |

Type declaration

263 |
    264 |
  • 265 |
    init: (gl: GLeaContext) => WebGLShader
    266 |
      267 |
    • 268 | 271 |
        272 |
      • 273 |

        Parameters

        274 | 279 |

        Returns WebGLShader

        280 |
      • 281 |
      282 |
    • 283 |
    284 |
  • 285 |
  • 286 |
    shaderType: string
    287 |
  • 288 |
289 |
290 |
291 |
292 |
293 |

Variables

294 |
295 | 296 |

Const FRAG_DEFAULT

297 |
FRAG_DEFAULT: string = SHADER_HEAD +`precision highp float;void main(){gl_FragColor = vec4(1.,0.,0.,1.);}`
298 | 303 |
304 |
305 | 306 |

Const SHADER_HEAD

307 |
SHADER_HEAD: "precision highp float;" = "precision highp float;"
308 | 313 |
314 |
315 | 316 |

Const VERT_DEFAULT

317 |
VERT_DEFAULT: string = SHADER_HEAD +'attribute vec2 position;void main(){gl_Position=vec4(position,0, 1.);}'
318 | 323 |
324 |
325 |
326 | 369 |
370 |
371 |
372 |
373 |

Legend

374 |
375 |
    376 |
  • Variable
  • 377 |
  • Type alias
  • 378 |
379 |
    380 |
  • Class
  • 381 |
382 |
383 |
384 |
385 |
386 |

Generated using TypeDoc

387 |
388 |
389 | 390 | 391 | -------------------------------------------------------------------------------- /examples/vendor/ella.esm.js: -------------------------------------------------------------------------------- 1 | class Vec { 2 | constructor(...values) { 3 | this.values = values; 4 | } 5 | get dim() { 6 | return this.values.length; 7 | } 8 | get x() { 9 | return this.values[0]; 10 | } 11 | get y() { 12 | return this.values[1]; 13 | } 14 | get z() { 15 | return this.values[2]; 16 | } 17 | get w() { 18 | return this.values[3]; 19 | } 20 | get xy() { 21 | return new Vec(this.values[0], this.values[1]); 22 | } 23 | get xz() { 24 | return new Vec(this.values[0], this.values[2]); 25 | } 26 | get yz() { 27 | return new Vec(this.values[1], this.values[2]); 28 | } 29 | get xyz() { 30 | return new Vec(this.values[0], this.values[1], this.values[2]); 31 | } 32 | /** 33 | * Create vector from Array 34 | * @param arr array of numbers 35 | */ 36 | static fromArray(arr) { 37 | return new Vec(...arr); 38 | } 39 | /** 40 | * Create vector with x = y = n 41 | * @param n the number 42 | * @param dim the dimension 43 | */ 44 | static fromNumber(n, dim) { 45 | return new Vec(...Array(dim).fill(n)); 46 | } 47 | /** 48 | * clone vector 49 | */ 50 | clone() { 51 | return new Vec(...this.values); 52 | } 53 | /** 54 | * add vector 55 | * @param otherVec addend 56 | * @returns addition result 57 | */ 58 | add(otherVec) { 59 | return new Vec(...this.values.map((v, idx) => v + otherVec.values[idx])); 60 | } 61 | /** 62 | * subtract vector 63 | * @param otherVec addend 64 | * @returns subtraction result 65 | */ 66 | sub(otherVec) { 67 | return new Vec(...this.values.map((v, idx) => v - otherVec.values[idx])); 68 | } 69 | /** 70 | * multiply vector with scalar 71 | * @param value scalar 72 | * @returns multiplication result 73 | */ 74 | mul(value) { 75 | return new Vec(...this.values.map((v) => v * value)); 76 | } 77 | /** 78 | * divide vector with scalar 79 | * @param value scalar 80 | * @returns multiplication result 81 | */ 82 | div(value) { 83 | return new Vec(...this.values.map((x) => x / value)); 84 | } 85 | /** 86 | * dot product 87 | * @param otherVec 88 | */ 89 | dot(otherVec) { 90 | return this.values 91 | .map((x, idx) => x * otherVec.values[idx]) 92 | .reduce((a, b) => a + b); 93 | } 94 | /** 95 | * check for equality 96 | * @param otherVec 97 | */ 98 | equals(otherVec) { 99 | return this.values 100 | .map((v, idx) => v === otherVec.values[idx]) 101 | .reduce((a, b) => a === b); 102 | } 103 | /** 104 | * Calculate length 105 | */ 106 | get length() { 107 | return Math.sqrt(this.values.map((v) => v ** 2).reduce((a, b) => a + b)); 108 | } 109 | /** 110 | * Convert to array 111 | */ 112 | toArray() { 113 | return this.values.slice(0); 114 | } 115 | /** 116 | * Convert to string, in the form of `(x, y)` 117 | */ 118 | toString() { 119 | return `(${this.values.join(', ')})`; 120 | } 121 | /** 122 | * cross product 123 | * @param otherVec 124 | * @returns new Vec3 instance containing cross product 125 | */ 126 | cross(otherVec) { 127 | if (this.dim !== 3 || otherVec.dim !== 3) { 128 | throw Error('dimension not supported'); 129 | } 130 | return new Vec( 131 | this.y * otherVec.z - this.z * otherVec.y, 132 | this.z * otherVec.x - this.x * otherVec.z, 133 | this.x * otherVec.y - this.y * otherVec.x 134 | ); 135 | } 136 | /** 137 | * normalized vector, 138 | * @returns vector normalized to length = 1 139 | */ 140 | get normalized() { 141 | return this.div(this.length); 142 | } 143 | } 144 | 145 | /** @class Mat */ 146 | class Mat { 147 | constructor(values, options) { 148 | this.values = values; 149 | if (options) { 150 | this.numRows = options.numRows; 151 | this.numCols = options.numCols; 152 | } else { 153 | const dimension = Math.sqrt(values.length); 154 | if (Number.isInteger(dimension)) { 155 | this.numCols = this.numRows = dimension; 156 | return; 157 | } 158 | throw Error('ArgumentError'); 159 | } 160 | } 161 | /** 162 | * create identity matrix 163 | * @param dimension dimension of the matrix 164 | */ 165 | static identity(dimension) { 166 | if (dimension <= 0 || !Number.isInteger(dimension)) { 167 | throw Error('ArgumentError'); 168 | } 169 | return new Mat( 170 | Array(dimension ** 2) 171 | .fill(0) 172 | .map((_, i) => (i % dimension === ((i / dimension) | 0) ? 1 : 0)) 173 | ); 174 | } 175 | /** 176 | * Converts a vector with dimension n into a matrix with 1 col and n rows 177 | * useful for matrix multiplication 178 | * @param value the input vector 179 | */ 180 | static fromVector(value) { 181 | if (value instanceof Vec) { 182 | return new Mat(value.toArray(), { numRows: value.dim, numCols: 1 }); 183 | } 184 | throw Error('unsupported type'); 185 | } 186 | /** 187 | * Converts a bunch of vectors into a matrix 188 | */ 189 | static fromVectors(vectors) { 190 | if (!vectors || vectors.length === 0) { 191 | throw Error('Argument error.'); 192 | } 193 | const dimensions = vectors.map((v) => v.dim); 194 | const dimensionsMatch = dimensions.every((x) => x === dimensions[0]); 195 | const dimension = dimensions[0]; 196 | if (!dimensionsMatch) { 197 | throw Error('Dimensions mismatch.'); 198 | } 199 | const matrix = Array(dimension * vectors.length); 200 | for (let i = 0; i < vectors.length; i++) { 201 | for (let j = 0; j < dimension; j++) { 202 | matrix[i + j * vectors.length] = vectors[i].values[j]; 203 | } 204 | } 205 | return new Mat(matrix, { numRows: dimension, numCols: vectors.length }); 206 | } 207 | /** 208 | * convert to array 209 | */ 210 | toArray() { 211 | return this.values; 212 | } 213 | /** 214 | * get value at a given position 215 | * @param row row index starting from 0 216 | * @param column column index starting from 0 217 | */ 218 | valueAt(row, column) { 219 | return this.values[column * this.numRows + row]; 220 | } 221 | /** 222 | * get column at a given index 223 | * @param column index of column starting from 0 224 | * @returns column as an array of numbers 225 | */ 226 | colAt(column) { 227 | const { numRows } = this; 228 | return this.values.slice(column * numRows, column * numRows + numRows); 229 | } 230 | /** 231 | * get row at a given index 232 | * @param row index of row starting from 0 233 | * @returns row as an array of numbers 234 | */ 235 | rowAt(row) { 236 | const { numRows, numCols } = this; 237 | return Array(numCols) 238 | .fill(0) 239 | .map((_, column) => this.values[column * numRows + row]); 240 | } 241 | /** 242 | * returns transposed matrix 243 | */ 244 | transpose() { 245 | const transposedValues = []; 246 | Array(this.numRows) 247 | .fill(0) 248 | .map((_, i) => { 249 | transposedValues.push(...this.rowAt(i)); 250 | }); 251 | return new Mat(transposedValues, { 252 | numRows: this.numCols, 253 | numCols: this.numRows, 254 | }); 255 | } 256 | /** 257 | * check for equality 258 | * @param otherMatrix matrix to compare 259 | * @returns true or false 260 | */ 261 | equals(otherMatrix) { 262 | if ( 263 | this.values.length !== otherMatrix.values.length || 264 | this.numCols !== otherMatrix.numCols || 265 | this.numRows !== otherMatrix.numRows 266 | ) { 267 | return false; 268 | } 269 | for (let i = 0; i < this.values.length; i++) { 270 | if (this.values[i] !== otherMatrix.values[i]) { 271 | return false; 272 | } 273 | } 274 | return true; 275 | } 276 | /** 277 | * add two matrices 278 | * @param otherMatrix matrix to add 279 | * @returns result matrix 280 | */ 281 | add(otherMatrix) { 282 | if ( 283 | this.numCols === otherMatrix.numCols && 284 | this.numRows === otherMatrix.numRows && 285 | this.values.length === otherMatrix.values.length 286 | ) { 287 | const newValues = this.values.map( 288 | (value, i) => value + otherMatrix.values[i] 289 | ); 290 | return new Mat(newValues, { 291 | numRows: this.numRows, 292 | numCols: this.numCols, 293 | }); 294 | } 295 | throw Error('ArgumentError'); 296 | } 297 | /** 298 | * subtract another matrix 299 | * @param otherMatrix matrix to subtract 300 | * @returns result matrix 301 | */ 302 | sub(otherMatrix) { 303 | if ( 304 | this.numCols === otherMatrix.numCols && 305 | this.numRows === otherMatrix.numRows && 306 | this.values.length === otherMatrix.values.length 307 | ) { 308 | const newValues = this.values.map( 309 | (value, i) => value - otherMatrix.values[i] 310 | ); 311 | return new Mat(newValues, { 312 | numRows: this.numRows, 313 | numCols: this.numCols, 314 | }); 315 | } 316 | throw Error('ArgumentError'); 317 | } 318 | /** 319 | * matrix multiplication 320 | * @param param can be a matrix, a number or a vector 321 | * @returns a vector in case of matrix vector multiplication, else a matrix 322 | * @throws dimension Mismatch if dimensions doesn't match 323 | */ 324 | mul(param) { 325 | if (typeof param === 'number') { 326 | const multipliedValues = this.values.map((value) => value * param); 327 | return new Mat(multipliedValues, { 328 | numRows: this.numRows, 329 | numCols: this.numCols, 330 | }); 331 | } 332 | if (param instanceof Vec) { 333 | if (param.dim !== this.numCols) { 334 | throw Error('dimension mismatch'); 335 | } 336 | const m = this.mul(Mat.fromVector(param)); 337 | return new Vec(...m.values); 338 | } 339 | if (param instanceof Mat) { 340 | const mat = param; 341 | const { numRows } = this; 342 | const { numCols } = mat; 343 | const multipliedValues = Array(numRows * numCols) 344 | .fill(0) 345 | .map((_, idx) => { 346 | const y = idx % numRows; 347 | const x = (idx / numRows) | 0; 348 | const row = this.rowAt(y); 349 | const col = mat.colAt(x); 350 | return row.map((value, i) => value * col[i]).reduce((a, b) => a + b); 351 | }); 352 | return new Mat(multipliedValues, { numRows, numCols }); 353 | } 354 | throw Error('ArgumentError'); 355 | } 356 | /** 357 | * calculate determinant 358 | */ 359 | determinant() { 360 | const { numRows, numCols } = this; 361 | const v = this.values; 362 | if (numRows !== numCols) { 363 | throw Error('ArgumentError'); 364 | } 365 | if (numRows === 2) { 366 | return v[0] * v[3] - v[1] * v[2]; 367 | } 368 | if (numRows === 3) { 369 | // a0 d1 g2 370 | // b3 e4 h5 371 | // c6 f7 i8 372 | // aei + bfg + cdh 373 | //-gec - hfa - idb 374 | return ( 375 | v[0] * v[4] * v[8] + 376 | v[3] * v[7] * v[2] + 377 | v[6] * v[1] * v[5] - 378 | v[2] * v[4] * v[6] - 379 | v[5] * v[7] * v[0] - 380 | v[8] * v[1] * v[3] 381 | ); 382 | } 383 | throw Error('NotImplementedYet'); 384 | } 385 | /** 386 | * convert matrix to string 387 | * @returns a string containing matROWSxCOLS(comma-separated-values) 388 | */ 389 | toString() { 390 | const { numRows, numCols, values } = this; 391 | return `mat${numRows}x${numCols}(${values.join(', ')})`; 392 | } 393 | } 394 | const Mat2 = { 395 | /** 396 | * create rotation matrix 397 | * @param angle angle in radians 398 | */ 399 | rotation(angle) { 400 | const S = Math.sin(angle); 401 | const C = Math.cos(angle); 402 | // prettier-ignore 403 | return new Mat([ 404 | C, S, 405 | -S, C 406 | ]); 407 | }, 408 | /** 409 | * create scaling matrix 410 | * @param sx X-scale factor 411 | * @param sy Y-scale factor 412 | */ 413 | scaling(sx, sy) { 414 | // prettier-ignore 415 | return new Mat([ 416 | sx, 0, 417 | 0, sy 418 | ]); 419 | }, 420 | }; 421 | const Mat3 = { 422 | /** 423 | * create translation matrix 424 | * @param x translation in x-direction 425 | * @param y translation in y-direction 426 | * @returns 3x3 translation matrix 427 | */ 428 | translation(x, y) { 429 | // prettier-ignore 430 | return new Mat([ 431 | 1, 0, 0, 432 | 0, 1, 0, 433 | x, y, 1 434 | ]); 435 | }, 436 | /** 437 | * create scaling matrix 438 | * @param sx scale X factor 439 | * @param sy scale Y factor 440 | * @param sz scale Z factor 441 | * @returns 3x3 scale matrix 442 | */ 443 | scaling(sx, sy, sz) { 444 | // prettier-ignore 445 | return new Mat([ 446 | sx, 0, 0, 447 | 0, sy, 0, 448 | 0, 0, sz 449 | ]); 450 | }, 451 | /** 452 | * create X-rotation matrix 453 | * @param angle rotation in radians 454 | */ 455 | rotX(angle) { 456 | const { sin, cos } = Math; 457 | const S = sin(angle); 458 | const C = cos(angle); 459 | // prettier-ignore 460 | return new Mat([ 461 | 1, 0, 0, 462 | 0, C, S, 463 | 0, -S, C 464 | ]); 465 | }, 466 | /** 467 | * create Y-rotation matrix 468 | * @param angle angle in radians 469 | */ 470 | rotY(angle) { 471 | const { sin, cos } = Math; 472 | const S = sin(angle); 473 | const C = cos(angle); 474 | // prettier-ignore 475 | return new Mat([ 476 | C, 0, -S, 477 | 0, 1, 0, 478 | S, 0, C 479 | ]); 480 | }, 481 | /** 482 | * create Z-rotation matrix 483 | * @param angle angle in radians 484 | */ 485 | rotZ(angle) { 486 | const { sin, cos } = Math; 487 | const S = sin(angle); 488 | const C = cos(angle); 489 | // prettier-ignore 490 | return new Mat([ 491 | C, S, 0, 492 | -S, C, 0, 493 | 0, 0, 1 494 | ]); 495 | }, 496 | }; 497 | const Mat4 = { 498 | /** 499 | * create 4x4 identity matrix 500 | */ 501 | identity() { 502 | // prettier-ignore 503 | return new Mat([ 504 | 1, 0, 0, 0, 505 | 0, 1, 0, 0, 506 | 0, 0, 1, 0, 507 | 0, 0, 0, 1 508 | ]); 509 | }, 510 | /** 511 | * create translation matrix 512 | * @param x translation in X direction 513 | * @param y translation in Y direction 514 | * @param z translation in Z direction 515 | */ 516 | translation(x, y, z) { 517 | // prettier-ignore 518 | return new Mat([ 519 | 1, 0, 0, 0, 520 | 0, 1, 0, 0, 521 | 0, 0, 1, 0, 522 | x, y, z, 1 523 | ]); 524 | }, 525 | /** 526 | * create scaling matrix 527 | * @param sx X-scale factor 528 | * @param sy Y-scale factor 529 | * @param sz Z-scale factor 530 | */ 531 | scaling(sx, sy, sz) { 532 | // prettier-ignore 533 | return new Mat([ 534 | sx, 0, 0, 0, 535 | 0, sy, 0, 0, 536 | 0, 0, sz, 0, 537 | 0, 0, 0, 1 538 | ]); 539 | }, 540 | /** 541 | * create x-rotation matrix 542 | * @param angle angle in radians 543 | */ 544 | rotX(angle) { 545 | const { sin, cos } = Math; 546 | const S = sin(angle); 547 | const C = cos(angle); 548 | // prettier-ignore 549 | return new Mat([ 550 | 1, 0, 0, 0, 551 | 0, C, S, 0, 552 | 0, -S, C, 0, 553 | 0, 0, 0, 1 554 | ]); 555 | }, 556 | /** 557 | * create y-rotation matrix 558 | * @param angle angle in radians 559 | */ 560 | rotY(angle) { 561 | const { sin, cos } = Math; 562 | const S = sin(angle); 563 | const C = cos(angle); 564 | // prettier-ignore 565 | return new Mat([ 566 | C, 0, -S, 0, 567 | 0, 1, 0, 0, 568 | S, 0, C, 0, 569 | 0, 0, 0, 1 570 | ]); 571 | }, 572 | /** 573 | * create z-rotation matrix 574 | * @param angle angle in radians 575 | */ 576 | rotZ(angle) { 577 | const { sin, cos } = Math; 578 | const S = sin(angle); 579 | const C = cos(angle); 580 | // prettier-ignore 581 | return new Mat([ 582 | C, S, 0, 0, 583 | -S, C, 0, 0, 584 | 0, 0, 1, 0, 585 | 0, 0, 0, 1 586 | ]); 587 | }, 588 | }; 589 | 590 | /** 591 | * Create a view matrix 592 | * @param eye position of the eye is (where you are) 593 | * @param center position where you want to look at 594 | * @param up it's a normalized vector, quite often (0,1,0) 595 | * @returns view matrix 596 | * @see https://www.khronos.org/opengl/wiki/GluLookAt_code 597 | */ 598 | function lookAt(eye, center, up) { 599 | const forward = eye.sub(center).normalized; 600 | const side = forward.cross(up).normalized; 601 | const up2 = side.cross(forward); 602 | // prettier-ignore 603 | return new Mat([ 604 | side.x, side.y, side.z, 0, 605 | up2.x, up2.y, up2.z, 0, 606 | -forward.x, -forward.y, -forward.z, 0, 607 | 0, 0, 0, 1 608 | ]); 609 | } 610 | 611 | /** 612 | * creates a transformation that produces a parallel projection 613 | * @param left coordinate for the left vertical clipping planes. 614 | * @param right coordinate for the right vertical clipping planes. 615 | * @param bottom coordinate for the bottom horizontal clippling pane. 616 | * @param top coordinate for the top horizontal clipping pane 617 | * @param zNear Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer. 618 | * @param zFar Specify the distances to the nearer and farther depth clipping planes. These values are negative if the plane is to be behind the viewer. 619 | * @returns 4x4 orthographic transformation matrix 620 | * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glOrtho.xml 621 | */ 622 | function ortho(left, right, bottom, top, zNear, zFar) { 623 | const tx = -(right + left) / (right - left); 624 | const ty = -(top + bottom) / (top - bottom); 625 | const tz = -(zFar + zNear) / (zFar - zNear); 626 | // prettier-ignore 627 | return new Mat([ 628 | 2 / (right - left), 0, 0, 629 | 0, 0, 2 / (top - bottom), 0, 630 | 0, 0, 0, -2 / (zFar - zNear), 631 | 0, tx, ty, tz, 1 632 | ]); 633 | } 634 | // glFrustum(left, right, bottom, top, zNear, zFar) 635 | /** 636 | * creates a perspective matrix that produces a perspective projection 637 | * @param left coordinates for the vertical left clipping pane 638 | * @param right coordinates for the vertical right clipping pane 639 | * @param bottom coordinates for the horizontal bottom clipping pane 640 | * @param top coodinates for the top horizontal clipping pane 641 | * @param zNear Specify the distances to the near depth clipping plane. Must be positive. 642 | * @param zFar Specify the distances to the far depth clipping planes. Must be positive. 643 | * @returns 4x4 perspective projection matrix 644 | * @see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml 645 | */ 646 | function frustum(left, right, bottom, top, zNear, zFar) { 647 | const t1 = 2 * zNear; 648 | const t2 = right - left; 649 | const t3 = top - bottom; 650 | const t4 = zFar - zNear; 651 | // prettier-ignore 652 | return new Mat([t1 / t2, 0, 0, 0, 653 | 0, t1 / t3, 0, 0, 654 | (right + left) / t2, (top + bottom) / t3, (-zFar - zNear) / t4, -1, 655 | 0, 0, (-t1 * zFar) / t4, 0]); 656 | } 657 | /** 658 | * creates a perspective projection matrix 659 | * @param fieldOfView Specifies the field of view angle, in degrees, in the y direction. 660 | * @param aspectRatio Specifies the aspect ratio that determines the field of view in the x direction. The aspect ratio is the ratio of x (width) to y (height). 661 | * @param zNear Specifies the distance from the viewer to the near clipping plane (always positive). 662 | * @param zFar Specifies the distance from the viewer to the far clipping plane (always positive). 663 | * @returns 4x4 perspective projection matrix 664 | */ 665 | function perspective(fieldOfView, aspectRatio, zNear, zFar) { 666 | const y = zNear * Math.tan((fieldOfView * Math.PI) / 360); 667 | const x = y * aspectRatio; 668 | return frustum(-x, x, -y, y, zNear, zFar); 669 | } 670 | 671 | /** @class Geometry */ 672 | class Geometry { 673 | constructor(vertices, faces, normals, texCoords) { 674 | this.vertices = vertices; 675 | this.faces = faces; 676 | this.normals = normals; 677 | this.texCoords = texCoords; 678 | } 679 | /** 680 | * converts to triangle array 681 | */ 682 | toTriangles() { 683 | const { faces, vertices } = this; 684 | return faces 685 | .map((face) => { 686 | if (face.length === 3) { 687 | return face.map((vertexIndex) => vertices[vertexIndex]); 688 | } 689 | if (face.length === 4) { 690 | const q = face.map((vertexIndex) => vertices[vertexIndex]); 691 | return [q[0], q[1], q[3], q[3], q[1], q[2]]; 692 | } 693 | throw Error('not supported'); 694 | }) 695 | .flat() 696 | .map((v) => v.toArray()) 697 | .flat(); 698 | } 699 | /** 700 | * Calculate the surface normal of a triangle 701 | * @param p1 3d vector of point 1 702 | * @param p2 3d vector of point 2 703 | * @param p3 3d vector of point 3 704 | */ 705 | static calculateSurfaceNormal(p1, p2, p3) { 706 | const u = p2.sub(p1); 707 | const v = p3.sub(p1); 708 | return new Vec( 709 | u.x * v.z - u.z * v.y, 710 | u.z * v.x - u.x * v.z, 711 | u.x * v.y - u.y * v.x 712 | ); 713 | } 714 | /** 715 | * Create a box geometry with the sizes a * b * c, 716 | * centered at (0, 0, 0), 2 triangles per side. 717 | * 718 | * @name box 719 | * @param {number} sizeA 720 | * @param {number} sizeB 721 | * @param {number} sizeC 722 | */ 723 | static box(sizeA = 1.0, sizeB = 1.0, sizeC = 1.0) { 724 | const a = sizeA * 0.5; 725 | const b = sizeB * 0.5; 726 | const c = sizeC * 0.5; 727 | const vertices = [ 728 | [-a, -b, -c], 729 | [a, -b, -c], 730 | [-a, b, -c], 731 | [a, b, -c], 732 | [-a, -b, c], 733 | [a, -b, c], 734 | [-a, b, c], 735 | [a, b, c], 736 | ].map((v) => new Vec(...v)); 737 | // 0______1 738 | // 4/|____5/| 739 | // |2|____|_|3 740 | // |/ ____|/ 741 | // 6 7 742 | const faces = [ 743 | // back 744 | [0, 1, 2], 745 | [2, 1, 3], 746 | // front 747 | [5, 4, 7], 748 | [7, 4, 6], 749 | // left 750 | [4, 0, 6], 751 | [6, 0, 2], 752 | // right 753 | [7, 5, 1], 754 | [1, 7, 3], 755 | // top 756 | [1, 0, 5], 757 | [5, 0, 4], 758 | // bottom 759 | [2, 3, 6], 760 | [6, 3, 7], 761 | ]; 762 | const normals = faces.map((f) => 763 | Geometry.calculateSurfaceNormal( 764 | vertices[f[0]], 765 | vertices[f[1]], 766 | vertices[f[2]] 767 | ) 768 | ); 769 | return new Geometry(vertices, faces, normals, []); 770 | } 771 | /** 772 | * create a cube 773 | * @param size 774 | */ 775 | static cube(size = 1.0) { 776 | return Geometry.box(size, size, size); 777 | } 778 | /** 779 | * create a plane grid mesh 780 | * @param x x-coord of the top left corner 781 | * @param y y-coord of the top left corner 782 | * @param width width of the plane 783 | * @param height height of the plane 784 | * @param rows number of rows 785 | * @param cols number of columns 786 | */ 787 | static grid(x, y, width, height, rows, cols) { 788 | const deltaX = width / cols; 789 | const deltaY = height / rows; 790 | const vertices = Array((cols + 1) * (rows + 1)) 791 | .fill(0) 792 | .map((_, i) => { 793 | const ix = i % cols; 794 | const iy = (i / cols) | 0; 795 | return new Vec(x + ix * deltaX, y + iy * deltaY, 0); 796 | }); 797 | const faces = Array(rows * cols) 798 | .fill(0) 799 | .map((_, i) => { 800 | const ix = i % cols; 801 | const iy = (i / cols) | 0; 802 | const idx = iy * rows + ix; 803 | return [ 804 | [idx, idx + 1, idx + rows], 805 | [idx + 1, idx + rows + 1, idx + rows], 806 | ]; 807 | }) 808 | .flat(1); 809 | const normals = faces.map((f) => 810 | Geometry.calculateSurfaceNormal( 811 | vertices[f[0]], 812 | vertices[f[1]], 813 | vertices[f[2]] 814 | ) 815 | ); 816 | return new Geometry(vertices, faces, normals, []); 817 | } 818 | /** 819 | * Create sphere geometry 820 | * @param r radius 821 | * @param sides number of sides (around the sphere) 822 | * @param segments number of segments (from top to bottom) 823 | * @see adapted from https://vorg.github.io/pex/docs/pex-gen/Sphere.html 824 | */ 825 | static sphere(r = 0.5, sides = 36, segments = 18) { 826 | const vertices = []; 827 | const texCoords = []; 828 | const faces = []; 829 | const dphi = 360 / sides; 830 | const dtheta = 180 / segments; 831 | const evalPos = (theta, phi) => { 832 | const deg = Math.PI / 180.0; 833 | var pos = new Vec( 834 | r * Math.sin(theta * deg) * Math.sin(phi * deg), 835 | r * Math.cos(theta * deg), 836 | r * Math.sin(theta * deg) * Math.cos(phi * deg) 837 | ); 838 | return pos; 839 | }; 840 | for (let segment = 0; segment <= segments; segment++) { 841 | const theta = segment * dtheta; 842 | for (let side = 0; side <= sides; side++) { 843 | const phi = side * dphi; 844 | const pos = evalPos(theta, phi); 845 | const texCoord = new Vec(phi / 360.0, theta / 180.0); 846 | vertices.push(pos); 847 | texCoords.push(texCoord); 848 | if (segment === segments) continue; 849 | if (side === sides) continue; 850 | if (segment == 0) { 851 | // first segment uses triangles 852 | faces.push([ 853 | segment * (sides + 1) + side, 854 | (segment + 1) * (sides + 1) + side, 855 | (segment + 1) * (sides + 1) + side + 1, 856 | ]); 857 | } else if (segment == segments - 1) { 858 | // last segment also uses triangles 859 | faces.push([ 860 | segment * (sides + 1) + side, 861 | (segment + 1) * (sides + 1) + side + 1, 862 | segment * (sides + 1) + side + 1, 863 | ]); 864 | } else { 865 | // A --- B 866 | // D --- C 867 | const A = segment * (sides + 1) + side; 868 | const B = (segment + 1) * (sides + 1) + side; 869 | const C = (segment + 1) * (sides + 1) + side + 1; 870 | const D = segment * (sides + 1) + side + 1; 871 | faces.push([A, B, D]); 872 | faces.push([B, C, D]); 873 | } 874 | } 875 | } 876 | const normals = faces.map((f) => 877 | Geometry.calculateSurfaceNormal( 878 | vertices[f[0]], 879 | vertices[f[1]], 880 | vertices[f[2]] 881 | ) 882 | ); 883 | return new Geometry(vertices, faces, normals, texCoords); 884 | } 885 | } 886 | 887 | export { 888 | Geometry, 889 | Mat, 890 | Mat2, 891 | Mat3, 892 | Mat4, 893 | Vec, 894 | frustum, 895 | lookAt, 896 | ortho, 897 | perspective, 898 | }; 899 | --------------------------------------------------------------------------------