├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── .vitepress │ ├── config.ts │ ├── sidebars.ts │ └── theme │ │ ├── index.ts │ │ └── styles.scss ├── components │ └── ExampleEditor.vue ├── examples │ ├── basics │ │ ├── drawing-modes │ │ │ ├── index.md │ │ │ └── index.ts │ │ ├── full-screen │ │ │ ├── index.md │ │ │ └── index.ts │ │ ├── indices │ │ │ ├── index.md │ │ │ └── index.ts │ │ ├── particles │ │ │ ├── index.md │ │ │ └── index.ts │ │ ├── play-pause │ │ │ ├── index.md │ │ │ └── index.ts │ │ ├── resize │ │ │ ├── index.md │ │ │ └── index.ts │ │ └── uniforms │ │ │ ├── index.md │ │ │ └── index.ts │ ├── gpgpu │ │ ├── boids │ │ │ ├── index.md │ │ │ ├── index.ts │ │ │ ├── positions.frag │ │ │ ├── render.frag │ │ │ ├── render.vert │ │ │ ├── styles.css │ │ │ └── velocities.frag │ │ ├── game-of-life │ │ │ ├── index.md │ │ │ └── index.ts │ │ └── particles │ │ │ ├── index.md │ │ │ └── index.ts │ ├── interactions │ │ └── pointer │ │ │ ├── index.md │ │ │ └── index.ts │ ├── post-processing │ │ ├── multi-pass │ │ │ ├── blur.frag │ │ │ ├── circles.frag │ │ │ ├── combine.frag │ │ │ ├── index.md │ │ │ ├── index.ts │ │ │ ├── mipmap.frag │ │ │ └── styles.css │ │ └── single-pass │ │ │ ├── index.md │ │ │ ├── index.ts │ │ │ └── styles.css │ └── textures │ │ ├── canvas2d │ │ ├── index.md │ │ └── index.ts │ │ ├── data │ │ ├── index.md │ │ └── index.ts │ │ ├── image │ │ ├── fragment.glsl │ │ ├── index.md │ │ ├── index.ts │ │ └── vertex.glsl │ │ └── video │ │ ├── index.md │ │ ├── index.ts │ │ └── styles.css ├── guide │ └── introduction │ │ └── quick-start.md ├── index.md ├── package.json ├── snippets │ ├── canvas-full │ │ └── styles.css │ ├── canvas-square │ │ └── styles.css │ ├── default │ │ └── index.html │ └── render-count │ │ └── index.html └── types.d.ts ├── lib ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── astro.config.mjs ├── eslint.config.mjs ├── package.json ├── playground │ ├── public │ │ ├── favicon.svg │ │ └── images │ │ │ ├── 2k_earth_color.jpeg │ │ │ ├── 2k_earth_night.jpeg │ │ │ └── lion.jpg │ ├── src │ │ ├── components │ │ │ ├── GlobalPlayPause.astro │ │ │ ├── PlayPause.astro │ │ │ ├── RenderCount.astro │ │ │ ├── SideNav.astro │ │ │ ├── renderCount.ts │ │ │ └── routes.ts │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ ├── pages │ │ │ ├── core │ │ │ │ ├── circle.astro │ │ │ │ ├── gradient.astro │ │ │ │ ├── indices.astro │ │ │ │ ├── pause.astro │ │ │ │ └── scissor.astro │ │ │ ├── gpgpu │ │ │ │ ├── boids (static).astro │ │ │ │ ├── boids.astro │ │ │ │ ├── maths.astro │ │ │ │ ├── particles - FBO (static).astro │ │ │ │ └── particles - FBO.astro │ │ │ ├── index.astro │ │ │ ├── particles │ │ │ │ └── particles.astro │ │ │ ├── pointer │ │ │ │ ├── blob.astro │ │ │ │ └── pointer.astro │ │ │ ├── post-processing │ │ │ │ ├── bloom.astro │ │ │ │ └── sepia.astro │ │ │ └── textures │ │ │ │ ├── dataTexture.astro │ │ │ │ ├── mipmap.astro │ │ │ │ ├── texture.astro │ │ │ │ └── video.astro │ │ └── shaders │ │ │ ├── blob.ts │ │ │ ├── bloom.ts │ │ │ └── boids.ts │ └── tsconfig.json ├── playwright.config.ts ├── src │ ├── core │ │ ├── attribute.ts │ │ ├── buffer.ts │ │ ├── program.ts │ │ ├── renderTarget.ts │ │ ├── shader.ts │ │ └── texture.ts │ ├── hooks │ │ ├── useBoundingRect.ts │ │ ├── useCompositeEffectPass.ts │ │ ├── useCompositor.ts │ │ ├── useEffectPass.ts │ │ ├── useLoop.ts │ │ ├── usePingPongFBO.ts │ │ ├── usePointerEvents.ts │ │ ├── useQuadRenderPass.ts │ │ ├── useRenderPass.ts │ │ ├── useResizeObserver.ts │ │ ├── useTransformFeedback.ts │ │ ├── useWebGLCanvas.ts │ │ └── useWebGLContext.ts │ ├── index.ts │ ├── internal │ │ ├── findName.ts │ │ ├── useAttributes.ts │ │ ├── useLifeCycleCallback.ts │ │ └── useUniforms.ts │ └── types.ts ├── tests │ ├── __screenshots__ │ │ ├── blob │ │ │ ├── blob-android.png │ │ │ ├── blob-chromium.png │ │ │ ├── blob-firefox.png │ │ │ ├── blob-iphone.png │ │ │ └── blob-safari.png │ │ ├── bloom │ │ │ ├── bloom-android.png │ │ │ ├── bloom-chromium.png │ │ │ ├── bloom-firefox.png │ │ │ ├── bloom-iphone.png │ │ │ └── bloom-safari.png │ │ ├── boids-static- │ │ │ ├── boids-static--android.png │ │ │ ├── boids-static--chromium.png │ │ │ ├── boids-static--firefox.png │ │ │ ├── boids-static--iphone.png │ │ │ └── boids-static--safari.png │ │ ├── circle │ │ │ ├── circle-android.png │ │ │ ├── circle-chromium.png │ │ │ ├── circle-firefox.png │ │ │ ├── circle-iphone.png │ │ │ └── circle-safari.png │ │ ├── gradient │ │ │ ├── gradient-android.png │ │ │ ├── gradient-chromium.png │ │ │ ├── gradient-firefox.png │ │ │ ├── gradient-iphone.png │ │ │ └── gradient-safari.png │ │ ├── indices │ │ │ ├── indices-android.png │ │ │ ├── indices-chromium.png │ │ │ ├── indices-firefox.png │ │ │ ├── indices-iphone.png │ │ │ └── indices-safari.png │ │ ├── maths │ │ │ ├── maths-android.png │ │ │ ├── maths-chromium.png │ │ │ ├── maths-firefox.png │ │ │ ├── maths-iphone.png │ │ │ └── maths-safari.png │ │ ├── mipmap │ │ │ ├── mipmap-android.png │ │ │ ├── mipmap-chromium.png │ │ │ ├── mipmap-firefox.png │ │ │ ├── mipmap-iphone.png │ │ │ └── mipmap-safari.png │ │ ├── particles---FBO-static- │ │ │ ├── particles---FBO-static--android.png │ │ │ ├── particles---FBO-static--chromium.png │ │ │ ├── particles---FBO-static--firefox.png │ │ │ ├── particles---FBO-static--iphone.png │ │ │ └── particles---FBO-static--safari.png │ │ ├── particles │ │ │ ├── particles-android.png │ │ │ ├── particles-chromium.png │ │ │ ├── particles-firefox.png │ │ │ ├── particles-iphone.png │ │ │ └── particles-safari.png │ │ ├── play-pause-controls---global │ │ │ ├── play-pause-controls---global-android.png │ │ │ ├── play-pause-controls---global-chromium.png │ │ │ ├── play-pause-controls---global-iphone.png │ │ │ └── play-pause-controls---global-safari.png │ │ ├── play-pause-controls---local │ │ │ ├── play-pause-controls---local-android.png │ │ │ ├── play-pause-controls---local-chromium.png │ │ │ ├── play-pause-controls---local-iphone.png │ │ │ └── play-pause-controls---local-safari.png │ │ ├── pointer-move │ │ │ ├── pointer-move-android.png │ │ │ ├── pointer-move-chromium.png │ │ │ ├── pointer-move-firefox.png │ │ │ ├── pointer-move-iphone.png │ │ │ └── pointer-move-safari.png │ │ ├── pointer │ │ │ ├── pointer-android.png │ │ │ ├── pointer-chromium.png │ │ │ ├── pointer-firefox.png │ │ │ ├── pointer-iphone.png │ │ │ └── pointer-safari.png │ │ ├── scissor │ │ │ ├── scissor-android.png │ │ │ ├── scissor-chromium.png │ │ │ ├── scissor-firefox.png │ │ │ ├── scissor-iphone.png │ │ │ └── scissor-safari.png │ │ ├── sepia │ │ │ ├── sepia-android.png │ │ │ ├── sepia-chromium.png │ │ │ ├── sepia-firefox.png │ │ │ ├── sepia-iphone.png │ │ │ └── sepia-safari.png │ │ ├── texture │ │ │ ├── texture-android.png │ │ │ ├── texture-chromium.png │ │ │ ├── texture-firefox.png │ │ │ ├── texture-iphone.png │ │ │ └── texture-safari.png │ │ └── video │ │ │ ├── video-android.png │ │ │ ├── video-chromium.png │ │ │ └── video-firefox.png │ └── screenshots.spec.ts └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | tab_width = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | PNPM_VERSION: 9 11 | NODE_VERSION: lts/* 12 | 13 | jobs: 14 | build: 15 | name: "build package" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4 20 | with: 21 | version: ${{ env.PNPM_VERSION }} 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ env.NODE_VERSION }} 25 | cache: pnpm 26 | 27 | - run: pnpm --filter="./lib" install 28 | - run: pnpm --filter="./lib" build 29 | 30 | static: 31 | name: "static checks" 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: pnpm/action-setup@v4 36 | with: 37 | version: ${{ env.PNPM_VERSION }} 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ env.NODE_VERSION }} 41 | cache: pnpm 42 | 43 | - run: pnpm install 44 | - run: pnpm run -r typecheck 45 | - run: pnpm run -r format 46 | - run: pnpm run -r lint 47 | 48 | tests: 49 | runs-on: ubuntu-latest 50 | container: 51 | image: mcr.microsoft.com/playwright:v1.49.1 52 | env: 53 | HOME: /root 54 | steps: 55 | - run: apt-get update && apt-get install git-lfs 56 | - uses: actions/checkout@v4 57 | with: 58 | lfs: true 59 | - uses: pnpm/action-setup@v4 60 | with: 61 | version: ${{ env.PNPM_VERSION }} 62 | - uses: actions/setup-node@v4 63 | with: 64 | node-version: ${{ env.NODE_VERSION }} 65 | cache: pnpm 66 | 67 | - run: pnpm --filter="./lib" install 68 | - run: xvfb-run pnpm --filter="./lib" exec playwright test 69 | 70 | - uses: actions/upload-artifact@v4 71 | if: ${{ !cancelled() }} 72 | with: 73 | name: test-results 74 | path: lib/playwright-report/ 75 | retention-days: 1 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | build/ 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | 24 | # jetbrains setting folder 25 | .idea/ 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /docs/src/content/docs/docs/introduction/quick-start.mdx 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useGL 2 | 3 | WIP 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | cache 2 | 3 | # build output 4 | dist/ 5 | # generated types 6 | .astro/ 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import { groupIconMdPlugin, groupIconVitePlugin } from "vitepress-plugin-group-icons"; 3 | import container from "markdown-it-container"; 4 | import { renderSandbox } from "vitepress-plugin-sandpack"; 5 | import { createRequire } from "node:module"; 6 | import { examplesSidebar } from "./sidebars"; 7 | const require = createRequire(import.meta.url); 8 | const pkg = require("../node_modules/usegl/package.json"); 9 | 10 | // https://vitepress.dev/reference/site-config 11 | export default defineConfig({ 12 | title: "useGL", 13 | description: "Lightweight, reactive WebGL library for working with shaders", 14 | themeConfig: { 15 | // https://vitepress.dev/reference/default-theme-config 16 | nav: [ 17 | { 18 | text: "Guide", 19 | link: "/guide/introduction/quick-start", 20 | activeMatch: "/guide/", 21 | }, 22 | { 23 | text: "Examples", 24 | link: "/examples/basics/full-screen/", 25 | activeMatch: "/examples/", 26 | }, 27 | { 28 | text: `v${pkg.version}`, 29 | items: [ 30 | { 31 | text: "Changelog", 32 | link: "https://github.com/jsulpis/usegl/blob/main/lib/CHANGELOG.md", 33 | }, 34 | ], 35 | }, 36 | ], 37 | 38 | sidebar: { 39 | "/guide/": [ 40 | { 41 | text: "Introduction", 42 | base: "/guide/introduction/", 43 | items: [{ text: "Quick start ", link: "quick-start" }], 44 | }, 45 | ], 46 | "/examples/": examplesSidebar, 47 | }, 48 | 49 | socialLinks: [ 50 | { icon: "github", link: "https://github.com/jsulpis/usegl" }, 51 | { icon: "bluesky", link: "https://bsky.app/profile/jsulpis.dev" }, 52 | { icon: "x", link: "https://x.com/jsulpis" }, 53 | ], 54 | 55 | search: { 56 | provider: "local", 57 | }, 58 | 59 | footer: { 60 | message: "Released under the MIT License.", 61 | copyright: "Copyright © 2024-present Julien Sulpis", 62 | }, 63 | }, 64 | 65 | markdown: { 66 | config(md) { 67 | md.use(groupIconMdPlugin); 68 | md.use(container, "example-editor", { 69 | render(tokens, idx) { 70 | return renderSandbox(tokens, idx, "example-editor"); 71 | }, 72 | }); 73 | }, 74 | }, 75 | 76 | transformPageData(pageData) { 77 | if (pageData.relativePath.startsWith("examples/")) { 78 | return { 79 | ...pageData, 80 | title: `${pageData.frontmatter.title} example`, 81 | frontmatter: { 82 | prev: false, 83 | next: false, 84 | aside: false, 85 | layout: "doc", 86 | pageClass: "example-page", 87 | ...pageData.frontmatter, 88 | }, 89 | }; 90 | } 91 | return pageData; 92 | }, 93 | 94 | outDir: "dist", 95 | 96 | vite: { 97 | plugins: [groupIconVitePlugin()], 98 | css: { 99 | preprocessorOptions: { 100 | scss: { 101 | api: "modern", 102 | }, 103 | }, 104 | }, 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /docs/.vitepress/sidebars.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import matter from "gray-matter"; 4 | 5 | const examplesDir = "examples"; 6 | 7 | const sections = fs 8 | .readdirSync(examplesDir) 9 | .filter((file) => fs.statSync(path.join(examplesDir, file)).isDirectory()); 10 | 11 | export const examplesSidebar = sections.map((section) => { 12 | const sectionPath = path.join(examplesDir, section); 13 | const pages = fs 14 | .readdirSync(sectionPath) 15 | .filter((file) => fs.statSync(path.join(sectionPath, file)).isDirectory()); 16 | 17 | return { 18 | text: upperFirst(section), 19 | items: pages 20 | .map((page) => { 21 | const indexPath = path.join(sectionPath, page, "index.md"); 22 | const { data } = matter(fs.readFileSync(indexPath, "utf8")); 23 | return { 24 | title: data.title || page, 25 | slug: page, 26 | position: data.position || 0, 27 | }; 28 | }) 29 | .sort((a, b) => a.position - b.position) 30 | .map((data) => { 31 | return { 32 | text: data.title, 33 | link: `/examples/${section}/${data.slug}/`, 34 | }; 35 | }), 36 | }; 37 | }); 38 | 39 | function upperFirst(str: string) { 40 | return str.charAt(0).toUpperCase() + str.slice(1); 41 | } 42 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import { Sandbox } from "vitepress-plugin-sandpack"; 3 | import ExampleEditor from "../../components/ExampleEditor.vue"; 4 | import "vitepress-plugin-sandpack/dist/style.css"; 5 | import "virtual:group-icons.css"; 6 | import "./styles.scss"; 7 | 8 | export default { 9 | ...DefaultTheme, 10 | enhanceApp(ctx) { 11 | DefaultTheme.enhanceApp(ctx); 12 | ctx.app.component("Sandbox", Sandbox); 13 | ctx.app.component("ExampleEditor", ExampleEditor); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles.scss: -------------------------------------------------------------------------------- 1 | .example-page { 2 | --vp-padding: 24px; 3 | --wrapper-width: calc(100svw - var(--vp-sidebar-width) - 2 * var(--vp-padding)); 4 | --wrapper-height: calc( 5 | 100svh - var(--vp-nav-height) - var(--vp-subnav-height, 0px) - 2 * var(--vp-padding) 6 | ); 7 | 8 | // full width page 9 | --vp-layout-max-width: 100%; 10 | .content-container, 11 | .container { 12 | max-width: 100% !important; 13 | } 14 | 15 | @media (width < 960px) { 16 | --vp-sidebar-width: 0px; 17 | --vp-subnav-height: 48px; 18 | } 19 | 20 | @media (width >= 768px) { 21 | --vp-padding: 32px; 22 | } 23 | 24 | @media (orientation: landscape) { 25 | --editor-height: var(--wrapper-height); 26 | --preview-height: var(--wrapper-height); 27 | 28 | .sp-layout > .sp-stack { 29 | min-width: 0 !important; 30 | height: 100%; 31 | } 32 | 33 | .sp-layout > .sp-resize-handler { 34 | display: initial !important; 35 | opacity: 0; 36 | background: linear-gradient( 37 | to right, 38 | transparent 30%, 39 | var(--vp-c-brand-1) 30%, 40 | var(--vp-c-brand-1) 70%, 41 | transparent 70% 42 | ); 43 | 44 | &:hover { 45 | opacity: 1; 46 | transition: opacity 150ms 50ms; 47 | } 48 | } 49 | } 50 | 51 | @media (orientation: portrait) { 52 | --editor-height: 40svh; 53 | --preview-height: calc(var(--wrapper-height) - var(--editor-height)); 54 | 55 | .sp-stack { 56 | flex: initial !important; 57 | height: auto !important; 58 | width: 100% !important; 59 | } 60 | 61 | .sp-editor { 62 | flex: initial !important; 63 | width: var(--wrapper-width) !important; 64 | min-width: 0 !important; 65 | } 66 | 67 | .sp-preview { 68 | flex: initial !important; 69 | width: var(--wrapper-width) !important; 70 | min-width: 0 !important; 71 | } 72 | } 73 | 74 | .VPDoc > .container > .content { 75 | padding: 0; 76 | } 77 | 78 | .VPDoc { 79 | padding-block: var(--vp-padding) 0; 80 | } 81 | 82 | .sp-wrapper { 83 | width: var(--wrapper-width) !important; 84 | height: var(--wrapper-height) !important; 85 | } 86 | 87 | .sp-layout { 88 | height: 100%; 89 | } 90 | 91 | .sp-editor { 92 | height: var(--editor-height) !important; 93 | } 94 | 95 | .sp-preview { 96 | height: var(--preview-height) !important; 97 | } 98 | 99 | @keyframes fadeIn { 100 | from { 101 | opacity: 0; 102 | } 103 | } 104 | 105 | .sp-code-editor { 106 | // hide a glitch where the file from the vanilla-ts template is displayed before the actual code 107 | animation: fadeIn 100ms; 108 | } 109 | 110 | html.dark & { 111 | .cm-scroller, 112 | .sp-tabs-scrollable-container { 113 | color-scheme: dark; 114 | } 115 | } 116 | 117 | [title="Open in CodeSandbox"] { 118 | width: var(--sp-space-7); 119 | height: var(--sp-space-7); 120 | justify-content: center; 121 | 122 | &:is(a) { 123 | padding-inline: 0 !important; 124 | } 125 | 126 | &:is(button) { 127 | padding: var(--sp-space-1); 128 | gap: 0; 129 | } 130 | 131 | span { 132 | display: none; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /docs/components/ExampleEditor.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /docs/examples/basics/drawing-modes/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Drawing modes 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/drawing-modes/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | fragment: /* glsl */ ` 7 | varying vec4 v_color; 8 | 9 | void main () { 10 | gl_FragColor = v_color; 11 | } 12 | `, 13 | vertex: /* glsl */ ` 14 | attribute vec4 position; 15 | attribute vec4 color; 16 | varying vec4 v_color; 17 | 18 | void main () { 19 | gl_Position = position; 20 | v_color = color; 21 | 22 | // Only for the drawing mode "POINTS" 23 | gl_PointSize = 40.; 24 | } 25 | `, 26 | attributes: { 27 | position: { 28 | data: [ 29 | [-0.5, -0.5], 30 | [-0.5, 0.5], 31 | [0.5, 0.5], 32 | [0.5, -0.5], 33 | ].flat(), 34 | size: 2, 35 | }, 36 | color: { 37 | data: [ 38 | [0, 1, 1], 39 | [0, 1, 0], 40 | [0, 0, 1], 41 | [0, 1, 1], 42 | ].flat(), 43 | size: 3, 44 | }, 45 | }, 46 | // drawMode: "LINES", 47 | // drawMode: "LINE_STRIP", 48 | // drawMode: "LINE_LOOP", 49 | // drawMode: "POINTS", 50 | // drawMode: "TRIANGLES", 51 | // drawMode: "TRIANGLE_FAN", 52 | drawMode: "TRIANGLE_STRIP", 53 | }); 54 | -------------------------------------------------------------------------------- /docs/examples/basics/full-screen/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Full screen canvas 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-full/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/full-screen/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | fragment: /* glsl */ ` 7 | varying vec2 vUv; // automatically provided 8 | uniform float uTime; // automatically provided and updated 9 | 10 | void main() { 11 | gl_FragColor = vec4(vUv, sin(uTime) / 2. + .5, 1.); 12 | } 13 | `, 14 | }); 15 | -------------------------------------------------------------------------------- /docs/examples/basics/indices/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Indices 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/indices/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | vertex: /*glsl*/ ` 7 | attribute vec2 aPosition; 8 | attribute vec3 aColor; 9 | varying vec3 vColor; 10 | 11 | void main() { 12 | gl_Position = vec4(aPosition, 0., 1.0); 13 | vColor = aColor; 14 | } 15 | `, 16 | fragment: /* glsl */ ` 17 | varying vec3 vColor; 18 | void main() { 19 | gl_FragColor = vec4(vColor, 1.); 20 | } 21 | `, 22 | attributes: { 23 | aPosition: { 24 | size: 2, 25 | data: [ 26 | [-0.5, 0.5], 27 | [-0.5, -0.5], 28 | [0.5, 0.5], 29 | [0.5, -0.5], 30 | ].flat(), 31 | }, 32 | aColor: { 33 | size: 3, 34 | data: [ 35 | [0, 1, 0], 36 | [0, 0, 1], 37 | [1, 1, 0], 38 | [1, 0, 0], 39 | ].flat(), 40 | }, 41 | index: { 42 | size: 1, 43 | data: [0, 1, 2, 1, 3, 2], 44 | }, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /docs/examples/basics/particles/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Particles 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/particles/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const vertex = /* glsl */ ` 5 | attribute vec3 random; 6 | uniform float uTime; 7 | varying vec4 vColor; 8 | 9 | #define PI acos(-1.) 10 | 11 | void main() { 12 | float t = uTime * 0.1; 13 | 14 | float rho = pow(random.x, .1) * .8; 15 | float theta = acos(2. * random.z - 1.) * (1. + t); 16 | float phi = random.y * 2. * PI * (1. + t); 17 | 18 | gl_Position = vec4( 19 | rho * sin(theta) * sin(phi), 20 | rho * cos(theta), 21 | rho * sin(theta) * cos(phi), 22 | 1. 23 | ); 24 | gl_PointSize = (gl_Position.z + 2.) * 5.; 25 | 26 | vColor.rgb = mix( 27 | vec3(0.1, 0.2, 0.4), // dark blue 28 | vec3(0.41, 0.84, 0.98), // light blue 29 | smoothstep(-1.5, 1., dot(gl_Position.xyz, vec3(1., 1., -.5))) 30 | ); 31 | vColor.a = smoothstep(-2., .8, gl_Position.z); 32 | } 33 | `; 34 | 35 | const fragment = /* glsl */ ` 36 | varying vec4 vColor; 37 | 38 | void main() { 39 | vec2 uv = gl_PointCoord.xy; 40 | gl_FragColor.rgb = vColor.rgb; 41 | gl_FragColor.a = vColor.a * smoothstep(0.5, 0.4, length(uv - 0.5)); 42 | } 43 | `; 44 | 45 | const count = 200; 46 | 47 | const { gl } = useWebGLCanvas({ 48 | canvas: "#glCanvas", 49 | fragment, 50 | vertex, 51 | attributes: { 52 | random: { 53 | data: Array.from({ length: count * 3 }).map(() => Math.random()), 54 | size: 3, 55 | }, 56 | }, 57 | }); 58 | 59 | gl.enable(gl.BLEND); 60 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 61 | -------------------------------------------------------------------------------- /docs/examples/basics/play-pause/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Play / Pause 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/render-count/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/play-pause/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const { play, pause, canvas, onAfterRender } = useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | fragment: /* glsl */ ` 7 | varying vec2 uv; 8 | uniform float uTime; 9 | #define RADIUS .1 10 | 11 | void main() { 12 | vec2 circleCenter = vec2(cos(uTime * .8) * .25 + .5, sin(uTime * .8) * .25 + .5); 13 | float circleMask = 1. - smoothstep(RADIUS*.99, RADIUS*1.01, length(uv - circleCenter)); 14 | float gradient = 1. - length(uv - RADIUS - circleCenter) / (RADIUS * 2.); 15 | vec3 color = mix(vec3(0.1, 0.6, 0.4), vec3(0.41, 0.98, 0.84), gradient) * circleMask; 16 | 17 | gl_FragColor = vec4(color, 1.); 18 | } 19 | `, 20 | }); 21 | 22 | pause(); 23 | 24 | const renderCount = document.querySelector("#renderCount"); 25 | onAfterRender(() => { 26 | renderCount.textContent = `${Number(renderCount.textContent) + 1}`; 27 | }); 28 | 29 | canvas.addEventListener("pointerenter", play); 30 | canvas.addEventListener("pointerleave", pause); 31 | -------------------------------------------------------------------------------- /docs/examples/basics/resize/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resize 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-full/styles.css 9 | <<< @/snippets/render-count/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/resize/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const { onAfterRender } = useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | fragment: /* glsl */ ` 7 | varying vec2 uv; // automatically provided 8 | uniform vec2 resolution; // automatically provided and updated 9 | #define RADIUS .2 10 | 11 | void main() { 12 | vec2 center = resolution / 2.; 13 | float dist = distance(uv * resolution, center); 14 | float radiusPx = min(resolution.x, resolution.y) * RADIUS; 15 | float circleMask = 1. - smoothstep(radiusPx * .99, radiusPx * 1.01, dist); 16 | vec3 color = vec3((uv - .5 + RADIUS) * 2., 1.) * circleMask; 17 | gl_FragColor = vec4(color, 1.); 18 | } 19 | `, 20 | }); 21 | 22 | const renderCount = document.querySelector("#renderCount"); 23 | onAfterRender(() => { 24 | renderCount.textContent = `${Number(renderCount.textContent) + 1}`; 25 | }); 26 | 27 | /** 28 | * When resizing the window (or just the canvas): 29 | * - The canvas size is updated 30 | * - The resolution uniform is updated 31 | * - The scene is re-rendered 32 | */ 33 | -------------------------------------------------------------------------------- /docs/examples/basics/uniforms/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Uniforms 3 | --- 4 | 5 | ::: example-editor {deps=tweakpane@^4.0.5} 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/render-count/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/basics/uniforms/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import { Pane } from "tweakpane"; 3 | import "./styles.css"; 4 | 5 | const { uniforms, onAfterRender } = useWebGLCanvas({ 6 | canvas: "#glCanvas", 7 | fragment: /* glsl */ ` 8 | varying vec2 vUv; 9 | uniform float uRadius; 10 | uniform float uSize; 11 | uniform float uRotation; 12 | uniform vec2 uPosition; 13 | 14 | mat2 rotateZ(float angle) { 15 | return mat2(cos(angle), sin(angle), -sin(angle), cos(angle)); 16 | } 17 | 18 | // https://iquilezles.org/articles/distfunctions2d/ 19 | float sdRoundedBox( in vec2 p, in vec2 b, in vec4 r ) { 20 | r.xy = (p.x>0.0)?r.xy : r.zw; 21 | r.x = (p.y>0.0)?r.x : r.y; 22 | vec2 q = abs(p)-b+r.x; 23 | return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; 24 | } 25 | 26 | void main() { 27 | vec2 p = (vUv - uPosition) * rotateZ(uRotation); 28 | vec4 radius = vec4(min(uRadius, uSize)); 29 | float squareDist = sdRoundedBox(p, vec2(uSize), radius); 30 | float squareMask = 1. - smoothstep(-.001, .001, squareDist); 31 | float gradient = length(p + uSize) / (2. * uSize * sqrt(2.)); 32 | vec3 color = mix(vec3(0.1, 0.2, 0.4), vec3(0.1, 0.7, 1.), gradient) * squareMask; 33 | 34 | gl_FragColor = vec4(color, 1.0); 35 | } 36 | `, 37 | uniforms: { 38 | uRadius: 0.03, 39 | uSize: 0.2, 40 | uRotation: 0.1, 41 | uPosition: [0.5, 0.5], 42 | }, 43 | }); 44 | 45 | const pane = new Pane({ title: "Uniforms" }); 46 | 47 | // updating the uniforms object will trigger a re-render 48 | pane.addBinding(uniforms, "uSize", { min: 0.01, max: 0.3 }); 49 | pane.addBinding(uniforms, "uRadius", { min: 0, max: 0.2 }); 50 | pane.addBinding(uniforms, "uRotation", { min: 0, max: 2 * Math.PI }); 51 | 52 | pane 53 | .addBinding({ uPosition: { x: 0.5, y: 0.5 } }, "uPosition", { 54 | x: { min: -1, max: 1 }, 55 | y: { min: -1, max: 1 }, 56 | }) 57 | .on("change", (e) => { 58 | // uniforms.uPosition is typed : [number, number] 59 | uniforms.uPosition = [e.value.x / 2 + 0.5, -e.value.y / 2 + 0.5]; 60 | }); 61 | 62 | const renderCount = document.querySelector("#renderCount"); 63 | onAfterRender(() => { 64 | renderCount.textContent = `${Number(renderCount.textContent) + 1}`; 65 | }); 66 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Boids 3 | position: 2 4 | --- 5 | 6 | ::: example-editor {deps=tweakpane@^4.0.5} 7 | 8 | <<< ./index.ts 9 | <<< ./positions.frag 10 | <<< ./velocities.frag 11 | <<< ./render.vert 12 | <<< ./render.frag 13 | <<< ./styles.css 14 | <<< @/snippets/default/index.html 15 | 16 | ::: 17 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLContext, useLoop, usePingPongFBO, useWebGLCanvas } from "usegl"; 2 | import { Pane } from "tweakpane"; 3 | import "./styles.css"; 4 | import boidsPositions from "./positions.frag?raw"; 5 | import boidsVelocities from "./velocities.frag?raw"; 6 | import renderPassVertex from "./render.vert?raw"; 7 | import renderPassFragment from "./render.frag?raw"; 8 | 9 | const { gl, canvas } = useWebGLContext("#glCanvas"); 10 | 11 | const count = 300; 12 | 13 | const velocities = usePingPongFBO(gl, { 14 | fragment: boidsVelocities, 15 | uniforms: { 16 | uDeltaTime: 0, 17 | uPerceptionRadius: 0.1, 18 | uMaxSpeed: 0.4, 19 | uSeparationWeight: 1.5, 20 | uAlignmentWeight: 1.0, 21 | uCohesionWeight: 0.8, 22 | uBorderForce: 1, 23 | uBorderDistance: 0.8, 24 | uPredatorRepulsionStrength: 2.5, 25 | uPredatorRepulsionRadius: 1, 26 | tPositions: () => positions.texture, 27 | }, 28 | dataTexture: { 29 | name: "tVelocities", 30 | initialData: Array.from({ length: count }).flatMap(() => [ 31 | /* R */ Math.random() * 0.2 - 0.1, 32 | /* G */ Math.random() * 0.2 - 0.1, 33 | /* B */ 0, 34 | /* A */ 0, 35 | ]), 36 | }, 37 | }); 38 | 39 | const positions = usePingPongFBO(gl, { 40 | fragment: boidsPositions, 41 | uniforms: { 42 | uDeltaTime: 0, 43 | tVelocities: () => velocities.texture, 44 | }, 45 | dataTexture: { 46 | name: "tPositions", 47 | initialData: Array.from({ length: count }).flatMap(() => [ 48 | /* R */ Math.random() * 2 - 1, 49 | /* G */ Math.random() * 2 - 1, 50 | /* B */ 0, 51 | /* A */ 0, 52 | ]), 53 | }, 54 | }); 55 | 56 | const renderPass = useWebGLCanvas({ 57 | canvas, 58 | vertex: renderPassVertex, 59 | fragment: renderPassFragment, 60 | uniforms: { 61 | tPositions: () => positions.texture, 62 | tVelocities: () => velocities.texture, 63 | }, 64 | attributes: { 65 | aCoords: positions.coords, 66 | }, 67 | transparent: true, 68 | }); 69 | 70 | // Wait for the canvas to be resized to avoid a flash at the first renders 71 | renderPass.onCanvasReady(() => { 72 | useLoop(({ deltaTime }) => { 73 | velocities.uniforms.uDeltaTime = deltaTime / 500; 74 | velocities.render(); 75 | 76 | positions.uniforms.uDeltaTime = deltaTime / 500; 77 | positions.render(); 78 | 79 | renderPass.render(); 80 | }); 81 | }); 82 | 83 | const pane = new Pane({ title: "Uniforms", expanded: false }); 84 | pane.addBinding(velocities.uniforms, "uMaxSpeed", { min: 0.2, max: 0.8 }); 85 | 86 | const flocking = pane.addFolder({ title: "Flocking" }); 87 | flocking.addBinding(velocities.uniforms, "uAlignmentWeight", { min: 0.5, max: 1.5 }); 88 | flocking.addBinding(velocities.uniforms, "uSeparationWeight", { min: 1, max: 2 }); 89 | flocking.addBinding(velocities.uniforms, "uCohesionWeight", { min: 0.5, max: 1.5 }); 90 | 91 | const predator = pane.addFolder({ title: "Predator" }); 92 | predator.addBinding(velocities.uniforms, "uPredatorRepulsionStrength", { min: 1, max: 3 }); 93 | predator.addBinding(velocities.uniforms, "uPredatorRepulsionRadius", { min: 0.5, max: 1.5 }); 94 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/positions.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D tPositions; 2 | uniform sampler2D tVelocities; 3 | uniform float uDeltaTime; 4 | 5 | varying vec2 uv; 6 | 7 | void main() { 8 | vec2 position = texture(tPositions, uv).xy; 9 | vec2 velocity = texture(tVelocities, uv).xy; 10 | 11 | position += velocity * uDeltaTime; 12 | 13 | gl_FragColor = vec4(position, 0.0, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/render.frag: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | varying mat2 vRotation; 3 | 4 | // https://iquilezles.org/articles/distfunctions2d/ 5 | float sdUnevenCapsule( vec2 p, float r1, float r2, float h ) { 6 | p.x = abs(p.x); 7 | float b = (r1-r2)/h; 8 | float a = sqrt(1.0-b*b); 9 | float k = dot(p,vec2(-b,a)); 10 | if( k < 0.0 ) return length(p) - r1; 11 | if( k > a*h ) return length(p-vec2(0.0,h)) - r2; 12 | return dot(p, vec2(a,b) ) - r1; 13 | } 14 | 15 | void main() { 16 | vec2 uv = vRotation * (gl_PointCoord.xy - .5); 17 | 18 | float h = .4; 19 | float r1 = .12; 20 | float r2 = .28; 21 | float offset = (h + r1) / 2.; 22 | float capsuleDist = sdUnevenCapsule(uv + vec2(0., offset), r1, r2, h); 23 | 24 | vec4 color = vColor; 25 | color.a *= 1. - step(0., capsuleDist); 26 | 27 | gl_FragColor = color; 28 | } 29 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/render.vert: -------------------------------------------------------------------------------- 1 | uniform sampler2D tPositions; 2 | uniform sampler2D tVelocities; 3 | attribute vec2 aCoords; 4 | varying vec4 vColor; 5 | varying mat2 vRotation; 6 | 7 | #define PI acos(-1.) 8 | 9 | void main() { 10 | vec2 position = texture2D(tPositions, aCoords).xy; 11 | vec2 velocity = texture2D(tVelocities, aCoords).xy; 12 | 13 | vec2 orientation = normalize(velocity.xy); 14 | float angle = atan(orientation.y, orientation.x) - PI / 2.; 15 | vRotation = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)); 16 | 17 | if(aCoords == vec2(0)) { 18 | // predator 19 | vColor = vec4(1, 0, 0, 1); 20 | gl_PointSize = 40.0; 21 | } else { 22 | // boid 23 | vec2 predatorPosition = texture2D(tPositions, vec2(0)).xy; 24 | float distance = length(position - predatorPosition); 25 | vColor = mix(vec4(1, .5, 0, 1), vec4(0, .8, 1, 1), smoothstep(0.2, .8, distance)); 26 | vColor = mix(vColor, vec4(0), smoothstep(.6, 2., distance)); 27 | gl_PointSize = 30.0; 28 | } 29 | 30 | gl_Position = vec4(position, 0, 1); 31 | } 32 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | canvas { 6 | width: 100svw; 7 | height: 100svh; 8 | display: block; 9 | background: black; 10 | } 11 | 12 | .tp-dfwv { 13 | width: 370px !important; 14 | } 15 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/boids/velocities.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D tPositions; 2 | uniform sampler2D tVelocities; 3 | uniform float uDeltaTime; 4 | 5 | uniform float uMaxSpeed; 6 | uniform float uPerceptionRadius; 7 | 8 | uniform float uSeparationWeight; 9 | uniform float uAlignmentWeight; 10 | uniform float uCohesionWeight; 11 | 12 | uniform float uBorderForce; 13 | uniform float uBorderDistance; 14 | 15 | uniform float uPredatorRepulsionStrength; 16 | uniform float uPredatorRepulsionRadius; 17 | 18 | varying vec2 uv; 19 | 20 | float random(vec2 st) { 21 | return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); 22 | } 23 | 24 | vec2 flockingForces(vec2 position, vec2 velocity) { 25 | vec2 texSize = vec2(textureSize(tVelocities, 0)); 26 | vec2 separation = vec2(0.0); 27 | vec2 alignment = vec2(0.0); 28 | vec2 cohesion = vec2(0.0); 29 | int count = 0; 30 | 31 | // Accumulate forces from neighbors 32 | for(float y = 1.0; y < texSize.y; y++) { 33 | for(float x = 1.0; x < texSize.x; x++) { 34 | vec2 neighborUV = vec2(x, y) / texSize; 35 | if(neighborUV == uv) continue; 36 | 37 | vec2 neighborPos = texture2D(tPositions, neighborUV).xy; 38 | vec2 neighborVel = texture2D(tVelocities, neighborUV).xy; 39 | float dist = distance(position, neighborPos); 40 | 41 | if(dist < uPerceptionRadius && dist > 0.0) { 42 | separation += normalize(position - neighborPos) * (1.0 - dist/uPerceptionRadius); 43 | alignment += neighborVel; 44 | cohesion += neighborPos; 45 | count++; 46 | } 47 | } 48 | } 49 | 50 | if(count > 0) { 51 | separation = normalize(separation) * uSeparationWeight; 52 | alignment = normalize(alignment/float(count)) * uAlignmentWeight; 53 | cohesion = normalize((cohesion/float(count)) - position) * uCohesionWeight; 54 | } 55 | return separation + alignment + cohesion; 56 | } 57 | 58 | vec2 borderRepulsion(vec2 position) { 59 | vec2 distToBorder = vec2(1.0 - abs(position.x), 1.0 - abs(position.y)); 60 | vec2 force = vec2(0.0); 61 | 62 | if(distToBorder.x < uBorderDistance) { 63 | force.x = (uBorderDistance - distToBorder.x) / uBorderDistance * -sign(position.x); 64 | } 65 | if(distToBorder.y < uBorderDistance) { 66 | force.y = (uBorderDistance - distToBorder.y) / uBorderDistance * -sign(position.y); 67 | } 68 | return force * uBorderForce; 69 | } 70 | 71 | vec2 randomWandering(vec2 position, vec2 velocity) { 72 | float rand = random(position.xy) * 2.0 - 1.0; 73 | float angle = rand * radians(10.0); 74 | mat2 rotationMatrix = mat2( 75 | cos(angle), -sin(angle), 76 | sin(angle), cos(angle) 77 | ); 78 | return normalize(rotationMatrix * velocity) * 5.; 79 | } 80 | 81 | vec2 predatorRepulsion(vec2 position, vec2 velocity) { 82 | vec2 predatorPos = texture2D(tPositions, vec2(0.0)).xy; 83 | vec2 toPredator = predatorPos - position; 84 | float predatorDist = length(toPredator); 85 | 86 | if (predatorDist < uPredatorRepulsionRadius) { 87 | return normalize(-1. * toPredator) * uPredatorRepulsionStrength * (1.0 - predatorDist / uPredatorRepulsionRadius); 88 | } 89 | return vec2(0.0); 90 | } 91 | 92 | void main() { 93 | vec2 position = texture2D(tPositions, uv).xy; 94 | vec2 velocity = texture2D(tVelocities, uv).xy; 95 | vec2 acceleration = borderRepulsion(position); 96 | 97 | bool isPredator = floor(gl_FragCoord.xy) == vec2(0.0); 98 | 99 | if (isPredator) { 100 | acceleration += randomWandering(position, velocity); 101 | } else { 102 | acceleration += flockingForces(position, velocity) + predatorRepulsion(position, velocity); 103 | } 104 | velocity += acceleration * uDeltaTime; 105 | 106 | float speedLimit = isPredator ? uMaxSpeed / 2.0 : uMaxSpeed; 107 | if(length(velocity) > speedLimit) { 108 | velocity = normalize(velocity) * speedLimit; 109 | } 110 | 111 | gl_FragColor = vec4(velocity, 0.0, 1.0); 112 | } 113 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/game-of-life/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Game of Life 3 | position: 3 4 | --- 5 | 6 | ::: example-editor 7 | 8 | <<< ./index.ts 9 | <<< @/snippets/canvas-square/styles.css 10 | <<< @/snippets/default/index.html 11 | 12 | ::: 13 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/game-of-life/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLContext, useLoop, usePingPongFBO, useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const { gl, canvas } = useWebGLContext("#glCanvas"); 5 | 6 | const gridSize = 100; 7 | 8 | const lifeUpdateFragment = /* glsl */ ` 9 | uniform sampler2D tCurrentState; 10 | varying vec2 vUv; 11 | 12 | int getCellState(vec2 coord) { 13 | vec4 color = texture2D(tCurrentState, coord); 14 | return color.r > 0.5 ? 1 : 0; 15 | } 16 | 17 | void main() { 18 | int neighbors = 0; 19 | float dx = 1.0 / float(${gridSize}); 20 | float dy = 1.0 / float(${gridSize}); 21 | 22 | neighbors += getCellState(vUv + vec2(-dx, -dy)); 23 | neighbors += getCellState(vUv + vec2(-dx, 0.0)); 24 | neighbors += getCellState(vUv + vec2(-dx, dy)); 25 | neighbors += getCellState(vUv + vec2(0.0, -dy)); 26 | neighbors += getCellState(vUv + vec2(0.0, dy)); 27 | neighbors += getCellState(vUv + vec2(dx, -dy)); 28 | neighbors += getCellState(vUv + vec2(dx, 0.0)); 29 | neighbors += getCellState(vUv + vec2(dx, dy)); 30 | 31 | int currentState = getCellState(vUv); 32 | float newState = 0.0; 33 | 34 | if (currentState == 0 && neighbors == 3) { 35 | newState = 1.0; // Birth 36 | } else if (currentState == 1 && (neighbors == 2 || neighbors == 3)) { 37 | newState = 1.0; // Survival 38 | } 39 | 40 | gl_FragColor = vec4(newState, newState, newState, 1.0); 41 | } 42 | `; 43 | 44 | const initialData = new Float32Array(gridSize * gridSize * 4); 45 | for (let i = 0; i < gridSize * gridSize; i++) { 46 | const alive = Math.random() < 0.5 ? 1 : 0; 47 | initialData.set([alive, alive, alive, 1], i * 4); 48 | } 49 | 50 | const gameState = usePingPongFBO(gl, { 51 | fragment: lifeUpdateFragment, 52 | dataTexture: { 53 | name: "tCurrentState", 54 | initialData, 55 | }, 56 | }); 57 | 58 | const renderPass = useWebGLCanvas({ 59 | canvas, 60 | fragment: /* glsl */ ` 61 | uniform sampler2D tCurrentState; 62 | attribute vec2 vUv; 63 | 64 | void main() { 65 | gl_FragColor = texture2D(tCurrentState, vUv); 66 | } 67 | `, 68 | uniforms: { 69 | tCurrentState: () => gameState.texture, 70 | }, 71 | }); 72 | 73 | let lastTime = 0; 74 | useLoop(({ elapsedTime }) => { 75 | if (elapsedTime - lastTime < 50) return; 76 | gameState.render(); 77 | renderPass.render(); 78 | lastTime = elapsedTime; 79 | }); 80 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/particles/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Particles 3 | position: 1 4 | --- 5 | 6 | ::: example-editor 7 | 8 | <<< ./index.ts 9 | <<< @/snippets/canvas-full/styles.css 10 | <<< @/snippets/default/index.html 11 | 12 | ::: 13 | -------------------------------------------------------------------------------- /docs/examples/gpgpu/particles/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useWebGLContext, 3 | useLoop, 4 | usePingPongFBO, 5 | useWebGLCanvas, 6 | createFloatDataTexture, 7 | } from "usegl"; 8 | import "./styles.css"; 9 | 10 | const { gl, canvas } = useWebGLContext("#glCanvas"); 11 | 12 | const positionsFragment = /* glsl */ ` 13 | uniform float uDeltaTime; 14 | uniform sampler2D tPositions; 15 | uniform sampler2D tVelocities; 16 | 17 | in vec2 uv; 18 | out vec4 fragColor; 19 | 20 | vec2 wrapToRange(vec2 value) { 21 | vec2 shifted = value + 1.0; 22 | vec2 wrapped = mod(mod(shifted, 2.0) + 2.0, 2.0); 23 | return wrapped - 1.0; 24 | } 25 | 26 | void main() { 27 | vec2 position = texture(tPositions, uv).xy; 28 | vec2 velocity = texture(tVelocities, uv).xy; 29 | 30 | fragColor = vec4(wrapToRange(position + velocity * uDeltaTime), 0.0, 0.0); 31 | } 32 | `; 33 | 34 | const count = 100; 35 | 36 | const positions = usePingPongFBO(gl, { 37 | fragment: positionsFragment, 38 | uniforms: { 39 | uDeltaTime: 0, 40 | tVelocities: createFloatDataTexture( 41 | Array.from({ length: count }).flatMap(() => [ 42 | /* R */ Math.random() * 0.2 - 0.1, 43 | /* G */ Math.random() * 0.2 - 0.1, 44 | /* B */ 0, 45 | /* A */ 0, 46 | ]), 47 | ), 48 | }, 49 | dataTexture: { 50 | name: "tPositions", 51 | initialData: Array.from({ length: count }).flatMap(() => [ 52 | /* R */ Math.random() * 2 - 1, 53 | /* G */ Math.random() * 2 - 1, 54 | /* B */ 0, 55 | /* A */ 0, 56 | ]), 57 | }, 58 | }); 59 | 60 | const renderPass = useWebGLCanvas({ 61 | canvas, 62 | vertex: ` 63 | uniform sampler2D uPositions; 64 | in vec2 aCoords; 65 | 66 | void main() { 67 | gl_Position = vec4(texture2D(uPositions, aCoords).xy, 0, 1); 68 | gl_PointSize = 20.0; 69 | } 70 | `, 71 | fragment: ` 72 | in vec2 uv; 73 | out vec4 outColor; 74 | 75 | void main() { 76 | vec2 uv = gl_PointCoord.xy; 77 | outColor = vec4(0, 1, .5, smoothstep(0.5, 0.4, length(uv - 0.5))); 78 | } 79 | `, 80 | uniforms: { 81 | uPositions: () => positions.texture, 82 | }, 83 | attributes: { 84 | aCoords: positions.coords, 85 | }, 86 | transparent: true, 87 | }); 88 | 89 | // Wait for the canvas to be resized to avoid a flash at the first renders 90 | renderPass.onCanvasReady(() => { 91 | useLoop(({ deltaTime }) => { 92 | positions.uniforms.uDeltaTime = deltaTime / 500; 93 | positions.render(); 94 | renderPass.render(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /docs/examples/interactions/pointer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pointer coordinates 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-full/styles.css 9 | <<< @/snippets/render-count/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/interactions/pointer/index.ts: -------------------------------------------------------------------------------- 1 | import { usePointerEvents, useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const canvas = document.querySelector("canvas")!; 5 | 6 | const { uniforms, onAfterRender } = useWebGLCanvas({ 7 | canvas, 8 | fragment: /* glsl */ ` 9 | varying vec2 vUv; 10 | uniform vec2 uPointerPosition; 11 | uniform vec2 uResolution; 12 | uniform vec3 uCircleColor; 13 | 14 | void main() { 15 | vec2 uv = (vUv - .5) * uResolution / min(uResolution.x, uResolution.y); 16 | float dist = distance(uv, uPointerPosition); 17 | float circleMask = 1. - smoothstep(.099, .101, dist); 18 | vec3 circle = mix(vec3(0.), uCircleColor, circleMask); 19 | gl_FragColor = vec4(circle, 1.); 20 | } 21 | `, 22 | uniforms: { 23 | uPointerPosition: [0, 0], 24 | uCircleColor: [1, 1, 1], 25 | }, 26 | }); 27 | 28 | usePointerEvents(canvas, { 29 | move: ({ pointer, canvasCenter, canvasRect }) => { 30 | uniforms.uPointerPosition = [ 31 | (pointer.x - canvasCenter.x) / Math.min(canvasRect.width, canvasRect.height), 32 | (canvasCenter.y - pointer.y) / Math.min(canvasRect.width, canvasRect.height), 33 | ]; 34 | }, 35 | down: () => { 36 | uniforms.uCircleColor = [1, 0, 0]; 37 | }, 38 | up: () => { 39 | uniforms.uCircleColor = [1, 1, 1]; 40 | }, 41 | }); 42 | 43 | const renderCount = document.querySelector("#renderCount"); 44 | onAfterRender(() => { 45 | renderCount.textContent = `${Number(renderCount.textContent) + 1}`; 46 | }); 47 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/blur.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D uImage; 2 | uniform vec2 uResolution; 3 | uniform vec2 uDirection; 4 | in vec2 vUv; 5 | out vec4 outColor; 6 | 7 | const float weights[5] = float[](0.19638062, 0.29675293, 0.09442139, 0.01037598, 0.00025940); 8 | const float offsets[5] = float[](0.0, 1.41176471, 3.29411765, 5.17647059, 7.05882353); 9 | 10 | void main() { 11 | vec3 color = texture(uImage, vUv).rgb * weights[0]; 12 | float weightSum = weights[0]; 13 | 14 | if (vUv.x < 0.52) { 15 | for(int i = 1; i < 5; i++) { 16 | vec2 offset = (offsets[i] / uResolution) * 0.5 * uDirection; 17 | color += texture(uImage, vUv + offset).rgb * weights[i]; 18 | color += texture(uImage, vUv - offset).rgb * weights[i]; 19 | weightSum += 2.0 * weights[i]; 20 | } 21 | color /= weightSum; 22 | } 23 | 24 | outColor = vec4(color, 1.0); 25 | } 26 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/circles.frag: -------------------------------------------------------------------------------- 1 | uniform float uTime; 2 | in vec2 vUv; 3 | out vec4 outColor; 4 | 5 | float sdCircle(vec2 p, float r) { 6 | return length(p) - r; 7 | } 8 | 9 | vec3 drawCircle(vec2 pos, float radius, vec3 color) { 10 | return smoothstep(radius * 1.01, radius * .99, sdCircle(pos, radius)) * color; 11 | } 12 | 13 | void main() { 14 | vec3 color = vec3(0, 0.07, 0.15); 15 | color += drawCircle(vUv - vec2(.4), .1 * (1. + sin(uTime/2.)/10.), vec3(vUv, 1.)); 16 | color += drawCircle(vUv - vec2(.65, .65), .015 * (1. + sin(uTime/2.-1.5)/4.), vec3(vUv, 1.)); 17 | color += drawCircle(vUv - vec2(.75, .4), .04 * (1. + sin(uTime/2.-1.)/4.), vec3(vUv, 1.)); 18 | outColor = vec4(color, 1.); 19 | } 20 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/combine.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D uBaseImage; 2 | uniform sampler2D uBloomTexture; 3 | uniform vec2 uResolution; 4 | uniform float uMix; 5 | 6 | in vec2 vUv; 7 | out vec4 outColor; 8 | 9 | vec2 offset(float octave) { 10 | vec2 padding = 10.0 / uResolution; 11 | float octaveFloor = min(1.0, floor(octave / 3.0)); 12 | vec2 offset = vec2( 13 | -octaveFloor * (0.25 + padding.x), 14 | -(1.0 - (1.0 / exp2(octave))) - padding.y * octave + octaveFloor * (0.35 + padding.y) 15 | ); 16 | return offset + 0.5 / uResolution; 17 | } 18 | 19 | vec3 blurredMipmapLevel(float octave) { 20 | vec2 offset = offset(octave - 1.0); 21 | return texture(uBloomTexture, vUv / exp2(octave) - offset).rgb; 22 | } 23 | 24 | vec3 bloomColor() { 25 | return blurredMipmapLevel(1.0) * 0.8 26 | + blurredMipmapLevel(3.0) * 0.5 27 | + blurredMipmapLevel(4.0) * 1.2; 28 | } 29 | 30 | void main() { 31 | vec4 baseColor = texture(uBaseImage, vUv); 32 | float baseColorGreyscale = dot(baseColor.rgb, vec3(0.299, 0.587, 0.114)); 33 | float mixFactor = (1.0 - baseColorGreyscale * baseColor.a) * uMix; 34 | 35 | vec4 combinedColor = baseColor; 36 | combinedColor.rgb += bloomColor() * mixFactor; 37 | 38 | outColor = combinedColor; 39 | } 40 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Multi pass (bloom)" 3 | --- 4 | 5 | ::: example-editor {deps=tweakpane@^4.0.5} 6 | 7 | <<< ./index.ts 8 | <<< ./circles.frag 9 | <<< ./mipmap.frag 10 | <<< ./blur.frag 11 | <<< ./combine.frag 12 | <<< ./styles.css 13 | <<< @/snippets/default/index.html 14 | 15 | ::: 16 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffectPass, useWebGLCanvas, useCompositeEffectPass } from "usegl"; 2 | import fragment from "./circles.frag?raw"; 3 | import mipmapsShader from "./mipmap.frag?raw"; 4 | import blurShader from "./blur.frag?raw"; 5 | import combineShader from "./combine.frag?raw"; 6 | import { Pane } from "tweakpane"; 7 | import "./styles.css"; 8 | 9 | const mipmaps = useEffectPass({ 10 | fragment: mipmapsShader, 11 | uniforms: { 12 | uThreshold: 0.2, 13 | }, 14 | }); 15 | 16 | const horizontalBlur = useEffectPass({ 17 | fragment: blurShader, 18 | uniforms: { 19 | uDirection: [1, 0], 20 | }, 21 | }); 22 | 23 | const verticalBlur = useEffectPass({ 24 | fragment: blurShader, 25 | uniforms: { 26 | uDirection: [0, 1], 27 | }, 28 | }); 29 | 30 | const combine = useEffectPass({ 31 | fragment: combineShader, 32 | uniforms: { 33 | uBaseImage: ({ inputPass }) => inputPass.target!.texture, 34 | uBloomTexture: () => verticalBlur.target!.texture, 35 | uMix: 1, 36 | }, 37 | }); 38 | 39 | const bloomEffect = useCompositeEffectPass({ 40 | mipmaps, 41 | horizontalBlur, 42 | verticalBlur, 43 | combine, 44 | }); 45 | 46 | const vignetteEffect = useEffectPass({ 47 | fragment: /* glsl */ ` 48 | uniform sampler2D uTexture; 49 | uniform float uSize; // (0.0 - 1.0) 50 | uniform float uRoundness; // (0.0 = rectangle, 1.0 = round) 51 | uniform float uStrength; // (0.0 - 1.0) 52 | varying vec2 vUv; 53 | 54 | float vignette() { 55 | vec2 centered = vUv * 2.0 - 1.0; 56 | float circDist = length(centered); 57 | float rectDist = max(abs(centered.x), abs(centered.y)); 58 | float dist = mix(rectDist, circDist, uRoundness); 59 | return 1. - smoothstep(uSize, uSize * 2., dist) * uStrength; 60 | } 61 | 62 | void main() { 63 | vec4 color = texture(uTexture, vUv); 64 | color.rgb *= vignette(); 65 | gl_FragColor = color; 66 | } 67 | `, 68 | uniforms: { 69 | uStrength: 0.5, 70 | uSize: 0.6, 71 | uRoundness: 0.7, 72 | }, 73 | }); 74 | 75 | useWebGLCanvas({ 76 | canvas: "#glCanvas", 77 | fragment: fragment, 78 | postEffects: [vignetteEffect, bloomEffect], 79 | }); 80 | 81 | const pane = new Pane({ title: "Uniforms" }); 82 | 83 | // You can update the uniforms of each individual pass, which will trigger a re-render 84 | const bloom = pane.addFolder({ title: "Bloom" }); 85 | bloom.addBinding(bloomEffect.passes.mipmaps.uniforms, "uThreshold", { min: 0, max: 1 }); 86 | bloom.addBinding(combine.uniforms, "uMix", { min: 0, max: 1 }); 87 | 88 | const vignette = pane.addFolder({ title: "Vignette" }); 89 | vignette.addBinding(vignetteEffect.uniforms, "uStrength", { min: 0, max: 1 }); 90 | vignette.addBinding(vignetteEffect.uniforms, "uSize", { min: 0, max: 1 }); 91 | vignette.addBinding(vignetteEffect.uniforms, "uRoundness", { min: 0, max: 1 }); 92 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/mipmap.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D uImage; 2 | uniform vec2 uResolution; 3 | uniform float uThreshold; 4 | 5 | in vec2 vUv; 6 | out vec4 outColor; 7 | 8 | vec2 offset(float octave) { 9 | vec2 padding = 10.0 / uResolution; 10 | float octaveFloor = min(1.0, floor(octave / 3.0)); 11 | return vec2( 12 | -octaveFloor * (0.25 + padding.x), 13 | -(1.0 - (1.0 / exp2(octave))) - padding.y * octave + octaveFloor * (0.35 + padding.y) 14 | ); 15 | } 16 | 17 | vec3 mipmapLevel(float octave) { 18 | float scale = exp2(octave); 19 | vec2 coord = (vUv + offset(octave - 1.0)) * scale; 20 | 21 | if (any(lessThan(coord, vec2(0.0))) || any(greaterThan(coord, vec2(1.0)))) { 22 | return vec3(0.0); 23 | } 24 | 25 | vec3 color = vec3(0.0); 26 | int spread = int(scale); 27 | 28 | for (int i = 0; i < spread; i++) { 29 | for (int j = 0; j < spread; j++) { 30 | vec2 offset = (vec2(i, j) / uResolution) * scale / float(spread); 31 | vec3 imageColor = texture(uImage, coord + offset).rgb; 32 | color += max(vec3(0.0), imageColor - vec3(uThreshold)); 33 | } 34 | } 35 | 36 | return color / float(spread * spread); 37 | } 38 | 39 | void main() { 40 | outColor = vec4(mipmapLevel(1.0) + mipmapLevel(3.0) + mipmapLevel(4.0), 1.0); 41 | } 42 | -------------------------------------------------------------------------------- /docs/examples/post-processing/multi-pass/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: black; 4 | height: 100svh; 5 | display: grid; 6 | place-items: center; 7 | } 8 | 9 | canvas { 10 | width: min(90svmin, 900px); 11 | aspect-ratio: 1; 12 | display: block; 13 | border-radius: 8px; 14 | border: 1px solid #555; 15 | } 16 | 17 | .tp-dfwv { 18 | width: 260px !important; 19 | } 20 | -------------------------------------------------------------------------------- /docs/examples/post-processing/single-pass/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Single pass (sepia) 3 | --- 4 | 5 | ::: example-editor {deps=tweakpane@^4.0.5} 6 | 7 | <<< ./index.ts 8 | <<< ./styles.css 9 | <<< @/snippets/render-count/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/post-processing/single-pass/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffectPass, useWebGLCanvas, loadTexture } from "usegl"; 2 | import { Pane } from "tweakpane"; 3 | import "./styles.css"; 4 | 5 | const image = loadTexture("https://picsum.photos/id/323/600/400"); 6 | 7 | const sepiaEffect = useEffectPass({ 8 | fragment: /* glsl */ ` 9 | uniform sampler2D uTexture; // output of the render pass 10 | uniform float uStrength; 11 | in vec2 vUv; 12 | out vec4 fragColor; 13 | 14 | #define SEPIA_COLOR vec3(1.2, 1.0, 0.7) 15 | 16 | vec3 sepia(vec3 color) { 17 | float grayScale = dot(color, vec3(0.299, 0.587, 0.114)); 18 | return grayScale * SEPIA_COLOR; 19 | } 20 | 21 | void main() { 22 | vec3 color = texture(uTexture, vUv).rgb; 23 | color = mix(color, sepia(color), uStrength); 24 | fragColor = vec4(color, 1.); 25 | } 26 | `, 27 | uniforms: { 28 | uStrength: 0.75, 29 | }, 30 | }); 31 | 32 | const { onAfterRender } = useWebGLCanvas({ 33 | canvas: "#glCanvas", 34 | fragment: /* glsl */ ` 35 | in vec2 vUv; 36 | uniform sampler2D uPicture; 37 | out vec4 fragColor; 38 | 39 | void main() { 40 | fragColor = texture(uPicture, vUv); 41 | } 42 | `, 43 | uniforms: { 44 | uPicture: image, 45 | }, 46 | postEffects: [sepiaEffect], 47 | }); 48 | 49 | const renderCount = document.querySelector("#renderCount"); 50 | onAfterRender(() => { 51 | renderCount.textContent = `${Number(renderCount.textContent) + 1}`; 52 | }); 53 | 54 | // You can update the uniforms of an effect pass 55 | const pane = new Pane({ title: "Uniforms" }); 56 | pane.addBinding(sepiaEffect.uniforms, "uStrength", { min: 0, max: 1 }); 57 | -------------------------------------------------------------------------------- /docs/examples/post-processing/single-pass/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | height: 100svh; 4 | display: grid; 5 | place-items: center; 6 | background: black; 7 | } 8 | 9 | canvas { 10 | width: 100svw; 11 | aspect-ratio: 600 / 400; 12 | display: block; 13 | } 14 | -------------------------------------------------------------------------------- /docs/examples/textures/canvas2d/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canvas 2D 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-square/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/textures/canvas2d/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const canvas2d = drawIn2dCanvas(); 5 | 6 | const webglCanvas = document.querySelector("canvas")!; 7 | // don't forget to make the content accessible ! 8 | webglCanvas.ariaLabel = "TEXT rendered in a 2D canvas"; 9 | 10 | useWebGLCanvas({ 11 | canvas: webglCanvas, 12 | fragment: /* glsl */ ` 13 | varying vec2 vUv; 14 | uniform sampler2D uTexture; 15 | 16 | void main() { 17 | gl_FragColor = texture(uTexture, vUv); 18 | } 19 | `, 20 | uniforms: { 21 | uTexture: { 22 | src: canvas2d, 23 | }, 24 | }, 25 | }); 26 | 27 | function drawIn2dCanvas() { 28 | const canvas2d = document.createElement("canvas"); 29 | const SIZE = 1024; 30 | canvas2d.width = canvas2d.height = SIZE; 31 | const ctx = canvas2d.getContext("2d", { alpha: false })!; 32 | 33 | ctx.fillStyle = "#012"; 34 | ctx.beginPath(); 35 | ctx.arc(SIZE / 2, SIZE / 2, 300, 0, Math.PI * 2); 36 | ctx.closePath(); 37 | ctx.fill(); 38 | 39 | ctx.fillStyle = "white"; 40 | ctx.textAlign = "center"; 41 | ctx.font = "100px sans-serif"; 42 | ctx.fillText("TEXT", SIZE / 2, SIZE / 2); 43 | ctx.font = "40px sans-serif"; 44 | ctx.fillText("rendered in a 2D canvas", SIZE / 2, SIZE / 2 + 60); 45 | 46 | return canvas2d; 47 | } 48 | -------------------------------------------------------------------------------- /docs/examples/textures/data/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< @/snippets/canvas-full/styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/textures/data/index.ts: -------------------------------------------------------------------------------- 1 | import { useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | useWebGLCanvas({ 5 | canvas: "#glCanvas", 6 | fragment: /* glsl */ ` 7 | in vec2 vUv; 8 | uniform sampler2D uDataTexture; 9 | out vec4 fragColor; 10 | 11 | void main() { 12 | fragColor = texture(uDataTexture, vUv); 13 | } 14 | `, 15 | uniforms: { 16 | uDataTexture: { 17 | data: new Uint8Array( 18 | [ 19 | [255, 0, 0, 255], // red 20 | [0, 255, 0, 255], // green 21 | [0, 0, 255, 255], // blue 22 | [255, 255, 0, 255], // yellow 23 | [255, 0, 255, 255], // magenta 24 | [0, 255, 255, 255], // cyan 25 | [255, 255, 255, 255], // white 26 | [128, 128, 128, 255], // gray 27 | [0, 0, 0, 255], // black 28 | ].flat(), 29 | ), 30 | width: 3, 31 | height: 3, 32 | magFilter: "nearest", 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /docs/examples/textures/image/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | uniform sampler2D uTexture; 3 | 4 | void main() { 5 | vec3 color = texture(uTexture, vUv).rgb; 6 | color *= step(0., vUv.x) * (1. - step(1., vUv.x)); 7 | color *= step(0., vUv.y) * (1. - step(1., vUv.y)); 8 | 9 | gl_FragColor = vec4(color, 1.); 10 | } 11 | -------------------------------------------------------------------------------- /docs/examples/textures/image/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Image" 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< ./vertex.glsl 9 | <<< ./fragment.glsl 10 | <<< @/snippets/canvas-full/styles.css 11 | <<< @/snippets/default/index.html 12 | 13 | ::: 14 | -------------------------------------------------------------------------------- /docs/examples/textures/image/index.ts: -------------------------------------------------------------------------------- 1 | import { loadTexture, useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | import fragment from "./fragment.glsl?raw"; 4 | import vertex from "./vertex.glsl?raw"; 5 | 6 | const texture = loadTexture("https://picsum.photos/id/669/600/400", { 7 | // the placeholder is optional 8 | placeholder: { 9 | data: new Uint8Array( 10 | [ 11 | [255, 255, 255, 255], 12 | [255, 255, 255, 255], 13 | [255, 255, 255, 255], 14 | [255, 255, 255, 255], 15 | [99, 46, 22, 255], 16 | [255, 255, 255, 255], 17 | ].flat(), 18 | ), 19 | width: 3, 20 | height: 2, 21 | //magFilter: "nearest", 22 | }, 23 | }); 24 | 25 | useWebGLCanvas({ 26 | canvas: "#glCanvas", 27 | vertex, 28 | fragment, 29 | uniforms: { 30 | uTexture: texture, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /docs/examples/textures/image/vertex.glsl: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example of implementation of the `object-fit: contain/cover` CSS property. 3 | * It would be much simpler to give the canvas the size of the image. 4 | */ 5 | 6 | attribute vec2 position; 7 | attribute vec2 uv; 8 | uniform sampler2D uTexture; 9 | uniform vec2 uResolution; 10 | varying vec2 vUv; 11 | 12 | #define CONTAIN 1 13 | #define COVER 2 14 | #define OBJECT_FIT CONTAIN 15 | 16 | void main() { 17 | vec2 textureResolution = vec2(textureSize(uTexture, 0)); 18 | float canvasRatio = uResolution.x / uResolution.y; 19 | float textureRatio = textureResolution.x / textureResolution.y; 20 | 21 | vUv = position; 22 | if(OBJECT_FIT == CONTAIN ? canvasRatio > textureRatio : canvasRatio < textureRatio) { 23 | vUv.x *= canvasRatio / textureRatio; 24 | } else { 25 | vUv.y *= textureRatio / canvasRatio; 26 | } 27 | vUv = (vUv + 1.0) / 2.0; 28 | gl_Position = vec4(position, 0.0, 1.0); 29 | } 30 | -------------------------------------------------------------------------------- /docs/examples/textures/video/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Video" 3 | --- 4 | 5 | ::: example-editor 6 | 7 | <<< ./index.ts 8 | <<< ./styles.css 9 | <<< @/snippets/default/index.html 10 | 11 | ::: 12 | -------------------------------------------------------------------------------- /docs/examples/textures/video/index.ts: -------------------------------------------------------------------------------- 1 | import { loadVideoTexture, useWebGLCanvas } from "usegl"; 2 | import "./styles.css"; 3 | 4 | const texture = loadVideoTexture( 5 | "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", 6 | ); 7 | 8 | useWebGLCanvas({ 9 | canvas: "#glCanvas", 10 | fragment: /* glsl */ ` 11 | varying vec2 vUv; 12 | uniform sampler2D uTexture; 13 | 14 | void main() { 15 | vec3 color = texture(uTexture, vUv).rgb; 16 | 17 | // try playing with the colors here 18 | //color = color.rbg; 19 | 20 | gl_FragColor = vec4(color, 1.); 21 | } 22 | `, 23 | uniforms: { 24 | uTexture: texture, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /docs/examples/textures/video/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | height: 100svh; 4 | display: grid; 5 | place-items: center; 6 | background: black; 7 | } 8 | 9 | canvas { 10 | width: 100svw; 11 | display: block; 12 | } 13 | -------------------------------------------------------------------------------- /docs/guide/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | The documentation is a work in progress, and the API of useGL is still subject to changes. For now, you can browse the [examples](/examples/basics/full-screen/) to get an idea of how the library works. 4 | 5 | A proper documentation will come soon ! 6 | 7 | ## Installation 8 | 9 | ::: code-group 10 | 11 | ```sh [npm] 12 | $ npm add -D usegl 13 | ``` 14 | 15 | ```sh [pnpm] 16 | $ pnpm add -D usegl 17 | ``` 18 | 19 | ```sh [yarn] 20 | $ yarn add -D usegl 21 | ``` 22 | 23 | ```sh [bun] 24 | $ bun add -D usegl 25 | ``` 26 | 27 | ::: 28 | 29 | ## Usage 30 | 31 | ```js 32 | import { useWebGLCanvas } from "usegl"; 33 | 34 | useWebGLCanvas({ 35 | canvas: "#glCanvas", 36 | fragment: /* glsl */ ` 37 | varying vec2 vUv; // automatically provided 38 | uniform float uTime; // automatically provided and updated 39 | 40 | void main() { 41 | gl_FragColor = vec4(vUv, sin(uTime) / 2. + .5, 1.); 42 | } 43 | `, 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: useGL 7 | text: Lightweight, reactive WebGL library 8 | tagline: for working with shaders 9 | actions: 10 | - theme: brand 11 | text: Get started 12 | link: /guide/introduction/quick-start 13 | - theme: alt 14 | text: Examples 15 | link: /examples/basics/full-screen/ 16 | 17 | features: 18 | - title: Lightweight 19 | icon: ⚡️ 20 | details: ~5kB minzipped for the full library. Down to ~3kB for the simplest setup. 21 | - title: Reactive 22 | icon: ↺ 23 | details: Automatically re-renders when uniforms are updated, or the canvas is resized 24 | - title: TypeScript 25 | icon: 🛠️ 26 | details: "Autocompletion and type-safety for everything, including uniforms" 27 | - title: Developer friendly 28 | icon: 🤝 29 | details: Modern hooks style syntax, time and resolution uniforms automatically provided and updated 30 | --- 31 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usegl-docs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vitepress build", 8 | "dev": "vitepress dev", 9 | "preview": "vitepress preview" 10 | }, 11 | "dependencies": { 12 | "@tweakpane/core": "2.0.5", 13 | "gray-matter": "4.0.3", 14 | "markdown-it-container": "4.0.0", 15 | "sass": "1.83.4", 16 | "tweakpane": "4.0.5", 17 | "typescript": "5.7.3", 18 | "usegl": "workspace:*", 19 | "vitepress": "1.6.3", 20 | "vitepress-plugin-group-icons": "1.3.5", 21 | "vitepress-plugin-sandpack": "1.1.4", 22 | "vue": "3.5.13" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/snippets/canvas-full/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | canvas { 6 | width: 100svw; 7 | height: 100svh; 8 | display: block; 9 | background: #000; 10 | } 11 | -------------------------------------------------------------------------------- /docs/snippets/canvas-square/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: black; 4 | height: 100svh; 5 | display: grid; 6 | place-items: center; 7 | } 8 | 9 | canvas { 10 | width: min(90svmin, 900px); 11 | aspect-ratio: 1; 12 | display: block; 13 | border-radius: 8px; 14 | border: 1px solid #555; 15 | } 16 | -------------------------------------------------------------------------------- /docs/snippets/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | useGL Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/snippets/render-count/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | useGL Example 7 | 8 | 9 | 19 | 20 |
Renders: 0
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.glsl" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.vert" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.frag" { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module "*?raw" { 17 | const content: string; 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /lib/.gitattributes: -------------------------------------------------------------------------------- 1 | tests/__screenshots__/gradient/gradient-android.png filter=lfs diff=lfs merge=lfs -text 2 | tests/__screenshots__/particles/particles-safari.png filter=lfs diff=lfs merge=lfs -text 3 | tests/__screenshots__/texture/texture-chromium.png filter=lfs diff=lfs merge=lfs -text 4 | tests/__screenshots__/blob/blob-iphone.png filter=lfs diff=lfs merge=lfs -text 5 | tests/__screenshots__/bloom/bloom-iphone.png filter=lfs diff=lfs merge=lfs -text 6 | tests/__screenshots__/circle/circle-firefox.png filter=lfs diff=lfs merge=lfs -text 7 | tests/__screenshots__/gradient/gradient-chromium.png filter=lfs diff=lfs merge=lfs -text 8 | tests/__screenshots__/pointer/pointer-iphone.png filter=lfs diff=lfs merge=lfs -text 9 | tests/__screenshots__/circle/circle-android.png filter=lfs diff=lfs merge=lfs -text 10 | tests/__screenshots__/circle/circle-iphone.png filter=lfs diff=lfs merge=lfs -text 11 | tests/__screenshots__/particles/particles-android.png filter=lfs diff=lfs merge=lfs -text 12 | tests/__screenshots__/pointer-move/pointer-move-android.png filter=lfs diff=lfs merge=lfs -text 13 | tests/__screenshots__/pointer-move/pointer-move-firefox.png filter=lfs diff=lfs merge=lfs -text 14 | tests/__screenshots__/pointer/pointer-android.png filter=lfs diff=lfs merge=lfs -text 15 | tests/__screenshots__/blob/blob-chromium.png filter=lfs diff=lfs merge=lfs -text 16 | tests/__screenshots__/bloom/bloom-firefox.png filter=lfs diff=lfs merge=lfs -text 17 | tests/__screenshots__/pointer-move/pointer-move-chromium.png filter=lfs diff=lfs merge=lfs -text 18 | tests/__screenshots__/pointer/pointer-firefox.png filter=lfs diff=lfs merge=lfs -text 19 | tests/__screenshots__/texture/texture-safari.png filter=lfs diff=lfs merge=lfs -text 20 | tests/__screenshots__/blob/blob-android.png filter=lfs diff=lfs merge=lfs -text 21 | tests/__screenshots__/bloom/bloom-android.png filter=lfs diff=lfs merge=lfs -text 22 | tests/__screenshots__/texture/texture-android.png filter=lfs diff=lfs merge=lfs -text 23 | tests/__screenshots__/bloom/bloom-chromium.png filter=lfs diff=lfs merge=lfs -text 24 | tests/__screenshots__/gradient/gradient-firefox.png filter=lfs diff=lfs merge=lfs -text 25 | tests/__screenshots__/gradient/gradient-safari.png filter=lfs diff=lfs merge=lfs -text 26 | tests/__screenshots__/pointer-move/pointer-move-iphone.png filter=lfs diff=lfs merge=lfs -text 27 | tests/__screenshots__/pointer-move/pointer-move-safari.png filter=lfs diff=lfs merge=lfs -text 28 | tests/__screenshots__/pointer/pointer-safari.png filter=lfs diff=lfs merge=lfs -text 29 | tests/__screenshots__/texture/texture-firefox.png filter=lfs diff=lfs merge=lfs -text 30 | tests/__screenshots__/texture/texture-iphone.png filter=lfs diff=lfs merge=lfs -text 31 | tests/__screenshots__/blob/blob-safari.png filter=lfs diff=lfs merge=lfs -text 32 | tests/__screenshots__/circle/circle-safari.png filter=lfs diff=lfs merge=lfs -text 33 | tests/__screenshots__/gradient/gradient-iphone.png filter=lfs diff=lfs merge=lfs -text 34 | tests/__screenshots__/blob/blob-firefox.png filter=lfs diff=lfs merge=lfs -text 35 | tests/__screenshots__/bloom/bloom-safari.png filter=lfs diff=lfs merge=lfs -text 36 | tests/__screenshots__/circle/circle-chromium.png filter=lfs diff=lfs merge=lfs -text 37 | tests/__screenshots__/particles/particles-chromium.png filter=lfs diff=lfs merge=lfs -text 38 | tests/__screenshots__/particles/particles-firefox.png filter=lfs diff=lfs merge=lfs -text 39 | tests/__screenshots__/particles/particles-iphone.png filter=lfs diff=lfs merge=lfs -text 40 | tests/__screenshots__/pointer/pointer-chromium.png filter=lfs diff=lfs merge=lfs -text 41 | tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-android.png filter=lfs diff=lfs merge=lfs -text 42 | tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-firefox.png filter=lfs diff=lfs merge=lfs -text 43 | tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-iphone.png filter=lfs diff=lfs merge=lfs -text 44 | tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-safari.png filter=lfs diff=lfs merge=lfs -text 45 | tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-chromium.png filter=lfs diff=lfs merge=lfs -text 46 | tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-firefox.png filter=lfs diff=lfs merge=lfs -text 47 | tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-iphone.png filter=lfs diff=lfs merge=lfs -text 48 | tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-safari.png filter=lfs diff=lfs merge=lfs -text 49 | tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-chromium.png filter=lfs diff=lfs merge=lfs -text 50 | tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-android.png filter=lfs diff=lfs merge=lfs -text 51 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | -------------------------------------------------------------------------------- /lib/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/playwright:v1.49.1 AS base 2 | RUN npm i -g pnpm 3 | 4 | FROM base AS deps 5 | WORKDIR /app 6 | COPY package.json ./ 7 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install 8 | 9 | FROM base AS runner 10 | WORKDIR /app 11 | COPY --from=deps /app/node_modules ./node_modules 12 | COPY playwright.config.ts ./ 13 | COPY astro.config.mjs ./ 14 | COPY tsconfig.json ./ 15 | COPY package.json ./ 16 | COPY tests ./tests 17 | COPY playground ./playground 18 | COPY src ./src 19 | 20 | CMD ["sh", "-c", "xvfb-run pnpm run test -u"] 21 | -------------------------------------------------------------------------------- /lib/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import { resolve, dirname } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)); 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | srcDir: "./playground/src", 10 | publicDir: "./playground/public", 11 | vite: { 12 | resolve: { 13 | alias: { 14 | usegl: resolve(__dirname, "./src/index.ts"), 15 | }, 16 | }, 17 | }, 18 | devToolbar: { 19 | enabled: false, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /lib/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unjs from "eslint-config-unjs"; 2 | 3 | export default unjs({ 4 | rules: { 5 | "unicorn/filename-case": "off", 6 | "unicorn/no-null": "off", 7 | "@typescript-eslint/consistent-type-imports": "error", 8 | "@typescript-eslint/triple-slash-reference": "off", 9 | "unicorn/prefer-top-level-await": "off", 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usegl", 3 | "version": "0.6.0", 4 | "description": "Lightweight, reactive WebGL library for working with shaders", 5 | "repository": "jsulpis/usegl", 6 | "license": "MIT", 7 | "author": "Julien SULPIS", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.mjs" 14 | } 15 | }, 16 | "main": "./dist/index.mjs", 17 | "module": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "build": "unbuild", 24 | "dev": "astro dev --port 3000", 25 | "format": "prettier src --check", 26 | "format:fix": "prettier src --write", 27 | "lint": "eslint src", 28 | "lint:fix": "eslint src --fix", 29 | "prepack": "pnpm build", 30 | "release": "changelogen --release --clean", 31 | "test": "playwright test", 32 | "test:local": "docker build -t usegl . && docker run --rm -v $(pwd)/test-results:/app/test-results usegl /bin/sh -c 'xvfb-run pnpm run test'", 33 | "test:ui": "playwright test --ui", 34 | "test:update": "docker build -t usegl . && docker run --rm -v $(pwd)/test-results:/app/test-results -v $(pwd)/tests/__screenshots__:/app/tests/__screenshots__ usegl", 35 | "typecheck": "tsc --noEmit && astro check --tsconfig playground/tsconfig.json" 36 | }, 37 | "devDependencies": { 38 | "@astrojs/check": "0.9.4", 39 | "@playwright/test": "1.49.1", 40 | "@types/node": "22.10.7", 41 | "astro": "5.1.8", 42 | "changelogen": "0.5.7", 43 | "eslint": "9.18.0", 44 | "eslint-config-unjs": "0.4.2", 45 | "prettier": "3.4.2", 46 | "typescript": "5.7.3", 47 | "unbuild": "3.3.1" 48 | }, 49 | "changelog": { 50 | "excludeAuthors": [ 51 | "" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/playground/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /lib/playground/public/images/2k_earth_color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/playground/public/images/2k_earth_color.jpeg -------------------------------------------------------------------------------- /lib/playground/public/images/2k_earth_night.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/playground/public/images/2k_earth_night.jpeg -------------------------------------------------------------------------------- /lib/playground/public/images/lion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/playground/public/images/lion.jpg -------------------------------------------------------------------------------- /lib/playground/src/components/GlobalPlayPause.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PlayPause from "./PlayPause.astro"; 3 | --- 4 | 5 | 16 | 17 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /lib/playground/src/components/PlayPause.astro: -------------------------------------------------------------------------------- 1 | 25 | 26 | 74 | -------------------------------------------------------------------------------- /lib/playground/src/components/RenderCount.astro: -------------------------------------------------------------------------------- 1 | Renders: 0 2 | 3 | 11 | -------------------------------------------------------------------------------- /lib/playground/src/components/SideNav.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { sections, routes } from "./routes"; 3 | 4 | const currentPath = Astro.url.pathname; 5 | 6 | const { class: className } = Astro.props; 7 | --- 8 | 9 | 34 | 35 | 82 | -------------------------------------------------------------------------------- /lib/playground/src/components/renderCount.ts: -------------------------------------------------------------------------------- 1 | export function incrementRenderCount() { 2 | const renderCountElement = document.querySelector("#renderCount")!; 3 | renderCountElement.textContent = `${Number(renderCountElement.textContent) + 1}`; 4 | } 5 | -------------------------------------------------------------------------------- /lib/playground/src/components/routes.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "node:fs"; 2 | 3 | export const sections = readdirSync("playground/src/pages", { withFileTypes: true }) 4 | .filter((file) => file.isDirectory()) 5 | .map((folder) => folder.name); 6 | 7 | export const routes = sections.flatMap((section) => { 8 | const files = readdirSync(`playground/src/pages/${section}`); 9 | 10 | return files.map((file) => { 11 | const route = file.replace(".astro", ""); 12 | return { section, route }; 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/playground/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /lib/playground/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { title } = Astro.props; 3 | 4 | import SideNav from "../components/SideNav.astro"; 5 | import RenderCount from "../components/RenderCount.astro"; 6 | --- 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 | 71 | -------------------------------------------------------------------------------- /lib/playground/src/pages/core/circle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /lib/playground/src/pages/core/gradient.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import GlobalPlayPause from "../../components/GlobalPlayPause.astro"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | --- 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/playground/src/pages/core/indices.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/playground/src/pages/core/pause.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PlayPause from "../../components/PlayPause.astro"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | --- 5 | 6 | 107 | 108 | 109 |
110 |
111 | Global : 112 |
113 |
114 | 115 | 116 | 117 |
118 | 119 |
120 |
121 | 122 | 159 | -------------------------------------------------------------------------------- /lib/playground/src/pages/core/scissor.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 41 | 42 | 43 | 44 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /lib/playground/src/pages/gpgpu/boids (static).astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 6 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /lib/playground/src/pages/gpgpu/boids.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /lib/playground/src/pages/gpgpu/maths.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
A
B
Sum
Product
77 |
78 |
79 | 80 | 89 | -------------------------------------------------------------------------------- /lib/playground/src/pages/gpgpu/particles - FBO (static).astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 6 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /lib/playground/src/pages/gpgpu/particles - FBO.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/playground/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../layouts/Layout.astro"; 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/playground/src/pages/particles/particles.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import GlobalPlayPause from "../../components/GlobalPlayPause.astro"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | --- 5 | 6 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/playground/src/pages/pointer/blob.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import GlobalPlayPause from "../../components/GlobalPlayPause.astro"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | --- 5 | 6 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/playground/src/pages/pointer/pointer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | -------------------------------------------------------------------------------- /lib/playground/src/pages/post-processing/bloom.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /lib/playground/src/pages/post-processing/sepia.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 88 | 89 | 90 | 91 | 92 | 93 | 98 | -------------------------------------------------------------------------------- /lib/playground/src/pages/textures/dataTexture.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/playground/src/pages/textures/mipmap.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import GlobalPlayPause from "../../components/GlobalPlayPause.astro"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | --- 5 | 6 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /lib/playground/src/pages/textures/texture.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 66 | 67 | 68 | 69 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /lib/playground/src/pages/textures/video.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | --- 4 | 5 | 64 | 65 | 66 | 67 | 68 | 69 | 74 | -------------------------------------------------------------------------------- /lib/playground/src/shaders/blob.ts: -------------------------------------------------------------------------------- 1 | export const fragment = /* glsl */ ` 2 | in vec2 vUv; 3 | uniform vec2 uResolution; 4 | uniform float uTime; 5 | uniform vec2 uPointer; 6 | out vec4 fragColor; 7 | 8 | #define BACKGROUND vec3(0) 9 | #define FOREGROUND vec3(1) 10 | #define BIG_CIRCLE_CENTER vec2(0) 11 | #define BIG_CIRCLE_RADIUS .4 12 | #define PI acos(-1.) 13 | 14 | float sdCircle(in vec2 p, in float r) { 15 | return length(p) - r; 16 | } 17 | 18 | float smin(float a, float b, float k) { 19 | float h = max(k - abs(a - b), 0.f) / k; 20 | return min(a, b) - h * h * h * k * 1.f / 6.f; 21 | } 22 | 23 | vec2 getSmallCircleCenter() { 24 | vec2 centerToPointer = uPointer - BIG_CIRCLE_CENTER; 25 | float rotation = atan(centerToPointer.y, centerToPointer.x); 26 | if(centerToPointer.x == 0.f) { 27 | if(centerToPointer.y < 0.f) 28 | rotation = -PI / 2.f; 29 | else if(centerToPointer.y > 0.f) 30 | rotation = PI / 2.f; 31 | else 32 | rotation = 0.f; 33 | } 34 | 35 | vec2 ellipseCenter = (BIG_CIRCLE_CENTER * .4f + uPointer * .6f); 36 | float distCenterToPointer = length(uPointer - BIG_CIRCLE_CENTER); 37 | float rSmall = BIG_CIRCLE_RADIUS * .2f + distCenterToPointer * .2f; 38 | float rLarge = BIG_CIRCLE_RADIUS * .4f + distCenterToPointer * .4f; 39 | float t = uTime * 2.f; 40 | 41 | return vec2(// 42 | rLarge * cos(t) * cos(rotation) - rSmall * sin(t) * sin(rotation) + ellipseCenter.x, // 43 | rLarge * cos(t) * sin(rotation) + rSmall * sin(t) * cos(rotation) + ellipseCenter.y// 44 | ); 45 | } 46 | 47 | void main() { 48 | float edgePrecision = 2.f / uResolution.y; 49 | 50 | float smallCircleRadius = BIG_CIRCLE_RADIUS * .2f; 51 | vec2 smallCircleCenter = getSmallCircleCenter(); 52 | 53 | float bigCircle = sdCircle(vUv - BIG_CIRCLE_CENTER, BIG_CIRCLE_RADIUS); 54 | float smallCircle = sdCircle(vUv - smallCircleCenter, smallCircleRadius); 55 | float pointerCircle = sdCircle(vUv - uPointer, smallCircleRadius / 2.f); 56 | 57 | vec3 col = BACKGROUND; 58 | 59 | col = mix(col, FOREGROUND, smoothstep(edgePrecision, 0.f, smin(bigCircle, smallCircle, BIG_CIRCLE_RADIUS * 2.f))); 60 | col = mix(col, BACKGROUND, 1.f - smoothstep(0.f, edgePrecision, bigCircle)); 61 | col = mix(col, vec3(1, 0, 0), 1.f - smoothstep(0.f, edgePrecision, pointerCircle)); 62 | 63 | fragColor = vec4(col, 1.0f); 64 | } 65 | `; 66 | 67 | export const vertex = /* glsl*/ ` 68 | in vec3 aPosition; 69 | uniform vec2 uResolution; 70 | out vec2 vUv; 71 | 72 | void main() { 73 | vUv = (2.f * aPosition.xy - 1.f) * uResolution / uResolution.y; 74 | 75 | gl_Position = vec4(2.0f * aPosition - 1.0f, 1.0f); 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /lib/playground/src/shaders/boids.ts: -------------------------------------------------------------------------------- 1 | export const boidsVelocities = /* glsl */ ` 2 | uniform sampler2D tPositions; 3 | uniform sampler2D tVelocities; 4 | uniform float uDeltaTime; 5 | 6 | uniform float uMaxSpeed; 7 | uniform float uPerceptionRadius; 8 | uniform float uSeparationWeight; 9 | uniform float uAlignmentWeight; 10 | uniform float uCohesionWeight; 11 | uniform float uBorderForce; 12 | uniform float uBorderDistance; 13 | 14 | in vec2 uv; 15 | 16 | float random(vec2 st) { 17 | return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); 18 | } 19 | 20 | vec2 flockingForces(vec2 position, vec2 velocity) { 21 | vec2 texSize = vec2(textureSize(tVelocities, 0)); 22 | vec2 separation = vec2(0.0); 23 | vec2 alignment = vec2(0.0); 24 | vec2 cohesion = vec2(0.0); 25 | int count = 0; 26 | 27 | // Accumulate forces from neighbors 28 | for(float y = 1.0; y < texSize.y; y++) { 29 | for(float x = 1.0; x < texSize.x; x++) { 30 | vec2 neighborUV = vec2(x, y) / texSize; 31 | if(neighborUV == uv) continue; 32 | 33 | vec2 neighborPos = texture2D(tPositions, neighborUV).xy; 34 | vec2 neighborVel = texture2D(tVelocities, neighborUV).xy; 35 | float dist = distance(position, neighborPos); 36 | 37 | if(dist < uPerceptionRadius && dist > 0.0) { 38 | separation += normalize(position - neighborPos) * (1.0 - dist/uPerceptionRadius); 39 | alignment += neighborVel; 40 | cohesion += neighborPos; 41 | count++; 42 | } 43 | } 44 | } 45 | 46 | if(count > 0) { 47 | separation = normalize(separation) * uSeparationWeight; 48 | alignment = normalize(alignment/float(count)) * uAlignmentWeight; 49 | cohesion = normalize((cohesion/float(count)) - position) * uCohesionWeight; 50 | } 51 | return separation + alignment + cohesion; 52 | } 53 | 54 | vec2 borderRepulsion(vec2 position) { 55 | vec2 distToBorder = vec2(1.0 - abs(position.x), 1.0 - abs(position.y)); 56 | vec2 force = vec2(0.0); 57 | 58 | if(distToBorder.x < uBorderDistance) { 59 | force.x = (uBorderDistance - distToBorder.x) / uBorderDistance * -sign(position.x); 60 | } 61 | if(distToBorder.y < uBorderDistance) { 62 | force.y = (uBorderDistance - distToBorder.y) / uBorderDistance * -sign(position.y); 63 | } 64 | return force * uBorderForce; 65 | } 66 | 67 | vec2 randomWandering(vec2 position, vec2 velocity) { 68 | float rand = random(position.xy) * 2.0 - 1.0; 69 | float angle = rand * radians(10.0); 70 | mat2 rotationMatrix = mat2( 71 | cos(angle), -sin(angle), 72 | sin(angle), cos(angle) 73 | ); 74 | return normalize(rotationMatrix * velocity) * 5.; 75 | } 76 | 77 | vec2 predatorRepulsion(vec2 position, vec2 velocity) { 78 | vec2 predatorPos = texture2D(tPositions, vec2(0.0)).xy; 79 | vec2 toPredator = predatorPos - position; 80 | float predatorDist = length(toPredator); 81 | float repulsionStrength = 2.5; 82 | float repulsionRadius = 1.; 83 | 84 | if (predatorDist < repulsionRadius) { 85 | return normalize(-1. * toPredator) * repulsionStrength * (1.0 - predatorDist / repulsionRadius); 86 | } 87 | return vec2(0.0); 88 | } 89 | 90 | void main() { 91 | vec2 position = texture2D(tPositions, uv).xy; 92 | vec2 velocity = texture2D(tVelocities, uv).xy; 93 | vec2 acceleration = borderRepulsion(position); 94 | 95 | bool isPredator = floor(gl_FragCoord.xy) == vec2(0.0); 96 | 97 | if (isPredator) { 98 | acceleration += randomWandering(position, velocity); 99 | } else { 100 | acceleration += flockingForces(position, velocity) + predatorRepulsion(position, velocity); 101 | } 102 | velocity += acceleration * uDeltaTime; 103 | 104 | float speedLimit = isPredator ? uMaxSpeed / 2.0 : uMaxSpeed; 105 | if(length(velocity) > speedLimit) { 106 | velocity = normalize(velocity) * speedLimit; 107 | } 108 | 109 | gl_FragColor = vec4(velocity, 0.0, 1.0); 110 | } 111 | `; 112 | 113 | export const boidsPositions = /* glsl */ ` 114 | uniform sampler2D tPositions; 115 | uniform sampler2D tVelocities; 116 | uniform float uDeltaTime; 117 | 118 | in vec2 uv; 119 | 120 | void main() { 121 | vec2 position = texture(tPositions, uv).xy; 122 | vec2 velocity = texture(tVelocities, uv).xy; 123 | 124 | position += velocity * uDeltaTime; 125 | 126 | gl_FragColor = vec4(position, 0.0, 1.0); 127 | }`; 128 | 129 | export const renderPassVertex = /* glsl */ ` 130 | uniform sampler2D tPositions; 131 | in vec2 aCoords; 132 | out vec4 vColor; 133 | 134 | void main() { 135 | vec2 position = texture2D(tPositions, aCoords).xy; 136 | 137 | if (aCoords == vec2(0, 0)) { 138 | vColor = vec4(1, 0, 0, 1); 139 | gl_PointSize = 35.0; 140 | } else { 141 | vec2 predatorPosition = texture2D(tPositions, vec2(0)).xy; 142 | float distance = length(position - predatorPosition); 143 | vColor = mix(vec4(1, .5, 0, 1), vec4(0, .8, 1, 1), smoothstep(0.2, .8, distance)); 144 | vColor.a = smoothstep(2., .6, distance); 145 | gl_PointSize = 20.0; 146 | } 147 | 148 | gl_Position = vec4(position, 0, 1); 149 | } 150 | `; 151 | 152 | export const renderPassFragment = /* glsl */ ` 153 | in vec4 vColor; 154 | out vec4 outColor; 155 | 156 | void main() { 157 | vec2 uv = gl_PointCoord.xy; 158 | outColor = vec4(vColor.rgb, vColor.a * smoothstep(0.5, 0.4, length(uv - 0.5))); 159 | } 160 | `; 161 | -------------------------------------------------------------------------------- /lib/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "include": ["../src", "src"], 4 | "compilerOptions": { 5 | "strict": true, 6 | "paths": { 7 | "usegl": ["../src/index.ts"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | const desktopViewport = { width: 700, height: 400 }; 4 | const mobileViewport = { width: 360, height: 640 }; 5 | 6 | const serverUrl = "http://localhost:3000"; 7 | 8 | /** 9 | * See https://playwright.dev/docs/test-configuration. 10 | */ 11 | export default defineConfig({ 12 | testDir: "./tests", 13 | /* Run tests in files in parallel */ 14 | fullyParallel: true, 15 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 16 | forbidOnly: !!process.env.CI, 17 | retries: process.env.CI ? 3 : 0, 18 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 19 | reporter: "html", 20 | use: { 21 | trace: "on-first-retry", 22 | baseURL: serverUrl, 23 | }, 24 | snapshotPathTemplate: "{testDir}/__screenshots__/{testName}/{testName}-{projectName}{ext}", 25 | expect: { 26 | toHaveScreenshot: { 27 | maxDiffPixelRatio: 0.02, 28 | }, 29 | }, 30 | projects: [ 31 | { 32 | name: "chromium", 33 | use: { 34 | ...devices["Desktop Chrome"], 35 | viewport: desktopViewport, 36 | launchOptions: { 37 | args: ["--use-angle=gl"], 38 | }, 39 | }, 40 | }, 41 | { 42 | name: "firefox", 43 | use: { 44 | ...devices["Desktop Firefox"], 45 | viewport: desktopViewport, 46 | launchOptions: { 47 | headless: false, 48 | }, 49 | }, 50 | grepInvert: /play \/ pause controls/, // Flaky on firefox, and there is no fancy API in the play/pause controls, so the other browsers should be enough 51 | }, 52 | { 53 | name: "safari", 54 | use: { ...devices["Desktop Safari"], viewport: desktopViewport }, 55 | grepInvert: /video/, // the video does not work in the docker image used for the CI, maybe due to a codec issue 56 | }, 57 | { 58 | name: "android", 59 | use: { 60 | ...devices["Pixel 5"], 61 | viewport: mobileViewport, 62 | launchOptions: { 63 | args: ["--use-gl=egl", "--ignore-gpu-blocklist", "--use-gl=angle"], 64 | }, 65 | }, 66 | }, 67 | { 68 | name: "iphone", 69 | use: { ...devices["iPhone 12"], viewport: mobileViewport }, 70 | grepInvert: /video/, // the video does not work in the docker image used for the CI, maybe due to a codec issue 71 | }, 72 | ], 73 | webServer: { 74 | command: "pnpm dev", 75 | url: serverUrl, 76 | reuseExistingServer: !process.env.CI, 77 | timeout: 20 * 1000, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /lib/src/core/attribute.ts: -------------------------------------------------------------------------------- 1 | import type { Attribute } from "../types"; 2 | import { createAndBindBuffer, getBufferData } from "./buffer"; 3 | 4 | export function setAttribute( 5 | gl: WebGL2RenderingContext, 6 | program: WebGLProgram, 7 | name: string, 8 | attribute: Attribute, 9 | ) { 10 | const bufferData = getBufferData(attribute.data, name === "index"); 11 | const location = gl.getAttribLocation(program, name); 12 | 13 | if (name === "index") { 14 | createAndBindBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, bufferData); 15 | 16 | if (location === -1) { 17 | return { location, vertexCount: bufferData.length }; 18 | } 19 | } 20 | 21 | if (location === -1) { 22 | console.warn(`No location found for attribute "${name}".`); 23 | return { location, vertexCount: 0 }; 24 | } 25 | 26 | createAndBindBuffer(gl, gl.ARRAY_BUFFER, bufferData); 27 | 28 | gl.enableVertexAttribArray(location); 29 | gl.vertexAttribPointer( 30 | location, 31 | attribute.size, 32 | attribute.type || getGLType(gl, bufferData), 33 | attribute.normalize || false, 34 | attribute.stride || 0, 35 | attribute.offset || 0, 36 | ); 37 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 38 | 39 | const vertexCount = attribute.stride 40 | ? bufferData.byteLength / attribute.stride 41 | : bufferData.length / attribute.size; 42 | 43 | if (!Number.isInteger(vertexCount)) { 44 | console.warn( 45 | `The computed vertex count of the "${name}" attribute is not an integer: ${vertexCount}. There might be an issue with the provided ${attribute.stride === undefined ? "size" : "stride"}.`, 46 | ); 47 | } 48 | 49 | return { location, vertexCount }; 50 | } 51 | 52 | function getGLType(gl: WebGL2RenderingContext, data: ArrayBufferView) { 53 | if (data instanceof Float32Array) return gl.FLOAT; 54 | if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) return gl.UNSIGNED_BYTE; 55 | if (data instanceof Int8Array) return gl.BYTE; 56 | if (data instanceof Uint16Array) return gl.UNSIGNED_SHORT; 57 | if (data instanceof Int16Array) return gl.SHORT; 58 | if (data instanceof Uint32Array) return gl.UNSIGNED_INT; 59 | if (data instanceof Int32Array) return gl.INT; 60 | return gl.FLOAT; 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/core/buffer.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray } from "../types"; 2 | 3 | export function createAndBindBuffer( 4 | gl: WebGL2RenderingContext, 5 | target: GLenum, 6 | data: TypedArray | number[], 7 | ) { 8 | const buffer = gl.createBuffer(); 9 | const bufferData = getBufferData(data); 10 | gl.bindBuffer(target, buffer); 11 | gl.bufferData(target, bufferData, gl.STATIC_DRAW); 12 | 13 | return buffer; 14 | } 15 | 16 | export function getBufferData(data: TypedArray | number[], isIndex?: boolean) { 17 | if (ArrayBuffer.isView(data)) { 18 | return data; 19 | } 20 | if (isIndex) { 21 | return data.length < 65_536 ? new Uint16Array(data) : new Uint32Array(data); 22 | } 23 | return new Float32Array(data); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/core/program.ts: -------------------------------------------------------------------------------- 1 | import { createShader } from "./shader"; 2 | 3 | export function createProgram( 4 | gl: WebGL2RenderingContext, 5 | fragment: string | WebGLShader, 6 | vertex: string | WebGLShader, 7 | transformFeedbackVaryings?: string[], 8 | ) { 9 | const vertexShader = 10 | vertex instanceof WebGLShader ? vertex : createShader(gl, vertex, gl.VERTEX_SHADER); 11 | const fragmentShader = 12 | fragment instanceof WebGLShader ? fragment : createShader(gl, fragment, gl.FRAGMENT_SHADER); 13 | 14 | const program = gl.createProgram(); 15 | if (program === null || vertexShader == null || fragmentShader == null) { 16 | console.error("could not create program"); 17 | gl.deleteProgram(program); 18 | return null; 19 | } 20 | 21 | if (transformFeedbackVaryings) { 22 | gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS); 23 | } 24 | 25 | gl.attachShader(program, vertexShader); 26 | gl.attachShader(program, fragmentShader); 27 | gl.linkProgram(program); 28 | 29 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 30 | console.error("could not link program: " + gl.getProgramInfoLog(program)); 31 | gl.deleteProgram(program); 32 | return null; 33 | } 34 | 35 | return program; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/core/renderTarget.ts: -------------------------------------------------------------------------------- 1 | import type { RenderTarget } from "../types"; 2 | import type { DataTextureParams } from "./texture"; 3 | import { fillTexture } from "./texture"; 4 | 5 | export function createRenderTarget( 6 | gl: WebGL2RenderingContext, 7 | params?: Omit, 8 | ): RenderTarget { 9 | let _width = params?.width ?? gl.canvas.width; 10 | let _height = params?.height ?? gl.canvas.height; 11 | 12 | const framebuffer = gl.createFramebuffer(); 13 | 14 | let _texture = gl.createTexture()!; 15 | fillTexture(gl, _texture, { 16 | data: null, 17 | ...params, 18 | width: _width, 19 | height: _height, 20 | generateMipmaps: false, 21 | }); 22 | 23 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 24 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, _texture, 0); 25 | gl.bindTexture(gl.TEXTURE_2D, null); 26 | 27 | function setSize(width: number, height: number) { 28 | _width = width; 29 | _height = height; 30 | 31 | const newTexture = gl.createTexture()!; 32 | fillTexture(gl, newTexture, { 33 | data: null, 34 | ...params, 35 | width, 36 | height, 37 | generateMipmaps: false, 38 | }); 39 | 40 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 41 | gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, width, height); 42 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTexture, 0); 43 | 44 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 45 | gl.bindTexture(gl.TEXTURE_2D, null); 46 | gl.deleteTexture(_texture); 47 | 48 | _texture = newTexture; 49 | } 50 | 51 | return { 52 | framebuffer, 53 | get texture() { 54 | return _texture; 55 | }, 56 | get width() { 57 | return _width; 58 | }, 59 | get height() { 60 | return _height; 61 | }, 62 | setSize, 63 | }; 64 | } 65 | 66 | export function setRenderTarget(gl: WebGL2RenderingContext, target: RenderTarget | null) { 67 | const framebuffer = target?.framebuffer || null; 68 | const { width, height } = target || gl.canvas; 69 | 70 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 71 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 72 | gl.viewport(0, 0, width, height); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/core/shader.ts: -------------------------------------------------------------------------------- 1 | export function createShader(gl: WebGL2RenderingContext, source: string, type: GLenum) { 2 | const shader = gl.createShader(type); 3 | if (shader == null) { 4 | console.error("could not create shader"); 5 | gl.deleteShader(shader); 6 | return null; 7 | } 8 | gl.shaderSource(shader, convertToGLSL300(source)); 9 | gl.compileShader(shader); 10 | 11 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 12 | console.error("could not compile shader: " + gl.getShaderInfoLog(shader)); 13 | gl.deleteShader(shader); 14 | } 15 | return shader; 16 | } 17 | 18 | function convertToGLSL300(shader: string): string { 19 | let glsl300Shader = shader.replace(/\battribute\b/g, "in").replace(/\btexture2D\b/g, "texture"); 20 | 21 | if (shader.includes("gl_FragColor")) { 22 | // Fragment shader 23 | glsl300Shader = "out vec4 fragColor;\n" + glsl300Shader; 24 | glsl300Shader = glsl300Shader 25 | .replace(/\bvarying\b/g, "in") 26 | .replace(/\bgl_FragColor\b/g, "fragColor"); 27 | } else { 28 | // Vertex shader 29 | glsl300Shader = glsl300Shader.replace(/\bvarying\b/g, "out"); 30 | } 31 | 32 | const precisionRegex = /precision\s+(highp|mediump|lowp)\s+float\s*;/; 33 | if (!precisionRegex.test(glsl300Shader)) { 34 | glsl300Shader = glsl300Shader.replace(/^(#version 300 es)?/, "$1\nprecision highp float;\n"); 35 | } 36 | 37 | if (!shader.startsWith("#version")) { 38 | glsl300Shader = "#version 300 es\n" + glsl300Shader; 39 | } 40 | 41 | return glsl300Shader; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/hooks/useBoundingRect.ts: -------------------------------------------------------------------------------- 1 | import { useResizeObserver } from "./useResizeObserver"; 2 | 3 | export interface UseBoundingRectOptions { 4 | /** 5 | * Listen to window resize event 6 | * 7 | * @default true 8 | */ 9 | windowResize?: boolean; 10 | /** 11 | * Listen to window scroll event 12 | * 13 | * @default true 14 | */ 15 | windowScroll?: boolean; 16 | } 17 | 18 | export interface BoundingRect { 19 | width: number; 20 | height: number; 21 | top: number; 22 | right: number; 23 | bottom: number; 24 | left: number; 25 | x: number; 26 | y: number; 27 | } 28 | 29 | /** 30 | * Dynamically get the bounding rectangle of an HTML element 31 | */ 32 | export function useBoundingRect(target: HTMLElement, options: UseBoundingRectOptions = {}) { 33 | /* eslint-disable unicorn/prefer-global-this */ 34 | const { 35 | windowResize = typeof window !== "undefined", 36 | windowScroll = typeof window !== "undefined", 37 | } = options; 38 | 39 | const rect: BoundingRect = { 40 | width: 0, 41 | height: 0, 42 | top: 0, 43 | right: 0, 44 | bottom: 0, 45 | left: 0, 46 | x: 0, 47 | y: 0, 48 | }; 49 | 50 | const center = { x: 0, y: 0 }; 51 | 52 | function update() { 53 | const newRect = target.getBoundingClientRect(); 54 | 55 | // update the rect object instead of reassagning to allow destructuring the output of the function 56 | for (const key of Object.keys(rect) as Array) { 57 | rect[key] = newRect[key]; 58 | } 59 | 60 | center.x = (rect.left + rect.right) / 2; 61 | center.y = (rect.top + rect.bottom) / 2; 62 | } 63 | 64 | useResizeObserver(target, update); 65 | 66 | if (windowScroll) window.addEventListener("scroll", update, { capture: true, passive: true }); 67 | if (windowResize) window.addEventListener("resize", update, { passive: true }); 68 | 69 | return { 70 | rect: rect as Readonly, 71 | center: center as Readonly, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/hooks/useCompositeEffectPass.ts: -------------------------------------------------------------------------------- 1 | import { createRenderTarget } from "../core/renderTarget"; 2 | import { useLifeCycleCallback } from "../internal/useLifeCycleCallback"; 3 | import type { 4 | CompositeEffectPass, 5 | EffectPass, 6 | RenderCallback, 7 | RenderTarget, 8 | UpdatedCallback, 9 | } from "../types"; 10 | 11 | export function useCompositeEffectPass

>>( 12 | passes: P, 13 | ): CompositeEffectPass

{ 14 | const effectPasses = Object.values(passes); 15 | const outputPass = effectPasses.at(-1)!; 16 | 17 | const [beforeRenderCallbacks, onBeforeRender] = useLifeCycleCallback>(); 18 | const [afterRenderCallbacks, onAfterRender] = useLifeCycleCallback>(); 19 | const [onUpdatedCallbacks, onUpdated] = useLifeCycleCallback>(); 20 | 21 | function render() { 22 | for (const callback of beforeRenderCallbacks) callback({ uniforms: {} }); 23 | for (const pass of effectPasses) pass.render(); 24 | for (const callback of afterRenderCallbacks) callback({ uniforms: {} }); 25 | } 26 | 27 | function initialize(gl: WebGL2RenderingContext) { 28 | for (const [index, pass] of effectPasses.entries()) { 29 | pass.initialize(gl); 30 | 31 | if (index < effectPasses.length - 1) { 32 | pass.setTarget(createRenderTarget(gl)); 33 | } 34 | 35 | pass.onUpdated((newUniforms, oldUniforms) => { 36 | for (const callback of onUpdatedCallbacks) { 37 | callback(newUniforms, oldUniforms); 38 | } 39 | }); 40 | } 41 | } 42 | 43 | function setSize(size: { width: number; height: number }) { 44 | for (const pass of effectPasses) { 45 | pass.setSize(size); 46 | } 47 | } 48 | 49 | function setTarget(target: RenderTarget | null) { 50 | outputPass.setTarget(target); 51 | } 52 | 53 | return { 54 | target: outputPass.target, 55 | passes, 56 | onBeforeRender, 57 | onAfterRender, 58 | onUpdated, 59 | initialize, 60 | render, 61 | setSize, 62 | setTarget, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/hooks/useCompositor.ts: -------------------------------------------------------------------------------- 1 | import { createRenderTarget } from "../core/renderTarget"; 2 | import { findUniformName } from "../internal/findName"; 3 | import type { CompositeEffectPass, EffectPass, RenderPass } from "../types"; 4 | 5 | /** 6 | * The compositor handles the combination of the render pass and the effects: 7 | * - initialize the gl context and create render targets for all effects 8 | * - provide each effect with its previousPass and inputPass to use in uniforms 9 | * - fill the texture uniforms with the previous pass if they are not provided in uniforms 10 | * - detect the first texture uniform of each effect and, if it has no value provided, fill it with the previous pass 11 | * - render all passes in the correct order 12 | */ 13 | export function useCompositor( 14 | gl: WebGL2RenderingContext, 15 | renderPass: RenderPass, 16 | effects: Array | CompositeEffectPass>>>, 17 | ) { 18 | if (effects.length > 0 && renderPass.target === null) { 19 | renderPass.setTarget(createRenderTarget(gl)); 20 | } 21 | 22 | let previousPass = renderPass; 23 | 24 | for (const [index, effect] of effects.entries()) { 25 | effect.initialize(gl); 26 | effect.setTarget(index === effects.length - 1 ? null : createRenderTarget(gl)); 27 | 28 | if (isCompositeEffectPass(effect)) { 29 | const inputPass = previousPass; 30 | for (const effectPass of Object.values(effect.passes)) { 31 | const previousPassRef = previousPass; 32 | setupEffectPass(effectPass, previousPassRef, inputPass); 33 | previousPass = effectPass; 34 | } 35 | } else { 36 | setupEffectPass(effect, previousPass); 37 | previousPass = effect; 38 | } 39 | } 40 | 41 | const allPasses = [renderPass, ...effects]; 42 | 43 | function render() { 44 | for (const pass of allPasses) { 45 | pass.render(); 46 | } 47 | } 48 | 49 | function setSize(size: { width: number; height: number }) { 50 | for (const pass of allPasses) { 51 | pass.setSize(size); 52 | } 53 | } 54 | 55 | return { render, setSize, allPasses }; 56 | } 57 | 58 | function isCompositeEffectPass( 59 | effect: EffectPass | CompositeEffectPass, 60 | ): effect is CompositeEffectPass { 61 | return typeof (effect as CompositeEffectPass).passes === "object"; 62 | } 63 | 64 | function setupEffectPass( 65 | effect: EffectPass, 66 | previousPass: EffectPass | RenderPass, 67 | inputPass?: EffectPass | RenderPass, 68 | ) { 69 | // provide the previousPass and inputPass to the uniforms functions 70 | for (const uniformName of Object.keys(effect.uniforms)) { 71 | const uniformValue = effect.uniforms[uniformName]; 72 | if (typeof uniformValue === "function") { 73 | effect.uniforms[uniformName] = () => uniformValue({ previousPass, inputPass }); 74 | } 75 | } 76 | 77 | // detect the first texture uniform and, if it has no texture provided, fill it with the previous pass 78 | const textureUniformName = 79 | findUniformName(effect.fragment, "image") || 80 | findUniformName(effect.fragment, "texture") || 81 | findUniformName(effect.fragment, "pass"); 82 | 83 | if (textureUniformName && effect.uniforms[textureUniformName] === undefined) { 84 | effect.uniforms[textureUniformName] = () => previousPass.target?.texture; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/hooks/useEffectPass.ts: -------------------------------------------------------------------------------- 1 | import type { EffectPass, EffectUniforms } from "../types"; 2 | import type { QuadPassOptions } from "./useQuadRenderPass"; 3 | import { useQuadRenderPass } from "./useQuadRenderPass"; 4 | 5 | export function useEffectPass( 6 | options: QuadPassOptions, 7 | ): EffectPass { 8 | return useQuadRenderPass(undefined, options); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/hooks/useLoop.ts: -------------------------------------------------------------------------------- 1 | interface LoopData { 2 | /** 3 | * time elapsed in milliseconds since the loop started, excluding pauses. 4 | * 5 | * This timer is paused when the loop is paused, to avoid jumps in animations. If you want to get the time elapsed including pauses, use `elapsedTime` instead. 6 | */ 7 | time: number; 8 | /** 9 | * Δt in milliseconds since the previous loop iteration. 10 | */ 11 | deltaTime: number; 12 | /** 13 | * time elapsed in milliseconds since the loop started, including pauses. 14 | * 15 | * This timer is NOT paused when the loop is paused, which can cause jumps in animations. If you want to get the time elapsed excluding pauses, use `time` instead. 16 | */ 17 | elapsedTime: number; 18 | } 19 | 20 | export interface UseLoopOptions { 21 | /** 22 | * If true, the loop will start immediately. 23 | * 24 | * If false, the loop will start when the `play` method is called. 25 | * @default true 26 | */ 27 | immediate?: boolean; 28 | } 29 | 30 | interface LoopObj { 31 | play: () => void; 32 | pause: () => void; 33 | } 34 | 35 | const allLoops: Array = []; 36 | 37 | /** 38 | * A custom hook that creates an animation loop. 39 | * @param callback A function that will be called on every animation frame. 40 | * @param options Options for the loop. 41 | * @returns An object with `play` and `pause` methods to control the animation loop. 42 | */ 43 | export function useLoop( 44 | callback: ({ time, deltaTime }: LoopData) => void, 45 | options?: UseLoopOptions, 46 | ) { 47 | let animationFrameHandle: number; 48 | let pauseTime: number | null; 49 | let loopStartTime: number; 50 | let delay = 0; 51 | 52 | const { immediate = true } = options || {}; 53 | 54 | function loopFn(previousTime: number, delay = 0) { 55 | const currentTime = performance.now(); 56 | const elapsedTime = currentTime - loopStartTime; 57 | const time = elapsedTime - delay; 58 | const deltaTime = currentTime - previousTime; 59 | callback({ time, elapsedTime, deltaTime }); 60 | 61 | animationFrameHandle = requestAnimationFrame(() => loopFn(currentTime, delay)); 62 | } 63 | 64 | function play() { 65 | const currentTime = performance.now(); 66 | if (loopStartTime === undefined) { 67 | loopStartTime = performance.now(); 68 | } 69 | delay += currentTime - (pauseTime || currentTime); 70 | cancelAnimationFrame(animationFrameHandle); 71 | animationFrameHandle = requestAnimationFrame(() => loopFn(currentTime, delay)); 72 | pauseTime = null; 73 | } 74 | 75 | function pause() { 76 | if (pauseTime == null) { 77 | pauseTime = performance.now(); 78 | } 79 | cancelAnimationFrame(animationFrameHandle); 80 | } 81 | 82 | if (immediate) { 83 | play(); 84 | } 85 | 86 | const loop = { 87 | /** 88 | * Play the animation loop. 89 | */ 90 | play, 91 | /** 92 | * Pause the animation loop. 93 | */ 94 | pause, 95 | }; 96 | 97 | allLoops.push(loop); 98 | 99 | return loop; 100 | } 101 | 102 | /** 103 | * Play all loops that have been registered with `useLoop`. 104 | */ 105 | export function playAllLoops() { 106 | for (const loop of allLoops) { 107 | loop.play(); 108 | } 109 | } 110 | 111 | /** 112 | * Pause all loops that have been registered with `useLoop`. 113 | */ 114 | export function pauseAllLoops() { 115 | for (const loop of allLoops) { 116 | loop.pause(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/hooks/usePingPongFBO.ts: -------------------------------------------------------------------------------- 1 | import { createRenderTarget } from "../core/renderTarget"; 2 | import { createFloatDataTexture, type DataTextureParams } from "../core/texture"; 3 | import type { Attribute, RenderPass, Uniforms } from "../types"; 4 | import { useQuadRenderPass } from "./useQuadRenderPass"; 5 | 6 | export type PingPongFBOOptions> = { 7 | uniforms?: U; 8 | fragment: string; 9 | dataTexture: { 10 | name?: string; 11 | initialData: Float32Array | number[]; 12 | }; 13 | }; 14 | 15 | interface PingPongFBOPass> extends RenderPass { 16 | texture: DataTextureParams | WebGLTexture; 17 | coords: Attribute; 18 | } 19 | 20 | export function usePingPongFBO( 21 | gl: WebGL2RenderingContext, 22 | { uniforms = {} as U, dataTexture, fragment }: PingPongFBOOptions, 23 | ) { 24 | // enable the extension for float textures 25 | gl.getExtension("EXT_color_buffer_float"); 26 | 27 | const { initialData, name: dataTextureName = "tData" } = dataTexture; 28 | const elementsCount = initialData.length / 4; 29 | 30 | const initialDataTexture = createFloatDataTexture(initialData); 31 | const { data: _, ...textureParams } = initialDataTexture; 32 | 33 | const coords = new Float32Array(elementsCount * 2); 34 | for (let i = 0; i < elementsCount; i++) { 35 | const u = (i % textureParams.width) / textureParams.width; 36 | const v = Math.floor(i / textureParams.width) / textureParams.height; 37 | coords.set([u, v], i * 2); 38 | } 39 | 40 | const fboPass = useQuadRenderPass(gl, { 41 | fragment, 42 | uniforms: Object.assign(uniforms, { 43 | [dataTextureName]: initialDataTexture, 44 | }), 45 | }); 46 | 47 | const pingPongFBOPass: PingPongFBOPass = Object.assign(fboPass, { 48 | texture: initialDataTexture, 49 | coords: { 50 | data: coords, 51 | size: 2, 52 | }, 53 | }); 54 | 55 | let fboRead = createRenderTarget(gl, textureParams); 56 | let fboWrite = createRenderTarget(gl, textureParams); 57 | 58 | function swap() { 59 | const temp = fboRead; 60 | fboRead = fboWrite; 61 | fboWrite = temp; 62 | } 63 | 64 | const renderFn = pingPongFBOPass.render; 65 | 66 | pingPongFBOPass.render = () => { 67 | renderFn({ target: fboWrite }); 68 | pingPongFBOPass.texture = fboWrite.texture; 69 | Object.assign(pingPongFBOPass.uniforms, { 70 | [dataTextureName]: () => fboRead.texture, 71 | }); 72 | swap(); 73 | }; 74 | 75 | return pingPongFBOPass; 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/hooks/usePointerEvents.ts: -------------------------------------------------------------------------------- 1 | import type { BoundingRect } from "./useBoundingRect"; 2 | import { useBoundingRect } from "./useBoundingRect"; 3 | 4 | type HandlerArgs = { 5 | pointer: { 6 | x: number; 7 | y: number; 8 | }; 9 | canvasRect: BoundingRect; 10 | canvasCenter: { 11 | x: number; 12 | y: number; 13 | }; 14 | }; 15 | 16 | type PointerEventsHandlers = { 17 | enter?: ({ pointer, canvasRect, canvasCenter }: HandlerArgs) => void; 18 | move?: ({ pointer, canvasRect, canvasCenter }: HandlerArgs) => void; 19 | leave?: ({ pointer, canvasRect, canvasCenter }: HandlerArgs) => void; 20 | down?: ({ pointer, canvasRect, canvasCenter }: HandlerArgs) => void; 21 | up?: ({ pointer, canvasRect, canvasCenter }: HandlerArgs) => void; 22 | }; 23 | 24 | /** 25 | * Listen to common pointer events and provide additional infos about the canvas 26 | */ 27 | export function usePointerEvents(canvas: HTMLCanvasElement, handlers: PointerEventsHandlers) { 28 | const { rect: canvasRect, center: canvasCenter } = useBoundingRect(canvas); 29 | 30 | const activeHandlers = Object.fromEntries( 31 | Object.entries(handlers) 32 | .filter(([, handler]) => typeof handler === "function") 33 | .map(([handlerName, handlerFunction]) => [ 34 | handlerName, 35 | (e: PointerEvent) => { 36 | handlerFunction({ 37 | pointer: { x: e.clientX, y: e.clientY }, 38 | canvasRect, 39 | canvasCenter, 40 | }); 41 | }, 42 | ]), 43 | ); 44 | 45 | function listen() { 46 | for (const [event, handler] of Object.entries(activeHandlers)) { 47 | canvas.addEventListener(`pointer${event as keyof PointerEventsHandlers}`, handler, { 48 | passive: true, 49 | }); 50 | } 51 | } 52 | 53 | function stop() { 54 | for (const [event, handler] of Object.entries(activeHandlers)) { 55 | canvas.removeEventListener(`pointer${event as keyof PointerEventsHandlers}`, handler); 56 | } 57 | } 58 | 59 | listen(); 60 | 61 | return { stop, listen }; 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/hooks/useQuadRenderPass.ts: -------------------------------------------------------------------------------- 1 | import type { Uniforms } from "../types"; 2 | import { useRenderPass, type RenderPassOptions } from "./useRenderPass"; 3 | import { findAttributeName, findVaryingName } from "../internal/findName"; 4 | 5 | export type QuadPassOptions> = Omit< 6 | RenderPassOptions, 7 | "vertex" 8 | > & { vertex?: string }; 9 | 10 | export function useQuadRenderPass( 11 | gl: WebGL2RenderingContext | undefined, 12 | { attributes = {}, fragment, vertex, ...renderPassOptions }: QuadPassOptions, 13 | ) { 14 | const uvVaryingName = findVaryingName(fragment, "uv"); 15 | 16 | const vertexShader = 17 | vertex || 18 | (uvVaryingName 19 | ? quadVertexShaderSource.replace(/\bvUv\b/g, uvVaryingName) 20 | : quadVertexShaderSource); 21 | 22 | const hasPositionAttribute = Object.keys(attributes).some((attributeName) => 23 | attributeName.toLocaleLowerCase().endsWith("position"), 24 | ); 25 | 26 | if (!hasPositionAttribute) { 27 | const positionAttributeName = findAttributeName(vertexShader, "position"); 28 | if (positionAttributeName) { 29 | attributes[positionAttributeName] = { 30 | size: 2, 31 | data: quadVertexPositions, 32 | }; 33 | } 34 | } 35 | 36 | return useRenderPass(gl, { 37 | ...renderPassOptions, 38 | attributes, 39 | fragment, 40 | vertex: vertexShader, 41 | }); 42 | } 43 | 44 | const quadVertexShaderSource = /*glsl*/ `#version 300 es 45 | 46 | in vec2 aPosition; 47 | out vec2 vUv; 48 | 49 | void main() { 50 | gl_Position = vec4(aPosition, 0.0, 1.0); 51 | vUv = (aPosition + 1.0) / 2.0; 52 | } 53 | `; 54 | 55 | /** 56 | * 1 big triangle filling the canvas offers better performance than 2 triangles : 57 | * @see https://github.com/pmndrs/postprocessing?tab=readme-ov-file#performance 58 | * @see https://michaldrobot.com/2014/04/01/gcn-execution-patterns-in-full-screen-passes/ 59 | */ 60 | const quadVertexPositions = [-1, -1, 3, -1, -1, 3]; 61 | -------------------------------------------------------------------------------- /lib/src/hooks/useRenderPass.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Attribute, 3 | DrawMode, 4 | RenderCallback, 5 | RenderPass, 6 | RenderTarget, 7 | Uniforms, 8 | } from "../types"; 9 | import { createProgram } from "../core/program"; 10 | import { setRenderTarget } from "../core/renderTarget"; 11 | import { findUniformName } from "../internal/findName"; 12 | import { useUniforms } from "../internal/useUniforms"; 13 | import { useAttributes } from "../internal/useAttributes"; 14 | import { useLifeCycleCallback } from "../internal/useLifeCycleCallback"; 15 | 16 | export type RenderPassOptions> = { 17 | target?: RenderTarget | null; 18 | fragment: string; 19 | vertex: string; 20 | attributes?: Record; 21 | uniforms?: U; 22 | transparent?: boolean; 23 | drawMode?: DrawMode; 24 | transformFeedbackVaryings?: string[]; 25 | }; 26 | 27 | export function useRenderPass( 28 | gl: WebGL2RenderingContext | undefined, 29 | { 30 | target = null, 31 | fragment, 32 | vertex, 33 | attributes = {}, 34 | uniforms: userUniforms = {} as U, 35 | transparent = false, 36 | drawMode: userDrawMode, 37 | transformFeedbackVaryings, 38 | }: RenderPassOptions, 39 | ): RenderPass { 40 | /** 41 | * INIT 42 | */ 43 | 44 | let _target = target; 45 | let _program: WebGLProgram; 46 | let _gl: WebGL2RenderingContext; 47 | 48 | const { 49 | initialize: initializeUniforms, 50 | onUpdated, 51 | setUniforms, 52 | getUniformsSnapshot, 53 | uniformsProxy, 54 | } = useUniforms(userUniforms); 55 | const { 56 | initialize: initializeAttributes, 57 | getVertexCount, 58 | bindVAO, 59 | hasIndices, 60 | indexType, 61 | } = useAttributes(attributes); 62 | 63 | function initialize(gl: WebGL2RenderingContext) { 64 | _gl = gl; 65 | const program = createProgram(_gl, fragment, vertex, transformFeedbackVaryings); 66 | if (program == null) { 67 | throw new Error("could not initialize the render pass"); 68 | } 69 | _program = program; 70 | _gl.useProgram(_program); 71 | 72 | initializeUniforms(_gl, _program); 73 | initializeAttributes(_gl, _program); 74 | } 75 | 76 | if (gl) { 77 | initialize(gl); 78 | } 79 | 80 | /** 81 | * UPDATE 82 | */ 83 | 84 | const resolutionUniformName = findUniformName(fragment + vertex, "resolution"); 85 | 86 | function setSize(size: { width: number; height: number }) { 87 | if (resolutionUniformName && userUniforms[resolutionUniformName] === undefined) { 88 | (uniformsProxy as Record)[resolutionUniformName] = [size.width, size.height]; 89 | } 90 | if (_target != null) { 91 | _target.setSize(size.width, size.height); 92 | } 93 | } 94 | 95 | function setTarget(target: RenderTarget | null) { 96 | _target = target; 97 | } 98 | 99 | /** 100 | * RENDER 101 | */ 102 | 103 | const drawMode = userDrawMode || (vertex.includes("gl_PointSize") ? "POINTS" : "TRIANGLES"); 104 | 105 | const [beforeRenderCallbacks, onBeforeRender] = useLifeCycleCallback>(); 106 | const [afterRenderCallbacks, onAfterRender] = useLifeCycleCallback>(); 107 | 108 | function render({ target }: { target?: RenderTarget | null } = {}) { 109 | if (_gl == undefined) { 110 | throw new Error("The render pass must be initialized before calling the render function"); 111 | } 112 | 113 | setRenderTarget(_gl, target ?? _target); 114 | _gl.useProgram(_program); 115 | 116 | if (transparent) { 117 | _gl.enable(_gl.BLEND); 118 | _gl.blendFunc(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA); 119 | } else { 120 | _gl.disable(_gl.BLEND); 121 | } 122 | 123 | bindVAO(); 124 | setUniforms(); 125 | 126 | for (const callback of beforeRenderCallbacks) { 127 | callback({ uniforms: getUniformsSnapshot() }); 128 | } 129 | 130 | if (hasIndices) { 131 | _gl.drawElements(_gl[drawMode], getVertexCount(), indexType, 0); 132 | } else { 133 | _gl.drawArrays(_gl[drawMode], 0, getVertexCount()); 134 | } 135 | 136 | for (const callback of afterRenderCallbacks) { 137 | callback({ uniforms: getUniformsSnapshot() }); 138 | } 139 | } 140 | 141 | return { 142 | render, 143 | initialize, 144 | setTarget, 145 | get target() { 146 | return _target; 147 | }, 148 | setSize, 149 | uniforms: uniformsProxy, 150 | vertex, 151 | fragment, 152 | onUpdated, 153 | onBeforeRender, 154 | onAfterRender, 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /lib/src/hooks/useResizeObserver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Listen for changes to the size of an element. 3 | */ 4 | export function useResizeObserver( 5 | target: HTMLElement, 6 | callback: (args: { 7 | /** 8 | * size of the observed element in CSS pixels 9 | */ 10 | size: { width: number; height: number }; 11 | /** 12 | * size of the observed element in device pixels 13 | */ 14 | devicePixelSize: { width: number; height: number }; 15 | /** 16 | * untouched, native observer entries 17 | */ 18 | entries: ResizeObserverEntry[]; 19 | }) => void, 20 | ) { 21 | let size: ResizeObserverSize; 22 | let devicePixelSize: ResizeObserverSize; 23 | 24 | const observer = new ResizeObserver((entries) => { 25 | const entry = entries.find((entry) => entry.target === target)!; 26 | 27 | size = entry.contentBoxSize[0]; 28 | devicePixelSize = entry.devicePixelContentBoxSize?.[0] || { 29 | blockSize: Math.round(size.blockSize * window.devicePixelRatio), 30 | inlineSize: Math.round(size.inlineSize * window.devicePixelRatio), 31 | }; 32 | 33 | // call the callback after the next paint, otherwise there are glitches when resizing a canvas 34 | // with an active render loop 35 | setTimeout(() => { 36 | callback({ 37 | size: { width: size.inlineSize, height: size.blockSize }, 38 | devicePixelSize: { width: devicePixelSize.inlineSize, height: devicePixelSize.blockSize }, 39 | entries, 40 | }); 41 | }, 0); 42 | }); 43 | 44 | observer.observe(target); 45 | 46 | return { 47 | disconnect: observer.disconnect, 48 | observe: () => { 49 | observer.observe(target); 50 | }, 51 | unobserve: () => { 52 | observer.unobserve(target); 53 | }, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/hooks/useTransformFeedback.ts: -------------------------------------------------------------------------------- 1 | import { createAndBindBuffer } from "../core/buffer"; 2 | import type { Attribute, RenderPass, Uniforms } from "../types"; 3 | import { useRenderPass } from "./useRenderPass"; 4 | 5 | interface TransformFeedbackOptions> { 6 | vertex: string; 7 | attributes?: Record; 8 | uniforms?: U; 9 | outputs: Record; 10 | } 11 | 12 | interface TransformFeedbackPass> 13 | extends Omit< 14 | RenderPass, 15 | "initialize" | "target" | "setTarget" | "setSize" | "vertex" | "fragment" 16 | > { 17 | getOutputData: (bufferName: O) => Float32Array; 18 | outputBuffers: Record; 19 | } 20 | 21 | export function useTransformFeedback( 22 | gl: WebGL2RenderingContext, 23 | { vertex, attributes = {}, uniforms = {} as U, outputs }: TransformFeedbackOptions, 24 | ) { 25 | const firstAttribute = Object.values(attributes)[0]; 26 | const vertexCount = firstAttribute ? firstAttribute.data!.length / firstAttribute.size : 0; 27 | 28 | const outputBuffers = Object.fromEntries( 29 | Object.entries<{ size: number }>(outputs).map(([name, { size }]) => [ 30 | name, 31 | createAndBindBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(vertexCount * size)), 32 | ]), 33 | ) as Record; 34 | 35 | const renderPass = useRenderPass(gl, { 36 | fragment: `void main() { gl_FragColor = vec4(0.0); }`, 37 | vertex, 38 | attributes, 39 | uniforms, 40 | transformFeedbackVaryings: Object.keys(outputs), 41 | drawMode: "POINTS", 42 | }); 43 | 44 | const tf = gl.createTransformFeedback(); 45 | gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); 46 | 47 | for (const [index, buffer] of Object.values(outputBuffers).entries()) { 48 | gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, buffer as WebGLBuffer); 49 | } 50 | 51 | renderPass.onBeforeRender(() => { 52 | gl.enable(gl.RASTERIZER_DISCARD); 53 | gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); 54 | gl.beginTransformFeedback(gl.POINTS); 55 | }); 56 | 57 | renderPass.onAfterRender(() => { 58 | gl.endTransformFeedback(); 59 | gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); 60 | gl.disable(gl.RASTERIZER_DISCARD); 61 | }); 62 | 63 | const tfRenderPass: TransformFeedbackPass = Object.assign(renderPass, { 64 | getOutputData: function (bufferName: O) { 65 | const output = new Float32Array(vertexCount * outputs[bufferName].size); 66 | const buffer = outputBuffers[bufferName]; 67 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 68 | gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output); 69 | return output; 70 | }, 71 | outputBuffers, 72 | }); 73 | 74 | return tfRenderPass; 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/hooks/useWebGLContext.ts: -------------------------------------------------------------------------------- 1 | export type WebGLContextOptions = WebGLContextAttributes & { 2 | colorSpace?: PredefinedColorSpace; 3 | }; 4 | 5 | export function useWebGLContext( 6 | canvas: T, 7 | options?: WebGLContextOptions, 8 | ) { 9 | const canvasElement: HTMLCanvasElement | OffscreenCanvas | null = 10 | typeof canvas === "string" ? document.querySelector(canvas) : canvas; 11 | 12 | if (canvasElement == null) { 13 | throw new Error("Canvas element not found."); 14 | } 15 | 16 | const gl = canvasElement.getContext("webgl2", options) as WebGL2RenderingContext; 17 | if (!gl) { 18 | throw new Error("No WebGL2 context available."); 19 | } 20 | 21 | if ("drawingBufferColorSpace" in gl && options?.colorSpace != undefined) { 22 | gl.drawingBufferColorSpace = options.colorSpace; 23 | } 24 | 25 | function setSize(width: number, height: number) { 26 | canvasElement!.width = width; 27 | canvasElement!.height = height; 28 | gl.viewport(0, 0, width, height); 29 | } 30 | 31 | return { 32 | canvas: canvasElement as T extends OffscreenCanvas ? OffscreenCanvas : HTMLCanvasElement, 33 | gl, 34 | setSize, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export { setAttribute } from "./core/attribute"; 2 | export { createAndBindBuffer } from "./core/buffer"; 3 | export { createProgram } from "./core/program"; 4 | export { createRenderTarget, setRenderTarget } from "./core/renderTarget"; 5 | export { createShader } from "./core/shader"; 6 | export { fillTexture, loadTexture, loadVideoTexture, createFloatDataTexture } from "./core/texture"; 7 | 8 | export { useCompositor } from "./hooks/useCompositor"; 9 | export { useEffectPass } from "./hooks/useEffectPass"; 10 | export { useCompositeEffectPass } from "./hooks/useCompositeEffectPass"; 11 | export { useQuadRenderPass } from "./hooks/useQuadRenderPass"; 12 | export { useRenderPass } from "./hooks/useRenderPass"; 13 | export { useWebGLCanvas } from "./hooks/useWebGLCanvas"; 14 | export { useWebGLContext } from "./hooks/useWebGLContext"; 15 | export { useLoop, playAllLoops, pauseAllLoops } from "./hooks/useLoop"; 16 | export { useResizeObserver } from "./hooks/useResizeObserver"; 17 | export { useBoundingRect } from "./hooks/useBoundingRect"; 18 | export { usePointerEvents } from "./hooks/usePointerEvents"; 19 | export { usePingPongFBO } from "./hooks/usePingPongFBO"; 20 | export { useTransformFeedback } from "./hooks/useTransformFeedback"; 21 | -------------------------------------------------------------------------------- /lib/src/internal/findName.ts: -------------------------------------------------------------------------------- 1 | function findName(source: string | undefined, keyword: string, word: string) { 2 | return source 3 | ?.split("\n") 4 | .find((line) => new RegExp(`^${keyword}.*${word};`, "i").test(line.trim())) 5 | ?.match(/(\w+);/)?.[1]; 6 | } 7 | 8 | export function findUniformName(source: string | undefined, word: string) { 9 | return findName(source, "uniform", word); 10 | } 11 | 12 | export function findVaryingName(source: string | undefined, word: string) { 13 | return findName(source, "varying", word) || findName(source, "in", word); 14 | } 15 | 16 | export function findAttributeName(source: string | undefined, word: string) { 17 | return findName(source, "attribute", word) || findName(source, "in", word); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/internal/useAttributes.ts: -------------------------------------------------------------------------------- 1 | import { setAttribute } from "../core/attribute"; 2 | import type { Attribute } from "../types"; 3 | 4 | const UNSIGNED_INT = WebGL2RenderingContext.UNSIGNED_INT; 5 | const UNSIGNED_SHORT = WebGL2RenderingContext.UNSIGNED_SHORT; 6 | 7 | export function useAttributes(attributes: Record) { 8 | let _gl: WebGL2RenderingContext; 9 | let _vao: WebGLVertexArrayObject | null; 10 | 11 | let vertexCount = 0; 12 | 13 | function initialize(gl: WebGL2RenderingContext, program: WebGLProgram) { 14 | _gl = gl; 15 | _vao = _gl.createVertexArray(); 16 | _gl.bindVertexArray(_vao); 17 | 18 | for (const [attributeName, attributeObj] of Object.entries(attributes)) { 19 | const attr = setAttribute(_gl, program, attributeName, attributeObj); 20 | vertexCount = Math.max(vertexCount, attr.vertexCount); 21 | } 22 | } 23 | 24 | const hasIndices = attributes.index != undefined; 25 | const indexType = attributes.index?.data.length < Math.pow(2, 16) ? UNSIGNED_SHORT : UNSIGNED_INT; 26 | 27 | function getVertexCount() { 28 | return vertexCount; 29 | } 30 | 31 | function bindVAO() { 32 | _gl.bindVertexArray(_vao); 33 | } 34 | 35 | return { 36 | initialize, 37 | getVertexCount, 38 | bindVAO, 39 | hasIndices, 40 | indexType, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/internal/useLifeCycleCallback.ts: -------------------------------------------------------------------------------- 1 | export function useLifeCycleCallback void>() { 2 | const callbacks: C[] = []; 3 | 4 | function addCallback(callback: C) { 5 | callbacks.push(callback); 6 | } 7 | 8 | return [callbacks, addCallback] as const; 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/internal/useUniforms.ts: -------------------------------------------------------------------------------- 1 | import type { DataTextureParams, ImageTextureParams } from "../core/texture"; 2 | import { fillTexture } from "../core/texture"; 3 | import type { Uniforms, UniformValue, UpdatedCallback } from "../types"; 4 | import { useLifeCycleCallback } from "./useLifeCycleCallback"; 5 | 6 | export function useUniforms(uniforms: U) { 7 | type UniformName = Extract; 8 | 9 | let textureUnitIndex = 0; 10 | const textureUnits = new Map(); 11 | 12 | let _gl: WebGL2RenderingContext; 13 | let _program: WebGLProgram; 14 | 15 | const [onUpdatedCallbacks, onUpdated] = useLifeCycleCallback>(); 16 | 17 | const uniformsLocations = new Map(); 18 | 19 | function initialize(gl: WebGL2RenderingContext, program: WebGLProgram) { 20 | _gl = gl; 21 | _program = program; 22 | 23 | const uniformsCount = _gl.getProgramParameter(_program, _gl.ACTIVE_UNIFORMS); 24 | for (let i = 0; i < uniformsCount; i++) { 25 | const uniformName = _gl.getActiveUniform(_program, i)?.name as UniformName; 26 | uniformsLocations.set(uniformName, _gl.getUniformLocation(_program, uniformName) ?? -1); 27 | } 28 | } 29 | 30 | const uniformsProxy = new Proxy( 31 | { ...uniforms }, 32 | { 33 | set(target, uniform: string, value) { 34 | if (value !== target[uniform]) { 35 | const oldTarget = getSnapshot(target); 36 | target[uniform as keyof U] = value; 37 | const newTarget = getSnapshot(target); 38 | for (const callback of onUpdatedCallbacks) callback(newTarget, oldTarget); 39 | } 40 | return true; 41 | }, 42 | }, 43 | ); 44 | 45 | function setUniforms() { 46 | for (const [uniformName, uniformValue] of Object.entries(uniformsProxy)) { 47 | setUniform( 48 | uniformName as UniformName, 49 | (typeof uniformValue === "function" ? uniformValue() : uniformValue) as U[UniformName], 50 | ); 51 | } 52 | } 53 | 54 | function setUniform(name: Uname, value: U[Uname] & UniformValue) { 55 | const uniformLocation = uniformsLocations.get(name) || -1; 56 | if (uniformLocation === -1) return -1; 57 | 58 | if (typeof value === "number") return _gl.uniform1f(uniformLocation, value); 59 | 60 | if (value instanceof WebGLTexture) { 61 | if (!textureUnits.has(name)) { 62 | textureUnits.set(name, { index: textureUnitIndex++ }); 63 | } 64 | const { index } = textureUnits.get(name)!; 65 | _gl.activeTexture(_gl.TEXTURE0 + index); 66 | _gl.bindTexture(_gl.TEXTURE_2D, value); 67 | 68 | return _gl.uniform1i(uniformLocation, index); 69 | } 70 | 71 | if (Array.isArray(value)) { 72 | switch (value.length) { 73 | case 2: { 74 | return _gl.uniform2fv(uniformLocation, value); 75 | } 76 | case 3: { 77 | return _gl.uniform3fv(uniformLocation, value); 78 | } 79 | case 4: { 80 | return _gl.uniform4fv(uniformLocation, value); 81 | } 82 | } 83 | } 84 | 85 | if ((value as ImageTextureParams).src || (value as DataTextureParams).data) { 86 | if (!textureUnits.has(name)) { 87 | const texture = _gl.createTexture(); 88 | textureUnits.set(name, { index: textureUnitIndex++, texture }); 89 | } 90 | const { index, texture } = textureUnits.get(name)!; 91 | _gl.activeTexture(_gl.TEXTURE0 + index); 92 | fillTexture(_gl, texture!, value); 93 | 94 | return _gl.uniform1i(uniformLocation, index); 95 | } 96 | } 97 | 98 | function getUniformsSnapshot() { 99 | return getSnapshot({ ...uniformsProxy }); 100 | } 101 | 102 | return { 103 | initialize, 104 | uniformsProxy, 105 | onUpdated, 106 | setUniforms, 107 | getUniformsSnapshot, 108 | }; 109 | } 110 | 111 | function getSnapshot>(object: Obj): Obj { 112 | return Object.freeze({ ...object }); 113 | } 114 | -------------------------------------------------------------------------------- /lib/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { TextureParams } from "./core/texture"; 2 | 3 | export type VectorUniform = 4 | | [number, number] 5 | | [number, number, number] 6 | | [number, number, number, number]; 7 | export type TextureUniform = TextureParams | WebGLTexture; 8 | export type UniformValue = number | VectorUniform | TextureUniform; 9 | export type Uniforms = Record; 10 | export type EffectUniforms = Record< 11 | string, 12 | | UniformValue 13 | | ((passes: { 14 | /** 15 | * - in an effect with only one pass, the inputPass is the pass rendered before this effect 16 | * - in an effect with multiple passes, the inputPass is the pass rendered before the first pass of the effect 17 | */ 18 | inputPass: RenderPass; 19 | /** 20 | * pass rendered immediately before this effect 21 | */ 22 | previousPass: RenderPass; 23 | }) => UniformValue) 24 | >; 25 | 26 | export type TypedArray = ArrayBufferView & { length: number }; 27 | 28 | export interface Attribute { 29 | size: number; 30 | data: TypedArray | number[]; 31 | type?: GLenum; 32 | normalize?: boolean; 33 | stride?: number; 34 | offset?: number; 35 | } 36 | 37 | export interface RenderTarget { 38 | framebuffer: WebGLFramebuffer; 39 | texture: WebGLTexture; 40 | width: number; 41 | height: number; 42 | setSize: (width: number, height: number) => void; 43 | } 44 | 45 | export interface RenderPass> extends Resizable { 46 | render: (opts?: { target?: RenderTarget | null }) => void; 47 | target: RenderTarget | null; 48 | setTarget: (target: RenderTarget | null) => void; 49 | uniforms: U; 50 | vertex: string; 51 | fragment: string; 52 | onUpdated: (callback: UpdatedCallback) => void; 53 | onBeforeRender: (callback: RenderCallback) => void; 54 | onAfterRender: (callback: RenderCallback) => void; 55 | initialize: (gl: WebGL2RenderingContext) => void; 56 | } 57 | 58 | export type EffectPass> = RenderPass; 59 | 60 | export interface CompositeEffectPass< 61 | P extends Record> = Record>, 62 | > extends Omit { 63 | passes: P; 64 | } 65 | 66 | export type DrawMode = 67 | | "POINTS" 68 | | "LINES" 69 | | "LINE_STRIP" 70 | | "LINE_LOOP" 71 | | "TRIANGLES" 72 | | "TRIANGLE_STRIP" 73 | | "TRIANGLE_FAN"; 74 | 75 | export interface Resizable { 76 | setSize: ({ width, height }: { width: number; height: number }) => void; 77 | } 78 | 79 | export type RenderCallback> = ( 80 | args: Readonly<{ uniforms: U }>, 81 | ) => void; 82 | 83 | export type UpdatedCallback> = ( 84 | uniforms: Readonly, 85 | oldUniforms: Readonly, 86 | ) => void; 87 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/blob/blob-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1718bd89bb5366318801421e2f2bdf3b1a6d10bdfb5a595ef66586ce5a02e499 3 | size 7876 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/blob/blob-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4c8200f701f58879fb401febc03c46a8184c86d9007e82aff96813abc3039eb3 3 | size 9921 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/blob/blob-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:960a1c426819a9e8ab13c912c801e2b045a74a78d57a616ac99c9e2e47e6241b 3 | size 18730 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/blob/blob-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:62a7a78aa613467772b636968c7f398e3ad2059e70d2209f9fd170501ee2c389 3 | size 8738 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/blob/blob-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:144cd5c581adad75de170a0f5a8103faa010c7f6ad85999d6d12f6fd24536cc1 3 | size 10334 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/bloom/bloom-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:679112107847463eaad04c98aa6d74a6f8d74ae5cd51f071f02fdd0380a1424a 3 | size 35702 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/bloom/bloom-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d8e6cba623bc0fbeda8475554b0f388f379b2227aa2410db22da8881be382120 3 | size 51842 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/bloom/bloom-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4f938184ed90d8e6c86e8b59af74c1200955c181a4819fe06f8b18555337560a 3 | size 79547 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/bloom/bloom-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:01de390e08ea95a9586ab5d8b60d5838ab99f6e0422e18607d8a14e212923bdf 3 | size 35909 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/bloom/bloom-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4a94ee99d0fa423c52d0d5d619f22998337563d8dfedf4e908d985807130c099 3 | size 48205 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/boids-static-/boids-static--android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/boids-static-/boids-static--android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/boids-static-/boids-static--chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/boids-static-/boids-static--chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/boids-static-/boids-static--firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/boids-static-/boids-static--firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/boids-static-/boids-static--iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/boids-static-/boids-static--iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/boids-static-/boids-static--safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/boids-static-/boids-static--safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/circle/circle-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fbd8b13b82b8768bfff6109f19431a8d76b1fd1d859036cf439cf81aed1182d0 3 | size 6833 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/circle/circle-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a78f975704b7a31cc49ec71e62785edc523e24d7cd73a0d9d642550e18282f0f 3 | size 8141 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/circle/circle-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e5e8c4d57b815346bea099a5539d0e84d27db48a7789edfbb3f5fac4c8d47183 3 | size 15591 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/circle/circle-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ad59d0bb27f4a5464ede02aade393a6e7829b76955b7c7db127b8f5c0437ed73 3 | size 7352 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/circle/circle-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2afcdd8d9bbf45c114455753f9a7b2efc3261098a832bc7341af3c5f333fb086 3 | size 9193 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/gradient/gradient-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5250e998b91da5e71da2b5e139b9a8bdb71e4246c2a9f2277aa29e0796125350 3 | size 4492 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/gradient/gradient-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e2a023da734bdd3596db4c54abf697df26726fd37f27feb6d9cd434daf16b235 3 | size 5743 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/gradient/gradient-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a136c6ef974f85e45999e183a1dc6fe039d60fd5689261e6cd2eeae670bec98e 3 | size 17289 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/gradient/gradient-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:809bc11f887a60ed70d15e8f0d6d27abb8995d44d6b5771f82459b4dcba1e699 3 | size 4570 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/gradient/gradient-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:12c01988b02ea432b1e02b211262f26e1e0fec8cf9d3b6ea51ec78629ac9f02b 3 | size 5756 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/indices/indices-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/indices/indices-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/indices/indices-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/indices/indices-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/indices/indices-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/indices/indices-firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/indices/indices-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/indices/indices-iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/indices/indices-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/indices/indices-safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/maths/maths-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/maths/maths-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/maths/maths-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/maths/maths-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/maths/maths-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/maths/maths-firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/maths/maths-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/maths/maths-iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/maths/maths-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/maths/maths-safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/mipmap/mipmap-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/mipmap/mipmap-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/mipmap/mipmap-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/mipmap/mipmap-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/mipmap/mipmap-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/mipmap/mipmap-firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/mipmap/mipmap-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/mipmap/mipmap-iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/mipmap/mipmap-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/mipmap/mipmap-safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/particles---FBO-static-/particles---FBO-static--safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles/particles-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6d69bc8b1354d5b71295fd2162a8c10fc66fce13a33ba497de062b00e95eb116 3 | size 23710 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles/particles-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a614822e745fd8bac6b9b2c1b73368caf5c78abf97346bc231ef8f3680fe86cb 3 | size 12379 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles/particles-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7039dfe3a8316b2c226b9e9feb842926ab0b50b8a7b92044c17f5c8a2809b1e7 3 | size 22250 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles/particles-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:86a52bcbf440fdddad0ebb7e10410f651fb746897ee4c5e6988e041c6c22b458 3 | size 27024 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/particles/particles-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4744f35f3943b3876175c809292aa5b1e6b1f2236f93f7409b850dccfb156de7 3 | size 25189 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0c32b474055878567a9f14a82452d629eb6e5d1a372b66deb1099145700a9730 3 | size 6002 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d53d491042f89add1d8e1094bfaafca8e0f0206ad44acc3019c440a674994edb 3 | size 7148 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:840c571db418c139fa6631bc56f93a23b9f9ac6ba2b0b32fb6d3ebeadcb31df4 3 | size 6252 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---global/play-pause-controls---global-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:afe6863339fa94e24e93e629783eaa6a1fcfb415abe123f95923411d9aa54a3a 3 | size 7341 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85b9412a9b8384407626cdaadd2cb5505227e8690253051208b14eb15e0acbcd 3 | size 6519 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:aaa3430659dc8fe14b9e7798b89d3c6f8af4095ccf73dfa0565efac175a78f59 3 | size 7279 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c77943effd956bcb5fb5c2f761d98a464c631a77e982fb2b7267342cfca8feae 3 | size 7041 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/play-pause-controls---local/play-pause-controls---local-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:08efb170be1ef92ec3bc28337db6033f82400acebb712997abf4e180a36aa414 3 | size 8420 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer-move/pointer-move-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c23be07638293c1eb500401c5f50be5c7d22db46a30ada43685da0aeeffdf956 3 | size 4359 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer-move/pointer-move-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f48975190da32d03955ce467aae6a7ce3dd560cedcd6e850454595f337f8f269 3 | size 4513 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer-move/pointer-move-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c32674b0dc38df49cb396067bfb2372949ecf4bc7790f0855d1965a11edd2612 3 | size 11659 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer-move/pointer-move-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:90044781673d171dfdd08355f327d67d8fd15954d437a4d1402b5edbf7e504d5 3 | size 4439 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer-move/pointer-move-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7ffe81c345d248d297fc4bc32224438c53c89058fb113178748535375c08c507 3 | size 4896 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer/pointer-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:77b44fd8a6f3dc5b0ad631159050b294aaa52b604bfb4e613e7275ae0ac30934 3 | size 4299 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer/pointer-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bb3f239fe8bc4439cffc710e7b7d4a1a724c6804a60748f46d9a7d49fc1c431b 3 | size 4395 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer/pointer-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:05af1258fabeb0c7e121bddd631974fc902ac506ebed9f88413a9002c8af8cf7 3 | size 11423 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer/pointer-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3f45824a34e9c34ba0b23b02e24adb188c002768a0708189156ed2a95cf97a95 3 | size 4519 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/pointer/pointer-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:47b60827234ce6f3c296bbad68c006256515898ba91f95a42541e3d30c4f41be 3 | size 4969 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/scissor/scissor-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/scissor/scissor-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/scissor/scissor-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/scissor/scissor-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/scissor/scissor-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/scissor/scissor-firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/scissor/scissor-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/scissor/scissor-iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/scissor/scissor-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/scissor/scissor-safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/sepia/sepia-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/sepia/sepia-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/sepia/sepia-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/sepia/sepia-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/sepia/sepia-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/sepia/sepia-firefox.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/sepia/sepia-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/sepia/sepia-iphone.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/sepia/sepia-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/sepia/sepia-safari.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/texture/texture-android.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8aff5daac3f90a3fba40b70c472f524c971b5f0196c6567ed36d1bc0b0496eea 3 | size 59379 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/texture/texture-chromium.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf873175091336c30484a6f8f5f4ffb5dadd5a78462a8ddb761d03e8ff0440c7 3 | size 85503 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/texture/texture-firefox.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d1290d089ee80a4249e75989a68eef1c6ec20e186f343d58aaed0a8e6e869982 3 | size 119211 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/texture/texture-iphone.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d72ac3d324d8644f4a265b6ecda19e67034feaea3c8e3b076ae19376f16c8120 3 | size 59806 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/texture/texture-safari.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b3737ba1b3b9b0d2840249a90af71e1ed10b6772e62f29b8137c853029e37682 3 | size 85065 4 | -------------------------------------------------------------------------------- /lib/tests/__screenshots__/video/video-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/video/video-android.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/video/video-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/video/video-chromium.png -------------------------------------------------------------------------------- /lib/tests/__screenshots__/video/video-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/usegl/5fbd838d22fde03e82af59653c8d242b7d2f3082/lib/tests/__screenshots__/video/video-firefox.png -------------------------------------------------------------------------------- /lib/tests/screenshots.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | import { routes } from "../playground/src/components/routes"; 4 | 5 | const ignoreRoutes = new Set(["pause", "dataTexture", "particles - FBO", "boids"]); 6 | 7 | const routesToTest = routes.filter(({ route }) => !ignoreRoutes.has(route)); 8 | 9 | const expectedRendersByDemo = { 10 | scissor: "2", 11 | video: "2", 12 | "particles - FBO (static)": "3", 13 | "boids (static)": "4", 14 | mipmap: /[1-3]/, 15 | texture: /1|2/, 16 | sepia: /1|2/, 17 | }; 18 | 19 | for (const { section, route } of routesToTest) { 20 | test(route, async ({ page, baseURL }) => { 21 | await page.goto(`${baseURL}/${section}/${route}`); 22 | 23 | await expect(page.locator("#renders strong")).toHaveText(expectedRendersByDemo[route] || "1"); 24 | await expect(page.locator("main")).toHaveScreenshot(); 25 | }); 26 | } 27 | 28 | test("pointer move", async ({ page, viewport, baseURL }) => { 29 | await page.goto(`${baseURL}/pointer/pointer`); 30 | 31 | await expect(page.getByText("Renders: 1")).toBeVisible(); 32 | 33 | await page.mouse.move((viewport?.width || 0) * 0.5, (viewport?.height || 0) * 0.45); 34 | 35 | await expect(page.getByText("Renders: 2")).toBeVisible(); 36 | 37 | await page.mouse.down(); 38 | 39 | await page.waitForTimeout(100); 40 | 41 | await expect(page.locator("main")).toHaveScreenshot(); 42 | }); 43 | 44 | test("play / pause controls - local", async ({ page, baseURL }) => { 45 | await page.goto(`${baseURL}/core/pause`); 46 | 47 | await expect(page.getByText("Renders: 1")).toBeVisible(); 48 | 49 | await page.evaluate(() => { 50 | return new Promise((resolve) => { 51 | const playPauseBtn = document.querySelector( 52 | ".controls.local button:nth-of-type(1)", 53 | )!; 54 | 55 | playPauseBtn.click(); 56 | 57 | requestAnimationFrame(() => { 58 | requestAnimationFrame(() => { 59 | requestAnimationFrame(() => { 60 | requestAnimationFrame(() => { 61 | playPauseBtn.click(); 62 | resolve(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | }); 69 | 70 | await expect(page.getByText("Renders: 5")).toBeVisible(); 71 | 72 | await expect(page.locator("main")).toHaveScreenshot(); 73 | }); 74 | 75 | test("play / pause controls - global", async ({ page, baseURL }) => { 76 | await page.goto(`${baseURL}/core/pause`); 77 | 78 | await expect(page.getByText("Renders: 1")).toBeVisible(); 79 | 80 | await page.evaluate(() => { 81 | return new Promise((resolve) => { 82 | const playPauseBtn = document.querySelector(".controls.global button")!; 83 | 84 | playPauseBtn.click(); 85 | 86 | requestAnimationFrame(() => { 87 | requestAnimationFrame(() => { 88 | requestAnimationFrame(() => { 89 | requestAnimationFrame(() => { 90 | playPauseBtn.click(); 91 | resolve(); 92 | }); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | 99 | await expect(page.getByText("Renders: 5")).toBeVisible(); 100 | 101 | await expect(page.locator("main")).toHaveScreenshot(); 102 | }); 103 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src", "playwright.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - lib 4 | --------------------------------------------------------------------------------