├── .changeset ├── README.md └── config.json ├── .eslintrc.js ├── .github └── workflows │ ├── publish_pages.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── README.md ├── apps ├── docs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── tsconfig.json │ ├── typedoc.base.json │ └── typedoc.json └── web │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── app │ ├── docs │ │ ├── [type] │ │ │ └── [id] │ │ │ │ ├── page.tsx │ │ │ │ └── views │ │ │ │ ├── class.module.scss │ │ │ │ ├── class.tsx │ │ │ │ ├── function.module.scss │ │ │ │ ├── function.tsx │ │ │ │ ├── item.module.scss │ │ │ │ ├── item.tsx │ │ │ │ ├── module.module.scss │ │ │ │ ├── module.tsx │ │ │ │ ├── type.module.scss │ │ │ │ └── type.tsx │ │ ├── components │ │ │ ├── code.module.scss │ │ │ ├── code.tsx │ │ │ ├── comment-content.tsx │ │ │ ├── doc-link.module.scss │ │ │ ├── doc-link.tsx │ │ │ ├── parameter-list.tsx │ │ │ ├── property.tsx │ │ │ ├── readme.module.scss │ │ │ ├── readme.tsx │ │ │ ├── reflections.module.scss │ │ │ ├── signature.tsx │ │ │ └── type-annotation.tsx │ │ ├── highlighting-utils.tsx │ │ ├── layout.tsx │ │ ├── nav.tsx │ │ ├── page.tsx │ │ ├── reflection-utils.ts │ │ └── styles.module.scss │ ├── examples │ │ ├── [slug] │ │ │ ├── code-button.tsx │ │ │ ├── code-overlay.tsx │ │ │ ├── example-frame.tsx │ │ │ ├── page.tsx │ │ │ ├── styles.module.scss │ │ │ ├── tabs.tsx │ │ │ └── webgpu-check.tsx │ │ ├── page.tsx │ │ └── styles.module.scss │ ├── header-screen.tsx │ ├── header.tsx │ ├── layout.tsx │ ├── main.scss │ ├── page.tsx │ ├── screen.tsx │ └── styles.module.scss │ ├── components │ ├── bright-theme.ts │ └── external-link-icon.tsx │ ├── env.d.ts │ ├── examples │ ├── conway │ │ ├── index.ts │ │ ├── main.ts │ │ └── shaders.ts │ ├── depth │ │ ├── index.ts │ │ ├── logo.png │ │ └── main.ts │ ├── diffuse │ │ ├── index.ts │ │ ├── main.ts │ │ └── sphere.ts │ ├── index.ts │ ├── indexedInstances │ │ ├── index.ts │ │ └── main.ts │ ├── multisampling │ │ ├── index.ts │ │ ├── logo.png │ │ └── main.ts │ ├── textures │ │ ├── index.ts │ │ ├── logo.png │ │ └── main.ts │ └── vertexDisplacement │ │ ├── index.ts │ │ ├── main.ts │ │ ├── shaders.ts │ │ └── sphere.ts │ ├── hooks │ └── use-page-scroll.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── scene │ ├── grid │ │ ├── index.tsx │ │ └── shader │ │ │ ├── frag.glsl │ │ │ ├── index.tsx │ │ │ └── vert.glsl │ └── index.tsx │ ├── tsconfig.json │ └── turbo.json ├── package.json ├── packages ├── core │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── lib │ │ └── MockWebGPU.ts │ ├── package.json │ ├── src │ │ ├── Attribute.ts │ │ ├── BindGroup.ts │ │ ├── Executor.ts │ │ ├── IndexBuffer.ts │ │ ├── Pipeline.ts │ │ ├── PipelineDescriptor.ts │ │ ├── PipelineGroup.test.ts │ │ ├── PipelineGroup.ts │ │ ├── Sampler.ts │ │ ├── Storage.ts │ │ ├── Texture.ts │ │ ├── Uniform.ts │ │ ├── VertexAttributeObject.test.ts │ │ ├── VertexAttributeObject.ts │ │ ├── components │ │ │ ├── Canvas.ts │ │ │ ├── ColorTarget.ts │ │ │ ├── CpuBuffer.ts │ │ │ ├── DepthStencil.ts │ │ │ ├── Device.ts │ │ │ ├── GpuBufferObject.ts │ │ │ ├── GpuSamplerObject.ts │ │ │ ├── GpuTextureObject.ts │ │ │ ├── Id.ts │ │ │ ├── Label.ts │ │ │ ├── MultiSampling.ts │ │ │ ├── Primitive.ts │ │ │ └── Shader.ts │ │ ├── index.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── typedoc.json │ └── vite.config.js ├── eslint-config-custom │ ├── CHANGELOG.md │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── shaders │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── color │ │ │ ├── gamma.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── lighting │ │ │ ├── diffuse.ts │ │ │ └── index.ts │ ├── tsconfig.json │ ├── typedoc.json │ └── vite.config.js ├── tsconfig │ ├── CHANGELOG.md │ ├── base.json │ ├── main.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── button │ ├── index.tsx │ └── styles.module.scss │ ├── card │ ├── index.tsx │ └── styles.module.scss │ ├── footer │ ├── index.tsx │ └── styles.module.scss │ ├── global-env.d.ts │ ├── header │ ├── index.tsx │ └── styles.module.scss │ ├── nav │ ├── index.tsx │ └── styles.module.scss │ ├── package.json │ ├── side-nav │ ├── index.tsx │ └── side-nav.module.scss │ ├── toast │ ├── index.tsx │ └── styles.module.scss │ ├── tsconfig.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "JMBeresford/webgpu-kit" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | extends: ["eslint-config-custom/library.js"], 5 | parser: "@typescript-eslint/parser", 6 | plugins: ["@typescript-eslint"], 7 | root: true, 8 | parserOptions: { 9 | project: [ 10 | "./tsconfig.json", 11 | "./packages/*/tsconfig.json", 12 | "./apps/*/tsconfig.json", 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/publish_pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Build and Deploy to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | publish_docs: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | - name: Setup Pnpm 38 | uses: pnpm/action-setup@v2 39 | with: 40 | version: 8 41 | - name: Install Deps 42 | run: pnpm install 43 | - name: Build Docs and Web 44 | run: pnpm run build --filter docs --filter web && mkdir ./apps/web/out/legacy && cp -r ./apps/docs/dist ./apps/web/out/legacy/docs 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v3 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v2 49 | with: 50 | path: "./apps/web/out" 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v2 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js 16.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | run_install: false 27 | 28 | - name: Get pnpm store directory 29 | shell: bash 30 | run: | 31 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 32 | 33 | - uses: actions/cache@v3 34 | name: Setup pnpm cache 35 | with: 36 | path: ${{ env.STORE_PATH }} 37 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pnpm-store- 40 | 41 | - name: Install dependencies 42 | run: pnpm install 43 | 44 | - name: Creating .npmrc 45 | run: | 46 | cat << EOF > "$HOME/.npmrc" 47 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 48 | EOF 49 | env: 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | 52 | - name: Run tests 53 | run: pnpm run test 54 | 55 | - name: Create Release Pull Request or Publish to npm 56 | id: changesets 57 | uses: changesets/action@v1 58 | with: 59 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 60 | publish: pnpm run publish-packages 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} 63 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 64 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Tests 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 16.x 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16.x 20 | 21 | - name: Setup pnpm 22 | uses: pnpm/action-setup@v2 23 | with: 24 | run_install: false 25 | 26 | - name: Get pnpm store directory 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 30 | 31 | - uses: actions/cache@v3 32 | name: Setup pnpm cache 33 | with: 34 | path: ${{ env.STORE_PATH }} 35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm-store- 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Run linter 43 | run: pnpm run lint 44 | 45 | - name: Run tests 46 | run: pnpm run test 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # vercel 36 | .vercel 37 | 38 | # vite 39 | dist/ 40 | vite.config.js.timestamp* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["./packages/*", "./apps/*"], 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 6 | }, 7 | "[javascript]": { 8 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # todo 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGPU-Kit 2 | 3 | ### A minimal webGPU toolkit for rendering and compute operations 4 | 5 | > UNDER HEAVY DEVELOPMENT 6 | 7 | ## Table of Contents 8 | 9 | - [About this project](#about-this-project) 10 | - [Packages](#packages) 11 | - [Installation](#installation) 12 | 13 | --- 14 | 15 | ## About this project 16 | 17 | WebGPU-Kit aims to provide varying levels of abstraction around the webGPU spec. 18 | 19 | The [core] package is intended to be a small wrapper around webGPU to reduce boilerplate and 20 | to make construction and execution of pipelines more straight-forward. 21 | 22 | The forward package is intended to be yet another small wrapper around the core package that 23 | exposes an API for a forward rendering solution akin to Three.js. 24 | 25 | Other packages are planned to expose additional APIs: 26 | 27 | - shaders: contains reusable, configurable wgsl shader chunks 28 | - react: contains react bindings for the core/forward packages 29 | 30 | ## Packages 31 | 32 | - [core] 33 | - [shaders] 34 | - forward - TODO 35 | - react - TODO 36 | 37 | ## Installation 38 | 39 | In order to install the packages run the following: 40 | 41 | ```sh 42 | # npm i @webgpu-kit/ 43 | 44 | # e.g. to install the core package 45 | npm i @webgpu-kit/core 46 | ``` 47 | 48 | [core]: ./packages/core/ 49 | [shaders]: ./packages/shaders/ 50 | -------------------------------------------------------------------------------- /apps/docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | ## 0.0.5 4 | 5 | ### Patch Changes 6 | 7 | - [#42](https://github.com/JMBeresford/webgpu-kit/pull/42) [`febb649`](https://github.com/JMBeresford/webgpu-kit/commit/febb649be47ac0bd16cc7d7c18e8fc79aff1a796) Thanks [@JMBeresford](https://github.com/JMBeresford)! - init new docs, major lint/tsconfig refactor 8 | 9 | ## 0.0.4 10 | 11 | ### Patch Changes 12 | 13 | - [`9cc8879`](https://github.com/JMBeresford/webgpu-kit/commit/9cc8879900dee9f9845307fa6f602cfcae27fb49) Thanks [@JMBeresford](https://github.com/JMBeresford)! - feat: resolve webgpu types via plugin 14 | 15 | ## 0.0.3 16 | 17 | ### Patch Changes 18 | 19 | - [`4538565`](https://github.com/JMBeresford/webgpu-kit/commit/4538565694b56dd97c1931a24d202ae60e041501) Thanks [@JMBeresford](https://github.com/JMBeresford)! - fix: export structure + internals removed from docs 20 | 21 | ## 0.0.2 22 | 23 | ### Patch Changes 24 | 25 | - [#23](https://github.com/JMBeresford/webgpu-kit/pull/23) [`112b599`](https://github.com/JMBeresford/webgpu-kit/commit/112b5993807176de8083530ee9c33805b5c62bb9) Thanks [@JMBeresford](https://github.com/JMBeresford)! - docs: add typedocs generation 26 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # webgpu-kit docs 2 | 3 | This is the app that generates the documentation for the webgpu-kit package. 4 | Currently the docs are built and deployed in CI, but eventually we will 5 | want to include static additions to the docs here. 6 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.5", 4 | "description": "Documentation for the webgpu-kit packages", 5 | "private": true, 6 | "files": [ 7 | "dist" 8 | ], 9 | "main": "dist/out.json", 10 | "scripts": { 11 | "build": "typedoc" 12 | }, 13 | "devDependencies": { 14 | "typedoc-plugin-resolve-external": "^0.2.1", 15 | "@webgpu/types": "0.1.34", 16 | "@webgpu-kit/core": "workspace:*", 17 | "tsconfig": "workspace:*", 18 | "typedoc": "npm:@jberesford/typedoc@0.25.10", 19 | "typescript": "^5.2.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/main.json", 3 | "files": [], 4 | "compilerOptions": { 5 | "types": ["@webgpu/types"] 6 | }, 7 | "references": [{ "path": "../../packages/core" }] 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/typedoc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "plugin": ["typedoc-plugin-resolve-external"], 4 | "entryPointStrategy": "resolve", 5 | "categorizeByGroup": true, 6 | "excludeInternal": true, 7 | "externalModulemap": { 8 | "@webgpu/types": "https://gpuweb.github.io/types/" 9 | }, 10 | "exclude": ["**/node_modules/**/*", "**/*.test.ts"], 11 | "logLevel": "Verbose" 12 | } 13 | -------------------------------------------------------------------------------- /apps/docs/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "extends": ["./typedoc.base.json"], 4 | "name": "webgpu-kit", 5 | "out": "./dist", 6 | "json": "dist/out.json", 7 | "entryPoints": ["../../packages/core/", "../../packages/shaders/"], 8 | "entryPointStrategy": "packages", 9 | "readme": "../../README.md" 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["eslint-config-custom/next.js"], 5 | ignorePatterns: ["next.config.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | rules: { 11 | "no-bitwise": "off", 12 | "no-undef": "off", 13 | "no-unsafe-call": "off", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { allGroups } from "../../reflection-utils"; 3 | import { ClassView } from "./views/class"; 4 | import { FunctionView } from "./views/function"; 5 | import { ModuleView } from "./views/module"; 6 | import { TypeView } from "./views/type"; 7 | 8 | export type ViewParams = { 9 | type: string; 10 | id: string; 11 | }; 12 | 13 | export type View = (props: { params: ViewParams }) => JSX.Element; 14 | 15 | const views: Record = { 16 | classes: ClassView, 17 | modules: ModuleView, 18 | types: TypeView, 19 | functions: FunctionView, 20 | }; 21 | 22 | export default function Page(props: { params: ViewParams }): JSX.Element { 23 | const type = normalizeTypeName(props.params.type); 24 | const View = views[type]; 25 | 26 | if (!View) { 27 | console.error(`No view for type: ${type}`); 28 | notFound(); 29 | } 30 | 31 | return ( 32 |
33 | 34 |
35 | ); 36 | } 37 | 38 | export function generateStaticParams(): ViewParams[] { 39 | return Array.from(allGroups) 40 | .map(([type, ids]) => 41 | ids.map((id) => ({ type: normalizeTypeName(type), id })), 42 | ) 43 | .flat(); 44 | } 45 | 46 | function normalizeTypeName(type: string): string { 47 | return type.toLowerCase().replaceAll(" ", ""); 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/class.module.scss: -------------------------------------------------------------------------------- 1 | @use "./item.module.scss"; 2 | 3 | main.class { 4 | display: flex; 5 | flex-direction: column; 6 | gap: 1.25rem; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/class.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getReflectionById, 3 | parseConstructorSignatures, 4 | parseMethodsSignatures, 5 | parseProperties, 6 | } from "@/app/docs/reflection-utils"; 7 | import { ViewParams } from "../page"; 8 | import { CommentContent } from "@/app/docs/components/comment-content"; 9 | import { Signature } from "@/app/docs/components/signature"; 10 | import { Property } from "@/app/docs/components/property"; 11 | import styles from "./class.module.scss"; 12 | import { Item } from "./item"; 13 | 14 | export function ClassView(props: { params: ViewParams }): JSX.Element { 15 | const { id } = props.params; 16 | const reflection = getReflectionById(parseInt(id)); 17 | 18 | if (!reflection) { 19 | throw new Error(`Reflection not found for id: ${id}`); 20 | } 21 | 22 | const { comment } = reflection; 23 | 24 | const ctorSignatures = parseConstructorSignatures(reflection); 25 | const properties = parseProperties(reflection); 26 | const methodSignatures = parseMethodsSignatures(reflection); 27 | 28 | return ( 29 |
30 |
31 |

{reflection.name}

32 | {comment !== undefined && } 33 |
34 | 35 | {ctorSignatures.length > 0 ? ( 36 |
37 |

Constructors

38 | 39 | {ctorSignatures.map((signature) => ( 40 | 41 | 42 | 43 | ))} 44 |
45 | ) : null} 46 | 47 | {properties.length > 0 ? ( 48 |
49 |

Properties

50 | 51 | {properties.map((reflection) => ( 52 | 53 |

{reflection.name}

54 | 55 |
56 | ))} 57 |
58 | ) : null} 59 | 60 | {methodSignatures.length > 0 ? ( 61 |
62 |

Methods

63 | 64 | {methodSignatures.map((signature) => ( 65 | 66 |

{signature.name}

67 | 68 |
69 | ))} 70 |
71 | ) : null} 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/function.module.scss: -------------------------------------------------------------------------------- 1 | @use "./item.module.scss"; 2 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/function.tsx: -------------------------------------------------------------------------------- 1 | import { ReflectionKind } from "@/app/docs/reflection-utils"; 2 | import { 3 | getReflectionById, 4 | getReflectionsByKind, 5 | } from "../../../reflection-utils"; 6 | import { Signature } from "../../../components/signature"; 7 | import styles from "./function.module.scss"; 8 | import { Item } from "./item"; 9 | 10 | type Params = { 11 | id: string; 12 | }; 13 | 14 | export function FunctionView(props: { params: Params }): JSX.Element { 15 | const reflection = getReflectionById(parseInt(props.params.id)); 16 | 17 | if (reflection === undefined) { 18 | throw new Error(`Reflection not found for id: ${props.params.id}`); 19 | } 20 | 21 | const signatures = reflection.signatures || []; 22 | 23 | return ( 24 |
25 |

{reflection.name}

26 | 27 | {signatures.map((signature) => ( 28 | 29 | 30 | 31 | ))} 32 |
33 | ); 34 | } 35 | 36 | export function generateStaticParams(): Params[] { 37 | return getReflectionsByKind(ReflectionKind.Module).map((reflection) => ({ 38 | id: reflection.id.toString(), 39 | })); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/item.module.scss: -------------------------------------------------------------------------------- 1 | .group { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1rem; 5 | } 6 | 7 | .item { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 1rem; 11 | 12 | border-left: solid 1px #333; 13 | padding-left: 1rem; 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/item.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./item.module.scss"; 2 | 3 | export function Item(props: JSX.IntrinsicElements["div"]): JSX.Element { 4 | const { className, ...rest } = props; 5 | 6 | const classes = `${className} ${styles.item}`; 7 | 8 | return
; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/module.module.scss: -------------------------------------------------------------------------------- 1 | .module { 2 | padding-top: 1rem; 3 | 4 | h1 { 5 | font-size: 2.5rem; 6 | } 7 | 8 | .group { 9 | margin: 1.5rem 0; 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | .groupItem { 17 | display: flex; 18 | align-items: center; 19 | margin: 0.5rem 0; 20 | gap: 0.5rem; 21 | } 22 | 23 | h2 { 24 | font-size: 1.85rem; 25 | } 26 | 27 | h3 { 28 | opacity: 0.8; 29 | font-weight: 400; 30 | } 31 | 32 | h4 { 33 | font-size: 0.8rem; 34 | font-weight: 400; 35 | color: white; 36 | border: 1px solid #555; 37 | border-radius: 10px; 38 | padding: 0.25em 0.5em; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/module.tsx: -------------------------------------------------------------------------------- 1 | import { JSONOutput as J } from "typedoc"; 2 | import styles from "./module.module.scss"; 3 | import { 4 | getLinkForDeclaration, 5 | getReflectionById, 6 | getReflectionsByKind, 7 | ReflectionKind, 8 | } from "@/app/docs/reflection-utils"; 9 | import DocLink from "../../../components/doc-link"; 10 | import { Readme } from "@/app/docs/components/readme"; 11 | 12 | type Params = { 13 | id: string; 14 | }; 15 | 16 | export function ModuleView(props: { params: Params }): JSX.Element { 17 | const reflection = getReflectionById(parseInt(props.params.id)); 18 | 19 | if (reflection === undefined) { 20 | throw new Error(`Reflection not found for id: ${props.params.id}`); 21 | } 22 | 23 | if (!reflection.groups) { 24 | throw new Error( 25 | `Expected reflection to have groups, got: ${JSON.stringify(reflection)}`, 26 | ); 27 | } 28 | 29 | return ( 30 |
31 | {reflection.readme ? ( 32 | 33 | ) : ( 34 |

{reflection.name}

35 | )} 36 | 37 | {reflection.groups.map((group) => ( 38 |
39 |

{group.title}

40 | 41 | {group.children 42 | ?.map(getReflectionById) 43 | .map((child) => 44 | child ? : null, 45 | )} 46 |
47 | ))} 48 |
49 | ); 50 | } 51 | 52 | export async function generateStaticParams(): Promise { 53 | return getReflectionsByKind(ReflectionKind.Module).map((reflection) => ({ 54 | id: reflection.id.toString(), 55 | })); 56 | } 57 | 58 | function ModuleChild(props: { 59 | reflection: J.DeclarationReflection; 60 | }): JSX.Element { 61 | const { reflection } = props; 62 | const link = getLinkForDeclaration(reflection) || ""; 63 | 64 | return ( 65 | 66 |
67 |

{reflection.name}

68 |

{ReflectionKind[reflection.kind]}

69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/type.module.scss: -------------------------------------------------------------------------------- 1 | @use "./item.module.scss"; 2 | 3 | .type { 4 | display: flex; 5 | flex-direction: column; 6 | gap: 1.25rem; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/docs/[type]/[id]/views/type.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getReflectionById, 3 | getReflectionsByKind, 4 | ReflectionKind, 5 | } from "../../../reflection-utils"; 6 | import { CommentContent } from "../../../components/comment-content"; 7 | import { Property } from "@/app/docs/components/property"; 8 | import { renderTypeReflection } from "@/app/docs/highlighting-utils"; 9 | import { Code } from "@/app/docs/components/code"; 10 | import styles from "./type.module.scss"; 11 | import { Item } from "./item"; 12 | 13 | type Params = { 14 | id: string; 15 | }; 16 | 17 | export function TypeView(props: { params: Params }): JSX.Element { 18 | const reflection = getReflectionById(parseInt(props.params.id)); 19 | 20 | if (!reflection) { 21 | throw new Error(`Reflection not found for id: ${props.params.id}`); 22 | } 23 | 24 | if (!reflection.type) { 25 | throw new Error( 26 | `Expected reflection to have a type, got: ${JSON.stringify(reflection)}`, 27 | ); 28 | } 29 | 30 | const { comment, name } = reflection; 31 | const renderedType = renderTypeReflection(reflection); 32 | 33 | function Common(): JSX.Element { 34 | return ( 35 | <> 36 |

{name}

37 | 38 | {comment !== undefined && } 39 | {renderedType} 40 | 41 | ); 42 | } 43 | 44 | if (reflection.type.type === "reflection") { 45 | const children = 46 | reflection.type.declaration.children?.filter( 47 | (child) => 48 | !child.flags.isPrivate && 49 | !child.flags.isProtected && 50 | !child.name.startsWith("_"), 51 | ) || []; 52 | 53 | return ( 54 |
55 | 56 | 57 |
58 |

Properties

59 | 60 | {children.map((child) => ( 61 | 62 | 63 | 64 | ))} 65 |
66 |
67 | ); 68 | } 69 | 70 | return ; 71 | } 72 | 73 | export function generateStaticParams(): Params[] { 74 | return getReflectionsByKind(ReflectionKind.TypeAlias) 75 | .concat(getReflectionsByKind(ReflectionKind.TypeLiteral)) 76 | .map((reflection) => ({ 77 | id: reflection.id.toString(), 78 | })); 79 | } 80 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/code.module.scss: -------------------------------------------------------------------------------- 1 | .code { 2 | width: min-content; 3 | margin: 0; 4 | tab-size: 2; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/code.tsx: -------------------------------------------------------------------------------- 1 | import { Code as CodeImpl } from "bright"; 2 | import { ComponentProps } from "react"; 3 | import { linkExtension } from "../highlighting-utils"; 4 | import styles from "./code.module.scss"; 5 | 6 | export function Code(props: ComponentProps): JSX.Element { 7 | const { extensions = [], className, ...rest } = props; 8 | 9 | const classes = `${className ?? ""} ${styles.code}`; 10 | 11 | return ( 12 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/comment-content.tsx: -------------------------------------------------------------------------------- 1 | import { JSONOutput as J } from "typedoc"; 2 | import { Code } from "./code"; 3 | import { getLinkForDeclaration, getReflectionById } from "../reflection-utils"; 4 | import DocLink from "./doc-link"; 5 | 6 | const CommentView: Record< 7 | J.CommentDisplayPart["kind"], 8 | (comment: J.CommentDisplayPart) => JSX.Element 9 | > = { 10 | text: CommentText, 11 | code: CommentCode, 12 | "inline-tag": CommentInlineTag, 13 | } as const; 14 | 15 | export function CommentContent(props: { comment: J.Comment }): JSX.Element { 16 | const comment = props.comment; 17 | const parts = comment.summary; 18 | 19 | return ( 20 |
21 | {parts.map((part, i) => { 22 | const View = CommentView[part.kind]; 23 | return ; 24 | })} 25 |
26 | ); 27 | } 28 | 29 | function CommentText(comment: J.CommentDisplayPart): JSX.Element { 30 | return {comment.text}; 31 | } 32 | 33 | function CommentCode(comment: J.CommentDisplayPart): JSX.Element { 34 | const text = comment.text; 35 | const lang = text.split("\n")[0].replace("```", ""); 36 | const code = text 37 | .split("\n") 38 | .filter((line) => !line.startsWith("```")) 39 | .join("\n"); 40 | 41 | return {code}; 42 | } 43 | 44 | function CommentInlineTag(comment: J.CommentDisplayPart): JSX.Element { 45 | const inline = comment as J.InlineTagDisplayPart; 46 | const { target, text } = inline; 47 | 48 | if (typeof target === "number") { 49 | const targetReflection = getReflectionById(target); 50 | const link = getLinkForDeclaration(targetReflection); 51 | 52 | if (link === undefined) { 53 | return {text}; 54 | } 55 | 56 | return {inline.text}; 57 | } 58 | 59 | return {text}; 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/doc-link.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | color: #888; 3 | 4 | transition: color 0.15s ease-in; 5 | 6 | &:hover { 7 | transition-timing-function: ease-out; 8 | color: #ddd; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/doc-link.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from "@/components/external-link-icon"; 2 | import Link from "next/link"; 3 | import { ComponentProps } from "react"; 4 | import styles from "./doc-link.module.scss"; 5 | 6 | type LinkProps = Omit, "href"> & { href: string }; 7 | 8 | export default function DocLink(props: LinkProps): JSX.Element { 9 | const isExternal = isExternalLink(props.href); 10 | const { className, ...rest } = props; 11 | 12 | const classes = `${className} ${styles.link}`; 13 | 14 | return ( 15 | 20 | {props.children} 21 | {isExternal && } 22 | 23 | ); 24 | } 25 | 26 | function isExternalLink(href: string): boolean { 27 | return href.startsWith("http"); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/parameter-list.tsx: -------------------------------------------------------------------------------- 1 | import type { JSONOutput as J } from "typedoc"; 2 | import { TypeAnnotation } from "./type-annotation"; 3 | 4 | export function ParameterList(props: { 5 | parameters: J.ParameterReflection[]; 6 | }): JSX.Element { 7 | return ( 8 | 9 | ( 10 | {props.parameters.map((param) => ( 11 | 12 | ))} 13 | ) 14 | 15 | ); 16 | } 17 | 18 | export function Parameter(props: { 19 | parameter: J.ParameterReflection; 20 | }): JSX.Element { 21 | const { name, type } = props.parameter; 22 | 23 | return ( 24 | 25 | {name} 26 | {type !== undefined ? ( 27 | 28 | : 29 | 30 | 31 | ) : null} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/property.tsx: -------------------------------------------------------------------------------- 1 | import { JSONOutput as J } from "typedoc"; 2 | import { renderProperty } from "../highlighting-utils"; 3 | import { Code } from "./code"; 4 | import { CommentContent } from "./comment-content"; 5 | import styles from "./reflections.module.scss"; 6 | 7 | export function Property(props: { 8 | reflection: J.DeclarationReflection; 9 | }): JSX.Element { 10 | const { comment } = props.reflection; 11 | const renderedProperty = renderProperty(props.reflection); 12 | 13 | return ( 14 |
15 | {renderedProperty} 16 | 17 | {comment !== undefined && } 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/readme.module.scss: -------------------------------------------------------------------------------- 1 | .readme { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5rem; 5 | 6 | h1 { 7 | margin: 0 0 0.25rem; 8 | } 9 | 10 | h2, 11 | h3 { 12 | margin: 0.5rem 0 0.125rem; 13 | } 14 | 15 | h1 { 16 | font-size: 2rem; 17 | } 18 | 19 | h2 { 20 | font-size: 1.5rem; 21 | } 22 | 23 | h3 { 24 | font-size: 1.25rem; 25 | } 26 | 27 | h4 { 28 | font-size: 1.125rem; 29 | } 30 | 31 | h5 { 32 | font-size: 1.1rem; 33 | font-weight: 400; 34 | } 35 | 36 | hr { 37 | border: 0; 38 | border-top: 1px solid #333; 39 | margin: 1rem 0; 40 | } 41 | 42 | a { 43 | color: #888; 44 | 45 | transition: color 0.15s ease-in; 46 | 47 | &:hover { 48 | transition-timing-function: ease-out; 49 | color: #ddd; 50 | } 51 | } 52 | 53 | ul, 54 | ol { 55 | list-style-type: disc; 56 | list-style-position: inside; 57 | 58 | ul { 59 | padding-left: 1rem; 60 | } 61 | } 62 | 63 | .code { 64 | width: auto; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/readme.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "./code"; 2 | import Markdown from "react-markdown"; 3 | import remarkGfm from "remark-gfm"; 4 | import { JSONOutput as J } from "typedoc"; 5 | import styles from "./readme.module.scss"; 6 | import rehypeSlug from "rehype-slug"; 7 | 8 | export function Readme(props: { readme: J.CommentDisplayPart[] }): JSX.Element { 9 | const { readme } = props; 10 | 11 | return ( 12 |
13 | 27 | {children} 28 | 29 | ); 30 | }, 31 | }} 32 | > 33 | {readme.map((part) => part.text).join("\n\n")} 34 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/reflections.module.scss: -------------------------------------------------------------------------------- 1 | .reflection { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5rem; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/signature.tsx: -------------------------------------------------------------------------------- 1 | import { JSONOutput as J } from "typedoc"; 2 | import { CommentContent } from "./comment-content"; 3 | import { renderSignature } from "../highlighting-utils"; 4 | import { Code } from "./code"; 5 | import styles from "./reflections.module.scss"; 6 | 7 | export function Signature(props: { 8 | signature: J.SignatureReflection; 9 | showName?: boolean; 10 | }): JSX.Element { 11 | const { comment } = props.signature; 12 | const renderedSignature = renderSignature(props.signature); 13 | 14 | return ( 15 |
16 | {renderedSignature} 17 | 18 | {comment !== undefined && } 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/docs/components/type-annotation.tsx: -------------------------------------------------------------------------------- 1 | import { JSONOutput as J } from "typedoc"; 2 | import { getLinkForType, getNameForType } from "../reflection-utils"; 3 | import DocLink from "./doc-link"; 4 | import { Fragment } from "react"; 5 | 6 | export function TypeAnnotation(props: { type?: J.SomeType }): JSX.Element { 7 | const typeReflection = props.type; 8 | if (typeReflection === undefined) { 9 | return void; 10 | } 11 | 12 | const link = getLinkForType(typeReflection); 13 | const name = getNameForType(typeReflection); 14 | const typeArgs = 15 | typeReflection.type === "reference" 16 | ? typeReflection.typeArguments 17 | : undefined; 18 | 19 | switch (typeReflection.type) { 20 | case "union": 21 | return ; 22 | case "reflection": 23 | return ( 24 | 25 | {link !== undefined ? : name} 26 | {"{\n\t"} 27 | {typeReflection.declaration.children?.map((child) => ( 28 | 29 | {child.name} 30 | : 31 | 32 | {";\n"} 33 | 34 | ))} 35 | {"}"} 36 | 37 | ); 38 | default: 39 | return ( 40 | 41 | {link !== undefined ? {name} : name} 42 | {typeArgs !== undefined ? ( 43 | 44 | < 45 | {typeArgs.map((arg) => ( 46 | 47 | ))} 48 | > 49 | 50 | ) : null} 51 | 52 | ); 53 | } 54 | } 55 | 56 | function UnionTypesView(props: { types: J.SomeType[] }): JSX.Element { 57 | const { types } = props; 58 | return ( 59 | 60 | {types.map((type, i) => ( 61 | 62 | 63 | {i < types.length - 1 ? | : null} 64 | 65 | ))} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /apps/web/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { Nav } from "./nav"; 3 | import styles from "./styles.module.scss"; 4 | import { Toast, ToastContent } from "ui/toast"; 5 | import Link from "next/link"; 6 | import { Button } from "ui/button"; 7 | 8 | export default function Layout(props: { children: ReactNode }): JSX.Element { 9 | return ( 10 |
11 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/app/docs/nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { JSONOutput as J } from "typedoc"; 4 | import { SideNav, SideNavGroup, SideNavItem } from "ui/side-nav"; 5 | import DocLink from "./components/doc-link"; 6 | import { 7 | getLinkForDeclaration, 8 | ReflectionKind, 9 | project, 10 | } from "./reflection-utils"; 11 | import styles from "./styles.module.scss"; 12 | import { usePathname } from "next/navigation"; 13 | 14 | export function Nav(): JSX.Element { 15 | const packages = project.children || []; 16 | 17 | return ( 18 | 19 | {packages.map((pkg) => ( 20 | }> 21 | {filterChildren(pkg, pkg.name).map((child) => ( 22 | 23 | ))} 24 | 25 | ))} 26 | 27 | ); 28 | } 29 | 30 | function NavGroupHeader(props: { node: J.DeclarationReflection }): JSX.Element { 31 | const { node } = props; 32 | const link = getLinkForDeclaration(node); 33 | 34 | if (!link) { 35 | throw new Error(`No link for ${node.name}`); 36 | } 37 | 38 | return ( 39 |

40 | {node.name} 41 |

42 | ); 43 | } 44 | 45 | function NavItem(props: { 46 | node: J.DeclarationReflection; 47 | packageName: string; 48 | }): JSX.Element { 49 | const { node, packageName } = props; 50 | const filteredChildren = filterChildren(node, packageName); 51 | const link = getLinkForDeclaration(node); 52 | const pathname = usePathname(); 53 | 54 | if (!link) { 55 | throw new Error(`No link for ${node.name}`); 56 | } 57 | 58 | if (node.variant !== "declaration") { 59 | throw new Error( 60 | `Expected a declaration node, but got ${JSON.stringify(node)}`, 61 | ); 62 | } 63 | 64 | const isActive = pathname === link; 65 | 66 | return ( 67 | <> 68 | 69 | {node.name} 70 | 71 | {filteredChildren.length > 0 ? ( 72 | 73 | {filteredChildren.map((child) => ( 74 | 75 | ))} 76 | 77 | ) : null} 78 | 79 | ); 80 | } 81 | 82 | function filterChildren( 83 | node: J.DeclarationReflection, 84 | packageName: string, 85 | ): J.DeclarationReflection[] { 86 | let children = node.children || []; 87 | 88 | // global filters 89 | children = children.filter( 90 | (child) => 91 | !child.flags.isPrivate && 92 | !child.flags.isProtected && 93 | !child.name.startsWith("_") && 94 | child.name !== "constructor", 95 | ); 96 | 97 | // package-specific filters 98 | switch (packageName.toLowerCase()) { 99 | case "core": 100 | children = children.filter( 101 | (child) => child.kind === ReflectionKind.Class, 102 | ); 103 | break; 104 | case "shaders": 105 | break; 106 | default: 107 | throw new Error(`Unknown package: ${packageName}`); 108 | } 109 | 110 | return children; 111 | } 112 | -------------------------------------------------------------------------------- /apps/web/app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { Readme } from "./components/readme"; 2 | import { readme } from "./reflection-utils"; 3 | 4 | export default function Page(): JSX.Element { 5 | return ( 6 |
7 |

Docs

8 | {readme && } 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/docs/styles.module.scss: -------------------------------------------------------------------------------- 1 | .docs { 2 | display: grid; 3 | grid-template-columns: auto 1fr; 4 | gap: 2rem; 5 | 6 | .nav { 7 | padding: 2rem 1rem 0 0; 8 | width: min-content; 9 | // border-right: 1px solid #333; 10 | 11 | h3 { 12 | color: #ddd; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | color: inherit; 18 | } 19 | 20 | ul { 21 | margin: 0.25rem 0; 22 | } 23 | 24 | li { 25 | color: #555; 26 | border-left: 1px solid #333; 27 | margin: 0.125em 0; 28 | 29 | transition: 30 | color 0.15s ease-in, 31 | border-color 0.1s ease-in; 32 | 33 | &:hover, 34 | &.active { 35 | transition-timing-function: ease-out; 36 | color: #ddd; 37 | border-color: #ddd; 38 | } 39 | } 40 | } 41 | 42 | .content { 43 | padding-top: 2rem; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/code-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "ui/button"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | import { useCallback, useMemo } from "react"; 6 | import styles from "./styles.module.scss"; 7 | 8 | export function CodeButton(): JSX.Element { 9 | const router = useRouter(); 10 | const params = useSearchParams(); 11 | const pathname = usePathname(); 12 | const showingCode = useMemo( 13 | () => params.get("showCode") === "true", 14 | [params], 15 | ); 16 | 17 | const handleClick = useCallback(() => { 18 | const newParams = new URLSearchParams(params); 19 | 20 | newParams.set("showCode", `${!showingCode}`); 21 | router.push(`${pathname}?${newParams.toString()}`, { scroll: false }); 22 | }, [params, router, pathname, showingCode]); 23 | 24 | return ( 25 |
26 |
27 | 30 |
31 | 32 |
33 | 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/code-overlay.tsx: -------------------------------------------------------------------------------- 1 | import { Code as SyntaxHighlighter } from "bright"; 2 | import type { Code } from "../../../examples"; 3 | import { Tabs } from "./tabs"; 4 | import { theme } from "@/components/bright-theme"; 5 | import * as prettier from "prettier"; 6 | 7 | type Props = { 8 | code: Code; 9 | }; 10 | 11 | async function CodeBlock(props: Props): Promise { 12 | const parser = props.code.language.startsWith("ts") 13 | ? "typescript" 14 | : undefined; 15 | 16 | const code = parser 17 | ? await prettier.format(props.code.text, { parser, useTabs: true }) 18 | : props.code.text; 19 | 20 | return ( 21 | 22 | {code} 23 | 24 | ); 25 | } 26 | 27 | export function CodeOverlay(props: { sources: Code[] }): JSX.Element { 28 | return ( 29 |
30 | 31 | {props.sources.map((code) => ( 32 |
33 | 34 |
35 | ))} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/example-frame.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useEffect, useMemo, useState } from "react"; 4 | import { Examples } from "../../../examples"; 5 | import type { Example as ExampleData } from "../../../examples"; 6 | import { WebgpuCheck } from "./webgpu-check"; 7 | 8 | type Props = { 9 | title: ExampleData["title"]; 10 | }; 11 | 12 | export function ExampleFrame(props: Props): JSX.Element { 13 | const ref = useRef(null); 14 | const [error, setError] = useState(false); 15 | 16 | const data = useMemo( 17 | () => Examples.find((example) => example.url === props.title), 18 | [props.title], 19 | ); 20 | 21 | if (!data) { 22 | throw new Error(`No example found for slug: ${props.title}`); 23 | } 24 | 25 | useEffect(() => { 26 | if (ref.current) { 27 | data.run(ref.current).catch((e) => { 28 | // eslint-disable-next-line no-console -- logging 29 | console.error(e); 30 | setError(true); 31 | }); 32 | } 33 | }, [data]); 34 | 35 | return ( 36 | <> 37 |
38 | 45 |
46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Examples } from "../../../examples"; 3 | import { ExampleFrame } from "./example-frame"; 4 | import styles from "./styles.module.scss"; 5 | import { CodeButton } from "./code-button"; 6 | import { CodeOverlay } from "./code-overlay"; 7 | 8 | export default function Page(props: { params: { slug: string } }): JSX.Element { 9 | const exampleData = Examples.find( 10 | (example) => example.url === props.params.slug, 11 | ); 12 | 13 | if (!exampleData) { 14 | throw new Error(`No example found for slug: ${props.params.slug}`); 15 | } 16 | 17 | return ( 18 |
19 |
20 | 21 | ← back to examples 22 | 23 | 24 |

{exampleData.title}

25 |

{exampleData.description}

26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 |
35 | ); 36 | } 37 | 38 | export function generateStaticParams(): { slug: string }[] { 39 | return Examples.map((example) => ({ slug: example.url })); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/styles.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100%; 3 | display: grid; 4 | grid-template-rows: auto 1fr; 5 | grid-template-columns: 1fr; 6 | padding: 1.5em 0; 7 | gap: 2em; 8 | } 9 | 10 | .info { 11 | display: grid; 12 | margin: 2em 0 1.5em; 13 | grid-template-areas: 14 | "back-btn ." 15 | "title code-btn" 16 | "description code-btn"; 17 | 18 | .back { 19 | grid-area: back-btn; 20 | color: inherit; 21 | text-decoration: none; 22 | 23 | opacity: 0.8; 24 | transition: opacity 0.2s ease-in-out; 25 | 26 | &:hover { 27 | opacity: 1; 28 | } 29 | } 30 | 31 | h1 { 32 | grid-area: title; 33 | font-size: 3em; 34 | margin: 0.125em 0; 35 | letter-spacing: -2px; 36 | } 37 | 38 | p { 39 | grid-area: description; 40 | margin: 0; 41 | font-size: 1.125em; 42 | opacity: 0.8; 43 | } 44 | 45 | .code-btn { 46 | position: relative; 47 | grid-area: code-btn; 48 | display: flex; 49 | align-items: flex-end; 50 | justify-content: flex-end; 51 | 52 | > :first-child { 53 | position: absolute; 54 | } 55 | 56 | > :not(.show) { 57 | opacity: 0; 58 | pointer-events: none; 59 | touch-action: none; 60 | transition: opacity 0.2s; 61 | transition-delay: 0; 62 | transition-timing-function: ease-out; 63 | } 64 | 65 | > .show { 66 | transition-delay: 0.2s; 67 | transition-timing-function: ease-in; 68 | } 69 | } 70 | } 71 | 72 | $drop-shadow-color: rgba(255, 255, 255, 0.05); 73 | 74 | .content { 75 | position: relative; 76 | width: 100%; 77 | 78 | border-radius: 10px; 79 | overflow: hidden; 80 | box-shadow: 81 | -8px -8px 25px 0px $drop-shadow-color, 82 | 8px 8px 25px 0px $drop-shadow-color, 83 | -8px 8px 25px 0px $drop-shadow-color, 84 | 8px -8px 25px 0px $drop-shadow-color; 85 | 86 | .source { 87 | background-color: rgba(0, 0, 0, 0.85); 88 | backdrop-filter: blur(40px); 89 | } 90 | } 91 | 92 | .code-overlay { 93 | position: absolute; 94 | inset: 0; 95 | display: grid; 96 | grid-template-rows: auto 1fr; 97 | grid-template-columns: 1fr; 98 | grid-template-areas: 99 | "tabs" 100 | "code"; 101 | 102 | &:not(.show) { 103 | opacity: 0; 104 | pointer-events: none; 105 | touch-action: none; 106 | } 107 | 108 | transition: opacity 0.2s ease-in-out; 109 | 110 | .tabs { 111 | background-color: #131313; 112 | grid-area: tabs; 113 | display: flex; 114 | align-items: center; 115 | justify-content: flex-start; 116 | gap: 1em; 117 | 118 | button { 119 | opacity: 0.25; 120 | font-size: 1em; 121 | transition: opacity 0.2s ease-in-out; 122 | 123 | &:hover, 124 | &.active { 125 | opacity: 1; 126 | } 127 | } 128 | } 129 | 130 | .source-text { 131 | grid-area: code; 132 | position: absolute; 133 | inset: 0; 134 | transition: opacity 0.2s; 135 | overflow-y: hidden; 136 | tab-size: 4; 137 | 138 | opacity: 0; 139 | pointer-events: none; 140 | touch-action: none; 141 | 142 | transition-delay: 0s; 143 | transition-timing-function: ease-in; 144 | 145 | &.show { 146 | opacity: 1; 147 | pointer-events: auto; 148 | touch-action: auto; 149 | overflow-y: auto; 150 | transition-delay: 0.2s; 151 | transition-timing-function: ease-out; 152 | } 153 | } 154 | } 155 | 156 | .show { 157 | opacity: 1; 158 | pointer-events: auto; 159 | touch-action: auto; 160 | } 161 | 162 | @media (max-width: 600px) { 163 | .content { 164 | canvas { 165 | width: 100%; 166 | aspect-ratio: 1.125; 167 | } 168 | } 169 | .info { 170 | gap: 1em; 171 | grid-template-areas: 172 | "back-btn" 173 | "title" 174 | "description" 175 | "code-btn"; 176 | 177 | .code-btn { 178 | justify-content: flex-start; 179 | font-size: 0.85em; 180 | } 181 | 182 | h1 { 183 | font-size: 2.5em; 184 | } 185 | 186 | p { 187 | font-size: 1em; 188 | } 189 | } 190 | 191 | .toast { 192 | position: fixed; 193 | width: fit-content; 194 | max-width: 90%; 195 | left: 50%; 196 | transform: translateX(-50%); 197 | bottom: 2em; 198 | padding: 0.5em 0.75em; 199 | gap: 1em; 200 | 201 | h4 { 202 | font-size: 1.125em; 203 | } 204 | 205 | p { 206 | max-width: unset; 207 | font-size: 0.85em; 208 | } 209 | 210 | button { 211 | font-size: 0.95em; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 4 | import styles from "./styles.module.scss"; 5 | import { ReactNode, useCallback, Children } from "react"; 6 | import { Code } from "@/examples"; 7 | import { Button } from "ui/button"; 8 | 9 | export function Tabs(props: { 10 | sources: Code[]; 11 | children?: ReactNode; 12 | }): JSX.Element { 13 | const params = useSearchParams(); 14 | const file = params.get("showFile") ?? "0"; 15 | const show = params.get("showCode") === "true"; 16 | const router = useRouter(); 17 | const pathname = usePathname(); 18 | 19 | const changeFile = useCallback( 20 | (fileIdx: number) => { 21 | const newParams = new URLSearchParams(params); 22 | newParams.set("showFile", fileIdx.toString()); 23 | 24 | router.push(`${pathname}?${newParams.toString()}`, { scroll: false }); 25 | }, 26 | [router, pathname, params], 27 | ); 28 | 29 | return ( 30 |
31 |
32 | {props.sources.map((code, i) => ( 33 | 44 | ))} 45 |
46 | 47 |
48 | {Children.map(props.children, (child, i) => { 49 | let classes = styles["source-text"]; 50 | if (file === i.toString()) { 51 | classes += ` ${styles.show}`; 52 | } 53 | 54 | return
{child}
; 55 | })} 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/app/examples/[slug]/webgpu-check.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | import { Toast, ToastAction, ToastContent } from "ui/toast"; 4 | import { ExternalLinkIcon } from "../../../components/external-link-icon"; 5 | import styles from "./styles.module.scss"; 6 | 7 | export function WebgpuCheck({ 8 | error = false, 9 | }: { 10 | error?: boolean; 11 | }): JSX.Element { 12 | const [hasGpu, setHasGpu] = useState(true); 13 | 14 | useEffect(() => { 15 | async function tryGetGpu(): Promise { 16 | const adapter = await navigator.gpu.requestAdapter(); 17 | if (!adapter) { 18 | throw new Error("No adapter"); 19 | } 20 | 21 | const _device = await adapter.requestDevice(); 22 | } 23 | 24 | tryGetGpu().catch(() => { 25 | setHasGpu(false); 26 | }); 27 | }, []); 28 | 29 | return ( 30 | 36 | The example could not run in this browser 37 | { 39 | window.open( 40 | "https://github.com/gpuweb/gpuweb/wiki/Implementation-Status", 41 | "_blank", 42 | ); 43 | }} 44 | > 45 | Learn more 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/app/examples/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Examples } from "../../examples"; 3 | import styles from "./styles.module.scss"; 4 | 5 | export default function Page(): JSX.Element { 6 | return ( 7 |
8 | ← back 9 |

Examples

10 | 11 |
12 | {Examples.map((example) => ( 13 | 14 |
15 |

{example.title}

16 |

{example.description}

17 |
18 |
19 | 20 | ))} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/examples/styles.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | padding-top: 1em; 3 | 4 | > a { 5 | color: rgba(255, 255, 255, 0.75); 6 | font-size: 1em; 7 | text-decoration: none; 8 | transition: color 0.15s ease-in-out; 9 | 10 | &:hover { 11 | color: rgba(255, 255, 255, 1); 12 | } 13 | } 14 | 15 | h1 { 16 | margin-bottom: 1em; 17 | font-size: 4em; 18 | } 19 | } 20 | 21 | .examples { 22 | display: flex; 23 | flex-direction: column; 24 | margin-bottom: 2em; 25 | gap: 1em; 26 | 27 | a { 28 | text-decoration: none; 29 | color: inherit; 30 | } 31 | 32 | hr { 33 | margin: 1em 0; 34 | opacity: 0.2; 35 | } 36 | } 37 | 38 | .example { 39 | display: flex; 40 | align-items: baseline; 41 | gap: 1em; 42 | 43 | filter: brightness(0.8); 44 | transition: filter 0.2s ease-in-out; 45 | 46 | h2 { 47 | margin: 0; 48 | font-size: 2em; 49 | } 50 | 51 | &:hover { 52 | filter: brightness(1.2); 53 | } 54 | } 55 | 56 | @media (max-width: 600px) { 57 | .main { 58 | h1 { 59 | font-size: 2.5em; 60 | } 61 | } 62 | 63 | .example { 64 | flex-direction: column; 65 | gap: 0.5em; 66 | 67 | h2 { 68 | font-size: 1.5em; 69 | } 70 | 71 | p { 72 | color: rgba(255, 255, 255, 0.75); 73 | font-size: 0.9em; 74 | } 75 | 76 | &, 77 | &:hover { 78 | filter: none; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /apps/web/app/header-screen.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { usePageScroll } from "../hooks/use-page-scroll"; 5 | import styles from "./styles.module.scss"; 6 | 7 | export function HeaderScreen(): JSX.Element { 8 | const pathname = usePathname(); 9 | const scrolled = usePageScroll({ 10 | threshold: pathname.startsWith("/docs") ? -1 : undefined, 11 | }); 12 | 13 | return ( 14 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/app/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { Nav } from "ui/nav"; 5 | import { Button } from "ui/button"; 6 | import { Header, HeaderTitle } from "ui/header"; 7 | import { FaBarsStaggered, FaCircleXmark } from "react-icons/fa6"; 8 | import { useRef } from "react"; 9 | import { ExternalLinkIcon } from "../components/external-link-icon"; 10 | import { HeaderScreen } from "./header-screen"; 11 | 12 | export function DesktopHeader(): JSX.Element { 13 | return ( 14 |
15 | 16 | WebGPU-kit 17 | 18 | 40 | 41 |
42 | ); 43 | } 44 | 45 | export function MobileHeader(): JSX.Element { 46 | const ref = useRef(null); 47 | 48 | return ( 49 |
50 | 51 | WebGPU-kit 52 | 53 | 54 | 64 | 65 | 86 | 87 | 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { Footer, FooterColumn } from "ui/footer"; 3 | import { GeistSans } from "geist/font/sans"; 4 | import Link from "next/link"; 5 | import "./main.scss"; 6 | import { ExternalLinkIcon } from "../components/external-link-icon"; 7 | import { DesktopHeader, MobileHeader } from "./header"; 8 | 9 | export default function Layout(props: { children: ReactNode }): JSX.Element { 10 | return ( 11 | 12 | 13 |
14 | 15 | 16 | 17 |
{props.children}
18 | 19 | 130 |
131 | 132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /apps/web/app/main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | min-width: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | background-color: #050505; 10 | color: #ddd; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6 { 19 | text-wrap: balance; 20 | } 21 | 22 | p { 23 | text-wrap: pretty; 24 | } 25 | 26 | #root { 27 | position: relative; 28 | display: grid; 29 | grid-template-areas: 30 | "header" 31 | "content"; 32 | grid-auto-flow: row; 33 | grid-template-rows: min-content; 34 | } 35 | 36 | #root-header { 37 | grid-area: header; 38 | position: sticky; 39 | top: 0; 40 | z-index: 1; 41 | } 42 | 43 | #root-header-mobile { 44 | display: none; 45 | grid-area: header; 46 | position: sticky; 47 | top: 0; 48 | z-index: 1; 49 | 50 | * { 51 | transition: opacity 0.25s ease-in-out; 52 | } 53 | 54 | nav { 55 | opacity: 0; 56 | pointer-events: none; 57 | touch-action: none; 58 | position: absolute; 59 | grid-column: 1/1; 60 | place-self: center stretch; 61 | } 62 | 63 | .icons { 64 | border: none; 65 | display: grid; 66 | background-color: rgba(0, 0, 0, 0); 67 | place-items: center; 68 | font-size: 1.5em; 69 | 70 | color: #ddd; 71 | 72 | .icon:nth-child(2) { 73 | opacity: 0; 74 | position: absolute; 75 | } 76 | } 77 | 78 | &.open { 79 | h1 { 80 | opacity: 0; 81 | } 82 | 83 | nav { 84 | opacity: 1; 85 | pointer-events: auto; 86 | touch-action: auto; 87 | transition-delay: 0.25s; 88 | } 89 | 90 | .icon:nth-child(1) { 91 | opacity: 0; 92 | } 93 | 94 | .icon:nth-child(2) { 95 | opacity: 1; 96 | transition-delay: 0.25s; 97 | } 98 | } 99 | 100 | &:not(.open) { 101 | h1 { 102 | transition-delay: 0.25s; 103 | } 104 | 105 | .icon:nth-child(1) { 106 | transition-delay: 0.25s; 107 | } 108 | } 109 | } 110 | 111 | #content { 112 | grid-area: content; 113 | position: relative; 114 | width: min(90%, 1500px); 115 | margin: 0 auto; 116 | 117 | min-height: 100%; 118 | 119 | display: flex; 120 | flex-direction: column; 121 | gap: 1em; 122 | } 123 | 124 | #root-footer { 125 | background-color: rgba(0, 0, 0, 0.65); 126 | border-top: 1px solid #333; 127 | margin-top: 2em; 128 | 129 | h1 { 130 | font-size: 2em; 131 | margin-bottom: 0.5em; 132 | } 133 | 134 | h3 { 135 | font-size: 1.125em; 136 | margin-bottom: 0.75em; 137 | } 138 | 139 | p { 140 | font-size: 0.9em; 141 | max-width: 25ch; 142 | line-height: 130%; 143 | } 144 | 145 | ul { 146 | list-style: none; 147 | } 148 | 149 | li { 150 | font-size: 0.9em; 151 | margin-bottom: 0.4em; 152 | 153 | color: rgba(255, 255, 255, 0.75); 154 | transition: color 0.15s ease-in-out; 155 | 156 | &:hover { 157 | color: rgba(255, 255, 255, 1); 158 | } 159 | } 160 | 161 | a { 162 | color: rgba(255, 255, 255, 0.75); 163 | color: inherit; 164 | text-decoration: none; 165 | } 166 | } 167 | 168 | header { 169 | overflow-x: hidden; 170 | } 171 | 172 | @media (max-width: 600px) { 173 | #root-header { 174 | display: none; 175 | } 176 | 177 | #root-header-mobile { 178 | display: initial; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { ButtonGroup, Button } from "ui/button"; 3 | import { Card, CardTitle } from "ui/card"; 4 | import { Scene } from "../scene"; 5 | import { ExternalLinkIcon } from "../components/external-link-icon"; 6 | import styles from "./styles.module.scss"; 7 | import { Screen } from "./screen"; 8 | 9 | export default function Page(): JSX.Element { 10 | return ( 11 | <> 12 | 13 | 14 | 15 |
16 |

The power of modern web graphics. In the palm of your hand.

17 | 18 |

19 | WebGPU-kit is a collection of libraries that make it easy to build 20 | high-performance, cross-platform, web-based graphics applications. 21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |

Why Choose WebGPU-kit?

37 | 38 |

39 | WebGPU-kit is built on the foundation of WebGPU, the next generation 40 | of web graphics. This low-level graphics API harnesses the power of 41 | modern GPU hardware, enabling visually rich and interactive 42 | applications that push the boundaries of what's possible in a 43 | browser. 44 |

45 | 46 |

47 | With WebGPU-kit, you get the combined power of WebGPU's 48 | future-proof technology and a library designed to make it easy to 49 | leverage that power. 50 |

51 |
52 | 53 | 101 |
102 | 103 |
104 | 105 |
106 | 107 | Streamlined Development 108 |

109 | WebGPU-kit minimizes the need for repetitive code by providing 110 | sensible defaults. However, it still allows for detailed 111 | configuration akin to raw WebGPU code when necessary. 112 |

113 |
114 | 115 | Harness the Power of Modern GPUs 116 |

117 | With WebGPU-kit, you're no longer constrained by outdated GPU 118 | APIs. Embrace the power of modern graphics workflows without any 119 | compromises or workarounds. 120 |

121 |
122 | 123 | Simplified API with Pipeline Groups 124 |

125 | WebGPU-kit's simple API allows you to group both render 126 | pipelines and compute pipelines by shared resources, streamlining 127 | your workflow. 128 |

129 |
130 | 131 | Flexible Levels of Abstraction 132 |

133 | Whether you need full control over pipeline creation, shader code, 134 | and operational control, or you prefer a simple, conventional scene 135 | graph for a straightforward renderer, WebGPU-kit has you covered. 136 |

137 |
138 |
139 | 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /apps/web/app/screen.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef } from "react"; 4 | import { usePageScroll } from "../hooks/use-page-scroll"; 5 | import styles from "./styles.module.scss"; 6 | 7 | export function Screen(): JSX.Element { 8 | const ref = useRef(null); 9 | const scrolled = usePageScroll({ threshold: 750, relativeToViewport: true }); 10 | 11 | return ( 12 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/styles.module.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1em; 5 | align-items: center; 6 | justify-content: center; 7 | text-align: center; 8 | height: 75svh; 9 | max-height: 900px; 10 | 11 | h1 { 12 | max-width: 30ch; 13 | font-size: calc(2.5em + 1.5vw); 14 | font-weight: 800; 15 | letter-spacing: -0.035em; 16 | 17 | background: rgb(221, 221, 221); 18 | background: linear-gradient( 19 | 75deg, 20 | rgba(255, 255, 255, 1) 0%, 21 | rgba(200, 200, 200, 1) 100% 22 | ); 23 | 24 | background-clip: text; 25 | -webkit-text-fill-color: transparent; 26 | -moz-text-fill-color: transparent; 27 | } 28 | 29 | p { 30 | max-width: 50ch; 31 | font-size: 1.25em; 32 | } 33 | 34 | .buttons { 35 | margin-top: 2em; 36 | filter: drop-shadow(0 0 0.5em rgb(0, 0, 0)); 37 | } 38 | } 39 | 40 | #scene { 41 | position: fixed; 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | height: 100lvh; 46 | z-index: -1; 47 | } 48 | 49 | .screen { 50 | position: fixed; 51 | inset: 0; 52 | z-index: -1; 53 | background-color: rgba(0, 0, 0, 0.5); 54 | opacity: 0; 55 | 56 | transition: opacity 0.75s ease-in-out; 57 | } 58 | 59 | .header-screen { 60 | position: absolute; 61 | top: 0; 62 | bottom: 0; 63 | left: 50%; 64 | transform: translateX(-50%); 65 | width: 100vw; 66 | z-index: -1; 67 | 68 | background-color: rgba(0, 0, 0, 0.5); 69 | backdrop-filter: blur(10px); 70 | border-bottom: solid 1px #333; 71 | 72 | opacity: 0; 73 | transition: opacity 0.35s ease-in-out; 74 | } 75 | 76 | .rule { 77 | margin: 3.5em 0; 78 | padding: 0; 79 | border: solid 1px #777; 80 | 81 | mask-image: radial-gradient( 82 | circle at 50% 50%, 83 | rgba(0, 0, 0, 1) 0%, 84 | rgba(0, 0, 0, 0) 100% 85 | ); 86 | } 87 | 88 | .why { 89 | display: flex; 90 | gap: 3em; 91 | text-align: center; 92 | flex-direction: column; 93 | align-items: center; 94 | justify-content: space-around; 95 | 96 | main { 97 | display: flex; 98 | flex-direction: column; 99 | gap: 1em; 100 | align-items: center; 101 | text-align: center; 102 | } 103 | 104 | h2 { 105 | font-size: 3.25em; 106 | margin-bottom: 0.35em; 107 | } 108 | 109 | p { 110 | max-width: 60ch; 111 | font-size: 1.2em; 112 | line-height: 130%; 113 | color: rgba(255, 255, 255, 0.75); 114 | } 115 | 116 | aside { 117 | border-radius: 8px; 118 | color: #ddd; 119 | 120 | h4 { 121 | font-size: 1.3em; 122 | margin-bottom: 0.5em; 123 | } 124 | 125 | ul { 126 | display: flex; 127 | gap: 1em; 128 | list-style: none; 129 | 130 | li { 131 | font-size: 0.9em; 132 | } 133 | 134 | a { 135 | color: inherit; 136 | 137 | opacity: 0.75; 138 | transition: opacity 0.15s ease-in-out; 139 | 140 | &:hover { 141 | opacity: 1; 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | .cards { 149 | width: 100%; 150 | display: flex; 151 | justify-content: center; 152 | flex-wrap: wrap; 153 | gap: 2em; 154 | 155 | > div { 156 | flex-grow: 1; 157 | flex-basis: 25%; 158 | min-width: 300px; 159 | max-width: fit-content; 160 | } 161 | 162 | p { 163 | color: rgba(255, 255, 255, 0.75); 164 | max-width: 45ch; 165 | line-height: 150%; 166 | } 167 | } 168 | 169 | @media (max-width: 600px) { 170 | .hero { 171 | gap: 1.5em; 172 | 173 | h1 { 174 | font-size: 2.25em; 175 | max-width: 100%; 176 | } 177 | 178 | p { 179 | font-size: 0.95em; 180 | max-width: 30ch; 181 | } 182 | 183 | .buttons { 184 | width: 100%; 185 | align-items: center; 186 | display: flex; 187 | flex-direction: column; 188 | gap: 1em; 189 | 190 | button { 191 | font-size: 1em; 192 | padding: 0.75em 1.25em; 193 | } 194 | } 195 | } 196 | 197 | .why { 198 | h2 { 199 | font-size: 2.5em; 200 | } 201 | 202 | p { 203 | max-width: 30ch; 204 | font-size: 1em; 205 | } 206 | 207 | aside { 208 | h4 { 209 | font-size: 1.1em; 210 | } 211 | 212 | ul { 213 | flex-direction: column; 214 | gap: 0.5em; 215 | 216 | li { 217 | font-size: 0.8em; 218 | } 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /apps/web/components/external-link-icon.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from "react"; 2 | import { FaExternalLinkAlt } from "react-icons/fa"; 3 | 4 | type Props = ComponentProps; 5 | 6 | export function ExternalLinkIcon({ style, ...props }: Props): JSX.Element { 7 | return ( 8 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.glsl" { 7 | const content: string; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/examples/conway/index.ts: -------------------------------------------------------------------------------- 1 | import * as code1 from "./main?raw"; 2 | import { runExample } from "./main"; 3 | import code2 from "./shaders?raw"; 4 | 5 | export const ConwayExample = { 6 | title: "Conway's Game of Life", 7 | url: "conway", 8 | code: [ 9 | { text: code1.default, language: "ts", filename: "main.ts" }, 10 | { text: code2, language: "ts", filename: "shaders.ts" }, 11 | ], 12 | description: "A GPU-accelerated implementation of Conway's Game of Life.", 13 | run: runExample, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/examples/conway/main.ts: -------------------------------------------------------------------------------- 1 | import { Attribute } from "@webgpu-kit/core/Attribute"; 2 | import { VertexAttributeObject } from "@webgpu-kit/core/VertexAttributeObject"; 3 | import { RenderPipeline, ComputePipeline } from "@webgpu-kit/core/Pipeline"; 4 | import { PipelineGroup } from "@webgpu-kit/core/PipelineGroup"; 5 | import { Executor } from "@webgpu-kit/core/Executor"; 6 | import { Uniform } from "@webgpu-kit/core/Uniform"; 7 | import { Storage } from "@webgpu-kit/core/Storage"; 8 | import { BindGroup } from "@webgpu-kit/core/BindGroup"; 9 | import { 10 | GRID_SIZE, 11 | WORKGROUP_SIZE, 12 | computeShader, 13 | renderShader, 14 | } from "./shaders"; 15 | 16 | export async function runExample(canvas: HTMLCanvasElement): Promise { 17 | const vertices = new Float32Array([ 18 | -0.8, -0.8, 0.8, -0.8, 0.8, 0.8, 19 | 20 | -0.8, -0.8, 0.8, 0.8, -0.8, 0.8, 21 | ]); 22 | 23 | const posAttribute = new Attribute({ 24 | label: "Position", 25 | format: "float32x2", 26 | shaderLocation: 0, 27 | arrayBuffer: vertices, 28 | itemCount: vertices.length / 2, 29 | itemSize: 2, 30 | }); 31 | 32 | const gridUniform = new Uniform({ 33 | label: "Grid Size Uniform", 34 | visibility: 35 | GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, 36 | binding: 0, 37 | arrayBuffer: new Float32Array([GRID_SIZE, GRID_SIZE]), 38 | }); 39 | 40 | const stepUniform = new Uniform({ 41 | label: "Step Uniform", 42 | visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, 43 | binding: 1, 44 | arrayBuffer: new Uint32Array([0]), 45 | }); 46 | 47 | const cellState = new Uint32Array(GRID_SIZE * GRID_SIZE * 2); 48 | 49 | for (let i = 0; i < cellState.length / 2; i++) { 50 | cellState[i] = Math.random() > 0.75 ? 1 : 0; 51 | } 52 | 53 | const gridStorage = new Storage({ 54 | label: "Cell State Storage", 55 | visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, 56 | binding: 2, 57 | arrayBuffer: cellState, 58 | readOnly: false, 59 | }); 60 | 61 | const vao = new VertexAttributeObject({ 62 | label: "Cell VAO", 63 | itemCount: vertices.length / 2, 64 | }); 65 | 66 | await vao.addAttributes(posAttribute); 67 | 68 | const renderPipeline = new RenderPipeline({ 69 | label: "Render Pipeline", 70 | shader: renderShader, 71 | canvas, 72 | }); 73 | 74 | const computePipeline = new ComputePipeline({ 75 | label: "Cell Simulation Pipeline", 76 | workgroupSize: [WORKGROUP_SIZE, WORKGROUP_SIZE, 1], 77 | workgroupCount: [GRID_SIZE / WORKGROUP_SIZE, GRID_SIZE / WORKGROUP_SIZE, 1], 78 | onAfterPass: async () => { 79 | if (!stepUniform.cpuBuffer) return; 80 | stepUniform.cpuBuffer[0] += 1; 81 | 82 | await stepUniform.updateGpuBuffer(); 83 | }, 84 | shader: computeShader, 85 | }); 86 | 87 | const pipelineGroup = new PipelineGroup({ 88 | label: "Cell Pipeline Group", 89 | pipelines: [computePipeline, renderPipeline], 90 | vertexCount: vertices.length / 2, 91 | instanceCount: GRID_SIZE * GRID_SIZE, 92 | }); 93 | 94 | const bindGroup = new BindGroup(); 95 | 96 | await bindGroup.addUniforms(gridUniform, stepUniform); 97 | await bindGroup.addStorages(gridStorage); 98 | 99 | await pipelineGroup.setBindGroups(bindGroup); 100 | pipelineGroup.addVertexAttributeObjects(vao); 101 | 102 | const executor = new Executor({ 103 | label: "Cell Executor", 104 | }); 105 | 106 | await executor.addPipelineGroups(pipelineGroup); 107 | 108 | async function tick(): Promise { 109 | await executor.run(); 110 | await new Promise(requestAnimationFrame); 111 | await tick(); 112 | } 113 | 114 | await tick(); 115 | } 116 | -------------------------------------------------------------------------------- /apps/web/examples/conway/shaders.ts: -------------------------------------------------------------------------------- 1 | export const GRID_SIZE = 256; 2 | export const WORKGROUP_SIZE = 16; 3 | 4 | export const renderShader = /* wgsl */ ` 5 | struct VertexInput { 6 | @location(0) pos: vec2f, 7 | @builtin(instance_index) instance: u32, 8 | }; 9 | 10 | struct VertexOutput { 11 | @builtin(position) pos: vec4f, 12 | @location(0) cell: vec2f, 13 | @location(1) @interpolate(flat) instance: u32, 14 | }; 15 | 16 | struct CellState { 17 | state: array, 18 | } 19 | 20 | struct State { 21 | states: array, 22 | } 23 | 24 | @group(0) @binding(0) var grid: vec2; 25 | @group(0) @binding(1) var step: u32; 26 | @group(0) @binding(2) var state: State; 27 | 28 | @vertex 29 | fn vertexMain(input: VertexInput) -> VertexOutput { 30 | let i = f32(input.instance); 31 | let cell = vec2f(i % grid.x, floor(i / grid.x)); 32 | let cellOffset = cell / grid * 2; 33 | let gridPos = (input.pos + 1) / grid - 1 + cellOffset; 34 | 35 | var output: VertexOutput; 36 | output.pos = vec4f(gridPos, 0, 1); 37 | output.cell = cell; 38 | output.instance = input.instance; 39 | return output; 40 | } 41 | 42 | @fragment 43 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { 44 | let c = input.cell / grid; 45 | 46 | let readIdx = select(0u, 1u, step % 2u == 0u); 47 | let stateIn = &state.states[readIdx]; 48 | 49 | let state = f32((*stateIn).state[input.instance]); 50 | return vec4f(c, 1-c.x, 1) * state * 2.0; 51 | } 52 | `; 53 | 54 | export const computeShader = /* wgsl */ ` 55 | struct CellState { 56 | state: array, 57 | } 58 | 59 | struct State { 60 | states: array, 61 | } 62 | 63 | @group(0) @binding(0) var grid: vec2; 64 | @group(0) @binding(1) var step: u32; 65 | @group(0) @binding(2) var state: State; 66 | 67 | fn cellIndex(cell: vec2u) -> u32 { 68 | return (cell.y % u32(grid.y)) * u32(grid.x) + 69 | (cell.x % u32(grid.x)); 70 | } 71 | 72 | @compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1) 73 | fn computeMain(@builtin(global_invocation_id) cell: vec3) { 74 | 75 | let stepIsEven = step % 2u == 0u; 76 | let readIdx = select(0u, 1u, stepIsEven); 77 | let writeIdx = select(1u, 0u, stepIsEven); 78 | 79 | let stateIn = &state.states[readIdx]; 80 | let stateOut = &state.states[writeIdx]; 81 | 82 | var activeNeighbors = 0u; 83 | for (var y: u32 = 0u; y < 3u; y = y + 1u) { 84 | for (var x: u32 = 0u; x < 3u; x = x + 1u) { 85 | if (x == 1u && y == 1u) { 86 | continue; 87 | } 88 | 89 | let cellActive: u32 = (*stateIn).state[cellIndex(vec2(cell.x + x - 1u, cell.y + y - 1u))]; 90 | activeNeighbors = activeNeighbors + cellActive; 91 | } 92 | } 93 | 94 | let i = cellIndex(cell.xy); 95 | 96 | switch activeNeighbors { 97 | case 2: { // Active cells with 2 neighbors stay active. 98 | (*stateOut).state[i] = (*stateIn).state[i]; 99 | } 100 | case 3: { // Cells with 3 neighbors become or stay active. 101 | (*stateOut).state[i] = 1; 102 | } 103 | default: { // Cells with < 2 or > 3 neighbors become inactive. 104 | (*stateOut).state[i] = 0; 105 | } 106 | } 107 | } 108 | `; 109 | -------------------------------------------------------------------------------- /apps/web/examples/depth/index.ts: -------------------------------------------------------------------------------- 1 | import * as code from "./main?raw"; 2 | import { runExample } from "./main"; 3 | 4 | export const DepthExample = { 5 | title: "Depth", 6 | url: "depth", 7 | code: [{ text: code.default, language: "ts", filename: "main.ts" }], 8 | description: "A simple example of depth testing.", 9 | run: runExample, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/examples/depth/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JMBeresford/webgpu-kit/5bf1eb6b6394ca077ed7ceb9ed279eabab0aab34/apps/web/examples/depth/logo.png -------------------------------------------------------------------------------- /apps/web/examples/depth/main.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { Attribute } from "@webgpu-kit/core/Attribute"; 3 | import { Executor } from "@webgpu-kit/core/Executor"; 4 | import { RenderPipeline } from "@webgpu-kit/core/Pipeline"; 5 | import { PipelineGroup } from "@webgpu-kit/core/PipelineGroup"; 6 | import { Uniform } from "@webgpu-kit/core/Uniform"; 7 | import { VertexAttributeObject } from "@webgpu-kit/core/VertexAttributeObject"; 8 | import { Texture } from "@webgpu-kit/core/Texture"; 9 | import { Sampler } from "@webgpu-kit/core/Sampler"; 10 | import { BindGroup } from "@webgpu-kit/core/BindGroup"; 11 | import texture from "./logo.png"; 12 | 13 | export async function runExample(canvas: HTMLCanvasElement): Promise { 14 | const vertices = new Float32Array([ 15 | -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 16 | 17 | -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 18 | ]); 19 | 20 | const posAttribute = new Attribute({ 21 | label: "Position attribute", 22 | format: "float32x2", 23 | arrayBuffer: vertices, 24 | itemCount: vertices.length / 2, 25 | itemSize: 2, 26 | shaderLocation: 0, 27 | }); 28 | 29 | const vao = new VertexAttributeObject({ 30 | itemCount: vertices.length / 2, 31 | }); 32 | 33 | await vao.addAttributes(posAttribute); 34 | 35 | const pipeline = new RenderPipeline({ 36 | label: "Render pipeline", 37 | enableDepthStencil: true, 38 | canvas, 39 | shader: /* wgsl */ ` 40 | struct VertexInput { 41 | @builtin(instance_index) instanceIndex: u32, 42 | @location(0) pos: vec2, 43 | } 44 | 45 | struct VertexOutput { 46 | @builtin(position) pos: vec4, 47 | @location(0) uv: vec2, 48 | } 49 | 50 | @group(0) @binding(2) var matrix: mat4x4; 51 | 52 | @vertex 53 | fn vertexMain(input: VertexInput) -> VertexOutput { 54 | var output = VertexOutput(); 55 | 56 | var position = vec4(input.pos * 0.5, f32(input.instanceIndex) * -1.0, 1.0); 57 | position = matrix * position; 58 | 59 | output.pos = position; 60 | output.uv = (input.pos + 1.0) / 2.0; 61 | output.uv = vec2(output.uv.x, 1.0 - output.uv.y); 62 | 63 | return output; 64 | } 65 | 66 | @group(0) @binding(0) var tex: texture_2d; 67 | @group(0) @binding(1) var texSampler: sampler; 68 | 69 | @fragment 70 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 71 | let tex = textureSample(tex, texSampler, input.uv); 72 | 73 | if (tex.a < 0.5) { 74 | discard; 75 | return vec4(1.0, 1.0, 1.0, 1.0); 76 | } 77 | 78 | return tex; 79 | } 80 | `, 81 | }); 82 | 83 | pipeline.setClearColor([0.9, 0.9, 1.0, 1.0]); 84 | 85 | const pipelineGroup = new PipelineGroup({ 86 | label: "Render pipeline group", 87 | instanceCount: 5, 88 | vertexCount: vertices.length / 2, 89 | pipelines: [pipeline], 90 | }); 91 | 92 | pipelineGroup.addVertexAttributeObjects(vao); 93 | 94 | const logoTex = new Texture({ 95 | label: "Logo texture", 96 | binding: 0, 97 | visibility: GPUShaderStage.FRAGMENT, 98 | }); 99 | 100 | await logoTex.setFromImage(texture.src); 101 | await logoTex.generateMipMaps(); 102 | 103 | const sampler = new Sampler({ 104 | label: "Logo sampler", 105 | binding: 1, 106 | visibility: GPUShaderStage.FRAGMENT, 107 | }); 108 | 109 | await sampler.updateSampler({ 110 | magFilter: "linear", 111 | minFilter: "linear", 112 | }); 113 | 114 | const viewMatrix = mat4.create(); 115 | const projMatrix = mat4.create(); 116 | mat4.lookAt(viewMatrix, [1, 0, 2], [0, 0, 0], [0, 1, 0]); 117 | mat4.perspective(projMatrix, 45, canvas.width / canvas.height, 0.1, 100); 118 | 119 | mat4.multiply(viewMatrix, projMatrix, viewMatrix); 120 | 121 | const matrixUniform = new Uniform({ 122 | label: "Matrix uniform", 123 | binding: 2, 124 | visibility: GPUShaderStage.VERTEX, 125 | arrayBuffer: new Float32Array(viewMatrix.values()), 126 | }); 127 | 128 | const bindGroup = new BindGroup(); 129 | 130 | await bindGroup.addTextures(logoTex); 131 | await bindGroup.addSamplers(sampler); 132 | await bindGroup.addUniforms(matrixUniform); 133 | 134 | await pipelineGroup.setBindGroups(bindGroup); 135 | 136 | const executor = new Executor({ 137 | label: "Render executor", 138 | }); 139 | 140 | await executor.addPipelineGroups(pipelineGroup); 141 | 142 | async function tick(): Promise { 143 | await executor.run(); 144 | await new Promise(requestAnimationFrame); 145 | await tick(); 146 | } 147 | 148 | await tick(); 149 | } 150 | -------------------------------------------------------------------------------- /apps/web/examples/diffuse/index.ts: -------------------------------------------------------------------------------- 1 | import * as code from "./main?raw"; 2 | import { runExample } from "./main"; 3 | 4 | export const DiffuseLightingExample = { 5 | title: "Diffuse Lighting", 6 | url: "diffuse", 7 | code: [{ text: code.default, language: "ts", filename: "main.ts" }], 8 | description: "A simple example of diffuse lighting.", 9 | run: runExample, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/examples/diffuse/sphere.ts: -------------------------------------------------------------------------------- 1 | interface SphereGeometry { 2 | vertices: number[]; 3 | normals: number[]; 4 | uvs: number[]; 5 | indices: number[]; 6 | } 7 | 8 | export function generateSphere( 9 | radius = 1, 10 | segmentsX = 5, 11 | segmentsY = 5, 12 | ): SphereGeometry { 13 | const sphere: SphereGeometry = { 14 | vertices: [], 15 | normals: [], 16 | uvs: [], 17 | indices: [], 18 | }; 19 | 20 | const sx = Math.max(3, segmentsX); 21 | const sy = Math.max(2, segmentsY); 22 | 23 | let index = 0; 24 | const grid: number[][] = []; 25 | 26 | for (let iy = 0; iy <= sy; iy++) { 27 | const row: number[] = []; 28 | const v = iy / sy; 29 | 30 | let offset = 0; 31 | 32 | if (iy === 0) { 33 | offset = 0.5 / sx; 34 | } else if (iy === sy) { 35 | offset = -0.5 / sx; 36 | } 37 | 38 | for (let ix = 0; ix <= sx; ix++) { 39 | const u = ix / sx; 40 | 41 | const x = -radius * Math.cos(u * Math.PI * 2) * Math.sin(v * Math.PI); 42 | const y = radius * Math.cos(v * Math.PI); 43 | const z = radius * Math.sin(u * Math.PI * 2) * Math.sin(v * Math.PI); 44 | 45 | const normals = normalize([x, y, z]); 46 | 47 | sphere.vertices.push(x, y, z); 48 | sphere.normals.push(...normals); 49 | sphere.uvs.push(u + offset, 1 - v); 50 | row.push(index++); 51 | } 52 | 53 | grid.push(row); 54 | } 55 | 56 | for (let iy = 0; iy < sy; iy++) { 57 | for (let ix = 0; ix < sx; ix++) { 58 | const a = grid[iy][ix + 1]; 59 | const b = grid[iy][ix]; 60 | const c = grid[iy + 1][ix]; 61 | const d = grid[iy + 1][ix + 1]; 62 | 63 | if (iy !== 0 || sy === 2) { 64 | sphere.indices.push(a, b, d); 65 | } 66 | 67 | if (iy !== sy - 1 || sy === 2) { 68 | sphere.indices.push(b, c, d); 69 | } 70 | } 71 | } 72 | 73 | return sphere; 74 | } 75 | 76 | function normalize(v: number[]): number[] { 77 | const squares = v.map((x) => x * x); 78 | const sumOfSquares = squares.reduce((a, b) => a + b, 0); 79 | const length = Math.sqrt(sumOfSquares); 80 | const normalized = v.map((x) => x / length); 81 | 82 | return normalized; 83 | } 84 | -------------------------------------------------------------------------------- /apps/web/examples/index.ts: -------------------------------------------------------------------------------- 1 | import { ConwayExample } from "./conway"; 2 | import { DepthExample } from "./depth"; 3 | import { InstancesExample } from "./indexedInstances"; 4 | import { MultisamplingExample } from "./multisampling"; 5 | import { TexturesExample } from "./textures"; 6 | import { VertexDisplacementExample } from "./vertexDisplacement"; 7 | import { DiffuseLightingExample } from "./diffuse"; 8 | 9 | export type Code = { 10 | text: string; 11 | filename: string; 12 | language: string; 13 | }; 14 | 15 | export type Example = { 16 | title: string; 17 | url: string; 18 | code: Code[]; 19 | description: string; 20 | run: (canvas: HTMLCanvasElement) => Promise; 21 | }; 22 | 23 | export const Examples: Example[] = [ 24 | ConwayExample, 25 | VertexDisplacementExample, 26 | DiffuseLightingExample, 27 | InstancesExample, 28 | MultisamplingExample, 29 | TexturesExample, 30 | DepthExample, 31 | ]; 32 | -------------------------------------------------------------------------------- /apps/web/examples/indexedInstances/index.ts: -------------------------------------------------------------------------------- 1 | import * as code from "./main?raw"; 2 | import { runExample } from "./main"; 3 | 4 | export const InstancesExample = { 5 | title: "Instances", 6 | url: "instances", 7 | code: [{ text: code.default, language: "ts", filename: "main.ts" }], 8 | description: "A simple example of instanced rendering.", 9 | run: runExample, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/examples/multisampling/index.ts: -------------------------------------------------------------------------------- 1 | import * as code from "./main?raw"; 2 | import { runExample } from "./main"; 3 | 4 | export const MultisamplingExample = { 5 | title: "Multisampling", 6 | url: "multisampling", 7 | code: [{ text: code.default, language: "ts", filename: "main.ts" }], 8 | description: "A simple example of multisampling.", 9 | run: runExample, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/examples/multisampling/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JMBeresford/webgpu-kit/5bf1eb6b6394ca077ed7ceb9ed279eabab0aab34/apps/web/examples/multisampling/logo.png -------------------------------------------------------------------------------- /apps/web/examples/multisampling/main.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { Attribute } from "@webgpu-kit/core/Attribute"; 3 | import { Executor } from "@webgpu-kit/core/Executor"; 4 | import { RenderPipeline } from "@webgpu-kit/core/Pipeline"; 5 | import { PipelineGroup } from "@webgpu-kit/core/PipelineGroup"; 6 | import { Uniform } from "@webgpu-kit/core/Uniform"; 7 | import { VertexAttributeObject } from "@webgpu-kit/core/VertexAttributeObject"; 8 | import { Texture } from "@webgpu-kit/core/Texture"; 9 | import { Sampler } from "@webgpu-kit/core/Sampler"; 10 | import { BindGroup } from "@webgpu-kit/core/BindGroup"; 11 | import texture from "./logo.png"; 12 | 13 | export async function runExample(canvas: HTMLCanvasElement): Promise { 14 | const vertices = new Float32Array([ 15 | -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 16 | 17 | -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 18 | ]); 19 | 20 | const posAttribute = new Attribute({ 21 | label: "Position attribute", 22 | format: "float32x2", 23 | arrayBuffer: vertices, 24 | itemCount: vertices.length / 2, 25 | itemSize: 2, 26 | shaderLocation: 0, 27 | }); 28 | 29 | const vao = new VertexAttributeObject({ 30 | itemCount: vertices.length / 2, 31 | }); 32 | 33 | await vao.addAttributes(posAttribute); 34 | 35 | const pipeline = new RenderPipeline({ 36 | label: "Render pipeline", 37 | canvas, 38 | enableMultiSampling: true, 39 | enableDepthStencil: true, 40 | shader: /* wgsl */ ` 41 | struct VertexInput { 42 | @builtin(instance_index) instanceIndex: u32, 43 | @location(0) pos: vec2, 44 | } 45 | 46 | struct VertexOutput { 47 | @builtin(position) pos: vec4, 48 | @location(0) uv: vec2, 49 | } 50 | 51 | @group(0) @binding(2) var matrix: mat4x4; 52 | 53 | @vertex 54 | fn vertexMain(input: VertexInput) -> VertexOutput { 55 | var output = VertexOutput(); 56 | 57 | var position = vec4(input.pos * 0.5, f32(input.instanceIndex) * -3.0, 1.0); 58 | position = matrix * position; 59 | 60 | output.pos = position; 61 | output.uv = (input.pos + 1.0) / 2.0; 62 | output.uv = vec2(output.uv.x, 1.0 - output.uv.y); 63 | 64 | return output; 65 | } 66 | 67 | @group(0) @binding(0) var tex: texture_2d; 68 | @group(0) @binding(1) var texSampler: sampler; 69 | 70 | @fragment 71 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 72 | var color = textureSample(tex, texSampler, input.uv); 73 | 74 | if (color.a < 0.5) { 75 | return vec4(1.0, 0.8, 0.8, 1.0); 76 | } 77 | 78 | return color; 79 | } 80 | `, 81 | }); 82 | 83 | pipeline.setClearColor([0.9, 0.9, 1.0, 1.0]); 84 | 85 | const pipelineGroup = new PipelineGroup({ 86 | label: "Render pipeline group", 87 | instanceCount: 5, 88 | vertexCount: vertices.length / 2, 89 | pipelines: [pipeline], 90 | }); 91 | 92 | pipelineGroup.addVertexAttributeObjects(vao); 93 | 94 | const logoTex = new Texture({ 95 | label: "Logo texture", 96 | binding: 0, 97 | visibility: GPUShaderStage.FRAGMENT, 98 | }); 99 | 100 | await logoTex.setFromImage(texture.src); 101 | await logoTex.generateMipMaps(); 102 | 103 | const sampler = new Sampler({ 104 | label: "Logo sampler", 105 | binding: 1, 106 | visibility: GPUShaderStage.FRAGMENT, 107 | }); 108 | 109 | await sampler.updateSampler({ 110 | magFilter: "linear", 111 | minFilter: "linear", 112 | mipmapFilter: "linear", 113 | }); 114 | 115 | const viewMatrix = mat4.create(); 116 | const projMatrix = mat4.create(); 117 | mat4.lookAt(viewMatrix, [1, 0, 2], [0, 0, 0], [0, 1, 0]); 118 | mat4.perspective(projMatrix, 45, canvas.width / canvas.height, 0.1, 100); 119 | 120 | mat4.multiply(viewMatrix, projMatrix, viewMatrix); 121 | 122 | const matrixUniform = new Uniform({ 123 | label: "Matrix uniform", 124 | binding: 2, 125 | visibility: GPUShaderStage.VERTEX, 126 | arrayBuffer: new Float32Array(viewMatrix.values()), 127 | }); 128 | 129 | const bindGroup = new BindGroup(); 130 | 131 | await bindGroup.addTextures(logoTex); 132 | await bindGroup.addSamplers(sampler); 133 | await bindGroup.addUniforms(matrixUniform); 134 | 135 | await pipelineGroup.setBindGroups(bindGroup); 136 | 137 | const executor = new Executor({ 138 | label: "Render executor", 139 | }); 140 | 141 | await executor.addPipelineGroups(pipelineGroup); 142 | 143 | async function tick(): Promise { 144 | await executor.run(); 145 | await new Promise(requestAnimationFrame); 146 | await tick(); 147 | } 148 | 149 | await tick(); 150 | } 151 | -------------------------------------------------------------------------------- /apps/web/examples/textures/index.ts: -------------------------------------------------------------------------------- 1 | import * as code from "./main?raw"; 2 | import { runExample } from "./main"; 3 | 4 | export const TexturesExample = { 5 | title: "Textures", 6 | url: "textures", 7 | code: [{ text: code.default, language: "ts", filename: "main.ts" }], 8 | description: "A simple example of using textures.", 9 | run: runExample, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/examples/textures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JMBeresford/webgpu-kit/5bf1eb6b6394ca077ed7ceb9ed279eabab0aab34/apps/web/examples/textures/logo.png -------------------------------------------------------------------------------- /apps/web/examples/textures/main.ts: -------------------------------------------------------------------------------- 1 | import { Attribute } from "@webgpu-kit/core/Attribute"; 2 | import { Executor } from "@webgpu-kit/core/Executor"; 3 | import { RenderPipeline } from "@webgpu-kit/core/Pipeline"; 4 | import { PipelineGroup } from "@webgpu-kit/core/PipelineGroup"; 5 | import { Uniform } from "@webgpu-kit/core/Uniform"; 6 | import { VertexAttributeObject } from "@webgpu-kit/core/VertexAttributeObject"; 7 | import { Texture } from "@webgpu-kit/core/Texture"; 8 | import { Sampler } from "@webgpu-kit/core/Sampler"; 9 | import { BindGroup } from "@webgpu-kit/core/BindGroup"; 10 | import texture from "./logo.png"; 11 | 12 | export async function runExample(canvas: HTMLCanvasElement): Promise { 13 | const vertices = new Float32Array([ 14 | -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 15 | 16 | -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 17 | ]); 18 | 19 | const posAttribute = new Attribute({ 20 | label: "Position attribute", 21 | format: "float32x2", 22 | arrayBuffer: vertices, 23 | itemCount: vertices.length / 2, 24 | itemSize: 2, 25 | shaderLocation: 0, 26 | }); 27 | 28 | const vao = new VertexAttributeObject({ 29 | itemCount: vertices.length / 2, 30 | }); 31 | 32 | await vao.addAttributes(posAttribute); 33 | 34 | const pipeline = new RenderPipeline({ 35 | label: "Render pipeline", 36 | canvas, 37 | shader: /* wgsl */ ` 38 | struct VertexInput { 39 | @location(0) pos: vec2, 40 | } 41 | 42 | struct VertexOutput { 43 | @builtin(position) pos: vec4, 44 | @location(0) uv: vec2, 45 | } 46 | 47 | @group(0) @binding(2) var scale: f32; 48 | 49 | @vertex 50 | fn vertexMain(input: VertexInput) -> VertexOutput { 51 | var output = VertexOutput(); 52 | 53 | output.pos = vec4(input.pos * scale, 0.0, 1.0); 54 | output.pos.x = output.pos.x / ${canvas.width / canvas.height}; 55 | output.uv = (input.pos + 1.0) / 2.0; 56 | output.uv = vec2(output.uv.x, 1.0 - output.uv.y); 57 | 58 | return output; 59 | } 60 | 61 | @group(0) @binding(0) var tex: texture_2d; 62 | @group(0) @binding(1) var texSampler: sampler; 63 | 64 | @fragment 65 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 66 | let tex = textureSample(tex, texSampler, input.uv); 67 | 68 | if (tex.a < 0.5) { 69 | discard; 70 | } 71 | 72 | return tex; 73 | } 74 | `, 75 | onAfterPass: async () => { 76 | if (scale.cpuBuffer !== undefined && scale.cpuBuffer.length > 0) { 77 | scale.cpuBuffer[0] = Math.sin(performance.now() / 1000); 78 | await scale.updateGpuBuffer(); 79 | } 80 | }, 81 | }); 82 | 83 | pipeline.setClearColor([0.9, 0.9, 1.0, 1.0]); 84 | 85 | const pipelineGroup = new PipelineGroup({ 86 | label: "Render pipeline group", 87 | pipelines: [pipeline], 88 | vertexCount: vertices.length / 2, 89 | }); 90 | 91 | pipelineGroup.addVertexAttributeObjects(vao); 92 | 93 | const logoTex = new Texture({ 94 | label: "Logo texture", 95 | binding: 0, 96 | visibility: GPUShaderStage.FRAGMENT, 97 | }); 98 | 99 | await logoTex.setFromImage(texture.src); 100 | await logoTex.generateMipMaps(); 101 | 102 | const sampler = new Sampler({ 103 | label: "Logo sampler", 104 | binding: 1, 105 | visibility: GPUShaderStage.FRAGMENT, 106 | }); 107 | 108 | await sampler.updateSampler({ 109 | magFilter: "linear", 110 | minFilter: "linear", 111 | }); 112 | 113 | const scale = new Uniform({ 114 | label: "Scale", 115 | binding: 2, 116 | visibility: GPUShaderStage.VERTEX, 117 | arrayBuffer: new Float32Array([1]), 118 | }); 119 | 120 | const bindGroup = new BindGroup(); 121 | 122 | await bindGroup.addTextures(logoTex); 123 | await bindGroup.addSamplers(sampler); 124 | await bindGroup.addUniforms(scale); 125 | 126 | await pipelineGroup.setBindGroups(bindGroup); 127 | 128 | const executor = new Executor({ 129 | label: "Render executor", 130 | }); 131 | 132 | await executor.addPipelineGroups(pipelineGroup); 133 | 134 | async function tick(): Promise { 135 | await executor.run(); 136 | await new Promise(requestAnimationFrame); 137 | await tick(); 138 | } 139 | 140 | await tick(); 141 | } 142 | -------------------------------------------------------------------------------- /apps/web/examples/vertexDisplacement/index.ts: -------------------------------------------------------------------------------- 1 | import * as code1 from "./main?raw"; 2 | import code2 from "./shaders?raw"; 3 | import code3 from "./sphere?raw"; 4 | import { runExample } from "./main"; 5 | 6 | export const VertexDisplacementExample = { 7 | title: "Vertex Displacement", 8 | url: "vertexDisplacement", 9 | code: [ 10 | { text: code1.default, language: "ts", filename: "main.ts" }, 11 | { text: code2, language: "ts", filename: "shaders.ts" }, 12 | { text: code3, language: "ts", filename: "sphere.ts" }, 13 | ], 14 | description: 15 | "A simple example of vertex displacement via a compute pipeline.", 16 | run: runExample, 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/examples/vertexDisplacement/shaders.ts: -------------------------------------------------------------------------------- 1 | import { cnoise4D, snoise3Dgrad, math } from "wgsl-noise"; 2 | import { diffuseLambertian, linearToGamma } from "@webgpu-kit/shaders/"; 3 | 4 | const instanceCount1D = 24; 5 | export const instanceCount = Math.pow(instanceCount1D, 3); 6 | export const workGroupSize: [number, number, number] = [2, 2, 2]; 7 | export const workGroupCount: [number, number, number] = [ 8 | Math.ceil(instanceCount1D / workGroupSize[0]), 9 | Math.ceil(instanceCount1D / workGroupSize[1]), 10 | Math.ceil(instanceCount1D / workGroupSize[2]), 11 | ]; 12 | 13 | const workgroupThreads = workGroupSize[0] * workGroupSize[1] * workGroupSize[2]; 14 | 15 | const common = /* wgsl */ ` 16 | struct ParticleState { 17 | offset: vec3, 18 | life: f32, 19 | weight: f32, 20 | speed: f32, 21 | timeOffset: f32 22 | } 23 | struct State { 24 | state: array 25 | } 26 | 27 | @group(0) @binding(0) var matrix: mat4x4; 28 | @group(0) @binding(1) var stateIn: State; 29 | @group(0) @binding(2) var stateOut: State; 30 | @group(0) @binding(3) var time: f32; 31 | @group(0) @binding(4) var normalMatrix: mat4x4; 32 | @group(0) @binding(5) var modelMatrix: mat4x4; 33 | `; 34 | 35 | export const shader = /* wgsl */ ` 36 | struct VertexInput { 37 | @builtin(instance_index) instance: u32, 38 | @location(0) pos: vec3f, 39 | @location(1) normal: vec3f, 40 | @location(2) color: vec3f 41 | }; 42 | 43 | struct VertexOutput { 44 | @builtin(position) pos: vec4f, 45 | @location(0) color: vec3f, 46 | @location(1) normal: vec4f, 47 | @location(2) modelPos: vec3f 48 | }; 49 | 50 | ${common} 51 | 52 | const LIGHT = vec3(0.0, 10.0, 20.0); 53 | 54 | @vertex 55 | fn vertexMain(input: VertexInput) -> VertexOutput { 56 | 57 | var output: VertexOutput; 58 | let state = stateIn.state[input.instance]; 59 | let lifeScale = 1.0 - (abs(state.life - 0.5) * 2.0); 60 | let scaledPos = input.pos * lifeScale; 61 | let modelPos = modelMatrix * vec4(scaledPos + state.offset, 1.0); 62 | 63 | output.pos = matrix * modelPos; 64 | output.normal = normalMatrix * vec4f(input.normal, 1.0); 65 | output.color = input.color; 66 | output.modelPos = modelPos.xyz; 67 | 68 | return output; 69 | } 70 | 71 | ${diffuseLambertian()} 72 | ${linearToGamma()} 73 | 74 | @fragment 75 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { 76 | var color = input.color; 77 | let pos: vec3f = input.modelPos; 78 | let L = normalize(LIGHT - pos); 79 | let N = normalize(input.normal.xyz); 80 | 81 | let ambient = 0.125; 82 | let diffuseFactor = diffuseLambertian(N, L, vec3(1.0, 1.0, 1.0)) * 0.8; 83 | let str = diffuseFactor + ambient; 84 | color *= str; 85 | return vec4(linearToGamma(color), 1.0); 86 | } 87 | `; 88 | 89 | export const computeShader = /* wgsl */ ` 90 | ${math} 91 | ${cnoise4D} 92 | ${snoise3Dgrad} 93 | ${common} 94 | 95 | struct ComputeInput { 96 | @builtin(workgroup_id) workgroup_id : vec3, 97 | @builtin(local_invocation_id) local_invocation_id : vec3, 98 | @builtin(global_invocation_id) global_invocation_id : vec3, 99 | @builtin(local_invocation_index) local_invocation_index: u32, 100 | @builtin(num_workgroups) num_workgroups: vec3 101 | } 102 | 103 | 104 | @compute @workgroup_size(${workGroupSize.join(",")}) 105 | fn computeMain(input: ComputeInput) { 106 | let workgroup_idx = input.workgroup_id.x + 107 | input.workgroup_id.y * input.num_workgroups.x + 108 | input.workgroup_id.z * input.num_workgroups.x * input.num_workgroups.y; 109 | 110 | let idx = 111 | workgroup_idx * ${workgroupThreads} + input.local_invocation_index; 112 | 113 | let state = stateIn.state[idx]; 114 | let curOffset = state.offset; 115 | let life = state.life - 0.001 * state.speed; 116 | 117 | let radius = length(curOffset); 118 | let noiseScale = 0.35; 119 | 120 | 121 | let noiseX = snoise3Dgrad(curOffset * noiseScale); 122 | let noiseY = snoise3Dgrad(curOffset * noiseScale + vec3(10000.0, 0.0, 0.0)); 123 | let noiseZ = snoise3Dgrad(curOffset * noiseScale + vec3(0.0, 10000.0, 0.0)); 124 | 125 | let newOffset = curOffset + vec3(noiseX, noiseY, noiseZ) * 0.02 * state.weight; 126 | stateOut.state[idx].offset = newOffset; 127 | stateOut.state[idx].life = life; 128 | 129 | if (life < 0.0) { 130 | let newLifeNoiseScale = 1000.0; 131 | let t = state.timeOffset + time * state.speed * 0.1; 132 | stateOut.state[idx].offset = normalize(vec3f( 133 | cnoise4D(vec4f(newOffset * newLifeNoiseScale, t)), 134 | cnoise4D(vec4f(newOffset * newLifeNoiseScale + vec3(10000.0, 0.0, 0.0), t)), 135 | cnoise4D(vec4f(newOffset * newLifeNoiseScale + vec3(0.0, 10000.0, 0.0), t)) 136 | )); 137 | stateOut.state[idx].life = 1.0; 138 | } 139 | } 140 | `; 141 | -------------------------------------------------------------------------------- /apps/web/examples/vertexDisplacement/sphere.ts: -------------------------------------------------------------------------------- 1 | interface SphereGeometry { 2 | vertices: number[]; 3 | normals: number[]; 4 | uvs: number[]; 5 | indices: number[]; 6 | } 7 | 8 | export function generateSphere( 9 | radius = 1, 10 | segmentsX = 5, 11 | segmentsY = 5, 12 | ): SphereGeometry { 13 | const sphere: SphereGeometry = { 14 | vertices: [], 15 | normals: [], 16 | uvs: [], 17 | indices: [], 18 | }; 19 | 20 | const sx = Math.max(3, segmentsX); 21 | const sy = Math.max(2, segmentsY); 22 | 23 | let index = 0; 24 | const grid: number[][] = []; 25 | 26 | for (let iy = 0; iy <= sy; iy++) { 27 | const row: number[] = []; 28 | const v = iy / sy; 29 | 30 | let offset = 0; 31 | 32 | if (iy === 0) { 33 | offset = 0.5 / sx; 34 | } else if (iy === sy) { 35 | offset = -0.5 / sx; 36 | } 37 | 38 | for (let ix = 0; ix <= sx; ix++) { 39 | const u = ix / sx; 40 | 41 | const x = -radius * Math.cos(u * Math.PI * 2) * Math.sin(v * Math.PI); 42 | const y = radius * Math.cos(v * Math.PI); 43 | const z = radius * Math.sin(u * Math.PI * 2) * Math.sin(v * Math.PI); 44 | 45 | const normals = normalize([x, y, z]); 46 | 47 | sphere.vertices.push(x, y, z); 48 | sphere.normals.push(...normals); 49 | sphere.uvs.push(u + offset, 1 - v); 50 | row.push(index++); 51 | } 52 | 53 | grid.push(row); 54 | } 55 | 56 | for (let iy = 0; iy < sy; iy++) { 57 | for (let ix = 0; ix < sx; ix++) { 58 | const a = grid[iy][ix + 1]; 59 | const b = grid[iy][ix]; 60 | const c = grid[iy + 1][ix]; 61 | const d = grid[iy + 1][ix + 1]; 62 | 63 | if (iy !== 0 || sy === 2) { 64 | sphere.indices.push(a, b, d); 65 | } 66 | 67 | if (iy !== sy - 1 || sy === 2) { 68 | sphere.indices.push(b, c, d); 69 | } 70 | } 71 | } 72 | 73 | return sphere; 74 | } 75 | 76 | function normalize(v: number[]): number[] { 77 | const squares = v.map((x) => x * x); 78 | const sumOfSquares = squares.reduce((a, b) => a + b, 0); 79 | const length = Math.sqrt(sumOfSquares); 80 | const normalized = v.map((x) => x / length); 81 | 82 | return normalized; 83 | } 84 | -------------------------------------------------------------------------------- /apps/web/hooks/use-page-scroll.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function usePageScroll(opts?: { 5 | threshold?: number; 6 | relativeToViewport?: boolean; 7 | }): boolean { 8 | const [scrolled, setScrolled] = useState(false); 9 | 10 | useEffect(() => { 11 | const listener: () => void = () => { 12 | const threshold = opts?.threshold ?? 20; 13 | let scroll = window.scrollY; 14 | 15 | if (opts?.relativeToViewport) { 16 | scroll += window.innerHeight; 17 | } 18 | 19 | if (scroll >= threshold) { 20 | setScrolled(true); 21 | } else { 22 | setScrolled(false); 23 | } 24 | }; 25 | 26 | listener(); 27 | window.addEventListener("scroll", listener); 28 | window.addEventListener("resize", listener); 29 | 30 | return () => { 31 | window.removeEventListener("scroll", listener); 32 | window.removeEventListener("resize", listener); 33 | }; 34 | }, [opts]); 35 | 36 | return scrolled; 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: "export", 3 | basePath: process.env.NODE_ENV === "production" ? "/webgpu-kit" : "", 4 | transpilePackages: ["ui", "@webgpu-kit/core"], 5 | 6 | reactStrictMode: false, 7 | experimental: { 8 | // serverActions: true, 9 | // serverComponentsExternalPackages: ["typedoc"], 10 | }, 11 | 12 | webpack: (config) => { 13 | config.module.rules = [ 14 | { 15 | resourceQuery: /raw/, 16 | type: "asset/source", 17 | }, 18 | { 19 | test: /\.(glsl|vs|fs|vert|frag)$/, 20 | exclude: /node_modules/, 21 | use: ["raw-loader", "glslify-loader"], 22 | }, 23 | ...config.module.rules, 24 | ]; 25 | 26 | return config; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.1.8", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@react-three/drei": "^9.90.1", 13 | "@react-three/fiber": "^8.15.12", 14 | "@types/three": "^0.159.0", 15 | "@webgpu-kit/core": "workspace:*", 16 | "@webgpu-kit/shaders": "workspace:*", 17 | "bright": "^0.8.4", 18 | "geist": "^1.2.0", 19 | "gl-matrix": "^3.4.3", 20 | "glsl-noise": "^0.0.0", 21 | "glslify-loader": "^2.0.0", 22 | "next": "^13.4.19", 23 | "prettier": "^3.2.5", 24 | "raw-loader": "^4.0.2", 25 | "react": "^18.2.0", 26 | "react-code-blocks": "^0.1.5", 27 | "react-dom": "^18.2.0", 28 | "react-icons": "^4.12.0", 29 | "react-markdown": "^9.0.1", 30 | "react-syntax-highlighter": "^15.5.0", 31 | "sass": "^1.69.5", 32 | "stats.js": "^0.17.0", 33 | "three": "^0.159.0", 34 | "ui": "workspace:*", 35 | "wgsl-noise": "^1.0.1" 36 | }, 37 | "devDependencies": { 38 | "@next/eslint-plugin-next": "^13.4.19", 39 | "@types/node": "^17.0.12", 40 | "@types/react": "^18.2.57", 41 | "@types/react-dom": "^18.2.19", 42 | "@types/react-syntax-highlighter": "^15.5.11", 43 | "autoprefixer": "^10.4.17", 44 | "docs": "workspace:*", 45 | "eslint": "^8.48.0", 46 | "eslint-config-custom": "workspace:*", 47 | "postcss": "^8.4.35", 48 | "rehype-slug": "^6.0.0", 49 | "remark-gfm": "^4.0.0", 50 | "tailwindcss": "^3.4.1", 51 | "tsconfig": "workspace:*", 52 | "typedoc": "^0.25.2", 53 | "typescript": "^5.2.2" 54 | }, 55 | "publishConfig": { 56 | "access": "restricted" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/scene/grid/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import type { BufferGeometry, Mesh } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import type { GridMaterialProps } from "./shader"; 5 | import { GridMaterial, GridMaterialKey } from "./shader"; 6 | 7 | export function Grid(): JSX.Element { 8 | const ref = useRef>(null); 9 | 10 | useFrame(({ clock }, dt) => { 11 | if (!ref.current) return; 12 | ref.current.material.uTime = clock.elapsedTime; 13 | 14 | if (ref.current.material.uFade === undefined) { 15 | ref.current.material.uFade = 0; 16 | } else { 17 | const fade = ref.current.material.uFade + dt * 0.5; 18 | if (ref.current.material.uFade < 1) ref.current.material.uFade = fade; 19 | } 20 | }); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/scene/grid/shader/frag.glsl: -------------------------------------------------------------------------------- 1 | #define EDGE_WIDTH 0.03 2 | 3 | uniform float uTime; 4 | uniform float uFade; 5 | uniform vec3 uColor; 6 | varying vec2 vUv; 7 | 8 | #pragma glslify: cnoise3 = require('glsl-noise/classic/3d'); 9 | #pragma glslify: snoise3 = require('glsl-noise/simplex/3d'); 10 | 11 | float random(float x) { 12 | return fract(sin(x) * 43758.5453123); 13 | } 14 | 15 | struct Grid { 16 | vec2 st; 17 | vec2 id; 18 | }; 19 | 20 | Grid getGrid(vec2 st, float scale) { 21 | vec2 gridSt = st / scale; 22 | vec2 coords = fract(gridSt) - 0.5; 23 | 24 | float gridX = smoothstep(0.5 - EDGE_WIDTH, 0.5 - EDGE_WIDTH / 2.0, coords.x); 25 | float gridY = smoothstep(0.5 - EDGE_WIDTH, 0.5 - EDGE_WIDTH / 2.0, coords.y); 26 | 27 | Grid grid = Grid(vec2(gridX, gridY), floor(gridSt)); 28 | 29 | return grid; 30 | } 31 | 32 | void main() { 33 | float t = (uTime + 1000.0) * 0.25; 34 | 35 | vec2 st = gl_FragCoord.xy; 36 | st += t * 100.0; 37 | Grid grid = getGrid(st, 50.0); 38 | 39 | float noise1 = cnoise3(vec3(grid.id * 0.2, t)); 40 | float noise2 = snoise3(vec3((grid.id + noise1 * 10.0) * 0.1, t)); 41 | float noise = noise1 + noise2; 42 | 43 | float outline = max(grid.st.x, grid.st.y); 44 | vec3 outlineColor = uColor * outline; 45 | vec3 cellColor = uColor * noise * 2.5; 46 | 47 | // accidentally stumbled upon a forbidden spell with this one 48 | vec3 color = mix(cellColor, outlineColor, step(0.001, outline)); 49 | color *= 0.1; 50 | 51 | // vignette 52 | vec2 correctedUv = vUv; 53 | float vignette = distance(vec2(0.5), correctedUv); 54 | vignette = smoothstep(0.75, 0.3, vignette); 55 | color *= vignette; 56 | 57 | gl_FragColor = vec4(color, color) * uFade; 58 | 59 | // #include 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/scene/grid/shader/index.tsx: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from "@react-three/drei"; 2 | import type { MaterialNode } from "@react-three/fiber"; 3 | import { extend } from "@react-three/fiber"; 4 | import type { ShaderMaterial } from "three"; 5 | import { AdditiveBlending, Color } from "three"; 6 | import fragmentShader from "./frag.glsl"; 7 | import vertexShader from "./vert.glsl"; 8 | import { generateUUID } from "three/src/math/MathUtils.js"; 9 | 10 | type Uniforms = { 11 | uTime?: number; 12 | uFade?: number; 13 | uColor?: Color | number; 14 | }; 15 | 16 | const uniforms: Uniforms = { 17 | uTime: 0, 18 | uFade: 0, 19 | uColor: new Color(1, 1, 1), 20 | }; 21 | 22 | const BaseGridMaterial = shaderMaterial( 23 | uniforms, 24 | vertexShader, 25 | fragmentShader, 26 | (m) => { 27 | if (!m) return; 28 | m.transparent = true; 29 | m.premultipliedAlpha = true; 30 | m.blending = AdditiveBlending; 31 | m.toneMapped = true; 32 | m.extensions = { 33 | ...m.extensions, 34 | derivatives: true, 35 | }; 36 | }, 37 | ); 38 | 39 | export const GridMaterialKey = generateUUID(); 40 | BaseGridMaterial.key = GridMaterialKey; 41 | 42 | extend({ BaseGridMaterial }); 43 | 44 | export type GridMaterialProps = Uniforms & ShaderMaterial; 45 | 46 | declare module "@react-three/fiber" { 47 | interface ThreeElements { 48 | baseGridMaterial: MaterialNode; 49 | } 50 | } 51 | 52 | export function GridMaterial(props: Uniforms): JSX.Element { 53 | return ; 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/scene/grid/shader/vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | gl_Position = vec4(position * 2.0, 1.0); 5 | 6 | vUv = uv; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/scene/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Canvas } from "@react-three/fiber"; 4 | import { Grid } from "./grid"; 5 | 6 | export function Scene( 7 | props: Omit, 8 | ): JSX.Element { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": [ 12 | "./*" 13 | ], 14 | "@webgpu-kit/core/*": [ 15 | "../../packages/core/src/*" 16 | ], 17 | "@webgpu-kit/shaders/*": [ 18 | "../../packages/shaders/src/*" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "next.config.js", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | ".next/types/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ], 32 | "references": [ 33 | { 34 | "path": "../docs/" 35 | }, 36 | { 37 | "path": "../../packages/core/" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "extends": ["//"], 4 | "pipeline": { 5 | "dev": { 6 | "dependsOn": ["docs#build"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgpu-kit", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "turbo run build", 7 | "dev": "turbo run dev", 8 | "lint": "turbo run lint", 9 | "docs": "turbo run docs", 10 | "test": "turbo run test", 11 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 12 | "clean": "git clean -fXd -e \\!node_modules -e \\!**/node_modules", 13 | "clean:deps": "git clean -fXd", 14 | "publish-packages": "turbo run build lint test && changeset version && changeset publish" 15 | }, 16 | "dependencies": { 17 | "@webgpu/types": "0.1.34" 18 | }, 19 | "devDependencies": { 20 | "@changesets/changelog-github": "^0.4.8", 21 | "@changesets/cli": "^2.26.2", 22 | "eslint": "^8.48.0", 23 | "prettier": "^3.0.3", 24 | "tsconfig": "workspace:*", 25 | "eslint-config-custom": "workspace:*", 26 | "turbo": "latest", 27 | "typedoc": "npm:@jberesford/typedoc@0.25.10" 28 | }, 29 | "pnpm": { 30 | "overrides": { 31 | "typedoc": "$typedoc", 32 | "next": "^13.4.19", 33 | "@next/eslint-plugin-next": "^13.4.19", 34 | "eslint-plugin-turbo": "^1.12.3", 35 | "@webgpu/types": "$@webgpu/types" 36 | } 37 | }, 38 | "packageManager": "pnpm@8.6.10" 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/library"], 3 | rules: { 4 | "no-await-in-loop": "off", 5 | }, 6 | root: true, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/core/lib/MockWebGPU.ts: -------------------------------------------------------------------------------- 1 | export const MockDevice = { 2 | createBuffer: () => ({}) as unknown as GPUBuffer, 3 | queue: { 4 | writeBuffer: () => {}, 5 | }, 6 | } as unknown as GPUDevice; 7 | 8 | export const MockCanvas = { 9 | getContext: () => ({}) as unknown as GPUCanvasContext, 10 | } as unknown as HTMLCanvasElement; 11 | 12 | export const MockContext = { 13 | configure: () => {}, 14 | getSwapChainPreferredFormat: () => "rgba8unorm", 15 | } as unknown as GPUCanvasContext; 16 | 17 | export const MockCanvasFormat: GPUTextureFormat = "rgba8unorm"; 18 | 19 | export const MockAdapter = {} as unknown as GPUAdapter; 20 | 21 | export const MockWebGPU: Record = { 22 | navigator: { 23 | gpu: { 24 | getPreferredCanvasFormat: () => "rgba8unorm", 25 | }, 26 | }, 27 | // note that these values are not necessarily correct, 28 | // they came from github copilot and that thing is never 29 | // wrong 30 | 31 | GPUBufferUsage: { 32 | MAP_READ: 1, 33 | MAP_WRITE: 2, 34 | COPY_SRC: 4, 35 | COPY_DST: 8, 36 | INDEX: 16, 37 | VERTEX: 32, 38 | UNIFORM: 64, 39 | STORAGE: 128, 40 | INDIRECT: 256, 41 | QUERY_RESOLVE: 512, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webgpu-kit/core", 3 | "version": "1.1.1", 4 | "type": "module", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/webgpu-kit_core.umd.cjs", 9 | "module": "dist/webgpu-kit_core.js", 10 | "types": "dist/webgpu-kit_core.d.ts", 11 | "scripts": { 12 | "build": "tsc && vite build", 13 | "preview": "vite preview", 14 | "lint": "eslint --ext .ts ./src", 15 | "test": "vitest run" 16 | }, 17 | "dependencies": { 18 | "@webgpu/types": "0.1.34" 19 | }, 20 | "devDependencies": { 21 | "@types/uuid": "^9.0.3", 22 | "eslint-config-custom": "workspace:*", 23 | "jsdom": "^22.1.0", 24 | "tsconfig": "workspace:*", 25 | "typescript": "^5.0.2", 26 | "uuid": "^9.0.0", 27 | "vite": "^4.5.0", 28 | "vite-plugin-dts": "3.5.3", 29 | "vitest": "^0.34.6" 30 | }, 31 | "author": { 32 | "name": "John Beresford", 33 | "email": "jberesford@volcaus.com" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/Attribute.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./components/Canvas"; 2 | import { WithCpuBuffer } from "./components/CpuBuffer"; 3 | import { WithDevice } from "./components/Device"; 4 | import { WithLabel } from "./components/Label"; 5 | import type { ArrayType } from "./utils"; 6 | 7 | const components = WithDevice(WithCanvas(WithCpuBuffer(WithLabel()))); 8 | 9 | /** 10 | * {@link Attribute} constructor parameters 11 | */ 12 | export type AttributeOptions = { 13 | label?: string; 14 | format: GPUVertexFormat; 15 | 16 | /** 17 | * The value of the location directive of the attribute in the shader 18 | * e.g. 19 | * ```wgsl 20 | * @location(0) pos: vec3 21 | * ``` 22 | * would have a shaderLocation of 0. 23 | */ 24 | shaderLocation: number; 25 | 26 | /** 27 | * The number of components in each item of the attribute 28 | * e.g. 29 | * ```wgsl 30 | * @location(0) pos: vec3 31 | * ``` 32 | * would have an itemSize of 3. 33 | */ 34 | itemSize: number; 35 | 36 | /** 37 | * The number of items in the attribute. For example, given a vertex 38 | * attribute this would the number of vertices stored in the buffer. 39 | * 40 | * 41 | * e.g. 42 | * ```ts 43 | * const triangleVertices = new Float32Array([ 44 | * -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 45 | * ]); 46 | * 47 | * const posAttribute = new Attribute({ 48 | * // other stuff 49 | * format: "float32x2", 50 | * itemCount: triangleVertices.length / 2, 51 | * }); 52 | * ``` 53 | * would have an itemCount of 3, because three 2D vertices are contained 54 | * in the buffer. 55 | */ 56 | itemCount: number; 57 | arrayBuffer: ArrayType; 58 | }; 59 | 60 | /** 61 | * An attribute object that contains vertex data 62 | */ 63 | export class Attribute extends components { 64 | /** The format that the data is stored in */ 65 | readonly format: GPUVertexFormat; 66 | readonly shaderLocation: number; 67 | readonly itemSize: number; 68 | readonly itemCount: number; 69 | readonly cpuBuffer: ArrayType; 70 | 71 | constructor(options: AttributeOptions) { 72 | super(); 73 | this.label = options.label; 74 | this.format = options.format; 75 | this.shaderLocation = options.shaderLocation; 76 | this.itemSize = options.itemSize; 77 | this.itemCount = options.itemCount; 78 | this.cpuBuffer = options.arrayBuffer; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/IndexBuffer.ts: -------------------------------------------------------------------------------- 1 | import { WithGpuBuffer } from "./components/GpuBufferObject"; 2 | import { WithDevice } from "./components/Device"; 3 | import { WithLabel } from "./components/Label"; 4 | import { WithCpuBuffer } from "./components/CpuBuffer"; 5 | import { WithCanvas } from "./components/Canvas"; 6 | 7 | type IndexArray = Uint16Array | Uint32Array; 8 | const components = WithCpuBuffer( 9 | WithGpuBuffer(WithDevice(WithCanvas(WithLabel()))), 10 | ); 11 | 12 | /** 13 | * {@link IndexBuffer} constructor parameters 14 | */ 15 | export type IndexBufferOptions = { 16 | label?: string; 17 | arrayBuffer: IndexArray; 18 | 19 | /** 20 | * The number of indices to draw. If not set, the length of the array buffer 21 | * will be used. 22 | */ 23 | indexCount?: number; 24 | firstIndex?: number; 25 | baseIndex?: number; 26 | }; 27 | 28 | /** 29 | * An index buffer that is used in a {@link VertexAttributeObject} to 30 | * direct indexed drawing operations. 31 | */ 32 | export class IndexBuffer extends components { 33 | declare cpuBuffer: IndexArray; 34 | indexCount?: number; 35 | firstIndex?: number; 36 | baseIndex?: number; 37 | 38 | constructor(options: IndexBufferOptions) { 39 | super(); 40 | this.label = options.label; 41 | this.cpuBuffer = options.arrayBuffer; 42 | this.indexCount = options.indexCount; 43 | this.firstIndex = options.firstIndex; 44 | this.baseIndex = options.baseIndex; 45 | } 46 | 47 | async setCpuBuffer(indexBuffer: IndexArray) { 48 | this.cpuBuffer = indexBuffer; 49 | await this.updateGpuBuffer(); 50 | } 51 | 52 | setIndexCount(indexCount: number) { 53 | this.indexCount = indexCount; 54 | } 55 | 56 | setFirstIndex(firstIndex: number) { 57 | this.firstIndex = firstIndex; 58 | } 59 | 60 | setBaseIndex(baseIndex: number) { 61 | this.baseIndex = baseIndex; 62 | } 63 | 64 | async updateGpuBuffer() { 65 | const device = await this.getDevice(); 66 | const sizeMismatch = this.cpuBuffer.byteLength !== this.gpuBuffer?.size; 67 | 68 | if (this.gpuBuffer === undefined || sizeMismatch) { 69 | this.gpuBuffer = device.createBuffer({ 70 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, 71 | label: `${this.label ?? "Unlabelled"} Index Buffer`, 72 | size: this.cpuBuffer.byteLength, 73 | }); 74 | } 75 | 76 | device.queue.writeBuffer(this.gpuBuffer, 0, this.cpuBuffer); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/PipelineDescriptor.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./components/Canvas"; 2 | import { WithColorTarget } from "./components/ColorTarget"; 3 | import { WithDepthStencil } from "./components/DepthStencil"; 4 | import { WithDevice } from "./components/Device"; 5 | import { WithLabel } from "./components/Label"; 6 | import { WithMultiSampling } from "./components/MultiSampling"; 7 | import { WithPrimitive } from "./components/Primitive"; 8 | import { WithShader } from "./components/Shader"; 9 | 10 | const components = WithShader(WithDevice(WithLabel())); 11 | 12 | type PipelineDescriptorOptions = { 13 | shader: string; 14 | }; 15 | 16 | /** 17 | * {@link RenderPipelineDescriptor} constructor parameters 18 | */ 19 | export type RenderPipelineDescriptorOptions = { 20 | multisample?: boolean; 21 | depthStencil?: boolean; 22 | canvas?: HTMLCanvasElement; 23 | } & PipelineDescriptorOptions; 24 | 25 | /** 26 | * {@link ComputePipelineDescriptor} constructor parameters 27 | */ 28 | export type ComputePipelineDescriptorOptions = PipelineDescriptorOptions; 29 | 30 | const renderComponents = WithDepthStencil( 31 | WithMultiSampling(WithColorTarget(WithPrimitive(WithCanvas(components)))), 32 | ); 33 | 34 | /** 35 | * A GPU render pipeline descriptor that is used in a {@link RenderPipeline}. 36 | */ 37 | export class RenderPipelineDescriptor extends renderComponents { 38 | descriptor?: GPURenderPipelineDescriptor; 39 | multiSampleEnabled: boolean; 40 | depthStencilEnabled: boolean; 41 | 42 | constructor(opts: RenderPipelineDescriptorOptions) { 43 | super(opts); 44 | 45 | this.multiSampleEnabled = opts.multisample ?? false; 46 | this.depthStencilEnabled = opts.depthStencil ?? false; 47 | 48 | this.setShader(opts.shader); 49 | if (opts.canvas) { 50 | this.setCanvas(opts.canvas); 51 | } 52 | } 53 | 54 | async build( 55 | layout: GPUPipelineLayout, 56 | buffers: GPUVertexBufferLayout[], 57 | ): Promise { 58 | await this.buildShaderModule(); 59 | 60 | if (this.multiSampleEnabled) { 61 | this.setMultiSampleCount(4); 62 | } else { 63 | this.setMultiSampleCount(1); 64 | } 65 | 66 | await this.buildMultiSampleTexture(); 67 | await this.buildDepthStencilTexture(); 68 | 69 | if (!this.shaderModule) { 70 | throw new Error("No shader module"); 71 | } 72 | 73 | this.descriptor = { 74 | label: this.label ?? "Unlabelled", 75 | layout, 76 | multisample: this.multiSampleState, 77 | depthStencil: this.depthStencilEnabled 78 | ? this.depthStencilState 79 | : undefined, 80 | primitive: this.primitiveState, 81 | vertex: { 82 | module: this.shaderModule, 83 | entryPoint: this.shaderEntries.vertex, 84 | buffers, 85 | }, 86 | fragment: { 87 | module: this.shaderModule, 88 | entryPoint: this.shaderEntries.fragment, 89 | targets: [this.colorTarget], 90 | }, 91 | }; 92 | } 93 | } 94 | 95 | /** 96 | * A GPU compute pipeline descriptor that is used in a {@link ComputePipeline}. 97 | */ 98 | export class ComputePipelineDescriptor extends components { 99 | declare descriptor?: GPUComputePipelineDescriptor; 100 | 101 | constructor(opts: ComputePipelineDescriptorOptions) { 102 | super(); 103 | this.setShader(opts.shader); 104 | } 105 | 106 | async build(layout: GPUPipelineLayout): Promise { 107 | await this.buildShaderModule(); 108 | 109 | if (!this.shaderModule) { 110 | throw new Error("No shader module"); 111 | } 112 | 113 | this.descriptor = { 114 | label: this.label ?? "Unlabelled", 115 | layout, 116 | compute: { 117 | module: this.shaderModule, 118 | entryPoint: this.shaderEntries.compute, 119 | }, 120 | }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/core/src/PipelineGroup.test.ts: -------------------------------------------------------------------------------- 1 | import { it, describe, expect, vi } from "vitest"; 2 | import { 3 | MockWebGPU, 4 | MockDevice, 5 | MockCanvas, 6 | MockContext, 7 | MockCanvasFormat, 8 | } from "../lib/MockWebGPU"; 9 | import { PipelineGroup } from "./PipelineGroup"; 10 | 11 | vi.mock("./utils", async () => { 12 | const actual = await vi.importActual("./utils"); 13 | for (const key in MockWebGPU) { 14 | vi.stubGlobal( 15 | key, 16 | vi.fn(() => MockWebGPU[key]), 17 | ); 18 | } 19 | 20 | return { 21 | ...(actual as Record), 22 | getDefaultDevice: () => MockDevice, 23 | getDefaultCanvas: () => MockCanvas, 24 | getDefaultContext: () => MockContext, 25 | getDefaultCanvasFormat: () => MockCanvasFormat, 26 | }; 27 | }); 28 | 29 | describe("PipelineGroup", () => { 30 | it("Should have a label", () => { 31 | const group = new PipelineGroup({ label: "Test", vertexCount: 0 }); 32 | expect(group.label).toBe("Test"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/core/src/PipelineGroup.ts: -------------------------------------------------------------------------------- 1 | import type { BindGroup } from "./BindGroup"; 2 | import type { IndexBuffer } from "./IndexBuffer"; 3 | import { RenderPipeline } from "./Pipeline"; 4 | import type { Pipeline } from "./Pipeline"; 5 | import type { VertexAttributeObject } from "./VertexAttributeObject"; 6 | import { WithDevice } from "./components/Device"; 7 | import { WithLabel } from "./components/Label"; 8 | 9 | const components = WithDevice(WithLabel()); 10 | 11 | /** 12 | * {@link PipelineGroup} constructor parameters 13 | */ 14 | export type PipelineGroupOptions = { 15 | label?: string; 16 | pipelines?: Pipeline[]; 17 | instanceCount?: number; 18 | 19 | /** 20 | * The number of vertices to draw in a render pipeline. 21 | */ 22 | vertexCount: number; 23 | }; 24 | 25 | /** 26 | * A group of {@link Pipeline}s that share the same {@link VertexAttributeObject}s 27 | * and {@link BindGroup}s. 28 | */ 29 | export class PipelineGroup extends components { 30 | private _pipelineLayout?: GPUPipelineLayout; 31 | private _bindGroups: BindGroup[] = []; 32 | 33 | vertexAttributeObjects: VertexAttributeObject[] = []; 34 | pipelines: Pipeline[]; 35 | instanceCount: number; 36 | indexBuffer?: IndexBuffer; 37 | vertexCount: number; 38 | 39 | constructor(options: PipelineGroupOptions) { 40 | super(); 41 | this.label = options.label; 42 | this.pipelines = options.pipelines ?? []; 43 | 44 | this.instanceCount = options.instanceCount ?? 1; 45 | this.vertexCount = options.vertexCount; 46 | } 47 | 48 | get bindGroups() { 49 | return this._bindGroups; 50 | } 51 | 52 | async setBindGroups(...bindGroups: BindGroup[]): Promise { 53 | await Promise.all( 54 | bindGroups.map(async (bindGroup) => { 55 | if (bindGroup.group === undefined) { 56 | await bindGroup.updateBindGroup(); 57 | } 58 | }), 59 | ); 60 | 61 | this._bindGroups = bindGroups; 62 | await this.updatePipelineLayout(); 63 | } 64 | 65 | addVertexAttributeObjects( 66 | ...vertexAttributeObjects: VertexAttributeObject[] 67 | ): void { 68 | this.vertexAttributeObjects.push(...vertexAttributeObjects); 69 | } 70 | 71 | setInstanceCount(count: number) { 72 | this.instanceCount = count; 73 | } 74 | 75 | async setIndexBuffer(indexBuffer: IndexBuffer) { 76 | this.indexBuffer = indexBuffer; 77 | await this.indexBuffer.updateGpuBuffer(); 78 | } 79 | 80 | async build() { 81 | await this.buildPipelines(); 82 | } 83 | 84 | private async updatePipelineLayout() { 85 | const device = await this.getDevice(); 86 | const layouts = this.bindGroups 87 | .sort((a, b) => a.index - b.index) 88 | .map((bg) => bg.layout); 89 | 90 | if (layouts.some((layout) => layout === undefined)) { 91 | throw new Error("Bind group layout not set"); 92 | } 93 | 94 | const bindGroupLayouts = layouts as GPUBindGroupLayout[]; 95 | 96 | this._pipelineLayout = device.createPipelineLayout({ 97 | label: `${this.label ?? "Unlabelled"} Pipeline Layout`, 98 | bindGroupLayouts, 99 | }); 100 | } 101 | 102 | private async buildPipelines() { 103 | await Promise.all( 104 | this.pipelines.map(async (pipeline) => { 105 | if (this._pipelineLayout === undefined) { 106 | throw new Error("Pipeline layout not built"); 107 | } 108 | 109 | if (pipeline instanceof RenderPipeline) { 110 | const buffers: GPUVertexBufferLayout[] = []; 111 | this.vertexAttributeObjects.forEach((vao) => { 112 | if (vao.layout === undefined) { 113 | throw new Error("Vertex attribute layout not set"); 114 | } 115 | buffers.push(vao.layout); 116 | }); 117 | 118 | await pipeline.build(this._pipelineLayout, buffers); 119 | } else { 120 | await pipeline.build(this._pipelineLayout); 121 | } 122 | }), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/core/src/Sampler.ts: -------------------------------------------------------------------------------- 1 | import { WithDevice } from "./components/Device"; 2 | import { WithLabel } from "./components/Label"; 3 | import { WithGpuSampler } from "./components/GpuSamplerObject"; 4 | import { WithCanvas } from "./components/Canvas"; 5 | 6 | const components = WithGpuSampler(WithDevice(WithCanvas(WithLabel()))); 7 | 8 | /** 9 | * {@link Sampler} constructor parameters 10 | */ 11 | export type SamplerOptions = { 12 | label?: string; 13 | binding: number; 14 | 15 | /** 16 | * The shader stages that this uniform is visible to. 17 | * e.g. GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT would make the uniform 18 | * visible to both the vertex and fragment shaders. 19 | */ 20 | visibility: GPUShaderStageFlags; 21 | options?: GPUSamplerDescriptor; 22 | }; 23 | 24 | /** 25 | * Sampler object used to sample textures in a shader. 26 | */ 27 | export class Sampler extends components { 28 | readonly binding: number; 29 | readonly visibility: GPUShaderStageFlags; 30 | 31 | constructor(options: SamplerOptions) { 32 | super(); 33 | this.label = options.label; 34 | if (options.options !== undefined) { 35 | void this.updateSampler(options.options); 36 | } 37 | 38 | this.binding = options.binding; 39 | this.visibility = options.visibility; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/Storage.ts: -------------------------------------------------------------------------------- 1 | import { WithGpuBuffer } from "./components/GpuBufferObject"; 2 | import { WithDevice } from "./components/Device"; 3 | import { WithLabel } from "./components/Label"; 4 | import { WithCpuBuffer } from "./components/CpuBuffer"; 5 | import { type ArrayType } from "./utils"; 6 | import { WithCanvas } from "./components/Canvas"; 7 | 8 | /** 9 | * {@link Storage} constructor parameters 10 | */ 11 | export type StorageOptions = { 12 | label?: string; 13 | 14 | /** 15 | * The binding number of the storage object in the {@link BindGroup}. 16 | */ 17 | binding: number; 18 | 19 | /** 20 | * The shader stages that this uniform is visible to. 21 | * e.g. GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT would make the uniform 22 | * visible to both the vertex and fragment shaders. 23 | */ 24 | visibility: GPUShaderStageFlags; 25 | readOnly?: boolean; 26 | arrayBuffer: ArrayType; 27 | }; 28 | 29 | const components = WithGpuBuffer( 30 | WithCpuBuffer(WithDevice(WithCanvas(WithLabel()))), 31 | ); 32 | 33 | /** 34 | * A GPU storage object that can be used in a {@link BindGroup}. 35 | */ 36 | export class Storage extends components { 37 | binding: number; 38 | readonly visibility: GPUShaderStageFlags; 39 | readonly bufferOptions: { 40 | type: "storage" | "read-only-storage"; 41 | }; 42 | 43 | constructor(options: StorageOptions) { 44 | super(); 45 | this.label = options.label; 46 | this.binding = options.binding; 47 | this.visibility = options.visibility; 48 | this.cpuBuffer = options.arrayBuffer; 49 | 50 | this.bufferOptions = { 51 | type: options.readOnly === false ? "storage" : "read-only-storage", 52 | }; 53 | } 54 | 55 | async setCpuBuffer(buffer: ArrayType): Promise { 56 | this.cpuBuffer = buffer; 57 | await this.updateGpuBuffer(); 58 | } 59 | 60 | async updateGpuBuffer() { 61 | if (this.cpuBuffer === undefined) { 62 | throw new Error("Cannot update GPU buffer without CPU buffer"); 63 | } 64 | 65 | const sizeMismatch = this.cpuBuffer.byteLength !== this.gpuBuffer?.size; 66 | const device = await this.getDevice(); 67 | 68 | if (this.gpuBuffer === undefined || sizeMismatch) { 69 | this.gpuBuffer = device.createBuffer({ 70 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 71 | label: `${this.label ?? "Unlabelled"} Storage Buffer`, 72 | size: this.cpuBuffer.byteLength, 73 | }); 74 | } 75 | 76 | device.queue.writeBuffer(this.gpuBuffer, 0, this.cpuBuffer); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/Texture.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./components/Canvas"; 2 | import { WithCpuBuffer } from "./components/CpuBuffer"; 3 | import { WithDevice } from "./components/Device"; 4 | import { WithGpuTexture } from "./components/GpuTextureObject"; 5 | import { WithLabel } from "./components/Label"; 6 | 7 | const components = WithGpuTexture( 8 | WithCpuBuffer(WithDevice(WithCanvas(WithLabel()))), 9 | ); 10 | 11 | /** 12 | * {@link Texture} constructor parameters 13 | */ 14 | export type TextureOptions = { 15 | label?: string; 16 | 17 | /** 18 | * The binding number of the texture object in the {@link BindGroup}. 19 | */ 20 | binding: number; 21 | 22 | /** 23 | * The shader stages that this uniform is visible to. 24 | * e.g. GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT would make the uniform 25 | * visible to both the vertex and fragment shaders. 26 | */ 27 | visibility: GPUShaderStageFlags; 28 | }; 29 | 30 | /** 31 | * A GPU texture object that can be used in a {@link BindGroup}. 32 | */ 33 | export class Texture extends components { 34 | readonly binding: number; 35 | readonly visibility: GPUShaderStageFlags; 36 | 37 | constructor(options: TextureOptions) { 38 | super(); 39 | this.label = options.label; 40 | this.binding = options.binding; 41 | this.visibility = options.visibility; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/Uniform.ts: -------------------------------------------------------------------------------- 1 | import { WithGpuBuffer } from "./components/GpuBufferObject"; 2 | import { WithDevice } from "./components/Device"; 3 | import { WithLabel } from "./components/Label"; 4 | import { WithCpuBuffer } from "./components/CpuBuffer"; 5 | import { type ArrayType } from "./utils"; 6 | import { WithCanvas } from "./components/Canvas"; 7 | 8 | /** 9 | * {@link Uniform} constructor parameters 10 | */ 11 | export type UniformOptions = { 12 | label?: string; 13 | 14 | /** 15 | * The binding number of the uniform object in the {@link BindGroup}. 16 | */ 17 | binding: number; 18 | 19 | /** 20 | * The shader stages that this uniform is visible to. 21 | * e.g. GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT would make the uniform 22 | * visible to both the vertex and fragment shaders. 23 | */ 24 | visibility: GPUShaderStageFlags; 25 | arrayBuffer?: ArrayType; 26 | }; 27 | 28 | const components = WithGpuBuffer( 29 | WithCpuBuffer(WithDevice(WithCanvas(WithLabel()))), 30 | ); 31 | 32 | /** 33 | * A GPU uniform object that can be used in a {@link BindGroup}. 34 | */ 35 | export class Uniform extends components { 36 | readonly binding: number; 37 | readonly visibility: GPUShaderStageFlags; 38 | 39 | constructor(options: UniformOptions) { 40 | super(); 41 | this.label = options.label; 42 | this.binding = options.binding; 43 | this.visibility = options.visibility; 44 | this.cpuBuffer = options.arrayBuffer; 45 | } 46 | 47 | async setCpuBuffer(buffer: ArrayType): Promise { 48 | this.cpuBuffer = buffer; 49 | await this.updateGpuBuffer(); 50 | } 51 | 52 | async updateGpuBuffer() { 53 | if (this.cpuBuffer === undefined) { 54 | throw new Error("Cannot update GPU buffer without CPU buffer"); 55 | } 56 | 57 | const device = await this.getDevice(); 58 | const sizeMismatch = this.cpuBuffer.byteLength !== this.gpuBuffer?.size; 59 | 60 | if (this.gpuBuffer === undefined || sizeMismatch) { 61 | this.gpuBuffer = device.createBuffer({ 62 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 63 | label: `${this.label ?? "Unlabelled"} Uniform Buffer`, 64 | size: this.cpuBuffer.byteLength, 65 | }); 66 | } 67 | 68 | device.queue.writeBuffer(this.gpuBuffer, 0, this.cpuBuffer); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/VertexAttributeObject.test.ts: -------------------------------------------------------------------------------- 1 | import { it, describe, expect, vi } from "vitest"; 2 | import { MockWebGPU, MockDevice, MockCanvasFormat } from "../lib/MockWebGPU"; 3 | import { Attribute } from "./Attribute"; 4 | import { VertexAttributeObject } from "./VertexAttributeObject"; 5 | 6 | vi.mock("./utils", async () => { 7 | const actual = await vi.importActual("./utils"); 8 | for (const key in MockWebGPU) { 9 | vi.stubGlobal( 10 | key, 11 | vi.fn(() => MockWebGPU[key]), 12 | ); 13 | } 14 | 15 | return { 16 | ...(actual as Record), 17 | getDefaultDevice: () => MockDevice, 18 | getDefaultCanvasFormat: () => MockCanvasFormat, 19 | }; 20 | }); 21 | 22 | describe("VertexAttributeObject", () => { 23 | it("should create an instance", () => { 24 | expect(new VertexAttributeObject({ itemCount: 0 })).toBeTruthy(); 25 | }); 26 | 27 | it("should create an instance with a label", () => { 28 | const attr = new VertexAttributeObject({ label: "vao", itemCount: 0 }); 29 | expect(attr).toBeTruthy(); 30 | 31 | expect(attr.label).toBe("vao"); 32 | }); 33 | 34 | it("should allow adding an attribute", async () => { 35 | const vertices = new Float32Array([-1, -1, 0, 1, 1, -1]); 36 | const attribute = new Attribute({ 37 | label: "position", 38 | format: "float32x2", 39 | itemSize: 2, 40 | itemCount: 3, 41 | arrayBuffer: vertices, 42 | shaderLocation: 0, 43 | }); 44 | 45 | const vao = new VertexAttributeObject({ itemCount: 3 }); 46 | await vao.addAttributes(attribute); 47 | expect(vao.attributes.length).toBe(1); 48 | expect(vao.cpuBuffer).toEqual(vertices); 49 | }); 50 | 51 | it("should allow adding multiple attributes", async () => { 52 | const vertices = new Float32Array([-1, -1, 0, 1, 1, -1]); 53 | const colors = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); 54 | 55 | const position = new Attribute({ 56 | label: "position", 57 | format: "float32x2", 58 | itemSize: 2, 59 | itemCount: 3, 60 | arrayBuffer: vertices, 61 | shaderLocation: 0, 62 | }); 63 | 64 | const color = new Attribute({ 65 | label: "color", 66 | format: "float32x3", 67 | itemSize: 3, 68 | itemCount: 3, 69 | arrayBuffer: colors, 70 | shaderLocation: 1, 71 | }); 72 | 73 | const vao = new VertexAttributeObject({ itemCount: 3 }); 74 | await vao.addAttributes(position); 75 | await vao.addAttributes(color); 76 | expect(vao.attributes.length).toBe(2); 77 | expect(vao.cpuBuffer).toEqual( 78 | new Float32Array([-1, -1, 1, 0, 0, 0, 1, 0, 1, 0, 1, -1, 0, 0, 1]), 79 | ); 80 | 81 | expect(vao.layout).toEqual({ 82 | arrayStride: 20, 83 | attributes: [ 84 | { 85 | format: "float32x2", 86 | offset: 0, 87 | shaderLocation: 0, 88 | }, 89 | { 90 | format: "float32x3", 91 | offset: 8, 92 | shaderLocation: 1, 93 | }, 94 | ], 95 | stepMode: "vertex", 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/core/src/VertexAttributeObject.ts: -------------------------------------------------------------------------------- 1 | import { WithGpuBuffer } from "./components/GpuBufferObject"; 2 | import { WithDevice } from "./components/Device"; 3 | import { WithLabel } from "./components/Label"; 4 | import type { Attribute } from "./Attribute"; 5 | import { WithCpuBuffer } from "./components/CpuBuffer"; 6 | import { WithCanvas } from "./components/Canvas"; 7 | 8 | const components = WithCpuBuffer( 9 | WithGpuBuffer(WithDevice(WithCanvas(WithLabel()))), 10 | ); 11 | 12 | /** 13 | * {@link VertexAttributeObject} constructor parameters 14 | */ 15 | export type VAOOptions = { 16 | label?: string; 17 | itemCount: number; 18 | stepMode?: GPUVertexStepMode; 19 | }; 20 | 21 | /** 22 | * A GPU vertex attribute object that is composed of multiple {@link Attribute}s 23 | * to be used in a {@link PipelineGroup}. 24 | */ 25 | export class VertexAttributeObject extends components { 26 | readonly attributes: Attribute[] = []; 27 | layout?: GPUVertexBufferLayout; 28 | itemCount: number; 29 | stepMode: GPUVertexStepMode; 30 | 31 | constructor(options: VAOOptions) { 32 | super(); 33 | this.label = options.label; 34 | this.itemCount = options.itemCount; 35 | this.stepMode = options.stepMode ?? "vertex"; 36 | } 37 | 38 | async addAttributes(...attributes: Attribute[]): Promise { 39 | this.attributes.push(...attributes); 40 | 41 | this.updateLayout(); 42 | await this.updateBuffer(); 43 | } 44 | 45 | private updateLayout(): void { 46 | if (this.attributes.length === 0) { 47 | return; 48 | } 49 | 50 | let offset = 0; 51 | const attributes: GPUVertexAttribute[] = []; 52 | for (const attribute of this.attributes) { 53 | attributes.push({ 54 | format: attribute.format, 55 | offset, 56 | shaderLocation: attribute.shaderLocation, 57 | }); 58 | 59 | offset += attribute.itemSize * attribute.cpuBuffer.BYTES_PER_ELEMENT; 60 | } 61 | 62 | this.layout = { 63 | arrayStride: offset, 64 | attributes, 65 | stepMode: this.stepMode, 66 | }; 67 | } 68 | 69 | private async updateBuffer(): Promise { 70 | if (this.attributes.length === 0) { 71 | return; 72 | } 73 | 74 | const data: number[] = []; 75 | 76 | if (!this.layout) { 77 | throw new Error("layout is undefined"); 78 | } 79 | 80 | for (let i = 0; i < this.itemCount; i++) { 81 | for (const attribute of this.attributes) { 82 | const offset = i * attribute.itemSize; 83 | for (let j = offset; j < offset + attribute.itemSize; j++) { 84 | data.push(attribute.cpuBuffer[j]); 85 | } 86 | } 87 | } 88 | 89 | this.setCpuBuffer(new Float32Array(data)); 90 | 91 | if (!this.cpuBuffer) { 92 | throw new Error("cpuBuffer is undefined"); 93 | } 94 | 95 | const device = await this.getDevice(); 96 | const size = this.cpuBuffer.byteLength; 97 | if (this.gpuBuffer === undefined || size !== this.gpuBuffer.size) { 98 | this.gpuBuffer = device.createBuffer({ 99 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, 100 | label: `${this.label ?? "Unlabelled"} Vertex Buffer`, 101 | size, 102 | }); 103 | } 104 | 105 | device.queue.writeBuffer(this.gpuBuffer, 0, this.cpuBuffer); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/core/src/components/Canvas.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultCanvasFormat } from "../utils"; 2 | import type { Constructor } from "../utils"; 3 | 4 | export function WithCanvas(Base: TBase) { 5 | return class extends Base { 6 | canvas?: HTMLCanvasElement; 7 | context?: GPUCanvasContext; 8 | canvasFormat: GPUTextureFormat = getDefaultCanvasFormat(); 9 | 10 | setCanvas(canvas: HTMLCanvasElement) { 11 | this.canvas = canvas; 12 | const ctx = this.canvas.getContext("webgpu"); 13 | 14 | if (!ctx) { 15 | throw new Error("Could not get WebGPU context"); 16 | } 17 | 18 | this.context = ctx; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/components/ColorTarget.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./Canvas"; 2 | import { WithLabel } from "./Label"; 3 | 4 | const components = WithCanvas(WithLabel()); 5 | 6 | export function WithColorTarget(Base: TBase) { 7 | return class extends Base { 8 | colorTarget: GPUColorTargetState = { 9 | format: this.canvasFormat, 10 | blend: { 11 | color: { 12 | srcFactor: "src-alpha", 13 | dstFactor: "one-minus-src-alpha", 14 | operation: "add", 15 | }, 16 | alpha: { 17 | srcFactor: "src-alpha", 18 | dstFactor: "one-minus-src-alpha", 19 | operation: "add", 20 | }, 21 | }, 22 | writeMask: GPUColorWrite.ALL, 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/components/CpuBuffer.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor, ArrayType } from "../utils"; 2 | 3 | export function WithCpuBuffer(Base: TBase) { 4 | return class extends Base { 5 | cpuBuffer?: ArrayType; 6 | 7 | setCpuBuffer(buffer: ArrayType): void { 8 | this.cpuBuffer = buffer; 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/components/DepthStencil.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./Canvas"; 2 | import { WithDevice } from "./Device"; 3 | import { WithLabel } from "./Label"; 4 | import { WithMultiSampling } from "./MultiSampling"; 5 | 6 | const components = WithMultiSampling(WithDevice(WithCanvas(WithLabel()))); 7 | 8 | export function WithDepthStencil(Base: TBase) { 9 | return class extends Base { 10 | depthStencilEnabled = false; 11 | depthStencilTexture?: GPUTexture; 12 | depthStencilTextureView?: GPUTextureView; 13 | depthStencilState: GPUDepthStencilState = { 14 | depthWriteEnabled: true, 15 | depthCompare: "less", 16 | format: "depth24plus-stencil8", 17 | stencilBack: { 18 | compare: "always", 19 | failOp: "keep", 20 | depthFailOp: "keep", 21 | passOp: "keep", 22 | }, 23 | stencilFront: { 24 | compare: "always", 25 | failOp: "keep", 26 | depthFailOp: "keep", 27 | passOp: "keep", 28 | }, 29 | depthBias: 0, 30 | depthBiasSlopeScale: 0, 31 | depthBiasClamp: 0, 32 | stencilReadMask: 0xff, 33 | stencilWriteMask: 0xff, 34 | }; 35 | 36 | depthStencilAttachment: Partial = { 37 | depthLoadOp: "clear", 38 | depthStoreOp: "store", 39 | stencilLoadOp: "clear", 40 | stencilStoreOp: "store", 41 | depthClearValue: 1.0, 42 | stencilClearValue: 0, 43 | }; 44 | 45 | setDepthWriteEnabled(enabled: boolean): void { 46 | this.depthStencilState.depthWriteEnabled = enabled; 47 | } 48 | 49 | setDepthCompare(compare: GPUCompareFunction): void { 50 | this.depthStencilState.depthCompare = compare; 51 | } 52 | 53 | setDepthStencilFormat(format: GPUTextureFormat): void { 54 | this.depthStencilState.format = format; 55 | } 56 | 57 | setStencilBack(state: GPUStencilFaceState): void { 58 | this.depthStencilState.stencilBack = state; 59 | } 60 | 61 | setStencilFront(state: GPUStencilFaceState): void { 62 | this.depthStencilState.stencilFront = state; 63 | } 64 | 65 | setDepthBias(bias: number): void { 66 | this.depthStencilState.depthBias = bias; 67 | } 68 | 69 | setDepthBiasSlopeScale(scale: number): void { 70 | this.depthStencilState.depthBiasSlopeScale = scale; 71 | } 72 | 73 | setDepthBiasClamp(clamp: number): void { 74 | this.depthStencilState.depthBiasClamp = clamp; 75 | } 76 | 77 | setStencilReadMask(mask: number): void { 78 | this.depthStencilState.stencilReadMask = mask; 79 | } 80 | 81 | setStencilWriteMask(mask: number): void { 82 | this.depthStencilState.stencilWriteMask = mask; 83 | } 84 | 85 | setDepthStencilAttachment( 86 | attachment: Partial, 87 | replace = true, 88 | ): void { 89 | if (replace) { 90 | this.depthStencilAttachment = attachment; 91 | } else { 92 | Object.assign(this.depthStencilAttachment, attachment); 93 | } 94 | } 95 | 96 | async buildDepthStencilTexture() { 97 | const device = await this.getDevice(); 98 | 99 | if (this.depthStencilTexture !== undefined) { 100 | this.depthStencilTexture.destroy(); 101 | } 102 | 103 | if (!this.canvas) { 104 | throw new Error("No canvas"); 105 | } 106 | 107 | if (this.depthStencilEnabled) { 108 | this.depthStencilTexture = device.createTexture({ 109 | label: "Depth stencil texture", 110 | size: [this.canvas.width, this.canvas.height], 111 | format: this.depthStencilState.format, 112 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 113 | sampleCount: this.multiSampleState.count, 114 | }); 115 | 116 | this.depthStencilTextureView = this.depthStencilTexture.createView(); 117 | } 118 | } 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /packages/core/src/components/Device.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultDevice } from "../utils"; 2 | import { WithLabel } from "./Label"; 3 | 4 | const components = WithLabel(); 5 | 6 | export type WithDevice = typeof WithDevice; 7 | export function WithDevice(Base: TBase) { 8 | return class extends Base { 9 | _device?: GPUDevice; 10 | 11 | async getDevice(): Promise { 12 | if (!this._device) { 13 | const d = await getDefaultDevice(); 14 | this.setDevice(d); 15 | return d; 16 | } 17 | 18 | return this._device; 19 | } 20 | 21 | setDevice(d: GPUDevice): void { 22 | this._device = d; 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/components/GpuBufferObject.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from "../utils"; 2 | 3 | export function WithGpuBuffer(Base: TBase) { 4 | return class extends Base { 5 | gpuBuffer?: GPUBuffer; 6 | usage: GPUBufferUsageFlags = GPUBufferUsage.COPY_DST; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/components/GpuSamplerObject.ts: -------------------------------------------------------------------------------- 1 | import { WithDevice } from "./Device"; 2 | import { WithLabel } from "./Label"; 3 | 4 | const components = WithDevice(WithLabel()); 5 | 6 | export function WithGpuSampler(Base: TBase) { 7 | return class extends Base { 8 | gpuSampler?: GPUSampler; 9 | samplerOptions: GPUSamplerDescriptor = { 10 | magFilter: "linear", 11 | minFilter: "linear", 12 | addressModeU: "repeat", 13 | addressModeV: "repeat", 14 | mipmapFilter: "linear", 15 | }; 16 | 17 | async updateSampler(options?: GPUSamplerDescriptor): Promise { 18 | if (options !== undefined) { 19 | this.samplerOptions = options; 20 | } 21 | 22 | const device = await this.getDevice(); 23 | 24 | this.gpuSampler = device.createSampler({ 25 | label: `${this.label ?? "Unlabelled"} Sampler`, 26 | ...this.samplerOptions, 27 | }); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/components/Id.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | import type { Constructor } from "../utils"; 3 | 4 | export function WithId(Base: TBase) { 5 | return class extends Base { 6 | readonly id: string = uuidv4(); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/components/Label.ts: -------------------------------------------------------------------------------- 1 | import type { ConstructorArgs } from "../utils"; 2 | 3 | export function WithLabel() { 4 | return class Label { 5 | label?: string; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor -- private 8 | constructor(..._args: ConstructorArgs) {} 9 | 10 | setLabel(label: string): void { 11 | this.label = label; 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/components/MultiSampling.ts: -------------------------------------------------------------------------------- 1 | import { WithCanvas } from "./Canvas"; 2 | import { WithDevice } from "./Device"; 3 | import { WithLabel } from "./Label"; 4 | 5 | const components = WithDevice(WithCanvas(WithLabel())); 6 | 7 | export function WithMultiSampling( 8 | Base: TBase, 9 | ) { 10 | return class extends Base { 11 | multiSampleTexture?: GPUTexture; 12 | multiSampleTextureView?: GPUTextureView; 13 | multiSampleState: Required = { 14 | count: 1, 15 | mask: 0xffffffff, 16 | alphaToCoverageEnabled: false, 17 | }; 18 | 19 | setMultiSampleCount(count: 1 | 4): void { 20 | this.multiSampleState.count = count; 21 | } 22 | 23 | setMultiSampleMask(mask: number): void { 24 | this.multiSampleState.mask = mask; 25 | } 26 | 27 | setMultiSampleAlphaToCoverageEnabled(enabled: boolean): void { 28 | if (this.multiSampleState.count === 1) return; 29 | 30 | this.multiSampleState.alphaToCoverageEnabled = enabled; 31 | } 32 | 33 | async buildMultiSampleTexture() { 34 | const device = await this.getDevice(); 35 | 36 | if (this.multiSampleTexture !== undefined) { 37 | this.multiSampleTexture.destroy(); 38 | } 39 | 40 | if (!this.canvas) { 41 | throw new Error("No canvas"); 42 | } 43 | 44 | if (this.multiSampleState.count > 1) { 45 | this.multiSampleTexture = device.createTexture({ 46 | label: "Multi-sample texture", 47 | size: [this.canvas.width, this.canvas.height], 48 | format: this.canvasFormat, 49 | sampleCount: this.multiSampleState.count, 50 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 51 | }); 52 | 53 | this.multiSampleTextureView = this.multiSampleTexture.createView(); 54 | } 55 | } 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/components/Primitive.ts: -------------------------------------------------------------------------------- 1 | import { WithLabel } from "./Label"; 2 | 3 | const components = WithLabel(); 4 | 5 | export function WithPrimitive(Base: TBase) { 6 | return class extends Base { 7 | primitiveState: GPUPrimitiveState = { 8 | topology: "triangle-list", 9 | stripIndexFormat: undefined, 10 | frontFace: "ccw", 11 | cullMode: "none", 12 | }; 13 | 14 | setTopology(topology: GPUPrimitiveTopology) { 15 | this.primitiveState.topology = topology; 16 | } 17 | 18 | setStripIndexFormat(format: GPUIndexFormat) { 19 | this.primitiveState.stripIndexFormat = format; 20 | } 21 | 22 | setFrontFace(face: GPUFrontFace) { 23 | this.primitiveState.frontFace = face; 24 | } 25 | 26 | setCullMode(mode: GPUCullMode) { 27 | this.primitiveState.cullMode = mode; 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/components/Shader.ts: -------------------------------------------------------------------------------- 1 | import { WithDevice } from "./Device"; 2 | import { WithLabel } from "./Label"; 3 | 4 | export type ShaderEntries = { 5 | vertex: string; 6 | fragment: string; 7 | compute: string; 8 | }; 9 | 10 | const components = WithDevice(WithLabel()); 11 | 12 | export function WithShader(Base: TBase) { 13 | return class extends Base { 14 | shader = ""; 15 | shaderModule?: GPUShaderModule; 16 | shaderEntries: ShaderEntries = { 17 | vertex: "vertexMain", 18 | fragment: "fragmentMain", 19 | compute: "computeMain", 20 | }; 21 | 22 | setShader(shader: string) { 23 | this.shader = shader; 24 | } 25 | 26 | async buildShaderModule() { 27 | const device = await this.getDevice(); 28 | this.shaderModule = device.createShaderModule({ 29 | label: `${this.label ?? "Unlabelled"} shader'}`, 30 | code: this.shader, 31 | }); 32 | } 33 | 34 | setShaderEntries(entries: ShaderEntries) { 35 | this.shaderEntries = entries; 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils"; 2 | 3 | export * from "./Attribute"; 4 | export * from "./Executor"; 5 | export * from "./Pipeline"; 6 | export * from "./PipelineGroup"; 7 | export * from "./Storage"; 8 | export * from "./Uniform"; 9 | export * from "./VertexAttributeObject"; 10 | export * from "./Texture"; 11 | export * from "./Sampler"; 12 | export * from "./IndexBuffer"; 13 | export * from "./BindGroup"; 14 | export { utils }; 15 | -------------------------------------------------------------------------------- /packages/core/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | 3 | test("testing test suite", () => { 4 | expect(1).toBe(1); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- private */ 4 | export type ConstructorArgs = any[]; 5 | 6 | /** @internal */ 7 | export type Constructor = new ( 8 | ...args: ConstructorArgs 9 | ) => NonNullable; 10 | 11 | export type ArrayType = 12 | | Float32Array 13 | | Uint32Array 14 | | Uint16Array 15 | | Uint8Array 16 | | Uint8ClampedArray; 17 | 18 | /** @internal */ 19 | export function fallbackToEmpty(Base?: T) { 20 | if (Base !== undefined) { 21 | return Base; 22 | } 23 | 24 | return class { 25 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor -- private 26 | constructor(..._args: ConstructorArgs) {} 27 | }; 28 | } 29 | 30 | let defaultDevice: GPUDevice | undefined; 31 | 32 | /** @internal */ 33 | export async function getDefaultDevice(): Promise { 34 | if (defaultDevice) return defaultDevice; 35 | 36 | const _adapter = await navigator.gpu.requestAdapter(); 37 | const _device = await _adapter?.requestDevice(); 38 | 39 | if (!_device) throw new Error("No GPU device found"); 40 | 41 | defaultDevice = _device; 42 | return _device; 43 | } 44 | 45 | /** @internal */ 46 | export function getDefaultCanvasFormat(): GPUTextureFormat { 47 | return navigator.gpu.getPreferredCanvasFormat(); 48 | } 49 | 50 | let tempCanvas: HTMLCanvasElement | undefined; 51 | 52 | /** @internal */ 53 | export function getDataFromImage(image: HTMLImageElement): Uint8ClampedArray { 54 | if (!tempCanvas) { 55 | tempCanvas = document.createElement("canvas"); 56 | } 57 | 58 | const canvas = tempCanvas; 59 | canvas.width = image.naturalWidth; 60 | canvas.height = image.naturalHeight; 61 | const ctx = canvas.getContext("2d"); 62 | if (ctx === null) { 63 | throw new Error("Could not get 2D context"); 64 | } 65 | 66 | ctx.drawImage(image, 0, 0); 67 | return ctx.getImageData(0, 0, image.naturalWidth, image.naturalHeight).data; 68 | } 69 | 70 | /** @internal */ 71 | export function lerp(a: number, b: number, t: number): number { 72 | return a + (b - a) * t; 73 | } 74 | 75 | /** @internal */ 76 | export function clamp(x: number, min: number, max: number): number { 77 | return Math.min(Math.max(x, min), max); 78 | } 79 | 80 | /** @internal */ 81 | export function mix(a: ArrayType, b: ArrayType, t: number): ArrayType { 82 | return a.map((v, i) => v + (b[i] - v) * t); 83 | } 84 | 85 | /** @internal */ 86 | export function bilinearInterpolation( 87 | topLeft: ArrayType, 88 | topRight: ArrayType, 89 | bottomLeft: ArrayType, 90 | bottomRight: ArrayType, 91 | t1: number, 92 | t2: number, 93 | ) { 94 | const top = mix(topLeft, topRight, t1); 95 | const bottom = mix(bottomLeft, bottomRight, t1); 96 | 97 | return mix(top, bottom, t2); 98 | } 99 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/main", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"], 7 | "exclude": ["dist", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../apps/docs/typedoc.base.json"], 3 | "name": "Core", 4 | "readme": "./README.md", 5 | "entryPoints": ["./src/index.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | 3 | import { resolve } from "path"; 4 | import { defineConfig } from "vite"; 5 | import dts from "vite-plugin-dts"; 6 | 7 | export default defineConfig({ 8 | test: { 9 | environment: "jsdom", 10 | }, 11 | build: { 12 | lib: { 13 | entry: resolve(__dirname, "./src/index.ts"), 14 | name: "webgpu-kit Core", 15 | fileName: "webgpu-kit_core", 16 | }, 17 | }, 18 | plugins: [ 19 | dts({ 20 | insertTypesEntry: true, 21 | rollupTypes: true, 22 | tsconfigPath: resolve(__dirname, "tsconfig.json"), 23 | }), 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # eslint-config-custom 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - [`3ded504`](https://github.com/JMBeresford/webgpu-kit/commit/3ded504c2547830b637c321280c84cd4c706d785) Thanks [@JMBeresford](https://github.com/JMBeresford)! - feat(Core): Mipmap generation for texture2d 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - [#5](https://github.com/JMBeresford/webgpu-kit/pull/5) [`14c0fd2`](https://github.com/JMBeresford/webgpu-kit/commit/14c0fd2cb1cb8b84936879d85103f9be4b07eb33) Thanks [@JMBeresford](https://github.com/JMBeresford)! - fix: remove bloat + update changelog gen 14 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "eslint:recommended", 17 | "prettier", 18 | "plugin:turbo/recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | ], 21 | parserOptions: { 22 | project, 23 | }, 24 | env: { node: true }, 25 | settings: { 26 | "import/resolver": { 27 | typescript: { 28 | project, 29 | }, 30 | }, 31 | }, 32 | plugins: ["tsdoc", "prettier", "turbo"], 33 | ignorePatterns: ["node_modules/", "dist/", "vite.config.js", ".*.js"], 34 | rules: { 35 | "prettier/prettier": "error", 36 | "@typescript-eslint/no-var-requires": "off", 37 | "@typescript-eslint/no-extraneous-class": "off", 38 | "@typescript-eslint/consistent-type-definitions": "off", 39 | "@typescript-eslint/no-unsafe-argument": "off", 40 | "no-undef": "off", 41 | "unicorn/filename-case": "off", 42 | "@typescript-eslint/explicit-function-return-type": "off", 43 | "@typescript-eslint/no-empty-function": "off", 44 | "@typescript-eslint/no-unused-vars": [ 45 | "error", 46 | { 47 | argsIgnorePattern: "^_", 48 | varsIgnorePattern: "^_", 49 | caughtErrorsIgnorePattern: "^_", 50 | destructuredArrayIgnorePattern: "^_", 51 | }, 52 | ], 53 | "no-bitwise": "off", 54 | "tsdoc/syntax": "warn", 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * Next.js apps. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "eslint:recommended", 17 | require.resolve("@vercel/style-guide/eslint/next"), 18 | "prettier", 19 | "plugin:turbo/recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | ], 22 | globals: { 23 | React: true, 24 | JSX: true, 25 | }, 26 | env: { 27 | browser: true, 28 | node: true, 29 | }, 30 | settings: { 31 | "import/resolver": { 32 | typescript: { 33 | project, 34 | }, 35 | }, 36 | }, 37 | plugins: ["prettier", "turbo"], 38 | ignorePatterns: ["node_modules/", "dist/", ".*.js"], 39 | // add rules configurations here 40 | rules: { 41 | "prettier/prettier": "error", 42 | "import/no-default-export": "off", 43 | "no-unused-vars": "off", 44 | "@typescript-eslint/no-unused-vars": [ 45 | "error", 46 | { 47 | argsIgnorePattern: "^_", 48 | varsIgnorePattern: "^_", 49 | caughtErrorsIgnorePattern: "^_", 50 | destructuredArrayIgnorePattern: "^_", 51 | }, 52 | ], 53 | "@typescript-eslint/consistent-type-definitions": "off", 54 | "@typescript-eslint/explicit-function-return-type": "error", 55 | "react/no-unknown-property": "off", 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "license": "MIT", 4 | "version": "0.0.2", 5 | "private": true, 6 | "files": [ 7 | "library.js", 8 | "react-internal.js", 9 | "next.js" 10 | ], 11 | "devDependencies": { 12 | "@typescript-eslint/eslint-plugin": "^6.17.0", 13 | "@typescript-eslint/parser": "^6.17.0", 14 | "@vercel/style-guide": "^5.1.0", 15 | "eslint-config-prettier": "^9.1.0", 16 | "eslint-plugin-only-warn": "^1.1.0", 17 | "eslint-plugin-prettier": "^5.1.3", 18 | "eslint-plugin-tsdoc": "^0.2.17", 19 | "eslint-plugin-turbo": "^1.12.3", 20 | "typescript": "^5.3.3" 21 | }, 22 | "publishConfig": { 23 | "access": "restricted" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | module.exports = { 16 | extends: [ 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/typescript", 19 | "@vercel/style-guide/eslint/react", 20 | ].map(require.resolve), 21 | parserOptions: { 22 | project, 23 | }, 24 | env: { 25 | browser: true, 26 | node: true, 27 | }, 28 | globals: { 29 | JSX: true, 30 | }, 31 | settings: { 32 | "import/resolver": { 33 | typescript: { 34 | project, 35 | }, 36 | }, 37 | }, 38 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"], 39 | 40 | rules: { 41 | // add specific rules configurations here 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/shaders/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/library"], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/shaders/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/shaders/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @webgpu-kit/shaders 2 | 3 | ## 0.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [#40](https://github.com/JMBeresford/webgpu-kit/pull/40) [`d6fb685`](https://github.com/JMBeresford/webgpu-kit/commit/d6fb6855ec9b193add5e39e129dd86a1f472b899) Thanks [@JMBeresford](https://github.com/JMBeresford)! - feat(shaders): init shaders package 8 | -------------------------------------------------------------------------------- /packages/shaders/README.md: -------------------------------------------------------------------------------- 1 | # @webgpu-kit/shaders 2 | 3 | ### A collection of reusable WGSL shader chunks for webGPU 4 | 5 | > UNDER HEAVY DEVELOPMENT 6 | > Expect frequent breaking changes 7 | -------------------------------------------------------------------------------- /packages/shaders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webgpu-kit/shaders", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/webgpu-kit_shaders.umd.cjs", 9 | "module": "dist/webgpu-kit_shaders.js", 10 | "types": "dist/webgpu-kit_shaders.d.ts", 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "tsc && vite build", 14 | "preview": "vite preview" 15 | }, 16 | "devDependencies": { 17 | "eslint-config-custom": "workspace:*", 18 | "tsconfig": "workspace:*", 19 | "typescript": "^5.2.2", 20 | "vite-plugin-dts": "3.5.3", 21 | "vite": "^5.0.8" 22 | }, 23 | "author": { 24 | "name": "John Beresford", 25 | "email": "jberesford@volcaus.com" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/shaders/src/color/gamma.ts: -------------------------------------------------------------------------------- 1 | export function linearToGamma(): string { 2 | const shader = /* wgsl */ ` 3 | fn linearToGamma(linear: vec3) -> vec3 { 4 | return pow(linear, vec3(1.0 / 2.2)); 5 | } 6 | `; 7 | 8 | return shader; 9 | } 10 | 11 | export function gammaToLinear(): string { 12 | const shader = /* wgsl */ ` 13 | fn gammaToLinear(gamma: vec3) -> vec3 { 14 | return pow(gamma, vec3(2.2)); 15 | } 16 | `; 17 | 18 | return shader; 19 | } 20 | -------------------------------------------------------------------------------- /packages/shaders/src/color/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./gamma"; 2 | -------------------------------------------------------------------------------- /packages/shaders/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./color"; 2 | export * from "./lighting"; 3 | -------------------------------------------------------------------------------- /packages/shaders/src/lighting/diffuse.ts: -------------------------------------------------------------------------------- 1 | export function diffuseLambertian(): string { 2 | const shader = /* wgsl */ ` 3 | fn diffuseLambertian( 4 | normal: vec3, 5 | lightDirection: vec3, 6 | lightColor: vec3, 7 | ) -> vec3 { 8 | 9 | let diffuseFactor = max(dot(normal, lightDirection), 0.0); 10 | return diffuseFactor * lightColor; 11 | } 12 | `; 13 | 14 | return shader; 15 | } 16 | 17 | export function diffuseOrenNayar(): string { 18 | const shader = /* wgsl */ ` 19 | fn diffuseOrenNayar( 20 | normal: vec3, 21 | // vector from the surface point to the light 22 | lightDirection: vec3, 23 | // vector from the surface point to the camera 24 | camDirection: vec3, 25 | lightColor: vec3, 26 | roughness: f32, 27 | ) -> vec3 { 28 | 29 | let NV = clamp(dot(normal, camDirection), 0.0, 1.0); 30 | let NL = clamp(dot(normal, lightDirection), 0.0, 1.0); 31 | 32 | let angleNV = acos(NV); 33 | let angleNL = acos(NL); 34 | 35 | let alpha = max(angleNV, angleNL); 36 | let beta = min(angleNV, angleNL); 37 | let gamma = cos(angleNV - angleNL); 38 | let roughnessSq = roughness * roughness; 39 | 40 | let A = 1.0 - 0.5 * (roughnessSq / (roughnessSq + 0.57)); 41 | let B = 0.45 * (roughnessSq / (roughnessSq + 0.09)); 42 | let C = sin(alpha) * tan(beta); 43 | 44 | let diffuseFactor = NL * (A + B * max(0.0, gamma) * C); 45 | 46 | return diffuseFactor * lightColor; 47 | } 48 | `; 49 | 50 | return shader; 51 | } 52 | -------------------------------------------------------------------------------- /packages/shaders/src/lighting/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./diffuse"; 2 | -------------------------------------------------------------------------------- /packages/shaders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/main", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"], 7 | "exclude": ["dist", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/shaders/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../apps/docs/typedoc.base.json"], 3 | "name": "Shaders", 4 | "entryPoints": ["./src/**/*.ts"], 5 | "exclude": "./src/**/index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /packages/shaders/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | 3 | import { resolve } from "path"; 4 | import { defineConfig } from "vite"; 5 | import dts from "vite-plugin-dts"; 6 | 7 | export default defineConfig({ 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, "./src/index.ts"), 11 | name: "webgpu-kit Shaders", 12 | fileName: "webgpu-kit_shaders", 13 | }, 14 | }, 15 | plugins: [ 16 | dts({ 17 | insertTypesEntry: true, 18 | rollupTypes: true, 19 | tsconfigPath: resolve(__dirname, "tsconfig.json"), 20 | }), 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /packages/tsconfig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # tsconfig 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - [#26](https://github.com/JMBeresford/webgpu-kit/pull/26) [`1b7a1b4`](https://github.com/JMBeresford/webgpu-kit/commit/1b7a1b4bd34fb8835f5604498daad44a82ce4b26) Thanks [@JMBeresford](https://github.com/JMBeresford)! - fix(core): fix offset calc for multiple attributes 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - [#5](https://github.com/JMBeresford/webgpu-kit/pull/5) [`14c0fd2`](https://github.com/JMBeresford/webgpu-kit/commit/14c0fd2cb1cb8b84936879d85103f9be4b07eb33) Thanks [@JMBeresford](https://github.com/JMBeresford)! - fix: remove bloat + update changelog gen 14 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "types": ["@webgpu/types"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "incremental": false, 10 | "isolatedModules": true, 11 | "lib": ["es2022", "DOM", "DOM.Iterable"], 12 | "module": "NodeNext", 13 | "moduleDetection": "force", 14 | "moduleResolution": "bundler", 15 | "noUncheckedIndexedAccess": false, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "ES2022" 20 | }, 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Main", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "ES2020", 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "strict": true, 15 | "noEmit": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["src", "lib"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "allowJs": true, 8 | "declaration": false, 9 | "declarationMap": false, 10 | "incremental": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "dom.iterable", "esnext"], 13 | "module": "esnext", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "strict": true, 17 | "target": "ES2020" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.2", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "restricted" 8 | }, 9 | "peerDependencies": { 10 | "@webgpu/types": "0.1.34" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015", "DOM"], 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "target": "es6" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/react-internal"], 3 | root: true, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/ui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | ## 0.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [#5](https://github.com/JMBeresford/webgpu-kit/pull/5) [`14c0fd2`](https://github.com/JMBeresford/webgpu-kit/commit/14c0fd2cb1cb8b84936879d85103f9be4b07eb33) Thanks [@JMBeresford](https://github.com/JMBeresford)! - fix: remove bloat + update changelog gen 8 | -------------------------------------------------------------------------------- /packages/ui/button/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ForwardedRef } from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./styles.module.scss"; 4 | 5 | type ButtonProps = { 6 | primary?: boolean; 7 | small?: boolean; 8 | bare?: boolean; 9 | outline?: boolean; 10 | } & JSX.IntrinsicElements["button"]; 11 | 12 | function ButtonImpl( 13 | { children, primary, small, bare, outline, className, ...props }: ButtonProps, 14 | ref: ForwardedRef, 15 | ): JSX.Element { 16 | let classes = styles.button; 17 | if (primary) { 18 | classes += ` ${styles.primary}`; 19 | } 20 | if (small) { 21 | classes += ` ${styles.small}`; 22 | } 23 | if (bare) { 24 | classes += ` ${styles.bare}`; 25 | } 26 | if (outline) { 27 | classes += ` ${styles.outline}`; 28 | } 29 | 30 | return ( 31 | 39 | ); 40 | } 41 | 42 | function Group( 43 | { children, className, ...props }: JSX.IntrinsicElements["div"], 44 | ref: ForwardedRef, 45 | ): JSX.Element { 46 | return ( 47 |
48 | {children} 49 |
50 | ); 51 | } 52 | 53 | export const Button = forwardRef(ButtonImpl); 54 | export const ButtonGroup = forwardRef(Group); 55 | -------------------------------------------------------------------------------- /packages/ui/button/styles.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: grid; 3 | place-items: center; 4 | padding: 1em 1.25em; 5 | border-radius: 3px; 6 | border: none; 7 | cursor: pointer; 8 | 9 | font-size: 1.125em; 10 | font-weight: bold; 11 | 12 | background-color: #333; 13 | border: 1px solid #333; 14 | color: #fff; 15 | 16 | transition: 17 | background-color 0.25s, 18 | color 0.25s; 19 | 20 | transition-timing-function: ease-in; 21 | 22 | &:hover { 23 | background-color: #000; 24 | color: #fff; 25 | transition-timing-function: ease-out; 26 | } 27 | } 28 | 29 | .small { 30 | padding: 0.5em 1em; 31 | } 32 | 33 | .primary { 34 | background-color: #fff; 35 | border: 1px solid #fff; 36 | color: #333; 37 | } 38 | 39 | .bare { 40 | background-color: rgba(255, 255, 255, 0); 41 | border: none; 42 | color: inherit; 43 | 44 | &:hover { 45 | background-color: unset; 46 | } 47 | } 48 | 49 | .outline { 50 | background-color: rgba(0, 0, 0, 0); 51 | border: 1px solid #fff; 52 | color: #fff; 53 | 54 | &:hover { 55 | background-color: rgba(0, 0, 0, 0.2); 56 | } 57 | } 58 | 59 | .buttongroup { 60 | display: flex; 61 | gap: 2em; 62 | 63 | a { 64 | text-decoration: none; 65 | color: inherit; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/ui/card/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ForwardedRef } from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./styles.module.scss"; 4 | 5 | function CardImpl( 6 | props: Omit, 7 | ref: ForwardedRef, 8 | ): JSX.Element { 9 | const { children, className, ...rest } = props; 10 | 11 | const classes = `${styles.card} ${className}`; 12 | 13 | return ( 14 |
15 |
16 |
{children}
17 |
18 | ); 19 | } 20 | 21 | function CardTitleImpl( 22 | props: Omit, 23 | ref: ForwardedRef, 24 | ): JSX.Element { 25 | const { children, className, ...rest } = props; 26 | 27 | const classes = `${styles.title} ${className}`; 28 | 29 | return ( 30 |

31 | {children} 32 |

33 | ); 34 | } 35 | 36 | export const Card = forwardRef(CardImpl); 37 | export const CardTitle = forwardRef(CardTitleImpl); 38 | -------------------------------------------------------------------------------- /packages/ui/card/styles.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | position: relative; 3 | padding: 2em; 4 | text-align: center; 5 | display: grid; 6 | grid-template-areas: 7 | "title" 8 | "content"; 9 | width: min-content; 10 | 11 | .background { 12 | position: absolute; 13 | inset: 0; 14 | border: 1px solid #222; 15 | border-radius: 10px; 16 | z-index: -1; 17 | 18 | background: black; 19 | mask-image: linear-gradient( 20 | 180deg, 21 | rgba(0, 0, 0, 0.8) 0%, 22 | rgba(0, 0, 0, 0.8) 50%, 23 | rgba(0, 0, 0, 0) 100% 24 | ); 25 | } 26 | 27 | .title { 28 | font-size: 1.75em; 29 | font-weight: bold; 30 | margin-bottom: 0.85em; 31 | } 32 | 33 | .content { 34 | margin-bottom: 2em; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ui/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ForwardedRef } from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./styles.module.scss"; 4 | 5 | function FooterImpl( 6 | props: Omit, 7 | ref: ForwardedRef, 8 | ): JSX.Element { 9 | const { children, className, ...rest } = props; 10 | 11 | const classes = `${styles.footer} ${className}`; 12 | 13 | return ( 14 |
15 |
{children}
16 |
17 | ); 18 | } 19 | 20 | function FooterColumnImpl( 21 | props: Omit, 22 | ref: ForwardedRef, 23 | ): JSX.Element { 24 | const { children, className, ...rest } = props; 25 | 26 | const classes = `${styles.column} ${className}`; 27 | 28 | return ( 29 |
30 | {children} 31 |
32 | ); 33 | } 34 | 35 | export const Footer = forwardRef(FooterImpl); 36 | export const FooterColumn = forwardRef(FooterColumnImpl); 37 | -------------------------------------------------------------------------------- /packages/ui/footer/styles.module.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | 4 | .wrapper { 5 | margin: 0 auto; 6 | width: min(90%, 1500px); 7 | display: flex; 8 | justify-content: space-between; 9 | flex-wrap: wrap; 10 | gap: 2em; 11 | 12 | padding: 3em 0 5em; 13 | } 14 | 15 | ul { 16 | list-style: none; 17 | } 18 | } 19 | 20 | .column { 21 | flex-grow: 1; 22 | flex-basis: 0; 23 | min-width: fit-content; 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/global-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.scss" { 2 | const classes: Record; 3 | 4 | // eslint-disable-next-line import/no-default-export -- scss modules are default exports 5 | export default classes; 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/header/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ForwardedRef } from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./styles.module.scss"; 4 | 5 | function HeaderImpl( 6 | { children, className, ...props }: JSX.IntrinsicElements["div"], 7 | ref: ForwardedRef, 8 | ): JSX.Element { 9 | return ( 10 |
11 |
{children}
12 |
13 | ); 14 | } 15 | 16 | function Title( 17 | { children, className, ...props }: JSX.IntrinsicElements["h1"], 18 | ref: ForwardedRef, 19 | ): JSX.Element { 20 | return ( 21 |

22 | {children} 23 |

24 | ); 25 | } 26 | 27 | export const Header = forwardRef(HeaderImpl); 28 | export const HeaderTitle = forwardRef(Title); 29 | -------------------------------------------------------------------------------- /packages/ui/header/styles.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | 4 | .wrapper { 5 | position: relative; 6 | margin: 0 auto; 7 | max-width: min(90%, 1500px); 8 | padding: 1em 0; 9 | 10 | display: grid; 11 | grid-auto-flow: column; 12 | grid-auto-columns: max-content; 13 | grid-template-columns: 1fr; 14 | justify-items: end; 15 | align-items: center; 16 | } 17 | } 18 | 19 | .title { 20 | font-size: 2em; 21 | justify-self: start; 22 | 23 | a { 24 | color: inherit; 25 | text-decoration: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/nav/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ForwardedRef } from "react"; 2 | import { forwardRef } from "react"; 3 | import styles from "./styles.module.scss"; 4 | 5 | function NavImpl( 6 | { children, className, ...props }: JSX.IntrinsicElements["nav"], 7 | ref: ForwardedRef, 8 | ): JSX.Element { 9 | return ( 10 | 13 | ); 14 | } 15 | 16 | export const Nav = forwardRef(NavImpl); 17 | -------------------------------------------------------------------------------- /packages/ui/nav/styles.module.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | align-items: center; 4 | gap: 1.25em; 5 | 6 | font-size: 1em; 7 | 8 | a { 9 | text-decoration: none; 10 | color: inherit; 11 | font-weight: bold; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.1", 4 | "private": true, 5 | "files": [ 6 | "." 7 | ], 8 | "license": "MIT", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "generate:component": "turbo gen react-component" 12 | }, 13 | "peerDependencies": { 14 | "sass": "^1.69.5" 15 | }, 16 | "devDependencies": { 17 | "@turbo/gen": "^1.10.12", 18 | "@types/node": "^20.5.2", 19 | "@types/react": "^18.2.57", 20 | "@types/react-dom": "^18.2.19", 21 | "eslint-config-custom": "workspace:*", 22 | "react": "^18.2.0", 23 | "tsconfig": "workspace:*", 24 | "typescript": "^4.5.2" 25 | }, 26 | "publishConfig": { 27 | "access": "restricted" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/side-nav/index.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import type { ForwardedRef } from "react"; 3 | import styles from "./side-nav.module.scss"; 4 | 5 | type Props = JSX.IntrinsicElements["div"]; 6 | 7 | function SideNavImpl( 8 | props: Props, 9 | ref: ForwardedRef, 10 | ): JSX.Element { 11 | const { children, className, ...rest } = props; 12 | 13 | let classes = styles["side-nav"]; 14 | if (className) { 15 | classes += ` ${className}`; 16 | } 17 | 18 | return ( 19 |
20 | {children} 21 |
22 | ); 23 | } 24 | 25 | type GroupProps = { HeaderSlot?: JSX.Element } & JSX.IntrinsicElements["ul"]; 26 | 27 | function SideNavGroupImpl( 28 | props: GroupProps, 29 | ref: ForwardedRef, 30 | ): JSX.Element { 31 | const { children, HeaderSlot, className, ...rest } = props; 32 | 33 | let classes = styles["side-nav-group"]; 34 | if (className) { 35 | classes += ` ${className}`; 36 | } 37 | 38 | return ( 39 | <> 40 | {HeaderSlot} 41 |
    42 | {children} 43 |
44 | 45 | ); 46 | } 47 | 48 | function SideNavItemImpl( 49 | props: JSX.IntrinsicElements["li"], 50 | ref: ForwardedRef, 51 | ): JSX.Element { 52 | const { children, className, ...rest } = props; 53 | 54 | let classes = styles["side-nav-item"]; 55 | if (className) { 56 | classes += ` ${className}`; 57 | } 58 | 59 | return ( 60 |
  • 61 | {children} 62 |
  • 63 | ); 64 | } 65 | 66 | export const SideNav = forwardRef(SideNavImpl); 67 | export const SideNavGroup = forwardRef(SideNavGroupImpl); 68 | export const SideNavItem = forwardRef(SideNavItemImpl); 69 | -------------------------------------------------------------------------------- /packages/ui/side-nav/side-nav.module.scss: -------------------------------------------------------------------------------- 1 | .side-nav { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .side-nav-group { 6 | display: flex; 7 | flex-direction: column; 8 | 9 | > .side-nav-group { 10 | padding-left: 1rem; 11 | } 12 | 13 | .side-nav-item { 14 | padding-left: 1rem; 15 | list-style: none; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/toast/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ForwardedRef } from "react"; 4 | import { useState, forwardRef } from "react"; 5 | import { Button } from "../button"; 6 | import styles from "./styles.module.scss"; 7 | 8 | type Props = { 9 | kind?: "error" | "info"; 10 | title?: string; 11 | visible?: boolean; 12 | } & Omit; 13 | 14 | const ClassNames: Record, string> = { 15 | error: styles.error, 16 | info: styles.info, 17 | }; 18 | 19 | function ToastImpl( 20 | { 21 | kind = "info", 22 | title, 23 | visible = true, 24 | children, 25 | className, 26 | ...props 27 | }: Props, 28 | ref: ForwardedRef, 29 | ): JSX.Element { 30 | const [show, setShow] = useState(true); 31 | 32 | let classes = `${styles.toast} ${ClassNames[kind]}`; 33 | 34 | if (className) { 35 | classes += ` ${className}`; 36 | } 37 | if (show && visible) { 38 | classes += ` ${styles.show}`; 39 | } 40 | if (title) { 41 | classes += ` ${styles.withTitle}`; 42 | } 43 | 44 | return ( 45 |
    46 | {title ? {title} : null} 47 | {children} 48 |
    56 | ); 57 | } 58 | 59 | function ToastContentImpl( 60 | { className, ...props }: Omit, 61 | ref: ForwardedRef, 62 | ): JSX.Element { 63 | let classes = styles.message; 64 | if (className) { 65 | classes += ` ${className}`; 66 | } 67 | 68 | return

    ; 69 | } 70 | 71 | function ToastTitle({ 72 | className, 73 | children, 74 | ...props 75 | }: JSX.IntrinsicElements["h4"]): JSX.Element { 76 | let classes = styles.title; 77 | if (className) { 78 | classes += ` ${className}`; 79 | } 80 | 81 | return ( 82 |

    83 | {children} 84 |

    85 | ); 86 | } 87 | 88 | function ToastActionImpl( 89 | { className, ...props }: Omit, 90 | ref: ForwardedRef, 91 | ): JSX.Element { 92 | let classes = styles.action; 93 | if (className) { 94 | classes += ` ${className}`; 95 | } 96 | 97 | return ( 98 |