├── .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 |
2 |
14 |
15 |
16 |
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 |
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 |
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 | A |
62 | |
63 |
64 |
65 | B |
66 | |
67 |
68 |
69 | Sum |
70 | |
71 |
72 |
73 | Product |
74 | |
75 |
76 |
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 |
--------------------------------------------------------------------------------